/* ==========================================================================
   TopDog Daycare: base and components
   Every color, font and radius comes from tokens.css. Do not hardcode here.
   ========================================================================== */

/* --- Reset ---------------------------------------------------------------- */

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

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

body {
  margin: 0;
  background: var(--c-bg);
  color: var(--c-text);
  font-family: var(--f-body);
  font-size: var(--t-base);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
}

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

h1, h2, h3, h4 {
  font-family: var(--f-heading);
  font-weight: var(--fw-bold);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight);
  margin: 0 0 var(--s-4);
  text-wrap: balance;
}

/* Display sizes get tighter tracking than headings do. At 4rem the default spacing
   reads slack; at 1.25rem the same value would read cramped. */
h1 { font-size: var(--t-3xl); letter-spacing: var(--ls-display); }
h2 { font-size: var(--t-2xl); letter-spacing: var(--ls-display); }
h3 { font-size: var(--t-xl); }
h4 { font-size: var(--t-lg); letter-spacing: 0; }

p, ul, ol, dl { margin: 0 0 var(--s-4); }
p { max-width: var(--measure); }

a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: 0.2em; }
a:hover { text-decoration-thickness: 2px; }

:focus-visible {
  outline: 3px solid var(--c-focus);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}
/* Dark Wine reads on white and on Pale Oak but sinks into the deep teal. The ring
   swaps to the warm colour on dark surfaces rather than compromising on one value.
   Kept from the yellow and blue experiment because it is an improvement either way. */
.section--deep :focus-visible,
.footer :focus-visible,
.infobar :focus-visible { outline-color: var(--c-warm); }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

/* --- Layout primitives ---------------------------------------------------- */

/* Two container widths and only two, so the choice is deliberate. Both run at 90%
   with the pixel value as a ceiling, which lets narrow viewports scale on their own
   without another breakpoint. Use .wrap by default and .wrap--wide for the few
   sections that genuinely need the room. */
/* The 90% IS the gutter. Keeping padding-inline as well would double-count it and
   cost mobile about 38px of content width, so --wrap-pad is no longer applied here. */
.wrap {
  width: 90%;
  max-width: var(--wrap);
  margin-inline: auto;
}

.wrap--wide { max-width: var(--wrap-wide); }

/* A centred block of copy. --measure still caps the line length, so the paragraph
   stays readable rather than stretching the full container. */
.block-center { text-align: center; }
.block-center p { max-width: var(--measure); margin-inline: auto; }
.block-center .btn-row { justify-content: center; }

.section { padding-block: var(--section-y); }
.section--alt { background: var(--c-bg-alt); }
/* A tinted band directly beneath the tinted hero renders as one dead slab: the pricing
   page opened on roughly 700px of a single value with nothing but text in it. Two equal
   values in a row are never a section boundary, so the first band after a hero always
   falls back to white and the hero keeps its own edge. */
.hero + .section--alt { background: var(--c-bg); }
.section--deep { background: var(--c-bg-deep); color: var(--c-text-invert); }
.section--deep a { color: var(--c-text-invert); }

/* Pale Oak. The single warm surface on an otherwise cool page, so it is used once
   and never twice in a row. Cards sitting on it go white to keep their edge. */
.section--warm { background: var(--c-warm); color: var(--c-warm-body); }
.section--warm h2, .section--warm h3 { color: var(--c-warm-text); }
.section--warm .section__head p { color: var(--c-warm-body); }
.section--warm .eyebrow { color: var(--c-cta); }
.section--warm .quote p { color: var(--c-warm-body); }
.section--warm .quote figcaption { color: color-mix(in srgb, var(--c-warm-text) 66%, transparent); }
.section--warm .card { background: var(--c-bg); }

/* On the deep teal, Pale Oak is the label colour. Plain white washes out against
   that green and the accent teal is far too close to the background to read. */
.section--deep .eyebrow,
.footer__heading { color: var(--c-warm); }
.section--deep .eyebrow { opacity: 1; }
.section--tight { padding-block: calc(var(--section-y) * 0.6); }
/* A band, not a section. Sits immediately under the hero carrying four numbers, so it
   wants the vertical space of a masthead rule rather than of a chapter. */
.section--band { padding-block: var(--s-6); }

/* Photographs in the body of the page get matted the same way, a hairline ring
   inside the radius plus a soft shadow, so they read as prints rather than as
   images dropped into a div. */
.split img { box-shadow: var(--ring-inset), var(--shadow-md); }

.section__head { max-width: var(--measure); margin-bottom: var(--s-7); }
.section__head p { color: var(--c-text-soft); font-size: var(--t-lg); }
.section--deep .section__head p { color: var(--c-text-invert); opacity: 0.82; }

/* --- Section lead: head plus panel ---------------------------------------- */

/* .section__head alone is capped at a reading measure, which is right for the words and
   wrong for the row: on a wide screen it leaves the whole right-hand half empty above a
   full-width set of steps. Pairing it with a panel fills the row without centring the
   copy, so the section keeps its left-aligned axis. */
.section__lead { display: grid; gap: var(--s-6); margin-bottom: var(--s-8); }
@media (min-width: 60rem) {
  .section__lead { grid-template-columns: 1.3fr 1fr; gap: var(--s-8); align-items: start; }
}
.section__lead > div > p { color: var(--c-text-soft); font-size: var(--t-lg); max-width: var(--measure); }

/* Neither ruled nor boxed. The kicker alone says "aside", and with no rule the panel
   starts at the top of the row, so its label sits on the same line as the section
   eyebrow beside it. */
.lead-card__kicker {
  display: block;
  font-size: var(--t-xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-accent);
  margin-bottom: var(--s-5);
}
.lead-card__list { display: grid; gap: var(--s-4); margin: 0; }
.lead-card__list > div {
  display: grid;
  grid-template-columns: 7rem 1fr;
  gap: var(--s-4);
  align-items: baseline;
}
.lead-card__list dt {
  font-size: var(--t-xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-text-soft);
}
.lead-card__list dd { margin: 0; font-family: var(--f-heading); font-size: var(--t-lg); line-height: var(--lh-snug); }

/* The panel was deliberately "neither ruled nor boxed", which read fine when every section
   was white. Once the page gained photographs and tinted bands, four bare facts stopped
   holding their own, so it carries the deep surface and becomes the focal point of the
   section that actually converts. */
.lead-card--panel {
  background: var(--c-bg-deep);
  color: var(--c-text-invert);
  border-radius: var(--r-lg);
  padding: var(--s-6);
  box-shadow: var(--ring-inset), var(--shadow-md);
}
/* Pale Oak, not Deep Ocean: the accent is chosen for white surfaces and vanishes here. */
.lead-card--panel .lead-card__kicker { color: var(--c-warm); }
.lead-card--panel .lead-card__list dt { color: var(--c-text-invert); opacity: 0.7; }
.lead-card--panel .lead-card__list dd { color: var(--c-text-invert); }

/* The panel was deliberately "neither ruled nor boxed", which read fine when every section
   was white. Once the page around it gained photographs and tinted bands, a bare list of
   four facts stopped holding its own in a half-empty column. It now carries the deep
   surface, which also makes it the one focal point in the section that converts.
   A portrait photo sits under it because that column had roughly 200px of dead space
   below the last row, and because every hero source in this set is portrait. */
/* A full-width band rather than a photo stacked in the right column. Stacking it there put
   a portrait image under the panel and left that column about 310px taller than the text
   beside it, which read as a hole rather than as a layout. Across the full width it also
   gives the section a beat between the invitation and the three steps. */
.section-band {
  margin: var(--s-8) 0 0;
  border-radius: var(--r-lg);
  overflow: hidden;
  box-shadow: var(--ring-inset), var(--shadow-sm);
}
.section-band img {
  display: block;
  width: 100%;
  /* 5:2, not the source's own 16:9. Across the 1180px wrap 16:9 renders 663px tall, which
     stops being a beat between two blocks and becomes a slab that pushes the three steps
     off the screen. 5:2 keeps roughly 70% of the frame height, and the dogs sit through
     the middle of it. */
  aspect-ratio: 5 / 2;
  object-fit: cover;
  /* Same steering hook the heroes use. A 5:2 band keeps only about half the height of a 4:3
     source, so which half it keeps decides whether you get the dogs or their feet. Set it
     inline on the figure, e.g. style="--band-focus: 50% 22%". */
  object-position: var(--band-focus, 50% 50%);
}
/* Shorter on a phone: 16:9 across a 375px screen is a 210px sliver of a room full of dogs,
   and the subject survives a squarer crop better than a letterbox. */
@media (max-width: 47.9375rem) {
  .section-band img { aspect-ratio: 3 / 2; }
}

/* A plain section label. It carried a short trailing rule; across fourteen pages and
   forty-one uses that stopped reading as a masthead marker and became a tic, so the
   rule is gone and the label carries the section on its own. */
.eyebrow {
  display: block;
  font-family: var(--f-body);
  font-size: var(--t-xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  /* Deep Ocean. This is the accent's main job on light surfaces and the reason the
     palette reads teal-forward rather than teal-only-in-the-footer. 5.18:1 on white,
     4.65:1 on Platinum. */
  color: var(--c-accent);
  margin-bottom: var(--s-4);
}
/* The deep-section eyebrow is set to the warm colour further up. The old invert-with-opacity
   rule lived here and, being equal specificity and later in the file, silently won. */

.grid { display: grid; gap: var(--s-5); }
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); }
.grid--4 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr)); }

