/* Site-wide CSS -- tokens, typography, header, footer, buttons, and generic
   layout primitives. Extracted from the home-page fidelity prototype
   (working-files/website-netlify-migration/wayfinder/home-page-fidelity-
   prototype/variant-2-pixel-match/style.css), which pixel-matched these
   elements against the live site's measurements.json. Home-page-specific
   section styles (hero video, About Me grid, Instagram gallery, podcast)
   are NOT here -- those belong to Phase 3's own stylesheet for index.html.
   Loaded on every page, after fonts, before any page-specific CSS. */

/* ---------- Self-hosted fonts (Phase 7 shared polish, 17.09.26) ----------
   Replaces the Google Fonts CDN link every page carried (removed from
   each page's <head>, see tools/partials/page-template.html) -- fixes a
   CLS (Cumulative Layout Shift -- how much visible content jumps around
   as the page loads) regression measured at 0.18 on the offer page vs
   live's 0.002 (Phase 6 report). The regression's cause was the extra
   cross-origin round trip to fonts.googleapis.com/fonts.gstatic.com
   (DNS + connect + request) before the real face could even start
   downloading, which pushed the swap well past first paint. Self-
   hosting removes that round trip entirely (same-origin, no separate
   connection); the two above-the-fold faces (Almarai 400 body copy,
   Lora 700 headings) are also `<link rel=preload>`-ed in the page
   template, so the browser fetches them in parallel with the HTML
   itself -- both together get the real face in place before first
   paint on a normal connection, so font-display: swap's fallback
   period is never actually visible or CLS-causing in practice (proven
   below, not assumed -- see the Phase 7 report's before/after
   Lighthouse CLS numbers). font-display: optional was considered and
   rejected: it would show the WRONG font permanently on a slow first
   visit (no later swap once the font arrives), a worse outcome than
   swap's brief, now-effectively-never-triggered fallback window.
   Deliberately NOT using size-adjust/ascent-override metric overrides
   on a fallback face -- those numbers have to be measured against each
   font's real metrics table (e.g. via Capsize) to be safe; an invented
   value can silently make the fallback MORE wrong, not less. Preload +
   same-origin is the lower-risk fix and was measured sufficient.
   Only the 4 weight/style combinations actually used anywhere on the
   site are shipped (public/fonts/, latin subset only -- this is an
   English-only site, so the other Google-served unicode-range subsets
   -- cyrillic, vietnamese, math, symbols -- are dropped): Almarai 400 +
   700 (body text, buttons), Lora 700 normal + 700 italic (headings; the
   italic is Lora's own em/strong-em runs on the offer page's hero intro
   and h4 "for a limited number of people" -- the offer page used to
   pull this in via its own separate Google Fonts link, now removed,
   see bray-new-student-offer.html). OFL.txt licence files (both
   families are SIL Open Font License) kept alongside the woff2 files. */
@font-face {
  font-family: "Almarai";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("/fonts/almarai-400.woff2") format("woff2");
}
@font-face {
  font-family: "Almarai";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/almarai-700.woff2") format("woff2");
}
@font-face {
  font-family: "Lora";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/lora-700.woff2") format("woff2");
}
@font-face {
  font-family: "Lora";
  font-style: italic;
  font-weight: 700;
  font-display: swap;
  src: url("/fonts/lora-700italic.woff2") format("woff2");
}

:root {
  --body-bg: #FCF9F7;
  --hero-cream: #FCEEDC;
  --about-peach: #F5A874;
  --text-navy: #343444;
  --text-black: #000000;
  --orange: #EC6C1C;
  --highlight-underline: #F9AD7D; /* live's own --accent-hsl (23.23,91.18%,73.33%) resolved -- NOT --orange, which is live's darkAccent; see the highlighted-word underline rule below */
  --btn-radius: 10px;
  --font-body: "Almarai", Arial, sans-serif;
  --font-heading: "Lora", Georgia, serif;
  --font-button: "Almarai", Arial, sans-serif; /* buttons use Almarai, same as body -- confirmed against measured button styles, see prototype NOTES.md */
  --max-width: 1440px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  padding: 0;
  background: var(--body-bg);
  color: var(--text-navy);
  font-family: var(--font-body);
  font-size: 16px;
  line-height: 1.5;
  /* Text antialiasing (Phase 7 shared polish, 17.09.26): live sets
     -webkit-font-smoothing: antialiased on .site-wrapper (a top-level
     div wrapping the whole page, just inside <body>), which every
     section's text inherits -- confirmed live, aboutme desktop capture:
     .site-wrapper computed "antialiased", <body> itself computed "auto"
     (unset). Set here on html/body instead of a wrapper div (this build
     has none at that exact spot) -- same inherited effect on every
     descendant, site-wide, so a new page needs no local override.
     -moz-osx-font-smoothing is NOT set on live at all (computed empty
     string everywhere checked) -- carried here anyway, paired with
     -webkit-font-smoothing per the usual convention, for the same
     antialiased rendering on Firefox/macOS; a pure rendering-quality
     addition with no live counterpart to mismatch against (Chromium,
     which every compare script screenshots, ignores it). Was
     previously set page-locally on bray-new-student-offer.css's
     .fe-section only -- removed there now that it's inherited from
     here instead (see that file's own comment). */
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}
/* Sticky-footer flex (defect fix, 18.09.26 -- footer-position defect):
   live's real <footer> (#footer-sections) rule is `margin-top: auto`,
   read directly from live's CSS bundle -- not a fixed px value, and not
   the 3-step "12px/6.875px/0px at 768-820-900/1024/1200" a previous
   verifier described (that description was itself measurement-fitted,
   from one short page at one viewport height -- the exact trap this
   project's own governing rule warns against). `margin-top:auto` only
   does anything inside a flex column container tall enough to leave
   slack above the footer -- live's own container is `#siteWrapper`
   (display:flex;flex-direction:column;min-height:100vh, confirmed via
   direct computed-style inspection at 900x1200), which pushes the
   footer down to fill whatever vertical space is left over once a
   page's real content is shorter than the viewport. Verified directly
   (not inferred): on live, the footer/main gap at a fixed width grows
   linearly with viewport height above the point content and viewport
   height cross (0px gap at 1100, 12px at 1200, 20px at 2000, 25px at
   2500, on the same page/width) -- textbook auto-margin behaviour, not
   a stepped constant. Applied to `body` directly (this build has no
   separate site-wrapper div, same substitution already used for
   -webkit-font-smoothing above) instead of adding a new wrapper element,
   which would be an HTML structure change outside this file's scope.
   `.site-header` stays position:absolute (out of flow, unaffected by
   its new flex ancestor) and every other direct body child (main,
   .site-footer, .cookie-pref-link) keeps its full width via flex's
   default align-items:stretch on a column axis -- no visible change on
   any page whose content already exceeds the viewport height (the
   normal case: margin-top:auto resolves to 0, identical to the previous
   default), only on short-content + tall-viewport combinations, which
   is exactly where the defect was reported. */
