/* ==========================================================================
   Riverview Vets - layout layer

   Filled at BLD-03 on 2026-09-17.

   The shell that every page shares: the sticky header and its nav, the page
   container, the footer, the modal, the cookie banner, and the two rules that
   make both decks degrade. Section styling is page.css.

   Everything here is transcribed from the inline styles in the design export
   unless a comment says otherwise. Where the export used a JavaScript state
   variable (`narrow`, `menuOpen`) this file uses the media query or the
   attribute that replaced it.

   THE BREAKPOINT IS 860px AND IT IS NOT ARBITRARY. The export drove every
   responsive branch from one matchMedia call:

     this.mq = window.matchMedia('(max-width: 860px)')

   Every `@media (max-width: 860px)` here is that line, and every
   `@media (min-width: 861px)` is its inverse. There is no second DESIGN
   breakpoint on this site and there should not be: the rest of the
   responsiveness is flex-wrap and auto-fit grids, which is why the design has
   no other one.

   AMENDED 2026-09-18 (row 23i item (g)), because this paragraph used to say
   "no second breakpoint" flatly and TWO already existed, so a reader either
   believed a false sentence or thought both were mistakes. They are neither.
   The rule is now stated the way it was always meant: 860 is the only width at
   which the DESIGN changes its mind. Two other widths exist and both are a
   component's own geometry expressed as a viewport, derived from numbers
   already in the CSS rather than chosen:

     page.css @media (max-width: 480px)   .legal-facts stops being two columns
     page.css @media (max-width: 676px)   the testimonial fan stops fanning,
                                          because 560px / 92% of a deck that is
                                          90% of the viewport IS 676px

   The test for adding a third: if you can DERIVE the width from a property
   already declared, it is a component cap and it belongs to that component. If
   you are picking a round number because a layout looks wrong, it is a design
   breakpoint and it has to be 860.

   Cascade order, set in the <link> tags and deliberate:
     tokens -> fonts -> base -> layout -> components -> page
   tokens first because every later file reads its custom properties;
   components before page so a page rule can override a component one.
   ========================================================================== */


/* --- The page container ---------------------------------------------------

   The export drew every band as two nested divs: an outer one carrying the
   background or the border, and an inner one carrying
   `max-width:1240px; margin:0 auto; padding:... clamp(24px,5vw,80px)`.

   BLD-02 wrote each band as ONE element, so the two jobs have to be done by
   one box. There are two shapes, and which one a section gets is decided by
   what is positioned against it, not by taste.

   1. .u-bleed - the section is the full-width box AND the container. The
      padding-inline expression below is exactly equivalent to the export's
      two divs: content starts at the gutter, or at the edge of a centred
      1240px box plus the gutter, whichever is further in.

      Anything absolutely positioned against one of these sections is
      positioned against the FULL WIDTH box, which is what the export did for
      the cat in #why and the cat and the tail in the sage strip.

   2. .u-wrap - a real centred 1240px box, used where the background sits on
      the parent. Anything absolutely positioned against one of these is
      positioned against the 1240px box, which is what the export did for the
      chalk paw and heart in #susannah (`left:2px` means 2px inside the
      content box, not 2px from the viewport edge).

   Getting these the wrong way round moves four decorations by up to 80px at
   a wide viewport and by nothing at all at a narrow one, which is exactly the
   kind of fault that survives testing. The class names are not in the markup:
   the selector lists below name the sections directly, because BLD-03 is a
   CSS task and adding classes to three HTML files is BLD-02's work, already
   done.
   -------------------------------------------------------------------------- */

/* Shape 1: full-bleed box that is also the container. */
.hero__body,
.mobile-vet,
.services,
.location,
.why,
.testimonials,
.ethos,
.register,
.legal-hero,
.legal-body,
.notfound {
  padding-inline: calc(max(0px, (100% - var(--width-content)) / 2) + var(--page-gutter));
}

/* Shape 2: centred container, background lives on the parent. */
.site-header__inner,
.quote-strip__inner,
.founders,
.sage-strip__inner,
.site-footer__inner,
.site-footer__wordmark,
.cookie-banner__inner {
  max-width: var(--width-content);
  margin-inline: auto;
  padding-inline: var(--page-gutter);
}