.split {
  display: grid;
  gap: clamp(var(--s-5), 4vw, var(--s-8));
  align-items: center;
  grid-template-columns: 1fr;
}
@media (min-width: 60rem) {
  .split { grid-template-columns: 1fr 1fr; }
  .split--media-first > :first-child { order: 2; }
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--c-accent);
  color: var(--c-accent-text);
  padding: var(--s-3) var(--s-4);
  z-index: 100;
}
.skip-link:focus { left: 0; }

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

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-2);
  /* Taller than the default, with the label set small and tracked out. A button
     reads expensive when the box is generous and the type inside is quiet. Height
     carries that, not width: uppercase plus tracking already widens a long label,
     and at --s-6 the two hero buttons no longer fit on one line. */
  padding: var(--s-4) var(--s-5);
  border: 1px solid transparent;
  border-radius: var(--r-md);
  font-family: var(--f-body);
  font-size: var(--t-xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease), box-shadow 320ms var(--ease),
              transform 320ms var(--ease);
}
.btn--primary { box-shadow: var(--shadow-xs); }
.btn--primary:hover { box-shadow: var(--shadow-md); transform: translateY(-1px); }
@media (prefers-reduced-motion: reduce) {
  .btn--primary:hover { transform: none; }
}

.btn--primary { background: var(--c-cta); color: var(--c-cta-text); }
.btn--primary:hover { background: var(--c-cta-hover); }

.btn--ghost { background: transparent; color: inherit; border-color: currentColor; }
.btn--ghost:hover { background: var(--c-text); color: var(--c-bg); }

.section--deep .btn--ghost:hover,
.hero--bleed .btn--ghost:hover { background: var(--c-text-invert); color: var(--c-bg-deep); }

.btn-row { display: flex; flex-wrap: wrap; gap: var(--s-3); margin-top: var(--s-5); }

/* --- Info bar ------------------------------------------------------------- */

.infobar {
  background: var(--c-bg-alt);
  border-bottom: 1px solid var(--c-line);
  font-size: var(--t-xs);
  color: var(--c-text-soft);
}
.infobar__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2) var(--s-5);
  padding-block: var(--s-2);
}
.infobar__link { display: inline-flex; text-decoration: none; }
.infobar__link:hover .infobar__value { text-decoration: underline; }
.infobar__value { font-weight: var(--fw-bold); color: var(--c-text); }
/* The review ask sits left, the three ways to reach them sit right. Grouping the
   contacts means one auto margin does the split, so adding or removing a contact never
   breaks the alignment the way a per-item auto margin would. */
.infobar__contact {
  margin-left: auto;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2) var(--s-5);
}
/* The address links to directions, but it stays the quietest thing in the bar.
   Keep it at the soft weight rather than inheriting the bold link value style. */
.infobar__note .infobar__value { font-weight: var(--fw-normal); color: inherit; }

@media (max-width: 47.9375rem) {
  /* Narrowest screens keep the review ask and the two tappable contacts. The address is
     the one item that is a nice-to-have rather than an action. */
  .infobar__note { display: none; }
  .infobar__inner { gap: var(--s-1) var(--s-4); }
  /* Once the bar wraps to two lines the auto margin is actively wrong: it right-aligns
     the contacts under a left-aligned review link, so the two rows read as a mistake.
     Drop it and both rows share the same left edge. */
  .infobar__contact { gap: var(--s-1) var(--s-4); margin-left: 0; }
}

/* --- Review link ----------------------------------------------------------
   Five outlined stars that sweep to filled on hover or keyboard focus. The
   label stays visible at all times: stars alone read as a rating display, not
   as a control, and we are asking for a review rather than reporting one.
   -------------------------------------------------------------------------- */

.review-link {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  text-decoration: none;
}

.review-link--stack {
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s-1);
}

/* Star colour follows the surface the star actually sits on. Gold measures 1.07:1 on
   full Pale Oak and 1.51:1 on the Oak Wash band, so every warm surface re-points this
   to Dark Wine, and .card re-points it back because cards stay white on warm and alt
   sections where gold is correct again.

   This replaced a hardcoded `.section--warm .stars--filled` rule. That rule was right
   about today's markup (the home page ratings sit directly on Pale Oak, not in cards)
   but it only covered one of the three warm surfaces. Once --c-bg-alt became a warm
   wash, the infobar and every alt band needed the same treatment, and a per-section
   selector would have needed extending each time. Inheriting a variable means a new
   warm surface declares itself once and both the filled and hover states follow. */
:root { --c-star-on-surface: var(--c-star); }
.section--warm,
.section--alt,
.infobar { --c-star-on-surface: var(--c-star-on-warm); }
.card { --c-star-on-surface: var(--c-star); }

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

.stars svg {
  width: 0.95em;
  height: 0.95em;
  overflow: visible;
}

.stars path {
  fill: transparent;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linejoin: round;
  transition: fill var(--dur) var(--ease), stroke var(--dur) var(--ease);
}

/* Sweep left to right rather than all five at once. */
.stars svg:nth-child(1) path { transition-delay: 0ms; }
.stars svg:nth-child(2) path { transition-delay: 40ms; }
.stars svg:nth-child(3) path { transition-delay: 80ms; }
.stars svg:nth-child(4) path { transition-delay: 120ms; }
.stars svg:nth-child(5) path { transition-delay: 160ms; }

.review-link:hover .stars path,
.review-link:focus-visible .stars path {
  fill: var(--c-star-on-surface);
  stroke: var(--c-star-on-surface);
}

/* Reporting a rating we already have, rather than asking for one. No hover
   behaviour and no sweep: these are filled from the start and are not a control. */
.stars--filled path { fill: var(--c-star-on-surface); stroke: var(--c-star-on-surface); transition: none; }

.review-link:hover .review-link__label { text-decoration: underline; }

.review-link__label {
  font-weight: var(--fw-bold);
  color: var(--c-text);
  white-space: nowrap;
}

.infobar .review-link__label { text-transform: uppercase; letter-spacing: var(--ls-wide); }

.footer .review-link__label { color: var(--c-text-invert); }
.footer__review { margin-top: var(--s-4); }
.footer__review-ask { font-size: var(--t-sm); opacity: 0.75; margin-bottom: var(--s-2); }

/* The sweep is decoration. Honour a reduced-motion preference by landing on the
   filled state immediately rather than staggering it. */
@media (prefers-reduced-motion: reduce) {
  .stars svg path { transition-delay: 0ms !important; }
}

/* --- Header and nav ------------------------------------------------------- */

.header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--c-bg);
  border-bottom: 1px solid var(--c-line);
}

.header__inner {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  padding-block: var(--s-3);
}

.brand { display: flex; align-items: center; gap: var(--s-3); text-decoration: none; margin-right: auto; }
.brand__mark { width: 56px; height: 56px; border-radius: var(--r-sm); }
.brand__text { display: flex; flex-direction: column; }
.brand__name { font-family: var(--f-heading); font-size: var(--t-lg); font-weight: var(--fw-bold); line-height: 1.1; white-space: nowrap; }
.brand__tag { font-size: var(--t-xs); color: var(--c-text-soft); }

/* Nav, CTA and logo all share one row. Grouping the three service pages under a
   Services dropdown is what keeps that row from wrapping. */
.nav { margin-left: auto; }

.nav__list {
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--s-5);
  margin: 0;
  padding: 0;
  font-size: var(--t-sm);
}

.nav__item { position: relative; }

/* A group is a link plus a caret button, sitting together as one unit. */
.nav__item--group { display: flex; align-items: center; gap: 2px; }

.nav__group-toggle {
  padding: var(--s-2) 2px;
  border: 0;
  background: none;
  color: inherit;
  line-height: 1;
  cursor: pointer;
}

/* The group toggle is a button but has to sit flush with the sibling links. */
.nav__item > a,
.nav__group-link,
.nav__group-toggle {
  display: inline-flex;
  align-items: center;
  white-space: nowrap;
  gap: var(--s-2);
  padding: var(--s-2) 0;
  margin: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  font-size: var(--t-sm);
  line-height: 1.4;
  text-decoration: none;
  cursor: pointer;
}

.nav__item > a:hover,
.nav__group-link:hover { text-decoration: underline; }
.nav__group-toggle:hover { text-decoration: none; }

.nav__item > a[aria-current="page"],
.nav__group-link[data-section-current="true"] {
  font-weight: var(--fw-bold);
  text-decoration: underline;
}

.nav__caret {
  width: 0;
  height: 0;
  border-left: 4px solid transparent;
  border-right: 4px solid transparent;
  border-top: 5px solid currentColor;
  transition: transform var(--dur) var(--ease);
}
.nav__group-toggle[aria-expanded="true"] .nav__caret { transform: rotate(180deg); }

.nav__menu { list-style: none; margin: 0; padding: 0; }
.nav__menu a { text-decoration: none; display: block; }

/* --- Dropdown, desktop only ------------------------------------------------ */

/* Same handover as the nav itself: there is no dropdown to open while the nav is
   display:none. See the NAV HANDOVER note below. */
