/* responsive.css — tablet & phone repairs only.
   Every rule below lives inside a max-width media query, so the desktop
   rendering (>1150px) is byte-for-byte the same as before this file existed.
   Kept separate from design.css because design.css is generated from
   design-source/ and is overwritten by `npm run convert:design`. */

/* ── Tablet and below ─────────────────────────────────────────────── */
@media (max-width: 1150px) {

  /* Decorative blobs are positioned with negative right/left offsets and
     push the document wider than the viewport on narrow screens. */
  html, body { overflow-x: hidden !important; max-width: 100% !important; }

  /* Section heroes reserve 80–90vh and centre their content, which on a tall
     narrow screen renders as a screenful of empty space above the headline. */
  main.page > section[id$="-hero"] {
    min-height: 0 !important;
    padding-top: 120px !important;
    padding-bottom: 56px !important;
    padding-left: 6vw !important;
    padding-right: 6vw !important;
    justify-content: flex-start !important;
  }

  /* Body sections: tighten the desktop 8vw gutters. */
  main.page > section { padding-left: 6vw; padding-right: 6vw; }

  /* Nothing may be wider than the screen. */
  main.page img,
  main.page svg,
  main.page image-slot,
  main.page video,
  main.page table { max-width: 100% !important; height: auto; }

  /* Long words / URLs must wrap rather than push the page sideways. */
  main.page h1, main.page h2, main.page h3,
  main.page p, main.page li, main.page a { overflow-wrap: break-word; }

  /* Buttons and pills: never truncate, never overflow, always tappable. */
  main.page a[style*="border-radius:999px"],
  main.page button,
  main.page a[href^="/admissions"],
  main.page a[href^="/visit"],
  main.page a[href^="/contact"],
  .rail-link {
    white-space: normal !important;
    max-width: 100% !important;
    min-height: 44px;
    text-align: center;
    line-height: 1.25;
  }

  /* ...and the half of that rule that was missing until 2026-09-15.
     The pack writes its pills with inline padding and no box-sizing, so they
     are content-box. `max-width: 100%` then caps the CONTENT box at the
     container width and adds the padding ON TOP: every pill rendered exactly
     its horizontal padding wider than the space it had, and the section's
     `overflow: hidden` sliced the right-hand end off. That is the whole of
     the "clipped CTA" bug — fifteen sections, every phone, every tablet:
     "Come watch them think →" (68px padding, 37px past the edge),
     "Open in Maps →" (60px padding, 29px past), "Book a visit →" (76px, 45px).
     The arithmetic is exact, which is how it was found.

     Scoped to the anchors on purpose: no <button> in the pack sets both an
     explicit width and padding, but the virtual-tour hotspots DO size
     themselves in px, and border-box would quietly shrink them. */
  main.page a[style*="border-radius:999px"],
  main.page a[href^="/admissions"],
  main.page a[href^="/visit"],
  main.page a[href^="/contact"] {
    box-sizing: border-box;
  }

  /* Rows of buttons wrap instead of squeezing. */
  main.page div[style*="display:flex"] { flex-wrap: wrap; }

  /* Sticky "Book a visit" CTA: the pack pads it by calc(6vh + 96px) at the
     bottom, which makes an enormous pill that sits over the content. */
  main.page > a[id$="-sticky-cta"] {
    padding: 13px 22px !important;
    bottom: max(14px, env(safe-area-inset-bottom)) !important;
    font-size: 15px !important;
    white-space: nowrap !important;
    max-width: calc(100vw - 32px);
  }

  /* Leave room under the footer so the sticky pill never covers a link. */
  footer.site-footer { padding-bottom: 96px !important; }
}