/* The rhythm the export repeated on every section:
   padding: clamp(48px,7vw,96px) clamp(24px,5vw,80px). The horizontal half is
   above; this is the vertical half. Sections that use a different vertical
   value set it themselves in page.css. */
.founders,
.mobile-vet,
.services,
.location,
.why,
.testimonials,
.ethos {
  padding-block: var(--section-y);
}


/* --- The progressive enhancement switch -----------------------------------

   `js` lands on <html> from a two-statement inline script in <head>, before
   first paint. These are the only rules on the site that read it, and every
   one of them is a thing that is dishonest with JavaScript off: a slide that
   cannot be reached, a control that does nothing, a form that cannot post.

   BOTH SELECTORS ARE ANCHORED ON `html` ON PURPOSE. A bare `:not(.js)`
   matches any element that lacks the class, <body> included, so it keeps
   matching up the ancestor chain and the rule appears to work whether the
   class landed or not. That is a rule which cannot fail visibly in testing
   and then fails in production. See PLAN-v3, "There are two decks, not one".
   -------------------------------------------------------------------------- */

/* Both decks ship every slide. With JavaScript on, everything but the active
   one is display:none BEFORE first paint, so the preload scanner never sees
   Andrew's or Lucy's portrait and a page load never fetches them.

   EXCEPT a deck that asks for the fan. Added 2026-09-18, and the exception is
   the point rather than an afterthought: display:none cannot transition and
   cannot show a neighbour, so this one rule is what quietly threw the
   testimonial fan away at BLD-02. A deck marked `data-deck-fan` in the markup
   keeps all its slides rendered and page.css stacks them; see page.css,
   "The fan". The founder deck is NOT marked and must never be, because this
   rule is the only thing keeping two portraits off the initial load. */
html.js .deck:not([data-deck-fan]) > :not(.is-active) { display: none; }

/* With JavaScript off the arrows and dots would be dead controls, so they are
   not drawn. The slides they would have moved between are all on the page. */
html:not(.js) .deck-controls { display: none; }

/* Same argument, the menu button. With JavaScript off the links are already
   on the page; a button that opens nothing is worse than no button. */
html:not(.js) .nav-toggle,
html:not(.js) .nav-close { display: none; }

/* Obligation 2 of choosing Forminit. Forminit is a headless API with no
   form-POST endpoint, so with JavaScript off there is nothing for the form to
   post to. The <noscript> block above it offers the mailto instead. Leaving
   six fields on screen that silently do nothing when submitted would be
   worse than not offering a form at all. */
html:not(.js) .register__form { display: none; }

/* The cookies page prints one of two paragraphs. We cannot read localStorage
   without JavaScript, so with JavaScript off we must not print a choice we
   cannot know, and the button that would change it is not drawn either. */
html.js .consent-panel__nojs { display: none; }
html:not(.js) .consent-panel__live,
html:not(.js) .consent-panel__reopen { display: none; }


/* --- Skip link ------------------------------------------------------------
   Not in the export, which had no landmarks to skip to. Off-screen until it
   takes focus, then it lands over the sticky header.
   -------------------------------------------------------------------------- */

.skip-link {
  position: absolute;
  left: var(--sp-lg);
  top: var(--sp-lg);
  z-index: 60;
  transform: translateY(-200%);
  background: var(--white);
  color: var(--forest);
  border: var(--border-width) solid var(--cream-300);
  border-radius: var(--radius-control);
  padding: var(--sp-md) var(--sp-lg);
  font-weight: var(--fw-semibold);
  text-decoration: none;
  transition: transform var(--dur-fast) var(--ease-standard);
}

/* :focus, NOT :focus-visible, and it is the one place on the site where the
   looser selector is the right one. Both match when the link is reached by
   Tab, which was checked with a real key event rather than assumed, so the
   difference is only in what happens when focus arrives some other way:
   :focus-visible is a heuristic about HOW focus arrived, and a skip link that
   holds focus without appearing is the same as no skip link at all. */
.skip-link:focus { transform: translateY(0); }