@media (min-width: 71.01rem) {
  .nav__menu {
    display: none;
    position: absolute;
    top: 100%;
    left: calc(var(--s-3) * -1);
    min-width: 14rem;
    padding: var(--s-2);
    background: var(--c-bg);
    border: 1px solid var(--c-line);
    border-radius: var(--r-md);
    box-shadow: var(--shadow-md);
    z-index: 60;
  }

  /* Open state has exactly one source of truth: the .is-open class, set by JS
     for hover, click and keyboard focus alike. Doing hover in CSS instead would
     let the menu be visibly open while aria-expanded still said false. */
  .nav__item--group.is-open > .nav__menu { display: block; }

  .nav__menu a {
    padding: var(--s-2) var(--s-3);
    border-radius: var(--r-sm);
    white-space: nowrap;
  }
  .nav__menu a:hover { background: var(--c-bg-alt); text-decoration: none; }
  .nav__menu a[aria-current="page"] { font-weight: var(--fw-bold); }

  /* The landing-page link reads as the parent of the list below it. */
  .nav__menu-overview {
    font-weight: var(--fw-bold);
    border-bottom: 1px solid var(--c-line-soft);
    border-radius: var(--r-sm) var(--r-sm) 0 0;
    margin-bottom: var(--s-1);
  }
}

.header__cta { white-space: nowrap; }

/* --- Mobile nav drawer -----------------------------------------------------
   Pattern: patterns/mobile-nav-drawer/. Adapted values: breakpoint 1024px to
   match the desktop bar, 82% / 380px drawer, and this site's own colour tokens.
   TopDog has 7 destinations, so per that pattern's own guidance the accordions
   and sticky section headers are omitted and every link renders flat.
   -------------------------------------------------------------------------- */

.nav__burger {
  display: none;
  align-items: center;
  justify-content: center;
  width: 44px;                 /* keep at 44px, it is the tap target */
  height: 44px;
  margin-left: var(--s-3);
  /* This used to be -10px, pulling the icon out into .wrap's padding so it sat flush
     with the text edge. .wrap has no padding any more, so a negative margin here hangs
     the tap target outside the container instead. */
  margin-right: 0;
  padding: 0;
  border: 0;
  background: transparent;
  border-radius: 10px;
  color: var(--c-text);
  flex-shrink: 0;
  cursor: pointer;
}

.nav__burger-bars,
.nav__burger-bars::before,
.nav__burger-bars::after {
  display: block;
  width: 22px;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
  transition: transform 180ms ease, opacity 120ms ease;
}
.nav__burger-bars { position: relative; }
.nav__burger-bars::before,
.nav__burger-bars::after { content: ''; position: absolute; left: 0; }
.nav__burger-bars::before { top: -7px; }
.nav__burger-bars::after { top: 7px; }

.nav__burger--open .nav__burger-bars { background: transparent; }
.nav__burger--open .nav__burger-bars::before { transform: translateY(7px) rotate(45deg); }
.nav__burger--open .nav__burger-bars::after { transform: translateY(-7px) rotate(-45deg); }

/* Dimmed, blurred page behind the drawer. Tapping it closes the menu, which
   makes the exit the largest target on screen. */
.m-nav-scrim {
  position: fixed;
  inset: 0;
  /* Above the header (50), unlike before. The drawer is now a full-height overlay with its
     own close button, so the header does not need to stay reachable through the scrim, and
     covering it fully avoids the half-dimmed top edge the old geometry produced. */
  z-index: 51;
  background: rgba(25, 25, 23, 0.34);
  -webkit-backdrop-filter: blur(7px);
  backdrop-filter: blur(7px);
  opacity: 0;
  visibility: hidden;
  transition: opacity 220ms ease, visibility 0s linear 220ms;
}
.m-nav-scrim--on {
  opacity: 1;
  visibility: visible;
  transition: opacity 220ms ease, visibility 0s;
}

.m-nav {
  position: fixed;
  /* Full viewport height. It was anchored to the header's bottom edge, measured in JS, but
     the scroll lock (overflow:hidden on html) breaks position:sticky, so the header dropped
     out from under it mid-open and left the drawer floating over page content with no way
     to close. Anchoring to the viewport cannot fail that way. */
  top: 0;
  right: 0;
  bottom: 0;
  width: 82%;
  max-width: 380px;
  z-index: 52;
  background: var(--c-bg);
  border-left: 1px solid var(--c-line);
  box-shadow: -24px 0 60px -20px rgba(36, 24, 12, 0.28);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior: contain;   /* stops scroll chaining to the page */
  /* visibility, not opacity: opacity:0 leaves every link in the tab order. */
  visibility: hidden;
  transform: translateX(100%);
  transition: transform 260ms cubic-bezier(0.32, 0.72, 0, 1),
              visibility 0s linear 260ms;
}
.m-nav--open {
  visibility: visible;
  transform: translateX(0);
  transition: transform 260ms cubic-bezier(0.32, 0.72, 0, 1), visibility 0s;
}

/* The bar is what sticks, not the button. A sticky 48px button carried only 48px of
   opaque background with it, so links scrolling up the drawer slid visibly past its left
   edge. The bar spans the full width and hides them. */
.m-nav__bar {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  justify-content: flex-end;
  padding: 8px 14px 0;
  background: var(--c-bg);
}

.m-nav__close {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  appearance: none;
  border: 0;
  background: transparent;
  color: var(--c-text);
  font-size: 30px;
  line-height: 1;
  cursor: pointer;
}
.m-nav__close { border-radius: var(--r-md); transition: background-color 140ms var(--ease), color 140ms var(--ease); }
@media (hover: hover) and (pointer: fine) {
  .m-nav__close:hover { background: var(--c-bg-alt); color: var(--c-cta); }
}
.m-nav__close:active { background: var(--c-warm); color: var(--c-warm-text); }
.m-nav__close:focus-visible { outline: 3px solid var(--c-focus); outline-offset: 2px; border-radius: var(--r-md); }

.m-nav__inner { padding: 4px 22px 44px; }
.m-nav__sec + .m-nav__sec { border-top: 1px solid var(--c-line); }

.m-nav__link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  /* The 12px inline padding is paid back by a matching negative margin, so the text sits
     exactly where it did before while the highlight chip can bleed into the drawer's own
     22px gutter. Nothing shifts when a state paints.

     No width: 100% here. It used to be set, and it pinned the box to the parent's content
     width, so the negative margins only shifted the chip 12px left instead of widening it
     by 24px: the highlight overhung on the left and stopped short on the right. A
     block-level flex container already fills its containing block, and letting the width
     stay auto is what lets both margins pull outward. */
  padding: 14px 12px;
  margin-inline: -12px;
  border-radius: var(--r-md);
  font-size: 16px;
  font-weight: var(--fw-medium);
  color: var(--c-text);
  text-decoration: none;
  transition: background-color 140ms var(--ease), color 140ms var(--ease);
}
.m-nav__link[aria-current="page"] { font-weight: var(--fw-bold); }

/* Pulled tight under its parent link and indented. The sub-items sat 10px below the
   "Services" link at the same left edge and one point smaller, which read as five
   siblings rather than a parent and its four children. The negative top margin closes
   the gap the parent link's own bottom padding opened, and the rule plus indent make the
   nesting legible without adding a heading nobody asked for. */
.m-nav__group {
  margin-top: -8px;
  padding: 0 0 14px 14px;
  border-left: 2px solid var(--c-line);
  margin-left: 2px;
}
.m-nav__group ul { list-style: none; margin: 0; padding: 0; }
.m-nav__group li a {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  padding: 9px 12px;
  margin-inline: -12px;
  border-radius: var(--r-md);
  font-size: 15px;
  line-height: 1.3;
  color: var(--c-text);
  text-decoration: none;
  transition: background-color 140ms var(--ease), color 140ms var(--ease);
}
.m-nav__group li a[aria-current="page"] { font-weight: var(--fw-bold); }

/* --- Drawer interaction states -------------------------------------------------
   Split by input type deliberately. :hover latches on a touchscreen: tap a link, the
   drawer closes, and the wash is still sitting on it the next time the menu opens. So the
   hover treatment is gated behind a real pointer, which is what tablets with a trackpad
   and small laptops inside the 1136px breakpoint actually have, and touch takes its
   feedback from :active, which releases with the finger. */

@media (hover: hover) and (pointer: fine) {
  .m-nav__link:hover,
  .m-nav__group li a:hover {
    background: var(--c-bg-alt);
    color: var(--c-accent);
  }

  /* The chevron fills the space-between slot that has sat empty since the drawer was
     built. content is set only inside this query, so on touch it is absent rather than
     transparent: a mark that can never show must not reserve width. */
  .m-nav__link::after {
    content: "\203A";
    font-size: 20px;
    line-height: 1;
    color: var(--c-accent);
    opacity: 0;
    transform: translateX(-6px);
    transition: opacity 140ms var(--ease), transform 140ms var(--ease);
  }
  .m-nav__link:hover::after { opacity: 1; transform: translateX(0); }

  /* Parent hover dims its own children so the group reads as one target, and each child
     lights independently on its own hover. */
  .m-nav__sec:hover .m-nav__group li a:not(:hover) { color: var(--c-text-soft); }
}

/* Press. One step firmer than the hover wash, and the only state a touch user ever
   sees, so it has to be legible on its own rather than as a follow-on. */