/* ── Tablet portrait and below: collapse multi-column layouts ──────── */
@media (max-width: 900px) {
  /* NOT EVERY GRID IS A COLUMN LAYOUT. This rule used to match every element
     with a grid-template-columns in its inline style, and it flattened three
     kinds of grid that were never columns of content:

       repeat(3,1fr) with 64px children   the three photo thumbnails inside the
                                          mock Friday note on the homepage. Stacked,
                                          they became three full-width coloured bars
                                          one under another — the "three big blank
                                          spaces which do nothing" in the school's
                                          screenshots. It is the only repeat(3,1fr)
                                          in body.html.

       repeat(7,1fr) with 15px children   the year-at-a-glance dot grids on
                                          the homepage and /programs/kindergarten.
                                          Twelve months of coloured dots became a
                                          single vertical line of fourteen dots in
                                          a tall white card. This is the calendar
                                          the school called "horribly ugly", and it
                                          was this rule, not the pack.

       auto 1fr / 44px 1fr / 90px 1fr     a number or icon badge BESIDE a line of
       / 120px 34px 1fr                   text. Collapsed, the badge sits on its own
                                          row above the words, which reads as a
                                          stray dot and adds a row of dead height
                                          to every one of them.

       repeat(auto-fit, minmax(Npx,1fr))  already collapses by itself, exactly when
                                          the column no longer fits. Forcing 1fr
                                          only ever takes away a second column that
                                          DID fit — /how-we-learn's project cards
                                          are minmax(130px,1fr) and fit two across
                                          on a 390px phone.

     So the rule now names what it is for: real multi-column content grids
     (`1fr 1fr`, `repeat(3,1fr)`, `minmax(0,1.12fr) minmax(0,.88fr)`, the
     three-column table heads) and nothing else. The exclusions are matched
     against the inline style text, which the pack writes without a space
     after the colon.

     Counted from src/legacy/body.html: 76 auto-fit grids, 2 dot grids and
     64 badge rows stop being collapsed; the ~30 genuine content grids still
     are. */
  main.page div[style*="grid-template-columns"]:not([style*="repeat(auto-fit"]):not([style*="repeat(7,1fr)"]):not([style*="repeat(3,1fr)"]):not([style*="columns:auto 1fr"]):not([style*="columns:44px 1fr"]):not([style*="columns:90px 1fr"]):not([style*="columns:120px 34px 1fr"]),
  main.page section[style*="grid-template-columns"]:not([style*="repeat(auto-fit"]):not([style*="repeat(7,1fr)"]):not([style*="repeat(3,1fr)"]):not([style*="columns:auto 1fr"]):not([style*="columns:44px 1fr"]):not([style*="columns:90px 1fr"]):not([style*="columns:120px 34px 1fr"]) {
    grid-template-columns: 1fr !important;
  }
  main.page div[style*="position:sticky"],
  main.page aside[style*="position:sticky"] { position: static !important; }

  /* Fluid type floors. The pack sizes text as min(Nvw, Xpx): correct on a
     desktop, but below ~900px the vw term wins and headlines/body collapse to
     unreadable sizes. Each pattern below is floored with a clamp() whose upper
     bound is the pack's own value, so nothing changes above this breakpoint.
     Generated from the inline styles in src/legacy/body.html. */
  [style*="font-size:min(34vw,340px)"]{font-size:clamp(44px,34vw,340px) !important}
  [style*="font-size:min(9.6vw,178px)"]{font-size:clamp(44px,9.6vw,178px) !important}
  [style*="font-size:min(9vw,116px)"]{font-size:clamp(44px,9vw,116px) !important}
  [style*="font-size:min(8.4vw,110px)"]{font-size:clamp(44px,8.4vw,110px) !important}
  [style*="font-size:min(8.4vw,104px)"]{font-size:clamp(44px,8.4vw,104px) !important}
  [style*="font-size:min(8vw,102px)"]{font-size:clamp(44px,8vw,102px) !important}
  [style*="font-size:min(7.6vw,100px)"]{font-size:clamp(44px,7.6vw,100px) !important}
  [style*="font-size:min(8vw,100px)"]{font-size:clamp(44px,8vw,100px) !important}
  [style*="font-size:min(7.4vw,98px)"]{font-size:clamp(44px,7.4vw,98px) !important}
  [style*="font-size:min(7.4vw,96px)"]{font-size:clamp(44px,7.4vw,96px) !important}
  [style*="font-size:min(7.6vw,96px)"]{font-size:clamp(44px,7.6vw,96px) !important}
  [style*="font-size:min(7vw,92px)"]{font-size:clamp(44px,7vw,92px) !important}
  [style*="font-size:min(7vw,88px)"]{font-size:clamp(44px,7vw,88px) !important}
  [style*="font-size:min(6.8vw,88px)"]{font-size:clamp(44px,6.8vw,88px) !important}
  [style*="font-size:min(7vw,86px)"]{font-size:clamp(44px,7vw,86px) !important}
  [style*="font-size:min(7vw,84px)"]{font-size:clamp(44px,7vw,84px) !important}
  [style*="font-size:min(8vw,80px)"]{font-size:clamp(42px,8vw,80px) !important}
  [style*="font-size:min(6.4vw,78px)"]{font-size:clamp(41px,6.4vw,78px) !important}
  [style*="font-size:min(6.4vw,76px)"]{font-size:clamp(40px,6.4vw,76px) !important}
  [style*="font-size:min(6vw,76px)"]{font-size:clamp(40px,6vw,76px) !important}
  [style*="font-size:min(6.4vw,74px)"]{font-size:clamp(38px,6.4vw,74px) !important}
  [style*="font-size:min(6.4vw,72px)"]{font-size:clamp(37px,6.4vw,72px) !important}
  [style*="font-size:min(6.2vw,72px)"]{font-size:clamp(37px,6.2vw,72px) !important}
  [style*="font-size:min(6vw,72px)"]{font-size:clamp(37px,6vw,72px) !important}
  [style*="font-size:min(6vw,70px)"]{font-size:clamp(36px,6vw,70px) !important}
  [style*="font-size:min(5.6vw,68px)"]{font-size:clamp(35px,5.6vw,68px) !important}
  [style*="font-size:min(5.8vw,68px)"]{font-size:clamp(35px,5.8vw,68px) !important}
  [style*="font-size:min(6vw,68px)"]{font-size:clamp(35px,6vw,68px) !important}
  [style*="font-size:min(6vw,66px)"]{font-size:clamp(34px,6vw,66px) !important}
  [style*="font-size:min(5.6vw,66px)"]{font-size:clamp(34px,5.6vw,66px) !important}
  [style*="font-size:min(5.4vw,64px)"]{font-size:clamp(33px,5.4vw,64px) !important}
  [style*="font-size:min(6vw,64px)"]{font-size:clamp(33px,6vw,64px) !important}
  [style*="font-size:min(5.6vw,64px)"]{font-size:clamp(33px,5.6vw,64px) !important}
  [style*="font-size:min(5vw,62px)"]{font-size:clamp(32px,5vw,62px) !important}
  [style*="font-size:min(5.6vw,62px)"]{font-size:clamp(32px,5.6vw,62px) !important}
  [style*="font-size:min(5.4vw,60px)"]{font-size:clamp(31px,5.4vw,60px) !important}
  [style*="font-size:min(5vw,60px)"]{font-size:clamp(31px,5vw,60px) !important}
  [style*="font-size:min(6vw,60px)"]{font-size:clamp(31px,6vw,60px) !important}
  [style*="font-size:min(5.6vw,60px)"]{font-size:clamp(31px,5.6vw,60px) !important}
  [style*="font-size:min(6vw,58px)"]{font-size:clamp(30px,6vw,58px) !important}
  [style*="font-size:min(5.6vw,58px)"]{font-size:clamp(30px,5.6vw,58px) !important}
  [style*="font-size:min(5.4vw,58px)"]{font-size:clamp(30px,5.4vw,58px) !important}
  [style*="font-size:min(5.2vw,56px)"]{font-size:clamp(29px,5.2vw,56px) !important}
  [style*="font-size:min(7vw,56px)"]{font-size:clamp(29px,7vw,56px) !important}
  [style*="font-size:min(4.6vw,56px)"]{font-size:clamp(29px,4.6vw,56px) !important}
  [style*="font-size:min(7.4vw,54px)"]{font-size:clamp(28px,7.4vw,54px) !important}
  [style*="font-size:min(4.6vw,54px)"]{font-size:clamp(28px,4.6vw,54px) !important}
  [style*="font-size:min(5vw,54px)"]{font-size:clamp(28px,5vw,54px) !important}
  [style*="font-size:min(5vw,52px)"]{font-size:clamp(27px,5vw,52px) !important}
  [style*="font-size:min(7vw,52px)"]{font-size:clamp(27px,7vw,52px) !important}
  [style*="font-size:min(4.8vw,50px)"]{font-size:clamp(26px,4.8vw,50px) !important}
  [style*="font-size:min(4.1vw,50px)"]{font-size:clamp(26px,4.1vw,50px) !important}
  [style*="font-size:min(5vw,46px)"]{font-size:clamp(24px,5vw,46px) !important}
  [style*="font-size:min(4.6vw,46px)"]{font-size:clamp(24px,4.6vw,46px) !important}
  [style*="font-size:min(4.6vw,44px)"]{font-size:clamp(23px,4.6vw,44px) !important}
  [style*="font-size:min(6vw,44px)"]{font-size:clamp(23px,6vw,44px) !important}
  [style*="font-size:min(4.4vw,42px)"]{font-size:clamp(22px,4.4vw,42px) !important}
  [style*="font-size:min(5.4vw,42px)"]{font-size:clamp(22px,5.4vw,42px) !important}
  [style*="font-size:min(4.2vw,42px)"]{font-size:clamp(22px,4.2vw,42px) !important}
  [style*="font-size:min(6vw,40px)"]{font-size:clamp(21px,6vw,40px) !important}
  [style*="font-size:min(4vw,40px)"]{font-size:clamp(21px,4vw,40px) !important}
  [style*="font-size:min(5vw,40px)"]{font-size:clamp(21px,5vw,40px) !important}
  [style*="font-size:min(4.4vw,40px)"]{font-size:clamp(21px,4.4vw,40px) !important}
  [style*="font-size:min(5vw,38px)"]{font-size:clamp(20px,5vw,38px) !important}
  [style*="font-size:min(5vw,36px)"]{font-size:clamp(19px,5vw,36px) !important}
  [style*="font-size:min(3.6vw,34px)"]{font-size:clamp(18px,3.6vw,34px) !important}
  [style*="font-size:min(4.4vw,34px)"]{font-size:clamp(18px,4.4vw,34px) !important}
  [style*="font-size:min(4vw,34px)"]{font-size:clamp(18px,4vw,34px) !important}
  [style*="font-size:min(5vw,34px)"]{font-size:clamp(18px,5vw,34px) !important}
  [style*="font-size:min(4.6vw,34px)"]{font-size:clamp(18px,4.6vw,34px) !important}
  [style*="font-size:min(5vw,32px)"]{font-size:clamp(17px,5vw,32px) !important}
  [style*="font-size:min(3.4vw,32px)"]{font-size:clamp(17px,3.4vw,32px) !important}
  [style*="font-size:min(4.4vw,32px)"]{font-size:clamp(17px,4.4vw,32px) !important}
  [style*="font-size:min(3.6vw,30px)"]{font-size:clamp(16px,3.6vw,30px) !important}
  [style*="font-size:min(4.6vw,30px)"]{font-size:clamp(16px,4.6vw,30px) !important}
  [style*="font-size:min(4.2vw,30px)"]{font-size:clamp(16px,4.2vw,30px) !important}
  [style*="font-size:min(5vw,30px)"]{font-size:clamp(16px,5vw,30px) !important}
  [style*="font-size:min(3.2vw,28px)"]{font-size:clamp(16px,3.2vw,28px) !important}
  [style*="font-size:min(3.4vw,28px)"]{font-size:clamp(16px,3.4vw,28px) !important}
  [style*="font-size:min(2.2vw,24px)"]{font-size:clamp(16px,2.2vw,24px) !important}
  [style*="font-size:min(2.2vw,23px)"]{font-size:clamp(16px,2.2vw,23px) !important}
  [style*="font-size:min(3vw,23px)"]{font-size:clamp(16px,3vw,23px) !important}
  [style*="font-size:min(2.5vw,21px)"]{font-size:clamp(16px,2.5vw,21px) !important}
  [style*="font-size:min(2.4vw,20px)"]{font-size:clamp(16px,2.4vw,20px) !important}
}