body {
  position: relative; /* containing block for .cookie-pref-link's position:absolute -- see that rule's own comment */
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}

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

a { color: inherit; text-decoration: none; }

/* ---------- Highlighted/bold-word underline (site-wide, defect fix
   18.09.26) ----------
   Live's own markup for this isn't a plain `<u>` at all -- it's a
   Squarespace "text highlight" span (`<span class="sqsrte-text-
   highlight">`, confirmed on index.html's hero copy, "Bray Movement
   Foundations Intensive"), which live draws with a JS-injected SVG
   stroke, not text-decoration: a straight `<path>` per wrapped line,
   `stroke: rgb(249,173,125)` (live's own --accent-hsl token), `stroke-
   width: 2.2px` = `0.1em` at that text's own 22px font-size, `stroke-
   linecap: square`, sitting tight under the text (its own box measures
   26px tall for a 22px line, the stroke drawn near its bottom edge, not
   the browser's default lower/skip-descenders underline position) --
   Playwright-probed directly against live, 18.09.26. This build uses a
   plain `<strong><u>` for the same phrase (see index.html hero copy) --
   text-decoration reproduces the same colour/thickness/tight-offset
   look without live's own SVG scroll-reveal plumbing (a "draws in" dash-
   offset animation on scroll into view, not attempted here -- the flag
   only asked to match the drawn STYLE, not the reveal motion).
   text-decoration-skip-ink: none keeps the line solid through
   descenders, matching live's own continuous stroke (the browser
   default skips around them, which live's straight SVG path never
   does). */
u {
  text-decoration-line: underline;
  text-decoration-color: var(--highlight-underline);
  text-decoration-thickness: 0.1em;
  text-underline-offset: 0.12em;
  text-decoration-skip-ink: none;
}

.skip-link {
  position: absolute;
  left: -16000px;
  top: 24px;
}
/* Prototype had no visible focus state for the skip link -- added here
   (small accessibility fix, no downstream effect, noted under DECISIONS). */
.skip-link:focus {
  left: 16px;
  top: 16px;
  background: #fff;
  color: var(--text-navy);
  padding: 8px 16px;
  border-radius: 4px;
  z-index: 1000;
}

/* ---------- Header (site-wide, every page) ----------
   Shared Phase 4 cleanup (overlay + mobile clutter): live sits the header
   `position: absolute` over every page's first section (Squarespace's
   reserveSpaceForHeader) -- confirmed on all 10 pages' own
   reference/<slug>/measurements.json (header.box.y == sections[0].box.y
   == 0, with the header's own height -- 82px phone / 80.6562px desktop --
   baked into that first section's own paddingTop). This used to be
   patched per page (body.page-<slug> .site-header { position: absolute })
   in each of home.css/aboutme.css/studywithme.css/privacy-policy.css/
   my-school-of-work.css -- now the shared default, so a new page (Phases
   4-6) needs no header-position override at all, only its own first-
   section top padding (kept page-local -- see each page's own CSS; it
   varies per page's live layout and does NOT reduce to a single shared
   formula, confirmed against aboutme's 135.6px/216.4px and
   my-school-of-work's 109px/250px, neither of which is just the header
   height). Background stays transparent by default (matches home,
   aboutme, studywithme, privacy-policy and, per Phase 4 screenshot checks
   16.09.26, all 5 still-unbuilt pages -- contact, bray-movement-school,
   thank-you-movement-school, bray-new-student-offer,
   bray-new-student-application-form -- every one of them sits the header
   over a plain cream/off-white section, no scrim needed). The one
   exception so far is my-school-of-work, whose header sits over a busy
   full-bleed photo and needs a white-fade legibility scrim -- that stays
   a page-local override (body.page-my-school-of-work .site-header {
   background: linear-gradient(...) } in my-school-of-work.css), opted in
   via the existing body-class-scoped pattern rather than a new markup
   hook -- check a new page's own top screenshot before assuming
   transparent is right. */