.m-nav__link:active,
.m-nav__group li a:active {
  background: var(--c-warm);
  color: var(--c-warm-text);
}

/* Keyboard. The chip already draws the shape, so the ring wraps it rather than the bare
   line of text. */
.m-nav__link:focus-visible,
.m-nav__group li a:focus-visible {
  outline: 3px solid var(--c-focus);
  outline-offset: 0;
  border-radius: var(--r-md);
  background: var(--c-bg-alt);
}

@media (prefers-reduced-motion: reduce) {
  .m-nav__link,
  .m-nav__group li a,
  .m-nav__link::after { transition: none; }
  .m-nav__link::after { transform: none; }
}

.m-nav__cta { margin-top: 24px; width: 100%; justify-content: center; }

/* NAV HANDOVER: 71rem / 1136px. Brand, six nav items and the call to action do not fit
   on one row below that, so the drawer takes over.

   This number is load bearing and it appears in FOUR places: here, the dropdown query,
   the drawer-hiding query below it, and MOBILE in main.js. When it was raised from 64rem
   and the other three were left at 64.0625rem, the band from 1025px to 1136px got a
   burger as its only navigation control and a drawer that was still display:none, so
   clicking it locked html and body to overflow:hidden and showed nothing, with no scrim
   to tap out of. Change one, change all four. */
@media (max-width: 71rem) {
  .nav__burger { display: inline-flex; }
  .nav { display: none; }
  .header__cta { display: none; }
  /* Scoped inside the query on purpose: a stranded lock must not be able to
     freeze the desktop page. */
  html.nav-locked, html.nav-locked body { overflow: hidden; }
}

/* Must be the exact complement of the handover above, or the burger and the drawer
   disagree about who is on duty. */
@media (min-width: 71.01rem) {
  .m-nav, .m-nav-scrim { display: none; }
}

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

.hero { padding-block: clamp(3.5rem, 2rem + 7vw, 9rem); background: var(--c-bg-alt); }
.hero__inner { display: grid; gap: clamp(var(--s-6), 5vw, var(--s-9)); align-items: center; }
/* Deliberately off a 50/50. An even split is the template default and reads static;
   giving the words the larger share says which one is the subject. */
@media (min-width: 60rem) { .hero__inner { grid-template-columns: 1.15fr 1fr; } }
.hero__title { margin-bottom: var(--s-5); }
.hero__lede {
  font-size: var(--t-lg);
  color: var(--c-text-soft);
  line-height: var(--lh-body);
  max-width: 46ch;
}
/* The one place on the page that gets the deep shadow. Portrait rather than
   landscape, so it holds the column and the eye goes to it after the headline. */
.hero__media {
  border-radius: var(--r-xl);
  overflow: hidden;
  box-shadow: var(--ring-inset), var(--shadow-lg);
}
.hero__media img { width: 100%; aspect-ratio: 5 / 4; object-fit: cover; }
@media (min-width: 60rem) { .hero__media img { aspect-ratio: 4 / 5; } }

.hero--page { padding-block: clamp(2.5rem, 2rem + 3vw, 4.5rem); }
.hero--page .hero__inner { grid-template-columns: 1fr; }
.hero--page .hero__lede { max-width: var(--measure); }

/* hero--page collapses the media column to a single track, which is the whole reason
   twelve of the thirteen pages opened on a text-only block while .hero__media sat
   unused. This restores the second column at the shorter page-hero height rather than
   the taller home-page one. Squarer crop than the home page portrait, because inner
   heroes are wider and shorter. */
.hero--split .hero__lede { max-width: 52ch; }
/* --- Full-bleed hero ------------------------------------------------------
   The photo is the surface and the words sit on it, for the home page, the four service
   pages and About. The remaining pages use .hero--split, because a full-bleed band on
   every single page stops being an event and just becomes the template.

   The tint is a scrim element rather than opacity on the image: dimming the photo itself
   makes the type contrast depend on whatever that particular photo happens to be doing
   behind the headline, and these twenty photos range from a dark kennel run to a white
   wash tub. A fixed scrim is predictable across all of them.
   -------------------------------------------------------------------------- */

.hero--bleed {
  position: relative;
  /* Own stacking context, so the negative-z background cannot escape behind the page.
     The parent background paints first, then the z-index:-1 child, then the content, so
     the Dark Teal below is what shows if the photo ever fails to load. */
  isolation: isolate;
  padding-block: clamp(4.5rem, 3rem + 8vw, 9.5rem);
  background: var(--c-bg-deep);
  color: var(--c-text-invert);
  overflow: hidden;
}
.hero--bleed .hero__bg { position: absolute; inset: 0; z-index: -1; }
/* --hero-focus lets a page steer its own crop. The band is far wider than 16:9, so
   cover always throws away top and bottom, and which part it keeps is a per-photo
   judgement rather than something one global value can get right. Set it inline on
   the section, e.g. style="--hero-focus: 50% 72%". */
.hero--bleed .hero__bg img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--hero-focus, 50% 50%);
  display: block;
}
/* Weighted to the left, where the words are, so the right of the photo stays readable as
   a photo. Three stops rather than two: a two-stop gradient bands visibly across a
   1440px band. */
.hero--bleed .hero__bg::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(100deg,
    rgba(11, 26, 30, 0.93) 0%,
    rgba(11, 26, 30, 0.88) 46%,
    rgba(11, 26, 30, 0.42) 100%);
}
/* Single column: there is no media column to balance, the media is the background. */
.hero--bleed .hero__inner { grid-template-columns: 1fr; }
/* Cap the text block so a long headline cannot run out of the dark end of the scrim
   and finish on top of a bright patch of photo. Capping the words is better than
   darkening the whole image, which would waste the photo to fix two words. */
.hero--bleed .hero__inner > div { max-width: 46rem; }
.hero--bleed h1 { color: var(--c-text-invert); }
/* Pale Oak, not Deep Ocean. The accent teal is a 5.18:1 colour picked for WHITE
   surfaces and it disappears against the scrim. */
.hero--bleed .eyebrow { color: var(--c-warm); opacity: 1; }
.hero--bleed .hero__lede { color: var(--c-text-invert); opacity: 0.92; max-width: 54ch; }
.hero--bleed .hero__lede a { color: var(--c-text-invert); }
/* --c-text-soft is a secondary colour picked for white surfaces and measures about
   2:1 against the scrim, so the home page's response-time note would be barely
   readable. Invert it and lean on opacity for the step down instead. */
.hero--bleed .form__note { color: var(--c-text-invert); opacity: 0.78; }
/* On a phone the scrim has to cover the whole band rather than fade out to the side,
   because the words now span the full width and would sit on bare photo. */
@media (max-width: 47.9375rem) {
  .hero--bleed .hero__bg::after {
    background: linear-gradient(180deg,
      rgba(11, 26, 30, 0.86) 0%,
      rgba(11, 26, 30, 0.90) 100%);
  }
}

/* 4:3 at both sizes rather than cropping square on desktop. These are group shots of
   dogs spread across a room, and a centre crop cuts the animals at the edges out of
   the frame, which is the whole subject. */
/* Split heroes get the same --hero-focus hook as the bleed ones. Four of the ten hero
   sources are portrait photographs, so a 4:3 slot crops them hard and which band it
   keeps decides whether you see the subject or the floor. */
.hero--split .hero__media img { aspect-ratio: 4 / 3; object-position: var(--hero-focus, 50% 50%); }
@media (min-width: 60rem) {
  .hero--split .hero__inner { grid-template-columns: 1.15fr 0.85fr; }
}

/* --- Cards ---------------------------------------------------------------- */

/* No outline box. A 1px grey rectangle around every card is the single strongest
   tell of a template: it makes each card a container instead of an object. These
   sit on the page on a shadow, and the hairline that remains is drawn inside the
   radius as a ring so it never reads as a border. */
.card {
  background: var(--c-bg);
  border: 0;
  border-radius: var(--r-lg);
  padding: var(--s-6);
  display: flex;
  flex-direction: column;
  box-shadow: var(--ring-inset), var(--shadow-sm);
}
.section--alt .card { background: var(--c-bg); }
/* Heading level is a document-structure decision, size is a layout one, and inside a
   card the two disagree: on the services page these are the page's main sections so they
   are h2, and a global h2 at --t-2xl is 44px in a 300px card body, which broke every
   title onto two lines. A card title is sized as a card title whatever its level. */
.card h2, .card h3 { font-size: var(--t-xl); letter-spacing: var(--ls-tight); margin-bottom: var(--s-3); }
.card p:last-child, .card ul:last-child { margin-bottom: 0; }
/* The footer link is the card's call to action, so it should look like one. It was
   bold text, which reads as a caption and gives no target to aim at. This is a real
   control: outlined, tracked out, sized like a .btn--ghost. The card is the anchor,
   so this stays a span and the whole card remains the click target. */