/* ── Phones ───────────────────────────────────────────────────────── */
@media (max-width: 640px) {
  main.page > section[id$="-hero"] {
    padding-top: 104px !important;
    padding-left: 20px !important;
    padding-right: 20px !important;
  }
  main.page > section { padding-left: 20px; padding-right: 20px; }
  main.page > section[id$="-hero"] h1 { font-size: clamp(30px, 9vw, 42px) !important; }

  /* Full-width stacked buttons read better than half-wrapped pills. */
  main.page a[style*="border-radius:999px"]:not([id$="-sticky-cta"]),
  main.page button[style*="border-radius:999px"] {
    width: 100%;
    justify-content: center;
  }

  /* Form controls must not zoom iOS in on focus. */
  main.page input, main.page select, main.page textarea {
    font-size: 16px !important;
    max-width: 100% !important;
    width: 100%;
    box-sizing: border-box;
  }
}

/* ── Header: three fixed-width items in a 390px bar ────────────────── */
@media (max-width: 1150px) {
  header.topbar { padding: 12px 18px !important; gap: 10px; }
  header.topbar #siteWordmark { min-width: 0; }
}
@media (max-width: 640px) {
  /* The header pill duplicates both the nav drawer's CTA and the sticky one,
     and it is what crowds the wordmark off the bar. Drop it on phones. */
  header.topbar a.rail-link[data-page="admissions-schedule-a-tour"] { display: none !important; }
  header.topbar { padding: 10px 14px !important; }
}