/* --- Sticky header -------------------------------------------------------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 40;
  background: var(--cream-50);
  border-bottom: var(--border-width) solid var(--cream-300);
  transition: background var(--dur-base) var(--ease-standard);
}

/* BLD-04 CONTRACT. The export set these three properties on the element from
   a scroll listener, at scrollY > 24. They are here so main.js toggles one
   class and writes no styles:

     .site-header.is-scrolled

   rgba(251,249,245,.92) is cream-50 at 92%, transcribed from the export.

   The blur is on a pseudo-element, NOT on the header, and that is load-
   bearing. An element with backdrop-filter becomes the containing block for
   every position:fixed descendant, and the mobile menu is one: with the blur
   on .site-header, `inset: 0` sized the menu to the 86px bar instead of the
   viewport, so once the page had scrolled past 24px the menu opened as a
   close button and nothing else. Reported by the client on an iPhone
   2026-09-23; it is not iOS-specific, every engine does it. z-index:-1
   stays inside the header's own stacking context (sticky + z-index 40), so
   it paints above the page and below the logo and links. */
.site-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
}

.site-header.is-scrolled { background: transparent; }

.site-header.is-scrolled::before {
  background: rgba(251, 249, 245, .92);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}

.site-header__inner {
  padding-block: var(--sp-lg);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-xl);
}

.site-header__home {
  display: block;
  flex: none;
  transition: opacity var(--dur-fast) var(--ease-standard);
}

/* The wordmark is artwork, so the `a:hover` colour change inherited from
   base.css lands on nothing a visitor can see. Opacity is the one hover
   signal that reads on an image. Mouse only: a tap on the logo goes to #top
   without a navigation, so a latched :hover would sit there dimmed. */
@media (hover: hover) and (pointer: fine) {
  .site-header__home:hover { opacity: .85; }
}

/* The wordmark is the artwork the design uses, at the size it is displayed
   at, never re-typeset. In the initial viewport on all three pages.

   Raised from clamp(40px, 4.5vw, 52px) at the client's request, 2026-09-22.
   It is a 12% bump and it has one consequence worth knowing about: at 52px
   the logo was NOT the tallest thing in the bar - the outline CTA is 53.19px
   - so the header height was independent of it. Above about 1064px viewport
   it now is the tallest, and the bar grows from 86.19px to 91px. --nav-h in
   tokens.css is measured against that and moved with it. Anything above 58px
   here has to be measured again, because scroll-margin-top subtracts it. */
.site-header__home img {
  height: clamp(44px, 5vw, 58px);
  width: auto;
  display: block;
}

/* Found 2026-09-23, measured, and a side effect of the 12% bump above: on the
   two legal pages the wordmark, the --sp-xl gap and "Back to home" came to
   323px at a 320px viewport, so privacy.html and cookies.html scrolled
   sideways by 3px on the smallest phones. The landing page is unaffected,
   because its header carries a menu button rather than a text button. A
   tighter gap on the narrowest screens is enough; the logo keeps its size. */
@media (max-width: 359px) {
  .site-header--legal .site-header__inner { gap: var(--sp-md); }
}


/* --- Navigation -----------------------------------------------------------

   ONE tree, not the export's two. The export rendered a desktop bar and a
   mobile overlay as separate `sc-if` branches that differ by exactly one link
   and one button treatment. This is the single list, presented two ways.
   -------------------------------------------------------------------------- */

.site-nav {
  display: flex;
  align-items: center;
  gap: clamp(16px, 2.5vw, 32px);
}

.site-nav__list {
  display: flex;
  align-items: center;
  gap: clamp(16px, 2.5vw, 32px);
  list-style: none;
  padding: 0;
}

.site-nav__list a {
  font-size: 15px;
  color: var(--forest);
  text-decoration: none;
  /* Measured at 861px, the first pixel the desktop bar exists at: without
     this three of the four labels break onto a second line and the header
     grows to 88px. The whole bar fits on one line at 861 with room to spare
     once they cannot wrap. Reset for the stacked menu below, where a label
     that cannot wrap is the opposite problem. */
  white-space: nowrap;
}

.site-nav__list a:hover { color: var(--teal); }

/* The one link the desktop bar drops. */
.site-nav__item--menu-only { display: none; }

.nav-close { display: none; }

.nav-toggle {
  display: none;
  width: 48px;
  height: 48px;
  margin-right: -12px;
  flex-direction: column;
  justify-content: center;
  gap: var(--sp-xs);
  padding: 0 var(--sp-md);
  background: transparent;
  border: none;
  cursor: pointer;
}