.card__foot {
  margin-top: auto;
  padding-top: var(--s-6);
}
.card__foot strong {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: var(--s-3) var(--s-5);
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
  font-family: var(--f-body);
  font-size: var(--t-xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  line-height: 1.2;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
/* An arrow drawn in CSS rather than an icon file. It nudges on hover, which is the
   cheapest way to make a control feel live. */
.card__foot strong::after {
  content: '';
  width: 0.5rem; height: 0.5rem;
  border-top: 1.5px solid currentColor;
  border-right: 1.5px solid currentColor;
  transform: rotate(45deg);
  transition: transform var(--dur) var(--ease);
}
.card--link:hover .card__foot strong {
  background: var(--c-cta);
  border-color: var(--c-cta);
  color: var(--c-cta-text);
}
.card--link:hover .card__foot strong::after { transform: rotate(45deg) translate(2px, -2px); }

/* Two up. .grid--3 auto-fits to four across in a wide container, which is what the
   services row used to do. This is explicit so it stays 2x2 at every desktop width. */
.grid--2up { grid-template-columns: 1fr; gap: var(--s-6); }
@media (min-width: 52rem) { .grid--2up { grid-template-columns: repeat(2, 1fr); } }

/* Three up, counted rather than fitted. .grid--3 auto-fits, and at --wrap its 17rem
   minimum resolves to FOUR tracks, so a set of six lands as four plus two orphans with
   a ragged hole beside them. Where the number of items is known and divides by three,
   the column count has to be stated. */
.grid--3up { grid-template-columns: 1fr; gap: var(--s-6); }
@media (min-width: 52rem) { .grid--3up { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 68rem) { .grid--3up { grid-template-columns: repeat(3, 1fr); gap: var(--s-7) var(--s-6); } }

/* At 2x2 in a 1440 container each card is around 700px wide. A stacked card there
   would carry a 500px tall photo and the row would run off the screen, so the media
   moves alongside the copy instead.

   The card itself carries no padding in this variant. .card__body supplies it, which
   is what lets the photo reach the card edge on both axes without negative margins
   fighting the border radius. */
.card--split { padding: 0; }
.card--split .card__media { margin: 0; border-radius: var(--r-lg) var(--r-lg) 0 0; }
.card--split .card__body { padding: var(--s-6); display: flex; flex-direction: column; flex: 1; }

@media (min-width: 40rem) {
  .card--split { display: grid; grid-template-columns: 0.85fr 1fr; align-items: stretch; }
  .card--split .card__media { border-radius: var(--r-lg) 0 0 var(--r-lg); height: 100%; }
  .card--split .card__media img { height: 100%; aspect-ratio: auto; min-height: 16rem; }
}

.card--link {
  text-decoration: none;
  transition: box-shadow 320ms var(--ease), transform 320ms var(--ease);
}
/* Slower and further than a default hover. A 2px nudge over 180ms reads twitchy;
   6px over 320ms reads like weight being lifted. */
.card--link:hover { box-shadow: var(--ring-inset), var(--shadow-lg); transform: translateY(-6px); }

.card__media {
  margin: calc(var(--s-6) * -1) calc(var(--s-6) * -1) var(--s-6);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  overflow: hidden;
}
.card__media img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  transition: transform 640ms var(--ease);
}
/* The photograph moves, the frame does not. Long duration on purpose: a fast zoom
   is a web effect, a slow one is barely noticed and just feels expensive. */
.card--link:hover .card__media img { transform: scale(1.04); }

@media (prefers-reduced-motion: reduce) {
  .card--link, .card--link:hover, .card__media img, .card--link:hover .card__media img {
    transition: none;
    transform: none;
  }
}

/* --- Pull quote and signature ---------------------------------------------
   The difference between a brand and a person is attribution. A claim in body copy
   is the company talking; the same words with a name under them are somebody saying
   it. These two components exist so the owners can be present on the page without
   needing a photograph, which is the asset we do not have.
   -------------------------------------------------------------------------- */

.pull {
  margin: var(--s-6) 0;
  padding-left: var(--s-5);
  border-left: 2px solid var(--c-accent);
}
.pull p {
  font-family: var(--f-heading);
  font-size: var(--t-xl);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-tight);
  margin-bottom: var(--s-3);
  text-wrap: pretty;
}
.pull footer {
  font-size: var(--t-xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-text-soft);
}
.section--deep .pull { border-left-color: var(--c-warm); }
.section--deep .pull footer { color: var(--c-warm); }
.section--warm .pull { border-left-color: var(--c-cta); }

/* Name over role, with a rule above. Reads as a sign-off at the end of a passage
   rather than as another heading. */
.signature {
  margin-top: var(--s-6);
  padding-top: var(--s-5);
  border-top: 1px solid color-mix(in srgb, currentColor 14%, transparent);
}
.signature__name {
  display: block;
  font-family: var(--f-heading);
  font-size: var(--t-lg);
  line-height: 1.2;
}
.signature__role {
  display: block;
  font-size: var(--t-xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-text-soft);
  margin-top: var(--s-2);
}

/* --- Service bar: breadcrumb ---------------------------------------------- */

/* Was a two-row affair: the trail on the left, four uppercase service pills on the
   right, at --s-4 top and bottom. The pills duplicated the header dropdown and set the
   bar's height; with them gone the trail is one short line, so the bar is sized to the
   line rather than to a control. */
.svc-bar { background: var(--c-bg); border-bottom: 1px solid var(--c-line); }
.svc-bar__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2) var(--s-5);
  padding-block: var(--s-2);
}

.crumbs ol {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s-2);
  margin: 0;
  padding: 0;
  font-size: var(--t-xs);
  color: var(--c-text-soft);
}
/* The separator is generated, not typed into the markup, so a screen reader reads
   the trail as words rather than as a row of slashes. */
.crumbs li + li::before {
  content: '';
  display: inline-block;
  width: 0.35rem; height: 0.35rem;
  margin-right: var(--s-2);
  border-top: 1px solid currentColor;
  border-right: 1px solid currentColor;
  transform: rotate(45deg) translateY(-1px);
  opacity: 0.6;
}
.crumbs a { color: var(--c-text-soft); text-decoration: none; }
.crumbs a:hover { color: var(--c-text); text-decoration: underline; }
.crumbs [aria-current="page"] { color: var(--c-text); font-weight: var(--fw-bold); }

/* --- Lists ---------------------------------------------------------------- */

/* No bullets. A dot marker is the default every template ships and it adds nothing:
   the line breaks already tell you these are separate items. Hairline rules do the
   same job with more authority and match how the stats band and the testimonials are
   set, so the page reads as one system rather than three unrelated list styles.

   The rule sits above each item and the last item closes the set, so the list reads
   as a bounded group instead of trailing off. */
.ticks { list-style: none; padding: 0; margin: 0 0 var(--s-5); }
.ticks li {
  padding-block: var(--s-4);
  border-top: 1px solid color-mix(in srgb, currentColor 14%, transparent);
}
.ticks li:last-child { border-bottom: 1px solid color-mix(in srgb, currentColor 14%, transparent); }

/* Optional lead-in. Wrap the first few words in <strong> and they set as a label,
   which gives a long list somewhere for the eye to land. */
.ticks li strong {
  display: block;
  font-family: var(--f-body);
  font-size: var(--t-xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-accent);
  margin-bottom: var(--s-1);
}
.section--deep .ticks li strong { color: var(--c-warm); }
.section--warm .ticks li strong { color: var(--c-cta); }

/* --- Labelled points ------------------------------------------------------ */

/* The unruled sibling of .ticks. Three of these used to be a .ticks list sitting between
   a pull quote and a signature block, and the three components' borders stacked into
   parallel hairlines that read as stray rules. A label carries the same "this is a
   separate claim" signal that a rule was carrying, without drawing a line to do it. */
.points { display: grid; gap: var(--s-5); margin: var(--s-6) 0; }
.point__label {
  display: block;
  font-size: var(--t-xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-accent);
  margin-bottom: var(--s-2);
}
.point p { margin: 0; color: var(--c-text-soft); font-size: var(--t-sm); line-height: var(--lh-body); }
.section--deep .point__label { color: var(--c-warm); }
.section--deep .point p { color: var(--c-text-invert); opacity: 0.82; }

/* Filled disc badges are the template default. An outlined numeral on a rail is the
   same information, read as a sequence rather than three buttons. The rail is
   structural, it connects step 1 to step 3, so it is not decoration.

   The direction changes with the space available. Stacked on narrow screens the rail
   runs down the left, which is how a list reads. Given room it turns and runs left to
   right across three columns, which is how a process reads. Same markup either way. */
.steps { list-style: none; counter-reset: step; padding: 0; margin: 0; display: grid; gap: var(--s-6); }
.steps > li {
  counter-increment: step;
  position: relative;
  padding-left: var(--s-8);
  padding-bottom: var(--s-6);
}
.steps > li:last-child { padding-bottom: 0; }

/* The rail. Stops at the last numeral rather than running off the end. */
.steps > li:not(:last-child)::after {
  content: '';
  position: absolute;
  left: 1.125rem;
  top: 2.75rem;
  bottom: 0.25rem;
  width: 1px;
  background: color-mix(in srgb, currentColor 14%, transparent);
}

@media (min-width: 60rem) {
  .steps { grid-template-columns: repeat(3, 1fr); gap: var(--s-7); align-items: start; }
  .steps > li { padding-left: 0; padding-top: var(--s-7); padding-bottom: 0; }

  /* The rail turns horizontal. It starts clear of this numeral and stops just short
     of the next one, so the eye is carried across rather than boxed in. 1.125rem is
     the vertical centre of the 2.25rem numeral. */
  .steps > li:not(:last-child)::after {
    left: calc(2.25rem + var(--s-3));
    right: calc(var(--s-7) * -1 + var(--s-2));
    top: 1.125rem;
    bottom: auto;
    width: auto;
    height: 1px;
  }
}