/* ── Hero eyebrow badges: recede and sit higher ─────────────────────
   The two pills above the wordmark compete with the headline below.
   Make them semi-transparent and lift the whole eyebrow block so the
   focus falls on the content underneath. Applies at all widths. */
#home-hero [data-edit-ok] > div:first-child {
  margin-top: -34px !important;
  margin-bottom: 10px !important;
  opacity: .62;
}
#home-hero [data-edit-ok] > div:first-child > span {
  background: rgba(250, 246, 236, .18) !important;
  border-color: rgba(34, 48, 30, .09) !important;
  /* The prefix is not optional here: iOS Safari before 18 supports only
     `-webkit-backdrop-filter`, and without it these chips lose the frost and
     sit as flat 18%-alpha rectangles over the hero photograph. Prefixed
     first, unprefixed second, so a browser that understands both takes the
     standard one. */
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
@media (max-width: 900px) {
  #home-hero [data-edit-ok] > div:first-child { margin-top: -18px !important; }
}

/* ── Homepage floating decorations must not sit over the hero ────────
   #paperBird, #paperBird2, #kiteRig and #kiteClouds are position:fixed and
   driven by view timelines on the Bird Feeder and Still-Childhood sections.
   With `both` fill they paint their start pose from the very first frame, so
   at scroll 0 the hummingbirds and the kite land on top of the hero headline.
   Gate them on document scroll so each only becomes visible as its own
   section approaches. visibility is not touched by their own keyframes, so
   the flight and fade animations are unchanged. */
@keyframes wpsDecoGate {
  0% { visibility: hidden }
  0.5% { visibility: visible }
  99.5% { visibility: visible }
  100% { visibility: hidden }
}

#page-home #paperBird {
  animation: birdFly linear both, wpsDecoGate steps(1, end) both !important;
  animation-timeline: --birdsec, --birdsec !important;
  animation-range: entry 0% exit 100%, entry 0% exit 100% !important;
}
#page-home #paperBird2 {
  animation: birdFly2 linear both, wpsDecoGate steps(1, end) both !important;
  animation-timeline: --birdsec, --birdsec !important;
  animation-range: entry 0% exit 100%, entry 0% exit 100% !important;
}
#page-home #kiteRig {
  animation: kiteAscend linear both, kiteFade linear both,
             wpsDecoGate steps(1, end) both !important;
  animation-timeline: --kitesec, --kitesec, --kitesec !important;
  animation-range: cover 0% cover 100%, cover 0% cover 100%, cover 0% cover 100% !important;
}
#page-home #kiteClouds {
  animation: cloudsIn linear both, wpsDecoGate steps(1, end) both !important;
  animation-timeline: --kitesec, --kitesec !important;
  animation-range: cover 0% cover 100%, cover 0% cover 100% !important;
}
/* Browsers without scroll-driven animations get no gate, so keep the
   decorations out of the way entirely rather than parked on the hero. */
@supports not (animation-timeline: scroll(root block)) {
  #page-home #paperBird, #page-home #paperBird2,
  #page-home #kiteRig, #page-home #kiteClouds { display: none !important }
}

/* Tablet and below get no gate either, for the same reason: below 1150px the
   view timelines these four are gated on stop resolving against the document
   scroller, and the gate reports 59.8% progress while its own section is still
   6,586px below the fold. The result is both hummingbirds, the kite and the
   cloud bank all painting their start pose across the hero headline, at every
   width from 1149px down.

   They are decorative (aria-hidden, pointer-events:none) and they are a
   desktop flourish: on a narrow screen the kite alone is a third of the
   viewport wide and lands squarely on the headline it is meant to frame. So
   below 1150px they do not ship at all, which is also the one behaviour that
   cannot break. The desktop gate above is unchanged. */
@media (max-width: 1150px) {
  #page-home #paperBird, #page-home #paperBird2,
  #page-home #kiteRig, #page-home #kiteClouds { display: none !important }
}
}