.nav-toggle__bar {
  height: 2px;
  background: var(--forest);
  display: block;
}


@media (max-width: 860px) {

  /* Both menu shapes below are stacked, so a long label wraps as it should. */
  .site-nav__list a { white-space: normal; }

  /* --- The overlay menu, JavaScript on ------------------------------------

     BLD-04 CONTRACT, and it is deliberately not a second class. The nav is
     the menu button's immediate next sibling, so aria-expanded drives the
     styling directly. main.js has to set that attribute anyway; this way
     there is one source of truth for whether the menu is open, and no way
     for a class and an ARIA state to disagree.
     ---------------------------------------------------------------------- */

  html.js .nav-toggle { display: flex; }

  html.js .site-nav {
    position: fixed;
    inset: 0;
    z-index: 50;
    display: none;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: var(--sp-xl) clamp(24px, 5vw, 32px) 40px;
    background: var(--cream-100);
    overflow-y: auto;
  }

  html.js .nav-toggle[aria-expanded="true"] + .site-nav { display: flex; }

  html.js .nav-close {
    display: block;
    align-self: flex-end;
    width: 48px;
    height: 48px;
    font-size: 26px;
    line-height: 1;
    color: var(--forest);
    background: transparent;
    border: none;
    cursor: pointer;
  }

  html.js .site-nav__list {
    flex-direction: column;
    align-items: stretch;
    gap: var(--sp-xs);
    margin-top: var(--sp-xxl);
  }

  html.js .site-nav__list a {
    display: block;
    font-size: 22px;
    font-weight: var(--fw-medium);
    padding: var(--sp-lg) 0;
    border-bottom: var(--border-width) solid var(--cream-300);
  }

  html.js .site-nav__item--menu-only { display: block; }

  /* The export's mobile menu CTA is a full-width primary at size lg, where
     the desktop bar's is an outline at md. The markup carries one set of
     classes for both, so the variant change is made here rather than by
     giving main.js a second job. These four declarations are btn--primary
     and btn--lg from components.css, and nothing else. */
  html.js .site-nav__cta {
    width: 100%;
    margin-top: var(--sp-xxl);
    padding: 16px 32px;
    font-size: var(--fs-lead);
    background: var(--coral);
    color: var(--white);
    border-color: var(--coral);
  }

  html.js .site-nav__cta:hover {
    background: var(--coral-600);
    border-color: var(--coral-600);
  }

  /* --- The stacked menu, JavaScript off -----------------------------------

     No button, so the links have to be readable where they are. The header
     stops being sticky here because the full list plus the wordmark is most
     of a phone viewport, and a sticky element that tall leaves nothing to
     read. The CTA keeps its outline treatment: coral is the single action
     colour and the register button further down the page is already using
     it, so a second coral button in a header that is now part of the
     document would be two.
     ---------------------------------------------------------------------- */

  html:not(.js) .site-header { position: static; }

  html:not(.js) .site-header__inner { flex-wrap: wrap; }

  html:not(.js) .site-nav {
    width: 100%;
    flex-direction: column;
    align-items: flex-start;
    gap: var(--sp-md);
  }

  html:not(.js) .site-nav__list {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--sp-sm);
  }

  html:not(.js) .site-nav__item--menu-only { display: list-item; }
}


/* --- Decks, the parts both share ------------------------------------------
   What each deck's slides look like is page.css. This is the frame and the
   controls, which are identical between the two bar the arrow size.
   -------------------------------------------------------------------------- */

/* BLD-04 CONTRACT, added 2026-09-17 and the one hook BLD-03 did not leave.

   Both decks can be swiped on a touch screen, and without this they cannot be.
   Measured, not reasoned about: under the default `touch-action: auto` Chrome
   hands the whole gesture to the compositor the moment a finger moves, and the
   deck receives pointerdown, pointermove, then POINTERCANCEL. No pointerup
   ever arrives, so there is nothing for a swipe handler to act on.

   `pan-y` gives the vertical axis back to the scroller, which is what reading
   a 536px card needs, and keeps the horizontal axis for main.js. `pinch-zoom`
   is kept explicitly: `pan-y` alone would disable pinch to zoom on the cards,
   which is a real cost to a low-vision visitor for no gain, since there is
   nothing to pan horizontally on this page anyway.

   The export had `touch-action: pan-y` on its testimonial deck for the same
   reason and it was not carried across, on the belief that the 3D fan which
   made dragging visible was gone. It was not gone, it was missing; it came
   back on 2026-09-18, and this declaration is now doing the job it was
   written for rather than a job that happened to still need doing. */