.site-header {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  width: 100%;
  z-index: 50;
  background: transparent;
}
/* Header-height formula (Phase 7 shared polish, 17.09.26): the header
   was previously a FIXED height per side of the 800px breakpoint (82px
   phone / 80.6562px desktop), which only matches live exactly at the
   two build_compare reference widths (375, 1440) -- live's own header
   height is actually fluid (Playwright height sweep against
   /aboutme, every 10-50px from 320 to 2200): under 800px it's a clean
   straight line, height = 37px + 12vw (exact match at every mobile
   sample: 320->75.375, 480->94.59, 600->109, 700->121, matching to
   within 0.03px). From 800px up it's fluid again but with a real,
   reproducible dip between ~800 and ~1000px (e.g. 800->71.97,
   900->68.78, 1024->71.5 -- confirmed twice per width, not measurement
   noise) that doesn't reduce to one clean formula across the whole
   desktop range -- reproducing that dip exactly would mean a third
   breakpoint plus its own formula, touching this file and 4 page CSS
   files (contact/offer/application-form/privacy-policy's own
   header-height-derived paddings) for a sub-10px cosmetic gain, which
   is the "risky multi-page rework" the phase brief says to stop short
   of. Used height = 48.96875px + 2.20052vw instead -- fitted from
   1440/1920 (both required checkpoints), which lands EXACTLY on 1440
   (preserves the build_compare reference width unchanged) and on 1024/
   1920 (both required "heroes line up" checkpoints), and is closer
   than the old fixed value everywhere else it was checked including
   inside the dip (worst case ~5.4px off, at 800-820px, down from the
   fixed value's ~8.7px there) -- a net improvement with no case worse
   than before. If a future page needs pixel-perfect 800-1000px
   alignment, the dip needs its own measured breakpoint -- not done
   here. */
.header-inner {
  position: relative;
  height: calc(37px + 12vw); /* live's own fluid formula, see comment above */
  display: flex;
  align-items: center;
  justify-content: flex-end;
  padding: 0 22.5px;
  margin: 0 auto;
}
/* Brand/logo (Phase 4 shared cleanup, 17.09.26 -- collapsed-header
   fix): live's mobile header is Squarespace's own "logo-center-nav-
   right" layout (raw header-inner class name, confirmed live) -- the
   title is centred on the FULL header width, not just the space left of
   the burger (measured live, aboutme phone capture: #site-title's own
   rendered box centres at x=187.5 on a 375 viewport -- exact mid-page).
   Under 800px .header-inner has only one visible flex child
   (.header-actions -- .site-nav is display:none and .brand is pulled
   out of flow here), so position:absolute + left:50% centres the brand
   independently of the burger, matching live exactly rather than only
   centering in the leftover flex space next to it. Font: Lora/700/16px/
   navy on mobile (live measured, was 22px -- wrong at this width);
   desktop resets to static positioning + its own measured 24.4px (live
   aboutme desktop capture, was 22px) in the min-width:800px block
   below. */
.brand {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  font-family: var(--font-heading);
  font-weight: 700;
  font-size: 16px;
  color: var(--text-navy);
  /* Phase 7 tablet-fidelity fix: never let the brand's own text wrap --
     live keeps "Ronan Lewins" on one line at every width (confirmed
     Playwright, aboutme desktop capture, 800-1024px); this build's brand
     wrapped to 2 lines ("Ronan"/"Lewins") at 800-900px because, once it
     re-enters flex flow at the desktop breakpoint below, the default
     flex-shrink:1 + min-width:auto let its box shrink below its own text
     width. flex-shrink:0 is set in the desktop media query below (not
     here -- brand is position:absolute, out of flow, under 800px). */
  white-space: nowrap;
}
.site-nav {
  display: none;
  font-size: 16px; /* live measured (aboutme desktop capture), was 15px */
  /* Phase 7 tablet-fidelity fix: flex-wrap:wrap is ALWAYS on (not gated
     to the 800-875.98px band) -- an earlier version of this fix forced
     flex-wrap:nowrap outside that exact band to mirror live's own real
     wrap window, but this build's brand/icon/CTA widths weren't pixel-
     identical to live's at the time, so this build's OWN content needed
     slightly more room and its true unwrap point landed a little past
     876px (confirmed then: forcing nowrap right at 876px overlapped
     "Podcast" into the Instagram icon). Leaving flex-wrap:wrap on
     unconditionally is self-correcting regardless -- it only wraps when
     this build's OWN content doesn't actually fit, at whatever width
     that turns out to be, so it can never collide, which is why this
     stayed the right approach even after the 18.09.26 defect fix below
     (.site-nav's own column-gap, .header-inner's own padding, and
     .brand's own font-size) closed most of that gap and moved the real
     unwrap point back to exactly 876px, matching live (confirmed via a
     20px-step Playwright sweep, 320-1920, 0 overlaps, single transition
     at 876px). min-width:0 lets this flex item actually shrink
     (the default automatic min-width of a flex item is its content
     size, which would otherwise block shrinking at all). gap uses
     row-gap:0 (not the 28px column-gap) because live packs its own two
     wrapped rows with zero px between them (confirmed: wrapped nav
     height is a constant, exact 54.375px = 2 x its own single-line
     27.1875px, live Playwright measurement) -- the plain `gap: 28px`
     shorthand would apply that same 28px between wrapped ROWS too,
     which both looked wrong and (with a fixed header-inner height) once
     pushed the second row below the header's own bottom edge. */
  flex-wrap: wrap;
  align-content: flex-start;
  row-gap: 0;
  column-gap: 2.8vw;
  min-width: 0;
}
/* Nav item gap formula (defect fix, 18.09.26): live's own header-nav-item
   margin-left formula, read directly from live's CSS bundle
   (.header-layout-nav-right .header-nav-item:not(:first-child){margin-
   left:2.8vw}) -- confirmed against live's own computed styles at 8
   widths from 780 to 1920 (22.4px@800, 24.53px@876, 40.32px@1440,
   53.76px@1920 -- exact every time). Was a flat 28px, this build's own
   fitted-at-1440 guess -- but 2.8vw is 40.32px at 1440, so 28px was never
   right even there; it just wasn't wrong enough to fail box_compare
   (which doesn't check nav-item x positions). column-gap only -- row-gap
   stays a separate, already-correct 0 (see .site-nav's own comment
   above for why). */