/* ── Six-things carousel ("What children are building") ──────────────
   The tap-target floor above (min-height:44px) also hit the pagination
   dots, stretching 11px circles into ovals. Keep them round, and let the
   card width flex between the desktop layout and the 900px mobile
   fallback so the first card lands centred on narrow laptops. */
@media (max-width: 1150px) {
  #buildDots button {
    min-height: 0 !important;
    width: 11px;
    height: 11px;
  }
  #buildDots button[data-on] {
    width: 30px;
    height: 11px;
  }
}
@media (max-width: 1150px) and (min-width: 901px) {
  #buildStage { --bcw: min(340px, 46vw); --bgap: 18px; }
}

/* ── 08 Built here: the postcard keeps its postcard shape ────────────
   The section's auto-fit grid dropped to one column and then squeezed the
   card itself, so every line wrapped and the stamp stub fell underneath.
   Below 1150px the card gets its own row at a fixed postcard width and is
   scaled down as a whole instead of being re-flowed — one line per line,
   just smaller. */
@media (max-width: 1150px) {
  #home-built-here > div[style*="grid-template-columns"] {
    grid-template-columns: 1fr !important;
    gap: 7vh !important;
  }
  #postcardScene {
    width: 560px;
    max-width: none;
    margin: 0 auto;
    transform-origin: top center;
  }
  #postcard { width: 100%; }
  #postcardBody {
    grid-template-columns: minmax(0, 1.15fr) minmax(150px, .85fr) !important;
  }
  #postcardBody p, #postcardBody span { white-space: nowrap; }
  #postcardBody p[data-land="3"] { white-space: normal; }
}
@media (max-width: 640px) { #postcardScene { transform: scale(.86); margin-bottom: -9%; } }
@media (max-width: 560px) { #postcardScene { transform: scale(.74); margin-bottom: -17%; } }
@media (max-width: 470px) { #postcardScene { transform: scale(.62); margin-bottom: -26%; } }
@media (max-width: 400px) { #postcardScene { transform: scale(.53); margin-bottom: -34%; } }

/* ── Programs dropdown (desktop topbar) ─────────────────────────────── */
.topbar .wps-has-drop > .wps-drop{
  position:absolute; top:100%; left:-14px; min-width:210px;
  display:flex; flex-direction:column; padding:10px 0;
  background:#FAF6EC; border:1px solid rgba(34,48,30,.1); border-radius:14px;
  box-shadow:0 18px 40px rgba(34,48,30,.16);
  opacity:0; visibility:hidden; transform:translateY(6px);
  transition:opacity .18s ease, transform .18s ease, visibility .18s;
  z-index:120;
}
.topbar .wps-has-drop:hover > .wps-drop,
.topbar .wps-has-drop:focus-within > .wps-drop{opacity:1;visibility:visible;transform:translateY(0)}
.topbar .wps-drop a{padding:9px 18px;font-size:14.5px;font-weight:600;color:#22301E;white-space:nowrap}
.topbar .wps-drop a:hover{background:rgba(143,160,115,.22)}
/* Age-band links sit indented under "All Ages". */
.topbar .wps-drop a.wps-sub{padding-left:34px;font-size:14px;color:#3B4A32}
/* The section's own page, repeated at the top of its dropdown. */
.topbar .wps-drop a.wps-top{font-weight:800;border-bottom:1px solid rgba(34,48,30,.10);margin-bottom:4px;padding-bottom:11px}

/* ---- Menu bar legibility ------------------------------------------------
   Every word in the header, the dropdowns and the mobile drawer is the SAME
   dark green as the wordmark in the logo: #003F2A, the fill of
   public/brand/wps-wordmark-green.svg. Set here rather than in design.css
   because that file is generated and the next convert:design run drops it.

   What it replaces, and why it had to move:
     nav links      inherited the pack's link colour #56663F   5.7:1 on #FAF6EC
     dropdown       #22301E, sub-links #3B4A32                 ~13:1 / ~9:1
     mobile drawer  the same two, written inline per link
   #56663F is a muted sage that measures as passing but does not read as one:
   at 14.5px over a bar that was letting the page scroll through it, the nav
   was the faintest text on the screen. #003F2A on the header's #FAF6EC is
   11.2:1, and every level of the menu now matches rather than fading down a
   ladder of three greens — hierarchy is carried by weight, size and indent.

   The mobile drawer writes its colours as inline styles on each link, and the
   pack's own `a{color:#56663F}` is later in the cascade than this file, so
   these need !important to land. The "Book a visit" pill is excluded in both
   menus: it is cream on a dark green ground and this rule would paint it
   dark-on-dark. */
header.topbar #navLinks a:not([href="/admissions/schedule-a-tour"]),
header.topbar .wps-drop a:not([href="/admissions/schedule-a-tour"]),
#mobileNavDrawer a:not([href="/admissions/schedule-a-tour"]){
  color:#003F2A !important;
}
header.topbar #navLinks a:hover,
header.topbar .wps-drop a:hover,
#mobileNavDrawer a:hover{color:#22301E !important}

/* The bar itself is opaque. It was rgba(250,246,236,.82) over a 12px blur, so
   whatever scrolled underneath tinted the nav text — worst over the hero photo
   and the dark green and yellow sections, where the same words changed colour
   as the page moved. A fixed bar that the content shows through cannot be
   guaranteed legible on any device, so it is the flat paper colour now; the
   blur is left in place for browsers that paint it during the scroll. */
header.topbar{background:#FAF6EC !important}

/* ...and dropping the blur with it fixes the mobile menu, which was opening to
   a 48px sliver with no reachable link on it. #mobileNavDrawer is
   position:fixed and lives INSIDE <header class="topbar">, and a
   backdrop-filter other than `none` makes its element a containing block for
   fixed-position descendants. So `top:70px;bottom:0` on the drawer resolved
   against the 48px header instead of the viewport, and no amount of the
   `max-height:calc(100vh - 70px)` the toggle sets could open it: measured
   before any change on this branch, checked=true, max-height=774px,
   opacity=1 — and height=48px.

   The blur was only ever there to soften content scrolling under a 0.82-alpha
   bar. Behind an opaque bar it renders nothing at all, so this costs no
   appearance and returns the drawer to the viewport. */
header.topbar{backdrop-filter:none !important;-webkit-backdrop-filter:none !important}

/* The drawer and the dropdown are already opaque #FAF6EC. Stated here so a
   future edit to either does not quietly reintroduce a see-through menu. */
#mobileNavDrawer{background:#FAF6EC !important}
.topbar .wps-has-drop > .wps-drop{background:#FAF6EC}

/* ---- Quotation marks: review cards and the CAPE closer ------------------
   Both live here rather than in design.css because that file is generated and
   the next convert:design run drops anything added to it.

   REVIEW CARDS. The card opened with a 96px ornament and closed with a 1.5em
   mark tucked against the last word, so the two ends of the same quotation
   were different sizes and only one of them was anywhere near a corner. They
   are one pair now: same glyph size, same tint, opening at the top-left,
   closing at the bottom-right.

   The closing mark is a block in the card's flex column, placed after a
   `flex:1 1 auto` paragraph, so it lands on the bottom edge of the quote area
   whatever the review's length — above the dashed rule, never over the
   reviewer's name. `height` is a fraction of the font-size on purpose: the
   glyph is drawn in the top of its em box, so a 96px mark reserves 38px of
   layout and overflows the rest, which keeps the cards the height they were.
   Both marks are aria-hidden ornaments, so the tint is the card's accent. */
.rev-mark{
  top:14px !important; left:20px !important;
  font-size:clamp(74px,7.4vw,100px) !important;
  opacity:.42 !important;
}
.rev-close{
  display:block; align-self:flex-end;
  font-family:'Bricolage Grotesque',sans-serif; font-weight:800;
  font-size:clamp(74px,7.4vw,100px); line-height:1;
  height:.4em; margin:-.04em 4px 12px 0;
  color:var(--rev-accent,#8FA073); opacity:.42;
  pointer-events:none; user-select:none;
}
/* A short review is centred in its card (`margin-top:auto;margin-bottom:auto`
   on the paragraph). Without this the two auto margins would split the slack
   and push the closing mark up with the text; it belongs on the bottom edge
   in every tier. */
.rev-card[data-len="xs"] .rev-quote,
.rev-card[data-len="s"] .rev-quote{margin-bottom:0 !important}
.rev-card[data-len="xs"] .rev-close,
.rev-card[data-len="s"] .rev-close{margin-top:auto}

/* THE CAPE CLOSER (#home-build-closer). The pack ships a straight ASCII quote
   (U+0022 — two vertical ticks) centred above the sentence in #3F4E36, which
   is 1.4:1 on the section's #22301E: a smudge rather than a quotation mark,
   and nothing closed it. Replaced with a real pair of curly marks at the
   corners of the quote, in #8FA073 — 5.1:1 on that ground, visible as an
   ornament without competing with the sentence.

   Done entirely in CSS, with no change to the markup, for two reasons: the
   section is editable, so anything saved over it from Admin -> Site Editor
   would replace added elements but cannot touch a pseudo-element; and the
   paragraph already carries `position:relative`, so the marks hang off the
   quote's own box and follow it when the school rewrites the sentence. */
#home-build-closer > span[aria-hidden="true"]{
  font-size:0 !important; line-height:0 !important;
  margin:0 !important; height:0 !important; overflow:hidden;
}
#home-build-closer > p[data-rv]::before,
#home-build-closer > p[data-rv]::after{
  position:absolute;
  font-family:'Bricolage Grotesque',sans-serif; font-weight:800;
  font-size:clamp(64px,7.2vw,118px); line-height:.72;
  color:#8FA073; pointer-events:none; user-select:none;
}
#home-build-closer > p[data-rv]::before{content:"\201C"; left:-.30em; top:-.46em}
#home-build-closer > p[data-rv]::after{content:"\201D"; right:-.30em; bottom:-.52em}
/* On a wide screen the marks hang outside the quote and the centred lines
   never reach far enough left or right to meet them. A phone has neither
   margin: the section keeps only ~51px above the paragraph (the wave crops
   the top of that) and the 22ch column fills the width to within 31px, so the
   opening mark landed on top of the first line and inside the crop.

   The room is made rather than borrowed — the paragraph takes a band of
   padding at each end and the marks sit in it, clear of both the text and the
   wave. Measured with the mark at 64px/.72 line-height: a 46px box, so 52px
   of padding leaves the glyph its own strip with a little air. */
@media (max-width:700px){
  #home-build-closer > p[data-rv]{padding:52px 0 50px}
  #home-build-closer > p[data-rv]::before{left:-.04em; top:0}
  #home-build-closer > p[data-rv]::after{right:-.04em; bottom:0}
}

/* ---- CAPE method panels: designed grounds -------------------------------
   Each of the four rotating panels takes the painted background that matches
   its own palette colour, instead of a flat fill. Set here (not in the
   generated stylesheet) so a design re-convert cannot drop it. */
#home-challenge-step,#home-analyze-step,#home-produce-step,#home-evaluate-step{
  background-size:cover !important;background-position:center !important;
  background-repeat:no-repeat !important;
}
/* Panel 1 paints NOTHING and lets the section's ground show through, which is
   what makes the band above the wave and the first panel literally one image
   rather than two crops of the same one. Panels 2-4 are opaque and 100vw, so
   they cover it completely as the rail scrolls. */
#home-challenge-step{background-color:transparent !important;background-image:none !important}
#home-analyze-step{background-color:#EBDFC7 !important;background-image:url('/legacy/assets/backgrounds/cape-sand.webp') !important}
#home-produce-step{background-color:#F5C531 !important;background-image:url('/legacy/assets/backgrounds/cape-yellow.webp') !important}
#home-evaluate-step{background-color:#56663F !important;background-image:url('/legacy/assets/backgrounds/cape-green.webp') !important}

/* ...and the section AROUND them takes the same ground as the first panel.
   THE RULE, because this has to stop happening: when anything inside a section
   paints a designed background, the section itself must paint the same one.
   The CAPE section is 340vh tall and its top 230px sit above the pinned rail,
   so that band showed flat #8FA073 + grain against the artwork of the panel
   directly below it — one ground meeting a different ground at the wave, which
   reads as a rendering fault rather than a design. Same image, anchored to the
   top, so the band and the first panel are one continuous surface.
   See DESIGN_RULES.md -> "A section's ground is the whole section". */
#home-cape-method{
  background-color:#8FA073 !important;
  background-image:url('/legacy/assets/backgrounds/cape-sage.webp') !important;
  background-size:cover !important;
  background-position:top center !important;
  background-repeat:no-repeat !important;
}

/* ---- Hero circle scales with the headline ------------------------------
   The headline stops growing at min(9.6vw,178px) — i.e. at a ~1854px wide
   viewport — but the green blob behind it was capped at 810px, which it hits
   at ~1285px. Between those widths the words kept growing while the circle
   stood still. Same cap width for both so they stay locked together. */
#page-home #heroBlobStage{
  width:min(63vw,1168px) !important;
  height:min(63vw,1168px) !important;
}

/* ---- Hero photo: stop it ballooning on wide monitors -------------------- */
#page-home #heroPhotoCol{max-width:clamp(280px,27vw,520px) !important}
#page-home #heroPhotoCol > div[style*="rotate(2deg)"]{height:min(40vw,520px) !important}

/* ---- Footer: tighter, evenly balanced, fluid to any width ---------------
   The link columns were long and ragged; the block now sits on a measured
   rhythm that scales with the viewport instead of stacking fixed gaps. */
footer.site-footer{
  padding:clamp(38px,4.4vw,60px) clamp(20px,5vw,72px) 24px !important;
  padding-top:calc(clamp(38px,4.4vw,60px) + 96px) !important;
}
footer.site-footer > div:last-of-type{max-width:1440px !important;gap:clamp(24px,2.6vw,36px) !important}
/* identity / contact / CTA band */
footer.site-footer > div:last-of-type > div:first-child{
  gap:clamp(22px,3vw,44px) !important;
  padding-bottom:clamp(20px,2.2vw,30px) !important;
}
/* link columns */
footer.site-footer nav{gap:8px !important}
footer.site-footer nav a{font-size:15px !important;line-height:1.3 !important}
footer.site-footer > div:last-of-type > div:nth-child(2){
  grid-template-columns:repeat(auto-fit,minmax(180px,1fr)) !important;
  gap:clamp(20px,2.4vw,40px) !important;
  align-items:start !important;
}
/* legal rows */
footer.site-footer > div:last-of-type > div:nth-child(3){padding-top:16px !important;gap:8px 26px !important}
footer.site-footer > div:last-of-type > div:last-child{margin-top:10px !important;padding-top:12px !important}

/* ── Illustrated mini-graphics inside cards ──────────────────────────────
   `data-fun-box` is the pack's little illustration panel — the Code of
   Ethics poster, the twelve-month calendar, the homepage trio. Inside it the
   pack lays out flex rows with `justify-content: space-between` and flex
   columns with `align-items: center`, and neither shrinks its children: a
   centred column item takes its max-content width, a space-between row keeps
   both ends at full width. On a desktop card that is invisible because the
   card is wide. On a phone the card is 271px of usable width and the panel
   measured a 439px scroll width — the calendar's "Holiday / Break" legend and
   the poster's "signed by every adult here" ran out of the card and off the
   screen.

   Three lines, no markup change: let the rows wrap, stop the children
   growing past the panel, and let a grid/flex child actually shrink
   (`min-width: 0` is what defeats the automatic minimum size). */
@media (max-width: 900px) {
  main.page [data-fun-box] > *,
  main.page [data-fun-box] > * > * {
    max-width: 100%;
    min-width: 0;
  }
  main.page [data-fun-box] > div {
    flex-wrap: wrap;
  }

  /* The belt to that pair of braces. The panel declares `display:flex;
     flex-direction:column`, but it is inside a <main> that starts hidden and
     is revealed by a class, and in that sequence the children were measured
     laid out side by side — an inline-block row, not a flex column — with the
     caption pushed off the card. Forcing a reflow re-ran it correctly, which
     is the signature of a stale flex layout rather than a cascade problem.

     Rather than depend on the reflow, make the children stack on their own:
     `display:block` items sit one per line with or without the flex column,
     so the panel is correct under either layout. Nothing moves on desktop,
     where the card is wide enough that the row fitted anyway. */
  main.page [data-fun-box] > span {
    display: block;
    text-align: center;
    max-width: 100%;
  }

  /* With the children stacked, a fixed 168px panel would clip them. */
  main.page [data-fun-box] {
    height: auto !important;
    min-height: 168px;
  }
}

/* ── /families/calendar: the squeezed middle column, 761px to 1024px ───────
   Each date is `grid-template-columns: minmax(150px,190px) 1fr auto` — the
   date, the description, and the three "add to calendar" buttons. The third
   track is `auto`, so it takes whatever the buttons need (297px), the first
   takes its 190px, and the DESCRIPTION gets whatever is left.

   The pack stacks the row at 760px and below. Between 761px and about 1000px
   nothing does, and "whatever is left" is 70px at 768 — a portrait iPad, one
   of the most likely screens a parent opens a school calendar on. Measured
   there: "First day of the school year" set one word per line, eleven lines
   deep, beside a column of single-word body text.

   So the stack point moves up to where the middle column is actually usable.
   At 1180 (landscape iPad) the middle measures 409px and the three columns
   are right, so the cap is 1024 rather than the coarse-pointer arm.

   Not in design.css, which is generated by convert:design and would lose it. */
@media (min-width: 761px) and (max-width: 1024px) {
  main.page #cal-list [data-event] {
    grid-template-columns: minmax(0, 1fr) !important;
    gap: 14px !important;
    align-items: start !important;
  }
  main.page #cal-list [data-event] > div:last-child {
    justify-content: flex-start !important;
  }
}

/* ── G4 · An iPad in landscape is a touch device, not a desktop ────────────
   MOBILE_TABLET_AUDIT.md § G4. Phase 3, 2026-09-15.

   The desktop navigation — five rail links, two of them with hover dropdowns —
   shows above 1150px. An iPad Air in landscape is 1180 logical pixels and an
   iPad Pro is 1366, so both got it, and neither has a hover state. Tapping
   "Programs" on one of those navigates straight to /programs and the six links
   inside its dropdown are unreachable: not hidden, not slow, unreachable.

   The collapse now keys on the pointer as well as the width, using the
   convention touch.css established for exactly this — `(max-width: 1400px) and
   (pointer: coarse)`. `pointer: coarse` describes the PRIMARY input, so a
   touchscreen laptop, which has a mouse as well, keeps the hover nav and is
   right to.

   The burger and the drawer need no help: the drawer is a checkbox
   (`#mobileNavToggle:checked ~ #mobileNavDrawer`), not a script, and that rule
   is already unconditional in design.css. Only the two display switches were
   behind the width.

   The dropdowns also open on `:focus-within` (line ~368 above), so a keyboard
   reaches them at desktop widths where hover is the only other way in. */
@media (max-width: 1400px) and (pointer: coarse) {
  header.topbar #navLinks { display: none !important; }
  #mobileNavBtn { display: flex !important; }
}

/* ── G12 · Scroll containment for the open drawer ─────────────────────────
   MOBILE_TABLET_AUDIT.md § G12.3. Phase 4, 2026-09-15.

   The mobile drawer is a tall scrolling list — 30 links once the programme
   dropdowns are flattened into it. Scroll to its end and the gesture keeps
   going into the page behind, so the visitor closes the menu, finds the page
   has moved, and has lost their place. `contain` stops the scroll chaining at
   the drawer's edge without disabling anything else.

   Deliberately NOT `overscroll-behavior-y: none` on <html>: that would kill
   pull-to-refresh, which is expected behaviour on Android Chrome and is not
   ours to take away. The containment belongs to the drawer alone. */
#mobileNavDrawer { overscroll-behavior: contain; }

/* ═══════════════════════════════════════════════════════════════
   AN EMPTY PHOTO GROUND MUST NOT COST A WHOLE SCREEN.

   <image-slot> keeps the shape the designer drew — 3/2, or a portrait
   inline height — whether or not a photograph has been dropped into it.
   That is right for a filled slot and wrong for an empty one, because
   until the school's photographs arrive most of them ARE empty.
   Measured at 390×844 across all 32 pages: 64 empty slots, 19,461px of
   blank ground. /gallery was 54% empty boxes by height; a single portrait
   slot on the programme pages was 540px, which is 64% of the phone's
   viewport given to a tinted rectangle. The three in the Bird Feeder
   Challenge are the "three big blank spaces which do nothing" in the
   school's screenshots.

   So below 900px an empty slot is capped: enough to read as "a
   photograph goes here", not enough to be a screen of its own. A FILLED
   slot is untouched at every width, so this cap disappears page by page
   as the photographs land, and desktop never sees it.

   `data-filled` is set by public/legacy/image-slot.js the moment a slot
   resolves an image, so the selector follows the real state rather than
   the markup. */
@media (max-width: 900px) {
  image-slot:not([data-filled]) {
    max-height: 32vh !important;
    min-height: 116px;
  }
}

/* On a short phone in landscape 32vh is nothing; hold a floor in px so the
   ground still reads as a deliberate frame rather than a stripe. */
@media (max-width: 900px) and (max-height: 560px) {
  image-slot:not([data-filled]) { max-height: 180px !important; }
}