.deck {
  width: 100%;
  touch-action: pan-y pinch-zoom;
}

.deck-controls {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-xl);
}

.deck-controls__arrow {
  background: none;
  border: none;
  padding: 0;
  cursor: pointer;
  line-height: 0;
}

/* THE TRANSFORM LIVES ON THE IMG, NEVER ON THE BUTTON. A transform of any
   value but `none` makes its element a stacking context, and that isolates
   the mix-blend-mode below from the page behind it: the chalk stops
   multiplying into the cream and flattens to solid pink. Scaling the button
   therefore changed how the arrow LOOKS, not just how big it is, and moving
   on and off it popped between two renderings. Measured 2026-09-18.

   On the img the blend still takes the page as its backdrop, because an
   element's own stacking context does not isolate it from the backdrop it
   blends with - only an ancestor's does. It also pins the button's box: the
   hit area never grows, so :hover cannot chatter at the edge mid-transition.
   The same applied to the press below, which had the pop before any of this
   and only showed it for the length of a click. */
.deck-controls__arrow img {
  width: 64px;
  height: 64px;
  display: block;
  mix-blend-mode: multiply;
  transition: transform var(--dur-fast) var(--ease-standard);
}

/* The arrow transitions transform and had only a press state to spend it on.
   Scale rather than a directional nudge, so that this and :active are both
   (0,2,1) and :active staying below keeps the press winning over the hover.
   --dur-fast is already 0ms under prefers-reduced-motion, so this settles
   instantly there rather than animating.

   Mouse only, and this is the rule that most needed it: a latched :hover
   after a tap would leave the arrow sitting 8% large until the next tap,
   and the deck is exactly where a touch visitor taps repeatedly. */
@media (hover: hover) and (pointer: fine) {
  .deck-controls__arrow:hover img { transform: scale(1.08); }
}

.deck-controls__arrow:active img { transform: scale(var(--press-scale)); }

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

.dot {
  width: 9px;
  height: 9px;
  padding: 0;
  border: none;
  border-radius: var(--radius-avatar);
  background: var(--cream-300);
  cursor: pointer;
  transition: background var(--dur-base) var(--ease-standard);
}

/* 9px is a small target to pick out by eye alone. Above the current-dot rules
   on purpose: `.dot:hover` and `.dot.is-current` are both (0,2,0), so the dot
   you are already on stays teal rather than dimming under the pointer, and
   wrapping it in a media query changes neither of those. Mouse only, like the
   arrows above: finding a 9px target by eye is not a touch problem. */
@media (hover: hover) and (pointer: fine) {
  .dot:hover { background: var(--ink-300); }
}

/* BLD-04 CONTRACT. The export computed the background from the active index;
   main.js moves `is-current` and sets aria-current instead. Both are named
   so the styling and the announcement cannot drift apart. */
.dot.is-current,
.dot[aria-current="true"] { background: var(--teal); }


/* --- The register modal ---------------------------------------------------

   A native <dialog>. Most of what the export hand-rolled out of divs is the
   element's own behaviour, measured at BLD-02b: focus enters on open, the
   background goes inert, Tab wraps, Escape closes, focus returns to the
   trigger. What is left for CSS is the reset (a dialog has a border, padding
   and a canvas background by default), the scrim, and making the box fill the
   viewport so that a click anywhere outside the panel is a click on the
   dialog itself, which is what BLD-04's one handler tests for.
   -------------------------------------------------------------------------- */

.modal {
  inset: 0;
  width: 100%;
  max-width: 100%;
  height: 100%;
  max-height: 100%;
  margin: 0;
  padding: clamp(16px, 4vw, 48px);
  border: none;
  background: transparent;
  overflow: auto;
  overscroll-behavior: contain;
}

.modal[open] {
  display: flex;
  align-items: flex-start;
  justify-content: center;
}