.site-nav a { white-space: nowrap; } /* keep each link's own text on one line so wrapping moves a whole link, not mid-phrase */
.header-actions {
  display: flex;
  align-items: center;
  flex-shrink: 0; /* icons + Contact Me never squeeze/wrap, matching live */
}
.icon-link {
  color: var(--text-navy);
  display: inline-flex;
}
.header-contact {
  display: none;
}
/* Mobile header clutter fix (Phase 4 shared cleanup, 17.09.26): under
   800px live shows only the logo + burger in the collapsed header
   (every page's reference/<slug>/measurements.json header.text reads
   "Skip to Content\nRonan Lewins\nOpen Menu" -- no Contact Me, no
   icons); the open .mobile-menu already carries its own separate copies
   of the icon links and the Contact Me button (see header.html), so
   this only needs to hide the COLLAPSED header's own instances.
   Two bugs, one fix: `.header-contact { display: none }` above lost to
   `.btn { display: inline-block }` further down this file at equal
   specificity (one class each, `.btn` wins on source order), and
   `.icon-link { display: inline-flex }` above had no under-800px hide
   at all. Scoping both to their `.header-actions` parent raises their
   specificity to 2 classes, beating `.btn`'s 1 class regardless of
   source order, without touching `.icon-link` inside `.mobile-menu`
   (not inside `.header-actions`, so unaffected). */
.header-actions .icon-link,
.header-actions .header-contact {
  display: none;
}
.burger {
  width: 47px;
  height: 37px;
  background: transparent;
  border: none;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 6px;
  cursor: pointer;
  padding: 1px 6px;
}
.burger span {
  display: block;
  height: 2px;
  background: var(--text-navy);
  width: 100%;
  transition: transform 0.2s ease, opacity 0.2s ease;
}
/* Burger -> close (X) toggle (defect fix, 17.09.26): live's collapsed-
   header icon becomes an X when the menu opens (raw header-burger-btn
   markup swaps a visually-hidden "Open Menu"/"Close Menu" pair and adds
   a `burger--active` class -- confirmed live, aboutme phone capture).
   Driven off the aria-expanded attribute site.js already sets on click
   and on Escape, so there is no separate JS-side class to keep in sync
   -- the icon and the accessible state can never drift apart. */
.burger[aria-expanded="true"] span:nth-child(1) {
  transform: translateY(8px) rotate(45deg);
}
.burger[aria-expanded="true"] span:nth-child(2) {
  opacity: 0;
}
.burger[aria-expanded="true"] span:nth-child(3) {
  transform: translateY(-8px) rotate(-45deg);
}

.mobile-menu {
  position: fixed;
  inset: 0;
  top: calc(37px + 12vw); /* tracks .header-inner's own mobile-only height formula */
  background: var(--hero-cream);
  display: none;
  flex-direction: column;
  align-items: flex-start;
  justify-content: flex-start;
  padding: 0 22.5px 22.5px;
  z-index: 40;
}
/* Open-menu layout (orchestrator, live-measured 17.09.26, aboutme 375x812):
   links centred in the space between header and the bottom group
   (live y 288-438); icons (y 659) and Contact Me (y 736.5, bottom edge
   22.5px above viewport bottom) pinned to the bottom; 52.5px between the
   icon row and the button; links x 26.3, icons x 27.5. */
.mobile-menu nav { margin: auto 0; padding-left: 3.8px; position: relative; top: -8px; }
.mobile-menu-icons { margin: 0 0 52.5px 5px; }
.mobile-menu.open { display: flex; }
/* Open-menu nav (defect fix, 17.09.26): live's links are sans-serif
   body font, regular weight, left-aligned, not the centred bold serif
   this carried before (live measured, aboutme phone capture: Almarai/
   400/31.875px, textAlign left, every link's own box starting at the
   same left edge regardless of text length). line-height is tightened
   on the links themselves (not inherited from the site-wide 1.5) to
   keep the measured ~39px step between lines, since a 32px heading-
   scale font at the default line-height would roughly double it. */
.mobile-menu nav {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 7px;
  font-family: var(--font-body);
  font-weight: 400;
  font-size: 32px;
}
.mobile-menu nav a { line-height: 1; }
.mobile-menu-icons { display: flex; gap: 15px; } /* live measured: 25px icon + 15px gap, was 20px gap */
body.menu-open { overflow: hidden; }
/* Live: the open menu is one cream panel from the top of the viewport -- the header sits on the same cream (beats page scrims). */
body.menu-open .site-header { background: var(--hero-cream) !important; }

/* Breakpoint: nav switches to burger under 800px, per measured probe
   (measurements.json breakpoint probe, run on every page in Phase 0). */
@media (min-width: 800px) {
  .site-nav { display: flex; }
  .header-actions .icon-link,
  .header-actions .header-contact { display: inline-flex; }
  .burger { display: none; }
  /* min-height, not height (Phase 7 tablet-fidelity pass): a fixed
     `height` fought .site-nav's own organic flex-wrap (see .site-nav's
     comment) whenever this build's real content needed a touch more
     room than live's own numbers give -- min-height matches live
     exactly in the normal case (content always fits well inside it,
     confirmed below) and is a no-clip safety net for any width in
     between the two bands where this build's own wrap point drifts a
     few px from live's. */
  .header-inner { justify-content: space-between; padding: 0 2vw; height: auto; min-height: calc(48.96875px + 2.20052vw); } /* height:auto resets the mobile rule's own fixed `height` above -- min-height alone doesn't override a different property */
  .brand { position: static; transform: none; font-size: min(16px + 0.6vw, 24.4px); flex-shrink: 0; } /* live measured (aboutme desktop capture), was 22px; flex-shrink:0 -- see .brand's own comment above */
}
/* Header-inner padding + brand font-size formulas (defect fix, 18.09.26,
   same nav-wrap investigation as .site-nav's column-gap comment above):
   both were flat numbers fitted at 1440 -- 28.8px and 24.4px -- and both
   ARE live's real numbers at 1440 (confirmed: live's own header padding
   container, `.header-announcement-bar-wrapper` which wraps `.header-
   inner` and carries live's real `padding-left/right: var(--pagePadding)`
   with `--pagePadding: 2vw`, measures exactly 28.8px at 1440 -- 2vw*1440
   = 28.8 -- and live's own `.header-title-text` font-size formula, read
   from live's CSS bundle: `min(calc((var(--site-title-font-font-size-
   value) - 1) * 1.2vw + 1rem), max(...))`, with `--site-title-font-font-
   size-value: 1.5`, reduces to `min(16px + 0.6vw, 24.4px)` -- caps at
   24.4px for any width >= 1400px, so 24.4 is exact at 1440 and 1920 too)
   -- but WRONG below 1440, where live's real padding and title shrink
   and this build's fixed values didn't, squeezing the nav's available
   row width and pushing its unwrap point ~60px later than live's own
   (measured: live's real unwrap is 876px on /aboutme, this build's was
   ~938px before this fix). Brand font-size formula confirmed against
   live's own rendered `#site-title` box width at 6 widths (800-1920):
   matches to within 0.1px every time, including the 163.578125px both
   live and this build already shared at 1440/1920. */