.steps > li::before {
  content: counter(step);
  position: absolute;
  left: 0; top: 0;
  width: 2.25rem; height: 2.25rem;
  display: grid; place-items: center;
  border-radius: var(--r-pill);
  border: 1px solid color-mix(in srgb, var(--c-accent) 45%, transparent);
  background: var(--c-bg);
  color: var(--c-accent);
  font-family: var(--f-heading);
  font-size: var(--t-sm);
  font-weight: var(--fw-bold);
  font-variant-numeric: lining-nums;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.section--deep .steps > li::before { background: transparent; color: var(--c-text-invert); }

/* The step under the pointer fills in. No cursor change and no underline, so it
   reads as "you are looking at this one" rather than promising a link that is not
   there. focus-within covers keyboard users when a step contains a link. */
.steps > li:hover::before,
.steps > li:focus-within::before {
  background: var(--c-accent);
  border-color: var(--c-accent);
  color: var(--c-accent-text);
}

/* Hierarchy: the heading is the step, the copy is the detail. Previously both sat at
   the same weight and the numeral was the only thing separating them. */
.steps h3 { margin-bottom: var(--s-3); font-size: var(--t-lg); }
.steps > li > p { color: var(--c-text-soft); font-size: var(--t-sm); margin-bottom: 0; }
.section--deep .steps > li > p { color: var(--c-text-invert); opacity: 0.8; }

/* --- Facts and stats ------------------------------------------------------ */

/* Ruled columns, the way a broadsheet sets a stat block. The dividers are real
   structure, they separate four things that belong together, which is why this does
   not read as decoration. */
.facts {
  display: grid;
  gap: var(--s-6);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 11rem), 1fr));
}
.fact { padding-left: var(--s-6); border-left: 1px solid currentColor; }
.fact:first-child { padding-left: 0; border-left: 0; }
/* currentColor at full strength would shout. The rule wants to be felt, not read. */
.fact { border-left-color: color-mix(in srgb, currentColor 18%, transparent); }

.fact__value {
  font-family: var(--f-heading);
  font-size: var(--t-3xl);
  line-height: 0.95;
  letter-spacing: var(--ls-display);
  display: block;
  margin-bottom: var(--s-3);
}
.fact__label {
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: var(--ls-wide);
  color: var(--c-text-soft);
  line-height: 1.5;
  display: block;
  max-width: 18ch;
}
.section--deep .fact__label { color: var(--c-text-invert); opacity: 0.7; }

/* Stacked, the vertical rules become horizontal ones or they read as a stray line. */
@media (max-width: 47.99rem) {
  .fact { padding-left: 0; border-left: 0; padding-top: var(--s-5); border-top: 1px solid color-mix(in srgb, currentColor 18%, transparent); }
  .fact:first-child { padding-top: 0; border-top: 0; }
}

/* Band variant. Same component, read at a glance instead of at a stop: the value drops
   two steps off display size so it stops competing with the h1 directly above it, and
   takes Pale Oak so four numbers on Dark Teal warm the band rather than shouting in
   white. Pale Oak on Dark Teal is 5.1:1, which carries at this size. */
.facts--band { gap: var(--s-5); grid-template-columns: repeat(auto-fit, minmax(min(100%, 12rem), 1fr)); }
.facts--band .fact__value { font-size: var(--t-xl); letter-spacing: var(--ls-tight); margin-bottom: var(--s-1); }
.section--deep .facts--band .fact__value { color: var(--c-warm); }
.facts--band .fact__label { max-width: 22ch; }

/* --- Hours and definition lists ------------------------------------------ */

.hours { display: grid; grid-template-columns: 1fr auto; gap: var(--s-2) var(--s-4); margin: 0; }
.hours dt { color: var(--c-text-soft); }
.hours dd { margin: 0; font-weight: var(--fw-medium); text-align: right; }

/* --- Tables --------------------------------------------------------------- */

.table-scroll { overflow-x: auto; margin-bottom: var(--s-5); }
table { width: 100%; border-collapse: collapse; font-size: var(--t-sm); }
caption { text-align: left; color: var(--c-text-soft); font-size: var(--t-sm); margin-bottom: var(--s-3); }
th, td { text-align: left; padding: var(--s-3) var(--s-4); border-bottom: 1px solid var(--c-line); vertical-align: top; }
th { font-family: var(--f-body); font-weight: var(--fw-bold); }
tbody tr:last-child td { border-bottom: 0; }

/* --- Notes and callouts --------------------------------------------------- */

.note {
  border-left: 3px solid var(--c-accent);
  background: var(--c-bg-alt);
  padding: var(--s-4) var(--s-5);
  border-radius: 0 var(--r-md) var(--r-md) 0;
  margin-bottom: var(--s-5);
}
.note p:last-child { margin-bottom: 0; }
.note--placeholder { border-left-style: dashed; }

/* --- FAQ ------------------------------------------------------------------ */

.faq { max-width: var(--measure); }
.faq details {
  border-bottom: 1px solid var(--c-line);
  padding-block: var(--s-4);
}
.faq summary {
  cursor: pointer;
  font-family: var(--f-heading);
  font-size: var(--t-lg);
  font-weight: var(--fw-bold);
  list-style: none;
  display: flex;
  justify-content: space-between;
  gap: var(--s-4);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: '+'; font-weight: var(--fw-normal); flex-shrink: 0; }
.faq details[open] summary::after { content: '\2212'; }
.faq details > *:not(summary) { margin-top: var(--s-3); }

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

/* Neither ruled nor boxed: the quotes sit on the page as text, and the grid gap is
   what separates one from the next. */
.quote {
  border: 0;
  border-radius: 0;
  padding: 0;
  margin: 0;
  background: transparent;
  /* Captions sit on the baseline of the card so a short quote next to a long one
     still lines its attribution up with its neighbours. */
  display: flex;
  flex-direction: column;
}
.quote .stars { margin-bottom: var(--s-4); font-size: var(--t-base); }
.quote p {
  font-size: var(--t-lg);
  font-family: var(--f-heading);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-tight);
  text-wrap: pretty;
}
.quote p:last-of-type { margin-bottom: var(--s-5); }
.quote figcaption {
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: var(--ls-wide);
  color: var(--c-text-soft);
  margin-top: auto;
}

/* --- Forms ---------------------------------------------------------------- */

.form { max-width: 40rem; }
.field { margin-bottom: var(--s-4); }
.field label { display: block; font-size: var(--t-sm); font-weight: var(--fw-bold); margin-bottom: var(--s-2); }
.field .req { color: var(--c-text-soft); font-weight: var(--fw-normal); }
.field input, .field textarea, .field select {
  width: 100%;
  padding: var(--s-3);
  border: 1px solid var(--c-line);
  border-radius: var(--r-md);
  background: var(--c-bg);
  color: var(--c-text);
  font: inherit;
  font-size: var(--t-sm);
}
.field textarea { min-height: 8rem; resize: vertical; }
.field-row { display: grid; gap: var(--s-4); grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); }
.form__note { font-size: var(--t-sm); color: var(--c-text-soft); }

/* --- Map ------------------------------------------------------------------ */

.map {
  border: 1px solid var(--c-line);
  border-radius: var(--r-lg);
  overflow: hidden;
  background: var(--c-bg-alt);
  aspect-ratio: 16 / 9;
}

/* Taller on narrow screens, where 16:9 leaves a letterbox too short to read. */
@media (max-width: 47.9375rem) {
  .map { aspect-ratio: 4 / 3; }
}

.map iframe {
  display: block;
  width: 100%;
  height: 100%;
  border: 0;
}

.map__link { margin-top: var(--s-4); }

/* --- Embed slot ----------------------------------------------------------- */

.embed-slot {
  border: 2px dashed var(--c-line);
  border-radius: var(--r-lg);
  padding: var(--s-7) var(--s-5);
  text-align: center;
  color: var(--c-text-soft);
}
.embed-slot h3 { color: var(--c-text); }

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

.footer { background: var(--c-bg-deep); color: var(--c-text-invert); padding-block: var(--s-8) var(--s-5); margin-top: var(--section-y); }
/* That margin is breathing room after a light section. When the page ends on a deep
   section the footer is the same colour, so the margin renders as a white stripe
   between two black blocks. Close it up and let the two dark blocks meet. */
main:has(> .section--deep:last-child) + .footer { margin-top: 0; }
.footer a { color: var(--c-text-invert); }
.footer__inner { display: grid; gap: var(--s-6); grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr)); }
/* Four equal columns starve the Hours list: it splits its own column again into
   label and value, so an equal share leaves the labels too narrow to hold a line
   and "Daycare, Sat and Sun" breaks across three. Give Hours the widest track and
   take it back off Pages, which is a list of short links. */
@media (min-width: 60rem) {
  .footer__inner { grid-template-columns: 1.1fr 1fr 1.45fr 0.85fr; }
}
.footer__heading { font-size: var(--t-base); text-transform: uppercase; letter-spacing: var(--ls-wide); margin-bottom: var(--s-4); }
/* Agency credit. Deliberately the quietest thing in the footer: it sits on the deep
   surface at reduced opacity so it reads as a signature rather than competing with the
   client's own copyright line above it. */