.modal::backdrop {
  background: rgba(27, 46, 36, .42);
  -webkit-backdrop-filter: blur(3px);
  backdrop-filter: blur(3px);
}

.modal__panel {
  position: relative;
  width: 100%;
  max-width: 680px;
  background: var(--white);
  border: var(--border-width) solid var(--cream-300);
  border-radius: var(--radius-card);
  box-shadow: 0 24px 64px rgba(27, 46, 36, .28);
  padding: clamp(24px, 4vw, 40px);
  display: flex;
  flex-direction: column;
  gap: var(--sp-lg);
}

.modal__close {
  position: absolute;
  top: var(--sp-md);
  right: var(--sp-md);
  width: 36px;
  height: 36px;
  border: none;
  border-radius: var(--radius-control);
  background: transparent;
  color: var(--forest);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-standard);
}

.modal__close:hover { background: var(--cream-100); }

.modal__intro {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
  /* Clears the close button. */
  padding-right: var(--sp-xxl);
}


/* --- Cookie banner --------------------------------------------------------
   Markup only until CNS-02. It ships with the hidden attribute, which
   base.css makes absolute, so none of this paints until that task removes it.
   -------------------------------------------------------------------------- */

.cookie-banner {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 55;
  background: var(--white);
  border-top: var(--border-width) solid var(--cream-300);
  box-shadow: 0 -12px 32px rgba(27, 46, 36, .12);
}

.cookie-banner__inner {
  padding-block: clamp(20px, 3vw, 24px);
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-lg) var(--sp-xxl);
  align-items: center;
  justify-content: space-between;
}

.cookie-banner__copy {
  flex: 1 1 320px;
  min-width: 0;
  font-size: 15px;
  line-height: var(--lh-body);
  color: var(--ink-700);
}

.cookie-banner__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-md);
  align-items: center;
}


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

.site-footer { background: var(--forest-900); }

.site-footer__inner {
  padding-block: clamp(32px, 5vw, 48px) clamp(16px, 2vw, 24px);
  display: flex;
  flex-wrap: wrap;
  gap: clamp(24px, 4vw, 48px);
  justify-content: space-between;
  align-items: center;
}

.site-footer__contact {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xl);
  align-items: flex-start;
}

.site-footer__address {
  display: flex;
  flex-direction: column;
  gap: var(--sp-xs);
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--cream-100);
}

.site-footer__name { font-weight: var(--fw-semibold); }

/* Plain text in the export; a mailto here, because a footer address that
   cannot be tapped on a phone is a worse footer. */
.site-footer__email {
  margin-top: var(--sp-md);
  color: var(--cream-100);
  text-decoration: none;
}

.site-footer__email:hover { color: var(--sage); }

.site-footer__legal {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 20px;
  font-size: 15px;
}

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

.site-footer__legal a:hover,
.site-footer__legal a[aria-current="page"] { color: var(--sage); }

/* CNS-02b. The footer control that re-opens the cookie banner, on all four
   pages. It sits in the legal nav beside the two links and is styled to match
   them exactly, because it is the same kind of thing to a reader: a button,
   not a link, because it changes state on this page rather than navigating.

   It is hidden without JavaScript for the same reason .consent-panel__reopen
   is, twelve rules up: with JavaScript off the banner cannot be shown and
   nothing optional can run in the first place, so the control would be a
   promise the page cannot keep. */
.site-footer__consent {
  appearance: none;
  -webkit-appearance: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 15px;
  color: var(--ink-300);
  cursor: pointer;
  text-align: left;
}

.site-footer__consent:hover { color: var(--sage); }

html:not(.js) .site-footer__consent { display: none; }

.site-footer__social {
  display: flex;
  flex-direction: column;
  gap: var(--sp-sm);
  align-items: flex-end;
  text-align: right;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--cream-100);
}

.site-footer__social-heading { font-weight: var(--fw-semibold); }

.site-footer__social a {
  display: flex;
  align-items: center;
  gap: var(--sp-sm);
  color: var(--cream-100);
  text-decoration: none;
}

.site-footer__social a:hover { color: var(--sage); }

.site-footer__social .icon { color: var(--white); }

.site-footer__wordmark img {
  display: block;
  width: 100%;
  height: auto;
}