/* Header-inner max-width + padding cap removed (defect fix, 18.09.26):
   `.header-inner` used to carry `max-width: var(--max-width)` (1440px)
   with this rule's own padding capped to `min(2vw, 28.8px)` -- a
   deliberate compromise the comment above used to note as "out of
   scope". Live has NO counterpart cap: live's own `.header-inner{width:
   100%;height:inherit}` carries no max-width at all (the conditional
   `.header-width-inset .header-inner{max-width:var(--maxPageWidth)}`
   rule that WOULD cap it only applies when that inset-width header
   style is selected, confirmed absent from live's own header class list
   -- live's header is the full-width style), and its real
   `--pagePadding: 2vw` has no clamp either (single definition in live's
   CSS bundle, no `min()`/`max()`). Playwright-probed directly against
   live/aboutme, 18.09.26: the header's own left edge sits at x=28.8 at
   1440 (2vw exact), x=38.39 at 1920 (2vw exact), x=44 at 2200 (2vw
   exact) -- genuinely uncapped, matching the brief's own "grows to at
   least 2200px" note. Both the max-width and the padding clamp are
   removed above so this build's logo/links/button keep spreading out
   past 1440px the same way. */
/* Header 800-875.98px band (Phase 7 tablet-fidelity pass, full option A,
   [escalation-lifted]): live's own header-nav genuinely wraps its LAST
   link ("Podcast") onto a second internal row ONLY in this narrow band --
   confirmed via direct Playwright measurement against live/aboutme, not
   inferred: nav height doubles from a constant 27.1875px (single line,
   every width from 876px to 1920px, font-size never scales) to a constant
   54.375px (two lines, exactly 2x) across 800-875px, then drops back to
   27.1875px at 876px and stays there. This is what produced the base
   formula's own "~5.4px off at 800-820px" note above -- not curve noise,
   a real, separate breakpoint the earlier pass declined to add. Height
   refitted the same way as the base formula (least-squares against the
   same live sweep, 12 samples 800-875px): calc(54.34375px + 2.20052vw)
   -- worst fit error 0.03px across the whole band, exact at both ends
   (800px and 875px). This build's own nav wraps a little more readily
   than live's in-between 876-888px (see .site-nav's comment) -- min-
   height (not height) absorbs that gracefully, since the formula value
   here is already well above the wrapped content's own ~48px, so it
   never actually has to grow past the formula in practice. */
@media (min-width: 800px) and (max-width: 875.98px) {
  .header-inner { min-height: calc(54.34375px + 2.20052vw); }
}

/* ---------- Buttons (site-wide) ---------- */
.btn {
  font-family: var(--font-button);
  font-weight: 400;
  font-size: 16px;
  display: inline-block;
  padding: 16px 20.8px;
  border-radius: var(--btn-radius);
  border: none;
  cursor: pointer;
  text-align: center;
}
.btn-strong { font-weight: 700; font-size: 16px; padding: 0 20.8px; line-height: 59px; height: 59px; }
.btn-primary {
  background: var(--orange);
  color: #fff;
}
.btn-dark {
  background: #000;
  color: #fff; /* live measured (Contact Me, both header + mobile menu), was #F9AD7D -- unused elsewhere so safe to correct */
}
/* Contact Me button (defect fix, 17.09.26; corrected below): phone menu renders it at 19.2px
   in both the desktop header and the phone open-menu -- larger than the
   16px base .btn size shared with every other button on the site
   (measured both places, aboutme desktop + phone captures). Scoped to
   just these two instances so no other .btn (e.g. home's "Learn More")
   is affected. */
/* Correction 17.09.26 (orchestrator, live computed styles on /aboutme):
   the DESKTOP header Contact Me is orange, 16px, line-height normal
   (121.3x49 box); only the phone open-menu Contact Me is black 19.2px
   (210x53). line-height: normal on both -- body's 1.8 inflated the box. */
.mobile-menu .btn {
  font-size: 19.2px;
  line-height: normal;
  min-width: 210px; /* live .header-menu-cta min-width, 210x53 box */
}
.header-contact { line-height: normal; }
/* Header-actions spacing (defect fix, 18.09.26): read straight off
   live's CSS bundle -- `.header-actions--right .header-actions-action--
   social .icon:not(:first-child){margin-left:2.8vw}` (icon-to-icon) and
   `.header-actions--right .header-actions-action{margin:0 0 0 1.3vw}`
   (the general gap before the next action, here the Contact Me button)
   -- both real vw formulas, not the flat 24px/3px this build had fitted
   at 1440 (which gave 40px/19px, close to 2.8vw@1440=40.32px and
   1.3vw@1440=18.72px but wrong at every other width, and wrong at 1440
   too once `.header-actions`'s own `gap` is removed rather than double-
   counted -- see that rule's own comment). Each margin-left below is
   now the FULL live formula on its own, matching live's real
   architecture (no separate base gap). */