.footer__credit { margin: var(--s-2) 0 0; font-size: var(--t-xs); }
.footer__credit a { color: var(--c-text-invert); opacity: 0.6; text-decoration: none; }
.footer__credit a:hover { opacity: 1; text-decoration: underline; }
.footer__mark { width: 72px; height: auto; margin-bottom: var(--s-4); filter: invert(1); }
.footer__blurb { font-size: var(--t-sm); opacity: 0.85; }
.footer__address { font-style: normal; margin-bottom: var(--s-3); }
/* Two lines, so a resting underline across both reads heavy. Underline on hover
   instead, matching how the address behaves in the top info bar. */
.footer__address a { text-decoration: none; }
.footer__address a:hover { text-decoration: underline; }
.footer__col p { margin-bottom: var(--s-2); }
.footer__links { list-style: none; padding: 0; margin: 0; }
.footer__links li { margin-bottom: var(--s-2); }
.footer .hours dt, .footer .fact__label { color: var(--c-text-invert); opacity: 0.75; }
.footer__base { border-top: 1px solid rgba(255, 255, 255, 0.16); margin-top: var(--s-7); padding-top: var(--s-5); }
.footer__spanish, .footer__legal { font-size: var(--t-xs); opacity: 0.75; max-width: var(--measure); }
.footer__legal { margin-bottom: 0; }

/* --- Steps: display variant ----------------------------------------------- */

/* The base .steps sets an outlined numeral on a connecting rail, which reads as a
   process. On the home page the three steps are the invitation itself, so the numeral
   stops being a marker and becomes the typography: Fraunces at display size, ghosted
   back so it sets the rhythm without out-shouting the step heading. The rail goes with
   it. Three parallel top rules already say "these are a set, read across", and a rail
   plus a rule is one line too many. */
.steps--display > li {
  padding-left: 0;
  padding-bottom: 0;
  padding-top: 0;
}
.steps--display > li:not(:last-child)::after { content: none; }
.steps--display > li::before {
  content: '0' counter(step);
  position: static;
  display: block;
  width: auto;
  height: auto;
  border: 0;
  border-radius: 0;
  background: transparent;
  font-family: var(--f-heading);
  font-size: clamp(2.75rem, 2rem + 2.4vw, 4rem);
  line-height: 0.9;
  letter-spacing: var(--ls-display);
  /* Was 38%, which measured about 1.6:1 and left the numerals almost invisible, so the
     hierarchy they exist to create never landed. Full strength is 4.47:1 on the Oak
     Wash band: present enough to carry the sequence, still well under the near-black
     step heading beside it. */
  color: var(--c-accent);
  margin-bottom: var(--s-4);
  transition: color var(--dur) var(--ease);
}
/* The base variant fills its disc on hover. Here there is no disc to fill, so the
   numeral comes up to full strength instead. Same signal, same trigger. */
.steps--display > li:hover::before,
.steps--display > li:focus-within::before {
  background: transparent;
  border-color: transparent;
  color: var(--c-accent);
}
.steps--display h3 { margin-bottom: var(--s-3); }

/* --- Rating panel --------------------------------------------------------- */

/* The count was a clause in the intro paragraph, which is the wrong place for the only
   number on the page that does any persuading. As the first cell of the quote grid it
   becomes the claim and the quotes become its evidence, and it gives six same-shaped
   text blocks something to be read against. Boxed on purpose: it borrows .card's
   treatment because it has to differ from the unboxed quotes beside it. */
.rating {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  background: var(--c-bg);
  border-radius: var(--r-lg);
  padding: var(--s-6);
  box-shadow: var(--ring-inset), var(--shadow-sm);
}
.rating__mark { display: block; margin-bottom: var(--s-4); font-size: var(--t-base); }
.rating__value {
  font-family: var(--f-heading);
  font-size: var(--t-3xl);
  line-height: 0.95;
  letter-spacing: var(--ls-display);
  display: block;
}
.rating__label {
  display: block;
  font-size: var(--t-sm);
  color: var(--c-text-soft);
  line-height: var(--lh-snug);
  max-width: 20ch;
  margin-top: var(--s-3);
}
.rating__note {
  font-size: var(--t-xs);
  color: var(--c-text-soft);
  margin: var(--s-4) 0 var(--s-5);
}
/* Pins the action to the bottom of the panel so it lines up with the baseline the
   quote captions sit on. */
.rating .btn { margin-top: auto; }

/* --- Rate board ----------------------------------------------------------- */

/* The pricing page was six stacked tables. A table is the right tool for a grid of
   facts, but these are not a grid: each line is one service, one price and one
   condition, which is a list. Set as a list the price can carry real typographic
   weight and the condition can sit quietly under the name instead of fighting it in
   a third column.

   Everything here is rendered from src/data/pricing.json at build time, so the markup
   below is in the served HTML and readable with the script blocked. The script adds
   the tabs and the calculator; it is not what draws the prices. */

.rate-board { margin-top: var(--s-2); }

/* The five-tab switcher is gone. Pricing is a scanning and comparing task, and tabs are
   the one pattern that prevents both: four fifths of the rates sat behind a click, the
   panel headings were visually hidden once the script ran, and on a phone the fifth tab
   was off-screen with no scroll cue. Every rate is on the page now. This nav is a jump
   list that sticks under the site header and marks the block you are in, so the page
   stays navigable without hiding anything.

   --header-h is measured in main.js. The fallback matters: with the script dead the nav
   degrades to plain in-page anchors, which still work. */
.rate-jump {
  position: sticky;
  top: var(--header-h, 5.5rem);
  z-index: 20;
  margin-bottom: var(--s-7);
  background: var(--c-bg);
  border-bottom: 1px solid var(--c-line);
}
.rate-jump__list {
  display: flex;
  gap: var(--s-2);
  list-style: none;
  margin: 0;
  padding: 0 0 var(--s-2);
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}
.rate-jump__link {
  display: block;
  flex-shrink: 0;
  padding: var(--s-3) var(--s-5);
  border: 1px solid var(--c-line);
  border-radius: var(--r-pill);
  background: var(--c-bg);
  font-size: var(--t-sm);
  font-weight: var(--fw-bold);
  letter-spacing: 0.06em;
  color: var(--c-text);
  text-decoration: none;
  white-space: nowrap;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.rate-jump__link:hover { border-color: var(--c-accent); color: var(--c-accent); text-decoration: none; }
/* Filled Dark Teal for the block you are in. That is already what "selected" means on
   this page: the calculator's day buttons below use exactly this treatment, so the page
   keeps one selected-state vocabulary instead of inventing a second. */
.rate-jump__link[aria-current="true"] {
  background: var(--c-bg-deep);
  border-color: var(--c-bg-deep);
  color: var(--c-text-invert);
}

/* Every block is on the page, so each needs real separation and its heading back rather
   than the sr-only treatment the tab version applied. scroll-margin keeps a jumped-to
   heading clear of the sticky header AND the sticky nav, instead of landing under both. */
.rate-block { scroll-margin-top: calc(var(--header-h, 5.5rem) + 5rem); }
.rate-block + .rate-block {
  margin-top: var(--s-9);
  padding-top: var(--s-8);
  border-top: 1px solid var(--c-line);
}
.rate-block__title { font-size: var(--t-2xl); letter-spacing: var(--ls-tight); margin-bottom: var(--s-3); }
.rate-block__blurb { color: var(--c-text-soft); font-size: var(--t-lg); max-width: var(--measure); margin-bottom: var(--s-6); }
.rate-block__sub { margin-top: var(--s-8); margin-bottom: var(--s-5); font-size: var(--t-xl); letter-spacing: var(--ls-tight); }

/* One rule per line, none under the last, so the set reads as bounded. Same treatment
   as .ticks and the stats band, which is why it does not look like a new idea. */
.rates { list-style: none; margin: 0; padding: 0; }
.rate {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s-5);
  padding-block: var(--s-4);
  border-top: 1px solid var(--c-line);
}
.rate:last-child { border-bottom: 1px solid var(--c-line); }
.rate__head { display: flex; flex-direction: column; gap: var(--s-1); }
.rate__name { font-family: var(--f-heading); font-size: var(--t-lg); line-height: var(--lh-snug); }
.rate__note { font-size: var(--t-sm); color: var(--c-text-soft); max-width: 52ch; }
/* Tabular figures so the column of prices lines up on the decimal without a table. */
.rate__price {
  flex-shrink: 0;
  font-family: var(--f-heading);
  font-size: var(--t-xl);
  line-height: 1;
  letter-spacing: var(--ls-tight);
  font-variant-numeric: tabular-nums lining-nums;
  text-align: right;
}
/* "Price varies" is a sentence, not a figure, and at figure size it shouts. */
.rate__price--words {
  font-family: var(--f-body);
  font-size: var(--t-sm);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-text-soft);
}

/* --- Packages ------------------------------------------------------------- */

/* Unboxed, on a top rule, except the one that wins. Marking the best value by filling
   it with the page's one warm surface means the recommendation is visible before any
   of the arithmetic is read, which is the whole job of the block. */