.header-actions .icon-link + .icon-link { margin-left: 2.8vw; }
.header-actions .header-contact { margin-left: 1.3vw; }
.btn-outline {
  background: transparent;
  color: var(--text-navy);
  border: 1.5px solid var(--text-navy);
  font-family: var(--font-body);
  font-weight: 400;
}

/* ---------- Wave divider (generic primitive, reused per section) ----------
   Phase 7 recheck fix (17.09.26, reference/section-dividers-recheck.json):
   live's own divider thickness scales as a clean `width * 0.06` formula at
   every width checked (375/820/1024/1440) -- confirmed to 2 decimal places
   -- not the flat 40px this used to be (too tall at 375, roughly half live's
   amplitude at 1440). `preserveAspectRatio="none"` on each `.wave svg`
   already stretches the 1440x60 viewBox non-uniformly, so switching this to
   `6vw` reproduces live's real scaling without touching any per-instance
   `d` path. */
.wave { line-height: 0; margin: 0; }
.wave svg { width: 100%; height: 6vw; display: block; }
/* Exception (home's hero-video-section divider only, live-confirmed
   17.09.26): this section alone carries Squarespace's `background-width--
   inset` class (confirmed via a live DOM check), which adds
   `var(--sqs-site-gutter)` (2vw) to the divider's own padding -- live
   measures this one instance at a true 8vw (30.0/65.6/81.92/115.2px at
   375/820/1024/1440), exactly 6vw + 2vw at every width checked (the
   arithmetic settles the recheck note's own "verify before encoding"
   flag: 115.2 - 86.4 = 28.8 = 2vw@1440; 30.0 - 22.5 = 7.5 = 2vw@375).
   `.hero-video-section` is home.css's own class for this exact section --
   reused here, not duplicated, so this stays a `.wave`-only change. */
.hero-video-section .wave svg { height: 8vw; }
/* bray-movement-school's hero wave (defect check, recheck report,
   17.09.26): that page's own CSS (bray-movement-school.css) pins this
   instance to a flat 29px/108px, a leftover from before this shared rule
   scaled with viewport -- live measures the SAME plain 6vw as every other
   `.wave` on the site (22.5/49.2/61.44/86.4px), no exception. Overriding
   here with one extra ancestor class (`.bms-hero .bms-hero-wave svg`,
   specificity 0,2,1) beats that page's own `.bms-hero-wave svg` rule
   (0,1,1) regardless of stylesheet load order, so bray-movement-school.css
   itself doesn't need editing (mirrors the technique its own comment
   already documents for why THAT rule was written the way it was). */
.bms-hero .bms-hero-wave svg { height: 6vw; }

/* ---------- Scrolling marquee banners (shared keyframes, defect fix
   18.09.26) ----------
   Live builds all three of these (home's Instagram banner, my-school-of-
   work's "Study With Me" banner, studywithme's "Contact Me" banner) with
   Squarespace's own JS-driven "Marquee" block -- confirmed by fetching
   live's own site.css bundle directly (curl, bypasses the CORS block on
   document.styleSheets): there is no @keyframes/animation rule for
   `.Marquee` anywhere in it. The motion is JS: live renders an SVG
   `<textPath>` per marquee and animates its `startOffset` every
   requestAnimationFrame (Playwright-probed live's own DOM 18.09.26).
   This build already repeats each marquee's text N identical times as
   static filler (the earlier, accepted "does not animate" decision,
   now reversed by this fix) -- since every repeat is pixel-identical,
   translating the WHOLE element by exactly one Nth of its own width
   loops forever with no visible seam, no JS needed. Direction, speed
   and hover-pause below are read straight off live's own DOM data
   attributes (data-animation-direction, data-animation-speed, data-
   paused-on-hover -- present on live's `.Marquee` container on all
   three pages, Playwright-probed 18.09.26), not inferred:
     home (Instagram):      data-animation-direction="right" data-animation-speed="2" data-paused-on-hover="true"
     my-school-of-work (Study With Me): direction="right" speed="1" paused-on-hover="true"
     studywithme (Contact Me): direction="right" speed="2" paused-on-hover="true"
   "direction: right" measured as the text visually sliding rightward
   (live's own startOffset sampled every rAF tick, live/1440px: home
   ~52px/s, my-school-of-work ~60px/s, studywithme ~117-120px/s -- each
   is a clean speed-attribute x 60px/s at every viewport width tested,
   768-1920, once the sample filters out this page's own occasional
   frame-rate dips rather than treating them as a real speed change).
   Each page sets --marquee-shift (-100/N%, N = that marquee's own
   repeat count) and animation-duration (this element's own rendered
   repeat-unit width at that breakpoint, divided by the matching
   target px/s above) on its own selector; this file only holds the
   reusable keyframes + the reduced-motion opt-out. */
@keyframes marquee-scroll-right {
  from { transform: translateX(var(--marquee-shift, -50%)); }
  to { transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
  /* Live doesn't gate its own JS marquee on this at all -- a static
     fallback here is a deliberate improvement, not a mismatch (per this
     defect's own brief). */
  .marquee-heading a,
  .msw-marquee,
  .contact-marquee {
    animation: none !important;
  }
}

/* ---------- Generic page/section layout primitives ----------
   For plain content pages (Phase 4+) that aren't the home page's bespoke
   section-by-section build. Not pixel-measured against a specific live
   page -- a starting scaffold, safe to override per-page. */
.page-wrap {
  max-width: var(--max-width);
  margin: 0 auto;
  padding: 0 22.5px;
}
@media (min-width: 800px) {
  .page-wrap { padding: 0 28.8px; }
}
main { display: block; }
.section { padding: 40px 22.5px; }
@media (min-width: 800px) {
  .section { padding: 86px 28.8px; }
}
h1, h2, h3 {
  font-family: var(--font-heading);
  font-weight: 700;
  margin: 0 0 20px;
}
p { margin: 0 0 24px; }

/* ---------- Footer (site-wide, every page) ----------
   Geometry measured against live via Playwright (WEBDRIVER_SPOOF,
   settled post-fade-in) on home/aboutme/bray-new-student-offer, 17.09.26
   -- identical on all three (footer height 358.25 phone / 297 desktop,
   confirmed against each page's reference measurements.json too --
   live's own `<footer>` element, which does NOT include its Cookie
   Preferences control -- that's a separate sibling, see below). Two-
   column row at >=800px (nav left, copyright+icons right, both top-
   aligned, left edge = standard 28.8px section padding); single stacked
   column, left-aligned nav, at <800px. See tools/partials/footer.html's
   header comment for the desktop nav re-order and the Cookie
   Preferences note. */
.site-footer {
  background: var(--body-bg);
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-top: auto; /* live's own #footer-sections rule, verbatim -- see body's own comment above for the sticky-footer flex context this depends on */
  /* padding-bottom 16px + 37.53125px (Phase 7 shared polish, 17.09.26):
     moving .cookie-pref-link outside <footer> (see footer.html's own
     comment) dropped the footer's own measured height by exactly that
     amount site-wide (build_compare's Footer row went GREEN -> AMBER by
     the same 37.53125px/47.84375px on all 10 pages, confirmed -- the
     old cookie-pref-btn's own line-height plus the flex gap that used
     to separate it from .footer-columns). Restored here as extra
     bottom padding so <footer>'s own box still lands on build_compare's
     measured-from-live target (358.25px/297px) -- purely a box-height
     fix; the cookie link's own visible position barely moves, since it
     already carries its own top-of-flow placement right after this
     padding ends. */
  padding: 27px 22.5px 53.53125px;
  gap: 20px;
}
.footer-columns {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 46px;
  width: 100%;
}
.footer-nav {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 11px;
  font-size: 16px;
  width: 100%;
}
.footer-right {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 11px;
  width: 100%;
}
.footer-copyright { font-size: 19.9px; text-align: center; margin: 0; }
.footer-icons { display: flex; justify-content: center; gap: 18px; margin: 0; }
.icon-boxed {
  width: 32px;
  height: 32px;
  border: 1.5px solid var(--text-navy);
  border-radius: 6px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.icon-filled { background: var(--text-navy); color: #fff; }
/* Cookie Preferences link -- a sibling right after <footer>, not a
   footer child (see tools/partials/footer.html's own comment). Plain
   text, no button chrome: no background/border/padding, matching live's
   own control exactly (measured: 12px Almarai 400, color == --text-navy,
   no background/border/padding). Same 22.5px/28.8px page gutter as
   everything else on the page.

   position:absolute (defect fix, 18.09.26, footer-margin investigation):
   was position:static/normal flow -- fine on its own, but as a normal-
   flow flex child of `body` (see body's own sticky-footer comment) its
   own ~20px of height competed with `.site-footer`'s new `margin-top:
   auto` for the same leftover vertical space, silently cancelling the
   footer-position fix on any page short enough for the auto margin to
   matter. Live avoids exactly this: measured directly (Playwright DOM
   walk of #siteWrapper's own children), live's real cookie-preferences
   control lives inside a dedicated sibling div that is ALWAYS a genuine
   zero-height flex item (rect height 0, confirmed even after a consent
   decision exists) -- the visible pill is positioned out of that div's
   own flow. Reproduced here the simpler way available without adding a
   markup wrapper: position:absolute removes `.cookie-pref-link` from
   `body`'s flex flow entirely (same net effect as live's zero-height
   item -- neither contributes anything to the sticky-footer slack
   calculation), anchored to `body` itself (now position:relative, see
   body's own rule above -- NOT `.site-footer`, which is a sibling, not
   an ancestor, of this element, so can't be its containing block). Since
   `.cookie-pref-link` is now the only out-of-flow child, `body`'s own
   rendered height always equals exactly `main` + `.site-footer`'s
   stacked height (whichever of that or the 100vh min-height is larger)
   -- so `bottom: 0` on `body` lands at exactly the footer's own bottom
   edge in every case: the normal (tall-content) case where body's
   height is its natural content height, AND the short-content edge case
   this whole fix targets, where body's height is pinned to the 100vh
   floor and the footer has already been pushed by its own margin-top:
   auto to end exactly there.

   Position + box (flag 12 defect fix, 18.09.26): this used to be
   `top: 100%` -- sitting BELOW the footer's bottom edge, padding-bottom
   pushing the text further down. Live's own equivalent (only reachable
   post-consent-decision -- `section.manage-cookies-bar.PILL`, a sibling
   of the cookie banner inside `.cookie-banner-manager`, not visible on
   a fresh visit; Playwright-probed live/aboutme by clicking "Accept"
   and reloading, 18.09.26, WEBDRIVER_SPOOF) is `position: absolute;
   bottom: 0; left: 0` (computed, flat, NOT `--pagePadding`/vw-based --
   confirmed identical at 375/768/800/1440/1920), height 30px,
   background rgb(252,249,247), padding 7px 10px, border-radius 0
   despite the "PILL" class name (that colour is live's own body-
   background token -- effectively invisible against a matching page/
   footer background, which is exactly what this build already has via
   `--body-bg`, so no visible chrome was ever needed here; kept for
   exactness anyway). Its rendered LEFT edge measured 60px from the
   viewport edge at every width tested -- that 60px comes from the
   `.cookie-banner-manager` ancestor's own offset, not from the pill's
   own `left: 0`, so this build reproduces the final 60px directly
   rather than the two-layer mechanism that produces it live. `bottom: 0`
   (not `top: 100%`) is the one-word fix that matches live: it puts
   THIS element's own bottom edge, not its top, at body's bottom edge --
   so the pill now overlaps the footer's own last 30px the same way
   live's does, instead of sitting in a new row below it. */
.cookie-pref-link {
  display: flex;
  align-items: center;
  position: absolute;
  bottom: 0;
  left: 60px;
  height: 30px;
  box-sizing: border-box;
  background: rgb(252, 249, 247); /* live's own pill background -- == --body-bg, invisible against this site's matching footer/body background too; kept for exactness */
  border: none;
  padding: 7px 10px;
  margin: 0;
  color: var(--text-navy);
  font-family: var(--font-body);
  font-size: 12px;
  line-height: 1.4;
  text-align: left;
  cursor: pointer;
}
.cookie-pref-link:empty { cursor: default; }

/* Footer desktop geometry -- REAL breakpoint 768px, not 800px (Phase 7
   tablet-fidelity pass, [escalation-lifted]): live's footer is its OWN
   Squarespace fluid-engine section (confirmed via the raw live HTML's
   inline fluid-engine <style> block, wayfinder/raw/aboutme.html -- 4
   collectionlink blocks for the nav links, 1 html-block for the
   copyright, 1 socialaccountlinks block for the icons, each its own grid
   item, not a flex row) that switches from an 8-column/9-row mobile grid
   to a 24-column/5-row desktop grid at `@media (min-width: 768px)` -- a
   DIFFERENT breakpoint from the header's own 800px (confirmed by direct
   measurement, not assumed: the header's nav genuinely doesn't exist
   yet at 768px, staying on the mobile burger formula through 799px).
   Previous footer used a single 800px breakpoint with 1440-fixed values
   (width:453.5px, padding:52px 28.8px 90.84375px, etc) -- exactly right
   at 1440 by construction, wrong everywhere else (measured defect:
   footer height -92px vs live at 820, +44px at 1024).

   Live's real total footer height is `max(33vh, contentHeight(vw))` --
   a genuine CSS min-height, not a fixed box: contentHeight comes from
   its grid's 5 rows (each `container-width * 0.0215` tall, container-
   width = min(1400px, 96vw)) + 4 row-gaps (11px) + 6.6vmax content-
   wrapper padding (the same "small section" 3.3vmax-per-side convention
   bray-new-student-offer.css already uses). Fitted against 4 live
   Playwright data points spanning both regimes (768x900=297px [vh-
   dominant, matches this build's own already-correct 1440x900 target],
   820x1180=389.390625px [vh-dominant], 1024x768=253.4375px [content-
   dominant], plus a direct sweep isolating each term at a fixed
   height=400 across 12 widths 768-2200) -- every point matches to
   within 0.05px, not estimated. Reproduced directly as one min-height
   formula on .site-footer (this build's flex layout isn't literally
   live's per-block grid, but min-height + justify-content:center on the
   footer's single child gives the same measurable box + the same
   vertical-centre placement live's own "vertical-alignment--middle"
   section class produces). .footer-nav/.footer-right go from a fixed
   453.5px to a fluid width that still resolves to 453.5px at 1440 (see
   .footer-nav's own comment below for why both columns keep sharing
   that one value) and .footer-copyright's font-size to the site's own
   existing "large text" fluid scale (min(0.48vw + 1rem, 22.72px) --
   same X=1.4 Squarespace formula bray-new-student-offer.css documents,
   landing on the same 22.72px this page already had at 1440). */
@media (min-width: 768px) {
  .site-footer {
    min-height: max(33vh, calc(min(1400px, 96vw) * 0.1075 + 44px + 6.6vmax));
    justify-content: center;
    padding: 2vw;
  }
  .footer-columns { flex-direction: row; align-items: flex-start; gap: 11px; }
  /* Both columns share ONE fluid width formula (fix loop, this pass):
     the pre-existing fixed 453.5px on BOTH .footer-nav and .footer-right
     wasn't a coincidence to preserve -- .footer-nav's own width, at
     453.5px, is really acting as a SPACER that pushes .footer-right (and
     so the copyright <p> box_compare grades) to the exact x box_compare
     already passed at 1440 (28.8px padding + 453.5 + 11px gap = 493.3px,
     live's own measured copyright x at 1440 exactly) -- .footer-nav's
     OWN width was never itself graded (box_compare doesn't cover plain
     nav links). A first attempt at this fix gave each column its live-
     measured REAL width instead (nav ~221px at 1440) and broke that
     spacer, regressing box_compare's copyright-paragraph check (flagged,
     dx=342 at 1440) -- reverted. min(453.5px, calc(32vw - 7.4px)) keeps
     the exact same 453.5px at 1440/1440+ (so the spacer, and the 1440
     box_compare check, are unchanged) while shrinking both columns
     together at tablet widths -- fitted against live's own copyright-
     paragraph WIDTH at 11 points (768-1440px, matches within 0.06px
     every time) so it lands on the same real live curve, not a guess. */
  .footer-nav { width: min(453.5px, calc(32vw - 7.4px)); gap: 17px; }
  /* live re-orders the last two links visually at this breakpoint only
     (DOM order stays Home / My School of Work / Contact / Bray New
     Student Offer -- see footer.html's header comment) */
  .footer-nav a:nth-child(3) { order: 4; }
  .footer-nav a:nth-child(4) { order: 3; }
  .footer-right { width: min(453.5px, calc(32vw - 7.4px)); gap: clamp(24px, 4vw, 54px); }
  .footer-copyright { font-size: min(0.48vw + 1rem, 22.72px); }
}
/* .cookie-pref-link's old >=800px override (padding: 0 28.8px 20px,
   matching the page's own desktop gutter) removed (flag 12 defect fix,
   18.09.26): live's real pill is flat/non-responsive -- position,
   height and padding measured identical at 375/768/800/1440/1920, see
   that rule's own comment -- so this build no longer needs a desktop
   variant either. */