.pkgs {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--s-5);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
}
.pkg {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--s-2);
  padding: var(--s-5) var(--s-5) var(--s-6);
}
.pkg--best { background: var(--c-warm); border-radius: var(--r-md); }
.pkg--best .pkg__count, .pkg--best .pkg__each, .pkg--best .pkg__saving { color: var(--c-warm-body); }
.pkg--best .pkg__price { color: var(--c-warm-text); }
.pkg__flag {
  align-self: flex-start;
  font-size: var(--t-xs);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-wide);
  text-transform: uppercase;
  color: var(--c-accent);
}
.pkg__count { font-size: var(--t-xs); font-weight: var(--fw-bold); letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--c-text-soft); }
.pkg__price {
  font-family: var(--f-heading);
  font-size: var(--t-2xl);
  line-height: 1;
  letter-spacing: var(--ls-display);
  font-variant-numeric: tabular-nums lining-nums;
}
.pkg__each { font-family: var(--f-heading); font-size: var(--t-base); color: var(--c-text-soft); }
.pkg__saving { margin-top: var(--s-2); font-size: var(--t-xs); color: var(--c-text-soft); line-height: 1.5; }

/* --- Package calculator --------------------------------------------------- */

/* The packages answer "what does a block of days cost". The question people actually
   arrive with is "we come twice a week, is a pack worth it", which needs the two
   numbers put against each other. Ruled rather than boxed, so it reads as a tool
   attached to the rates above rather than as an advert dropped beside them. */
.calc { margin-top: var(--s-8); }
.calc__q {
  font-family: var(--f-heading);
  font-size: var(--t-xl);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-tight);
  margin-bottom: var(--s-4);
}
.calc__opts { display: flex; flex-wrap: wrap; gap: var(--s-2); margin-bottom: var(--s-5); }
.calc__opt {
  appearance: none;
  min-width: 3rem;
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--c-line);
  border-radius: var(--r-pill);
  background: var(--c-bg);
  font-family: var(--f-heading);
  font-size: var(--t-base);
  font-variant-numeric: lining-nums;
  color: var(--c-text);
  cursor: pointer;
  transition: background var(--dur) var(--ease), color var(--dur) var(--ease),
              border-color var(--dur) var(--ease);
}
.calc__opt:hover { border-color: var(--c-accent); color: var(--c-accent); }
.calc__opt[aria-pressed="true"] {
  background: var(--c-bg-deep);
  border-color: var(--c-bg-deep);
  color: var(--c-text-invert);
}
.calc__out { display: grid; gap: var(--s-4); }
@media (min-width: 44rem) { .calc__out--filled { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
.calc__fig { display: flex; flex-direction: column; gap: var(--s-1); }
.calc__fig-label { font-size: var(--t-xs); font-weight: var(--fw-bold); letter-spacing: var(--ls-wide); text-transform: uppercase; color: var(--c-text-soft); }
.calc__fig-value {
  font-family: var(--f-heading);
  font-size: var(--t-xl);
  line-height: 1;
  letter-spacing: var(--ls-tight);
  font-variant-numeric: tabular-nums lining-nums;
}
.calc__fig--win .calc__fig-value { color: var(--c-accent); }
.calc__fig-note { font-size: var(--t-xs); color: var(--c-text-soft); line-height: 1.5; }
.calc__verdict { grid-column: 1 / -1; font-size: var(--t-sm); color: var(--c-text-soft); margin: 0; }
/* The group footnote is a sibling of the calculator, and the verdict line inside the
   calculator has no bottom margin, so without this the two run together as one
   paragraph. */
.calc + .form__note { margin-top: var(--s-5); }

/* Book Online sits in the nav rather than as a second header button, because the
   header row is already brand plus five items plus a call to action and only fits
   because Services is a dropdown. It is the one nav item that is not one of our pages,
   so it takes the accent to say so. */
.nav__book {
  color: var(--c-accent);
  font-weight: var(--fw-bold);
  text-decoration: none;
  white-space: nowrap;
}
.nav__book:hover { color: var(--c-accent-hover); text-decoration: underline; }

/* Book Online makes six nav items sharing a row with the brand and the call to
   action. Between the burger breakpoint and roughly 1200px that row had about 16px
   of slack left, so the gap tightens through the crowded band only. Wider than this
   it goes back to --s-5. */
@media (min-width: 71.01rem) and (max-width: 78rem) {
  .nav__list { gap: var(--s-4); }
}
/* Belt and braces: the brand must never give up width to the nav, or its nowrap
   name overruns the box instead of the row simply running out. */
.brand { flex-shrink: 0; }
.brand__name { overflow: hidden; text-overflow: ellipsis; }

/* --- Gallery --------------------------------------------------------------
   Multi-column rather than a grid of equal boxes. Fourteen of the twenty photos are
   landscape and five portrait, and a shared aspect ratio would crop the subject out of
   half of them. Columns keep every photo the shape it was shot in, and the count steps
   down by viewport so a portrait shot never ends up thinner than a phone gutter.
   -------------------------------------------------------------------------- */

.gallery {
  columns: 4;
  column-gap: var(--s-4);
}
@media (max-width: 74.9375rem) { .gallery { columns: 3; } }
@media (max-width: 47.9375rem) { .gallery { columns: 2; } }
@media (max-width: 29.9375rem) { .gallery { columns: 1; } }

/* A button, not a link: it opens an overlay rather than going anywhere, and the
   appearance reset is what stops the browser drawing a control around a photo. */
.shot {
  display: block;
  width: 100%;
  margin: 0 0 var(--s-4);
  padding: 0;
  border: 0;
  appearance: none;
  background: none;
  border-radius: var(--r-lg);
  overflow: hidden;
  cursor: zoom-in;
  box-shadow: var(--ring-inset), var(--shadow-sm);
  /* break-inside matters here: without it a column can split a photo across the gap. */
  break-inside: avoid;
  transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease);
}
.shot img { display: block; width: 100%; height: auto; }
.shot:hover { box-shadow: var(--ring-inset), var(--shadow-md); transform: translateY(-2px); }
.shot:focus-visible { outline: 3px solid var(--c-focus); outline-offset: 3px; }

/* --- Lightbox -------------------------------------------------------------
   Fixed to the viewport, so like the mobile nav drawer it must not sit inside anything
   carrying a transform, filter or backdrop-filter, or it would pin to that box instead.
   It is a direct child of the section, not of a card or a wrap.
   -------------------------------------------------------------------------- */

.lightbox {
  position: fixed;
  inset: 0;
  /* Above everything: skip link 100, nav menu 60, header 50, mobile drawer 49 and
     its scrim 48, rate jump nav 20. This project keeps z-index literals in this
     file rather than tokenising them, so no --z-* variable is invented here. */
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-3);
  padding: var(--s-5);
  background: rgba(11, 18, 21, 0.92);
}
.lightbox[hidden] { display: none; }
.lightbox__figure { margin: 0; max-width: min(100%, 1600px); max-height: 100%; display: flex; }
.lightbox__figure img {
  max-width: 100%;
  /* Height is capped in viewport units so a portrait photo cannot push the controls
     off screen on a short window. */
  max-height: calc(100vh - var(--s-8));
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: var(--r-md);
}
.lightbox__close,
.lightbox__nav {
  appearance: none;
  border: 1px solid rgba(240, 243, 245, 0.35);
  background: rgba(11, 18, 21, 0.6);
  color: var(--c-text-invert);
  border-radius: var(--r-pill);
  cursor: pointer;
  line-height: 1;
  flex-shrink: 0;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
.lightbox__close:hover,
.lightbox__nav:hover { background: rgba(11, 18, 21, 0.9); border-color: var(--c-text-invert); }
.lightbox__close:focus-visible,
.lightbox__nav:focus-visible { outline: 3px solid var(--c-warm); outline-offset: 2px; }
.lightbox__nav { width: 3rem; height: 3rem; font-size: 1.75rem; }
.lightbox__close {
  position: absolute;
  top: var(--s-4);
  right: var(--s-4);
  width: 2.75rem;
  height: 2.75rem;
  font-size: 1.5rem;
}
/* On a phone there is no room for side arrows beside the photo, so they move under it
   and the photo gets the width back. */
@media (max-width: 47.9375rem) {
  .lightbox { flex-wrap: wrap; padding: var(--s-4); }
  .lightbox__figure { order: -1; flex-basis: 100%; justify-content: center; }
  .lightbox__figure img { max-height: calc(100vh - 10rem); }
}

/* The display variant drops the rail, so three sequential steps render as three unrelated
   islands. This puts a connector back for that variant only: a hairline leaving each
   numeral and crossing the grid gap to the next, so the eye is carried through 01-02-03.
   Only above 60rem, because below that the steps stack and a horizontal line is nonsense. */
@media (min-width: 60rem) {
  .steps--linked > li:not(:last-child)::after {
    content: '';
    position: absolute;
    /* Both offsets derive from the numeral's own clamp so they track its fluid size:
       0.45x down is its optical centre, 1.3x across clears the two digits. */
    top: calc(clamp(2.75rem, 2rem + 2.4vw, 4rem) * 0.45);
    left: calc(clamp(2.75rem, 2rem + 2.4vw, 4rem) * 1.3);
    right: calc(var(--s-7) * -1 + var(--s-2));
    bottom: auto;
    width: auto;
    height: 1px;
    background: color-mix(in srgb, var(--c-accent) 34%, transparent);
  }
}

/* Space the three steps off the band above them. */
.section-band + .steps--display { margin-top: var(--s-8); }
