/* ===== tokens.css ===== */
/* ============================================================================
   YO1 Design Tokens — the single source of truth.
   Every value here was measured from the live site's rendered computed styles
   (see docs/DESIGN_SYSTEM.md). No component may hard-code a colour or size.
   ========================================================================== */
:root {
  /* ---- Colour: brand ---------------------------------------------------- */
  --c-brand:          #294C4B;   /* deep teal — primary; matches meta theme-color */
  --c-brand-dark:     #203B3B;
  --c-brand-a70:      rgba(41, 76, 75, .7);
  --c-sage:           #6C8F7D;   /* secondary */
  --c-green:          #69A770;   /* accent — absorbs #69A76F and #6CA773 */
  --c-navy:           #042441;   /* absorbs #042541 */
  --c-navy-band:      #0E273F;
  --c-navy-deep:      #0A151E;   /* .btn-forlink — Book Your Stay */
  --c-near-black:     #0A151E;

  /* ---- Colour: text ----------------------------------------------------- */
  --c-ink:            #4B5761;   /* body text — dominant, 1631 elements */
  --c-ink-black:      #000000;   /* band + connect h2 — live renders these black */
  --c-ink-muted:      #7A8894;
  --c-ink-slate:      #34464C;
  --c-ink-grey:       #64696E;
  --c-on-dark:        #FFFFFF;
  --c-heading-alt:    #89B59E;   /* h4 in the connect block */

  /* ---- Colour: surface -------------------------------------------------- */
  --c-surface:        #FFFFFF;
  --c-surface-warm:   #F5F3EF;   /* cream section background */
  --c-surface-foot:   #F5F5F5;   /* footer */
  --c-surface-grey:   #F9F9F9;
  --c-surface-band:   #EAEBEC;   /* press-coverage band; absorbs #E9EAEB */
  --c-surface-black:  #000000;

  /* ---- Colour: line & overlay ------------------------------------------- */
  --c-hairline:       #EAEBEC;
  --c-rule:           #BBBDBF;
  --c-overlay-60:     rgba(0, 0, 0, .6);
  --c-overlay-50:     rgba(0, 0, 0, .5);
  --c-overlay-20:     rgba(0, 0, 0, .2);

  /* ---- Colour: state ---------------------------------------------------- */
  --c-focus:          var(--c-brand);
  --c-link:           var(--c-sage);    /* G-3: live rich-text links are #6C8F7D */
  --c-link-hover:     var(--c-brand);
  --c-link-utility:   #586066;          /* header phone — intentional bold variant */
  --c-disabled:       #BBBDBF;

  /* ---- Type ------------------------------------------------------------- */
  --ff-body: Poppins, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  --ff-icon: "Font Awesome 5 Pro";

  --fw-light:    300;
  --fw-regular:  400;
  --fw-medium:   500;   /* newsletter h2 — live renders STAY INFORMED at 500 */
  --fw-semibold: 600;   /* live h2 renders at 600; we load it rather than synthesise */
  --fw-bold:     700;

  /* Scale collapses the live site's em-compounding artefacts
     (12.25 / 15.4 / 19.6px) onto real steps. */
  --fs-3xs: 10px;
  --fs-2xs: 12px;
  --fs-xs:  13px;
  --fs-sm:  14px;   /* base — 1760 elements */
  --fs-md:  16px;
  --fs-lg:  18px;
  --fs-xl:  21px;
  --fs-2xl: 25px;
  --fs-3xl: 28px;

  /* G-5 — line-height is per-role and in fixed px, matching live. A single
     unitless multiplier was correct only at the 14px base and wrong everywhere
     else (168 elements). Values below are measured from the live computed styles. */
  --lh-tight:   1.1;      /* headings — 28/30.8, 25/27.5, 21/23.1, 18/19.8 all = 1.1 */
  --lh-body:    19.6px;   /* body copy, links, labels, strong  (14px text) */
  --lh-table:   20px;     /* table cells                        (14px text) */
  --lh-review-title: 20px;/* .review_data_title                 (14px text) */
  --lh-review-body:  18px;/* .review_data_review                (12px text) */
  --lh-button:  13px;     /* .btn / card CTA                             */
  --lh-footer-link: 20px; /* footer top-row links                        */
  --lh-nav:     42px;     /* main nav links — with 22.5px padding = the 90px bar */

  --ls-default: .2px;
  --ls-link:    .1px;

  /* ---- Spacing (4px-derived; every observed live value maps on) ---------- */
  --sp-0: 0;
  --sp-1: 5px;
  --sp-2: 10px;
  --sp-3: 15px;    /* grid gutter (half) */
  --sp-4: 20px;    /* paragraph rhythm, button padding-x */
  --sp-5: 30px;    /* widget vertical padding, gutter (full) */
  --sp-6: 40px;
  --sp-7: 60px;
  --sp-8: 80px;    /* full-bleed section padding-x (desktop) */
  --sp-9: 120px;

  /* ---- Layout ----------------------------------------------------------- */
  --container-ratio: 85%;      /* measured: content = 85% of viewport … */
  --container-max:   1340px;   /* … capped at 1340px (cap engages at 1576px) */
  --container-narrow: 65%;     /* two-col widgets — 936@1440, 832@1280, 1248@1920 */
  --container-therapy: 67%;    /* Healing Therapies — 965@1440, 858@1280, 1286@1920; capped at 1340 */
  --container-cards:   70%;    /* 2-col listing cards — 1008@1440, 896@1280, 1340(cap)@1920; capped at 1340 */
  --container-wide:    80%;    /* enquiry form + card carousels — 1152@1440, 1024@1280, 1536@1920; NO cap */
  --container-pills:   96%;    /* gallery filter bar — 1229@1280, 1340(cap)@1440 and @1920.
                                  Gallery-only: every other page's content_wrapper is 100%/none. */
  --gutter:          var(--sp-3);
  --bleed-pad:       var(--sp-8);

  --header-h:      90px;
  /* Below 1024 the header is sticky and auto-height: 125px from 601 to 1023, and
     144.59px on a phone once the nav wraps. 145 is the worst case, measured. */
  --header-h-sm:   145px;
  /* Everything pinned over the top of the page. On desktop the promo strip is a CHILD
     of the fixed header and overflows its 90px box, so the real overlay is 90 + 55 =
     145 — the same total `.outer-page` reserves as flow space. Below 1024 the strip is
     inside the header's own auto height, so `--header-h-sm` is already the full
     figure. Measured at scroll 2000: 145 at 1440, 125 at 768, 145 at 390. */
  --chrome-h:      calc(var(--header-h) + var(--promo-h));
  /* Where an in-page anchor stops: clear of the chrome, with a line of air. */
  --anchor-offset: calc(var(--chrome-h) + var(--sp-4));
  --promo-h:       55px;
  --hero-h:        735px;   /* video band; 90 + 55 + 735 = 880 total */
  --logo-max-w:    200px;

  /* ---- Component tokens added by Pass 4E ---------------------------------
     Only values that a consistency fix needed. Each replaces a literal that was
     duplicated across two divergent implementations of the same component; nothing
     unrelated was tokenised in this pass.

     CTA — the authored `a.btn.txt-wdgt-btn`. Measured 96x36 on the live capture.
     Previously these literals existed only inside an inner-page-scoped rule, which
     is why the homepage fell through to the generic .btn (DS-04). */
  --cta-pad: 12px 20px;
  --cta-fs:  var(--fs-2xs);

  /* FAQ — the shared interaction language of both disclosure components (DS-05).
     Measured from live's `simp-custom-info` rules, which are the 129-instance
     treatment; the homepage accordion had drifted to its own values. The two
     components keep their own density and measure — see the note in accordion.css. */
  --faq-q-size:   20px;
  --faq-q-size-m: 18px;   /* both components step to this below 1024 */
  --faq-q-lh:     1.1;
  --faq-q-lh-m:   30px;   /* the mobile heading line-height the 129 index rows use */
  --faq-q-weight: var(--fw-regular);
  --faq-q-color:  var(--c-ink-black);
  --faq-line:     1px solid grey;
  --faq-icon:     #414042;
  --faq-open:     #6CA773;
  --faq-panel-pt: var(--sp-4);
  /* Row rhythm, measured on the 129-row implementation: 20px above the question,
     12px between question and divider, 10px below. Pitch 20+35+10 = 65px. */
  --faq-q-pb:      12px;
  --faq-item-pt:   var(--sp-4);
  --faq-item-pb:   var(--sp-2);
  /* Below 1024 the authored rows measured 15/15 on 13 of the 14 pages. The FAQ page
     is the exception only because it carries no captured mobile values at all
     (the DS-03 dataless-page set), so the majority value is the canonical one. */
  --faq-item-pt-m: var(--sp-3);
  --faq-item-pb-m: var(--sp-3);
  --faq-icon-size: 20px;

  /* ---- Radius ----------------------------------------------------------- */
  --radius-0:    0;      /* the design language is square: 2273/2289 elements */
  --radius-sm:   4px;
  --radius-pill: 50%;

  /* ---- Shadow (only the 5 that actually render on the live site) --------- */
  --shadow-sm:    2px 2px 3px rgba(0, 0, 0, .2);
  --shadow-md:    0 12px 24px rgba(0, 0, 0, .2);
  --shadow-brand: 0 10px 28px -12px #294C4B;
  --shadow-input: inset 0 1px 1px rgba(0, 0, 0, .075);

  /* ---- Z-index (from measured live values) ------------------------------ */
  --z-base:    0;
  --z-raised:  10;
  --z-promo:   205;
  --z-header:  1030;
  --z-modal:   1050;
  --z-consent: 9999;

  /* ---- Motion ----------------------------------------------------------- */
  --dur-fast: 150ms;
  --dur-base: 250ms;
  --dur-slow: 400ms;
  --ease:     cubic-bezier(.4, 0, .2, 1);
  --carousel-interval: 3000ms;  /* live JSSOR data-slideinterval */
}

/* Breakpoints are documented here for reference; media queries need literals.
   --bp-sm 480px · --bp-md 768px · --bp-lg 1024px · --bp-xl 1280px · --bp-2xl 1576px */

@media (max-width: 1023px) {
  :root { --chrome-h: var(--header-h-sm); }
}

@media (prefers-reduced-motion: reduce) {
  :root { --dur-fast: 0ms; --dur-base: 0ms; --dur-slow: 0ms; }
}

/* ===== reset.css ===== */
/* Minimal, modern reset. */
*, *::before, *::after { box-sizing: border-box; }

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

body {
  margin: 0;
  font-family: var(--ff-body);
  font-size: var(--fs-sm);
  font-weight: var(--fw-regular);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-default);
  color: var(--c-ink);
  background: var(--c-surface);
  -webkit-font-smoothing: antialiased;
}

/* Live site sets margin:0 on h1–h4; vertical rhythm comes from the widget. */
h1, h2, h3, h4, h5, h6 {
  margin: 0;
  font-weight: var(--fw-bold);
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-default);
  color: inherit;
}
h1 { font-size: var(--fs-3xl); }
h2 { font-size: var(--fs-2xl); font-weight: var(--fw-semibold); }
h3, h4 { font-size: var(--fs-lg); }
h5 { font-size: var(--fs-md); }
h6 { font-size: var(--fs-sm); }

p { margin: 0 0 var(--sp-4); }
p:last-child { margin-bottom: 0; }

ul, ol { margin: 0 0 var(--sp-4); padding-left: var(--sp-4); }
li { line-height: var(--lh-body); }

/* G-3 — measured from live rich-text links: 14px / 19.6px / weight 400 /
   letter-spacing 0.2px / #6C8F7D / no underline.
   The previous reset used 700 / 0.1px / #294C4B, generalised from a single sample
   (the header phone number, which genuinely IS bold). Intentional bold variants —
   the header phone and the main nav links — set their own values. */
a {
  color: var(--c-link);
  font-weight: var(--fw-regular);
  letter-spacing: var(--ls-default);
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease);
}
a:hover { color: var(--c-link-hover); }

strong, b { font-weight: var(--fw-bold); }
small { font-size: var(--fs-2xs); }

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

button, input, select, textarea { font: inherit; color: inherit; letter-spacing: inherit; }
button { cursor: pointer; background: none; border: 0; padding: 0; }

table { border-collapse: collapse; width: 100%; }
th, td { text-align: left; font-size: var(--fs-sm); line-height: var(--lh-table); }  /* G-5: 20px */
th { font-weight: var(--fw-bold); }

hr { border: 0; border-top: 1px solid var(--c-rule); margin: var(--sp-5) 0; }

/* Restores the keyboard affordance the live site removes entirely (LSI-08). */
:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 2px;
}
/* Never leave a focusable element with no indicator, even inside dark bands. */
.on-dark :focus-visible { outline-color: var(--c-on-dark); }

[hidden] { display: none !important; }

/* ===== vendor/intl-tel-input.css ===== */
/* Vendored intl-tel-input v24.5.0 (MIT) — see the licence header in
   scripts/vendor/intl-tel-input.js.

   One edit from upstream: the flag sprites were referenced as `../img/…`,
   relative to this file at styles/vendor/. This file is concatenated into
   styles/index.css by tools/bundle-css.mjs, and a url() resolves against the
   stylesheet it ends up in — so from there `../img/` pointed at /img/ and every
   flag 404'd. Rewritten to `vendor/img/`, which resolves correctly from the
   bundle and keeps working under a sub-path deployment. */
:root{--iti-hover-color:rgba(0, 0, 0, 0.05);--iti-border-color:#ccc;--iti-dialcode-color:#999;--iti-dropdown-bg:white;--iti-spacer-horizontal:8px;--iti-flag-height:12px;--iti-flag-width:16px;--iti-border-width:1px;--iti-arrow-height:4px;--iti-arrow-width:6px;--iti-triangle-border:calc(var(--iti-arrow-width) / 2);--iti-arrow-padding:6px;--iti-arrow-color:#555;--iti-path-flags-1x:url("vendor/img/flags.webp");--iti-path-flags-2x:url("vendor/img/flags@2x.webp");--iti-path-globe-1x:url("vendor/img/globe.webp");--iti-path-globe-2x:url("vendor/img/globe@2x.webp");--iti-flag-sprite-width:3904px;--iti-flag-sprite-height:12px;--iti-mobile-popup-margin:30px}.iti{position:relative;display:inline-block}.iti *{box-sizing:border-box}.iti__hide{display:none}.iti__v-hide{visibility:hidden}.iti__a11y-text{width:1px;height:1px;clip:rect(1px,1px,1px,1px);overflow:hidden;position:absolute}.iti input.iti__tel-input,.iti input.iti__tel-input[type=tel],.iti input.iti__tel-input[type=text]{position:relative;z-index:0;margin:0!important}.iti__country-container{position:absolute;top:0;bottom:0;padding:var(--iti-border-width)}.iti__selected-country{z-index:1;position:relative;display:flex;align-items:center;height:100%;background:0 0;border:0;margin:0;padding:0;font-family:inherit;font-size:inherit;color:inherit;border-radius:0;font-weight:inherit;line-height:inherit;text-decoration:none}.iti__selected-country-primary{display:flex;align-items:center;height:100%;padding:0 var(--iti-arrow-padding) 0 var(--iti-spacer-horizontal)}.iti__arrow{margin-left:var(--iti-arrow-padding);width:0;height:0;border-left:var(--iti-triangle-border) solid transparent;border-right:var(--iti-triangle-border) solid transparent;border-top:var(--iti-arrow-height) solid var(--iti-arrow-color)}[dir=rtl] .iti__arrow{margin-right:var(--iti-arrow-padding);margin-left:0}.iti__arrow--up{border-top:none;border-bottom:var(--iti-arrow-height) solid var(--iti-arrow-color)}.iti__dropdown-content{border-radius:3px;background-color:var(--iti-dropdown-bg)}.iti--inline-dropdown .iti__dropdown-content{position:absolute;z-index:2;margin-top:3px;margin-left:calc(var(--iti-border-width) * -1);border:var(--iti-border-width) solid var(--iti-border-color);box-shadow:1px 1px 4px rgba(0,0,0,.2)}.iti__search-input{width:100%;border-width:0;border-radius:3px}.iti__search-input+.iti__country-list{border-top:1px solid var(--iti-border-color)}.iti__country-list{list-style:none;padding:0;margin:0;overflow-y:scroll;-webkit-overflow-scrolling:touch}.iti--inline-dropdown .iti__country-list{max-height:185px}.iti--flexible-dropdown-width .iti__country-list{white-space:nowrap}@media (max-width:500px){.iti--flexible-dropdown-width .iti__country-list{white-space:normal}}.iti__country{display:flex;align-items:center;padding:8px var(--iti-spacer-horizontal);outline:0}.iti__dial-code{color:var(--iti-dialcode-color)}.iti__country.iti__highlight{background-color:var(--iti-hover-color)}.iti__country-list .iti__flag,.iti__country-name{margin-right:var(--iti-spacer-horizontal)}[dir=rtl] .iti__country-list .iti__flag,[dir=rtl] .iti__country-name{margin-right:0;margin-left:var(--iti-spacer-horizontal)}.iti--allow-dropdown .iti__country-container:not(:has(+ input[disabled])):not(:has(+ input[readonly])):hover,.iti--allow-dropdown .iti__country-container:not(:has(+ input[disabled])):not(:has(+ input[readonly])):hover button{cursor:pointer}.iti--allow-dropdown .iti__country-container:not(:has(+ input[disabled])):not(:has(+ input[readonly])) .iti__selected-country-primary:hover,.iti--allow-dropdown .iti__country-container:not(:has(+ input[disabled])):not(:has(+ input[readonly])) .iti__selected-country:has(+ .iti__dropdown-content:hover) .iti__selected-country-primary{background-color:var(--iti-hover-color)}.iti .iti__selected-dial-code{margin-left:4px}[dir=rtl] .iti .iti__selected-dial-code{margin-left:0;margin-right:4px}.iti--container{position:fixed;top:-1000px;left:-1000px;z-index:1060;padding:var(--iti-border-width)}.iti--container:hover{cursor:pointer}.iti--fullscreen-popup.iti--container{background-color:rgba(0,0,0,.5);top:0;bottom:0;left:0;right:0;position:fixed;padding:var(--iti-mobile-popup-margin);display:flex;flex-direction:column;justify-content:flex-start}.iti--fullscreen-popup .iti__dropdown-content{display:flex;flex-direction:column;max-height:100%;position:relative}.iti--fullscreen-popup .iti__country{padding:10px 10px;line-height:1.5em}.iti__flag{--iti-flag-offset:100px;height:var(--iti-flag-height);width:var(--iti-flag-width);border-radius:1px;box-shadow:0 0 1px 0 #888;background-image:var(--iti-path-flags-1x);background-repeat:no-repeat;background-position:var(--iti-flag-offset) 0;background-size:var(--iti-flag-sprite-width) var(--iti-flag-sprite-height)}.iti__ac{--iti-flag-offset:0px}.iti__ad{--iti-flag-offset:-16px}.iti__ae{--iti-flag-offset:-32px}.iti__af{--iti-flag-offset:-48px}.iti__ag{--iti-flag-offset:-64px}.iti__ai{--iti-flag-offset:-80px}.iti__al{--iti-flag-offset:-96px}.iti__am{--iti-flag-offset:-112px}.iti__ao{--iti-flag-offset:-128px}.iti__ar{--iti-flag-offset:-144px}.iti__as{--iti-flag-offset:-160px}.iti__at{--iti-flag-offset:-176px}.iti__au{--iti-flag-offset:-192px}.iti__aw{--iti-flag-offset:-208px}.iti__ax{--iti-flag-offset:-224px}.iti__az{--iti-flag-offset:-240px}.iti__ba{--iti-flag-offset:-256px}.iti__bb{--iti-flag-offset:-272px}.iti__bd{--iti-flag-offset:-288px}.iti__be{--iti-flag-offset:-304px}.iti__bf{--iti-flag-offset:-320px}.iti__bg{--iti-flag-offset:-336px}.iti__bh{--iti-flag-offset:-352px}.iti__bi{--iti-flag-offset:-368px}.iti__bj{--iti-flag-offset:-384px}.iti__bl{--iti-flag-offset:-400px}.iti__bm{--iti-flag-offset:-416px}.iti__bn{--iti-flag-offset:-432px}.iti__bo{--iti-flag-offset:-448px}.iti__bq{--iti-flag-offset:-464px}.iti__br{--iti-flag-offset:-480px}.iti__bs{--iti-flag-offset:-496px}.iti__bt{--iti-flag-offset:-512px}.iti__bw{--iti-flag-offset:-528px}.iti__by{--iti-flag-offset:-544px}.iti__bz{--iti-flag-offset:-560px}.iti__ca{--iti-flag-offset:-576px}.iti__cc{--iti-flag-offset:-592px}.iti__cd{--iti-flag-offset:-608px}.iti__cf{--iti-flag-offset:-624px}.iti__cg{--iti-flag-offset:-640px}.iti__ch{--iti-flag-offset:-656px}.iti__ci{--iti-flag-offset:-672px}.iti__ck{--iti-flag-offset:-688px}.iti__cl{--iti-flag-offset:-704px}.iti__cm{--iti-flag-offset:-720px}.iti__cn{--iti-flag-offset:-736px}.iti__co{--iti-flag-offset:-752px}.iti__cr{--iti-flag-offset:-768px}.iti__cu{--iti-flag-offset:-784px}.iti__cv{--iti-flag-offset:-800px}.iti__cw{--iti-flag-offset:-816px}.iti__cx{--iti-flag-offset:-832px}.iti__cy{--iti-flag-offset:-848px}.iti__cz{--iti-flag-offset:-864px}.iti__de{--iti-flag-offset:-880px}.iti__dj{--iti-flag-offset:-896px}.iti__dk{--iti-flag-offset:-912px}.iti__dm{--iti-flag-offset:-928px}.iti__do{--iti-flag-offset:-944px}.iti__dz{--iti-flag-offset:-960px}.iti__ec{--iti-flag-offset:-976px}.iti__ee{--iti-flag-offset:-992px}.iti__eg{--iti-flag-offset:-1008px}.iti__eh{--iti-flag-offset:-1024px}.iti__er{--iti-flag-offset:-1040px}.iti__es{--iti-flag-offset:-1056px}.iti__et{--iti-flag-offset:-1072px}.iti__fi{--iti-flag-offset:-1088px}.iti__fj{--iti-flag-offset:-1104px}.iti__fk{--iti-flag-offset:-1120px}.iti__fm{--iti-flag-offset:-1136px}.iti__fo{--iti-flag-offset:-1152px}.iti__fr{--iti-flag-offset:-1168px}.iti__ga{--iti-flag-offset:-1184px}.iti__gb{--iti-flag-offset:-1200px}.iti__gd{--iti-flag-offset:-1216px}.iti__ge{--iti-flag-offset:-1232px}.iti__gf{--iti-flag-offset:-1248px}.iti__gg{--iti-flag-offset:-1264px}.iti__gh{--iti-flag-offset:-1280px}.iti__gi{--iti-flag-offset:-1296px}.iti__gl{--iti-flag-offset:-1312px}.iti__gm{--iti-flag-offset:-1328px}.iti__gn{--iti-flag-offset:-1344px}.iti__gp{--iti-flag-offset:-1360px}.iti__gq{--iti-flag-offset:-1376px}.iti__gr{--iti-flag-offset:-1392px}.iti__gt{--iti-flag-offset:-1408px}.iti__gu{--iti-flag-offset:-1424px}.iti__gw{--iti-flag-offset:-1440px}.iti__gy{--iti-flag-offset:-1456px}.iti__hk{--iti-flag-offset:-1472px}.iti__hn{--iti-flag-offset:-1488px}.iti__hr{--iti-flag-offset:-1504px}.iti__ht{--iti-flag-offset:-1520px}.iti__hu{--iti-flag-offset:-1536px}.iti__id{--iti-flag-offset:-1552px}.iti__ie{--iti-flag-offset:-1568px}.iti__il{--iti-flag-offset:-1584px}.iti__im{--iti-flag-offset:-1600px}.iti__in{--iti-flag-offset:-1616px}.iti__io{--iti-flag-offset:-1632px}.iti__iq{--iti-flag-offset:-1648px}.iti__ir{--iti-flag-offset:-1664px}.iti__is{--iti-flag-offset:-1680px}.iti__it{--iti-flag-offset:-1696px}.iti__je{--iti-flag-offset:-1712px}.iti__jm{--iti-flag-offset:-1728px}.iti__jo{--iti-flag-offset:-1744px}.iti__jp{--iti-flag-offset:-1760px}.iti__ke{--iti-flag-offset:-1776px}.iti__kg{--iti-flag-offset:-1792px}.iti__kh{--iti-flag-offset:-1808px}.iti__ki{--iti-flag-offset:-1824px}.iti__km{--iti-flag-offset:-1840px}.iti__kn{--iti-flag-offset:-1856px}.iti__kp{--iti-flag-offset:-1872px}.iti__kr{--iti-flag-offset:-1888px}.iti__kw{--iti-flag-offset:-1904px}.iti__ky{--iti-flag-offset:-1920px}.iti__kz{--iti-flag-offset:-1936px}.iti__la{--iti-flag-offset:-1952px}.iti__lb{--iti-flag-offset:-1968px}.iti__lc{--iti-flag-offset:-1984px}.iti__li{--iti-flag-offset:-2000px}.iti__lk{--iti-flag-offset:-2016px}.iti__lr{--iti-flag-offset:-2032px}.iti__ls{--iti-flag-offset:-2048px}.iti__lt{--iti-flag-offset:-2064px}.iti__lu{--iti-flag-offset:-2080px}.iti__lv{--iti-flag-offset:-2096px}.iti__ly{--iti-flag-offset:-2112px}.iti__ma{--iti-flag-offset:-2128px}.iti__mc{--iti-flag-offset:-2144px}.iti__md{--iti-flag-offset:-2160px}.iti__me{--iti-flag-offset:-2176px}.iti__mf{--iti-flag-offset:-2192px}.iti__mg{--iti-flag-offset:-2208px}.iti__mh{--iti-flag-offset:-2224px}.iti__mk{--iti-flag-offset:-2240px}.iti__ml{--iti-flag-offset:-2256px}.iti__mm{--iti-flag-offset:-2272px}.iti__mn{--iti-flag-offset:-2288px}.iti__mo{--iti-flag-offset:-2304px}.iti__mp{--iti-flag-offset:-2320px}.iti__mq{--iti-flag-offset:-2336px}.iti__mr{--iti-flag-offset:-2352px}.iti__ms{--iti-flag-offset:-2368px}.iti__mt{--iti-flag-offset:-2384px}.iti__mu{--iti-flag-offset:-2400px}.iti__mv{--iti-flag-offset:-2416px}.iti__mw{--iti-flag-offset:-2432px}.iti__mx{--iti-flag-offset:-2448px}.iti__my{--iti-flag-offset:-2464px}.iti__mz{--iti-flag-offset:-2480px}.iti__na{--iti-flag-offset:-2496px}.iti__nc{--iti-flag-offset:-2512px}.iti__ne{--iti-flag-offset:-2528px}.iti__nf{--iti-flag-offset:-2544px}.iti__ng{--iti-flag-offset:-2560px}.iti__ni{--iti-flag-offset:-2576px}.iti__nl{--iti-flag-offset:-2592px}.iti__no{--iti-flag-offset:-2608px}.iti__np{--iti-flag-offset:-2624px}.iti__nr{--iti-flag-offset:-2640px}.iti__nu{--iti-flag-offset:-2656px}.iti__nz{--iti-flag-offset:-2672px}.iti__om{--iti-flag-offset:-2688px}.iti__pa{--iti-flag-offset:-2704px}.iti__pe{--iti-flag-offset:-2720px}.iti__pf{--iti-flag-offset:-2736px}.iti__pg{--iti-flag-offset:-2752px}.iti__ph{--iti-flag-offset:-2768px}.iti__pk{--iti-flag-offset:-2784px}.iti__pl{--iti-flag-offset:-2800px}.iti__pm{--iti-flag-offset:-2816px}.iti__pr{--iti-flag-offset:-2832px}.iti__ps{--iti-flag-offset:-2848px}.iti__pt{--iti-flag-offset:-2864px}.iti__pw{--iti-flag-offset:-2880px}.iti__py{--iti-flag-offset:-2896px}.iti__qa{--iti-flag-offset:-2912px}.iti__re{--iti-flag-offset:-2928px}.iti__ro{--iti-flag-offset:-2944px}.iti__rs{--iti-flag-offset:-2960px}.iti__ru{--iti-flag-offset:-2976px}.iti__rw{--iti-flag-offset:-2992px}.iti__sa{--iti-flag-offset:-3008px}.iti__sb{--iti-flag-offset:-3024px}.iti__sc{--iti-flag-offset:-3040px}.iti__sd{--iti-flag-offset:-3056px}.iti__se{--iti-flag-offset:-3072px}.iti__sg{--iti-flag-offset:-3088px}.iti__sh{--iti-flag-offset:-3104px}.iti__si{--iti-flag-offset:-3120px}.iti__sj{--iti-flag-offset:-3136px}.iti__sk{--iti-flag-offset:-3152px}.iti__sl{--iti-flag-offset:-3168px}.iti__sm{--iti-flag-offset:-3184px}.iti__sn{--iti-flag-offset:-3200px}.iti__so{--iti-flag-offset:-3216px}.iti__sr{--iti-flag-offset:-3232px}.iti__ss{--iti-flag-offset:-3248px}.iti__st{--iti-flag-offset:-3264px}.iti__sv{--iti-flag-offset:-3280px}.iti__sx{--iti-flag-offset:-3296px}.iti__sy{--iti-flag-offset:-3312px}.iti__sz{--iti-flag-offset:-3328px}.iti__tc{--iti-flag-offset:-3344px}.iti__td{--iti-flag-offset:-3360px}.iti__tg{--iti-flag-offset:-3376px}.iti__th{--iti-flag-offset:-3392px}.iti__tj{--iti-flag-offset:-3408px}.iti__tk{--iti-flag-offset:-3424px}.iti__tl{--iti-flag-offset:-3440px}.iti__tm{--iti-flag-offset:-3456px}.iti__tn{--iti-flag-offset:-3472px}.iti__to{--iti-flag-offset:-3488px}.iti__tr{--iti-flag-offset:-3504px}.iti__tt{--iti-flag-offset:-3520px}.iti__tv{--iti-flag-offset:-3536px}.iti__tw{--iti-flag-offset:-3552px}.iti__tz{--iti-flag-offset:-3568px}.iti__ua{--iti-flag-offset:-3584px}.iti__ug{--iti-flag-offset:-3600px}.iti__us{--iti-flag-offset:-3616px}.iti__uy{--iti-flag-offset:-3632px}.iti__uz{--iti-flag-offset:-3648px}.iti__va{--iti-flag-offset:-3664px}.iti__vc{--iti-flag-offset:-3680px}.iti__ve{--iti-flag-offset:-3696px}.iti__vg{--iti-flag-offset:-3712px}.iti__vi{--iti-flag-offset:-3728px}.iti__vn{--iti-flag-offset:-3744px}.iti__vu{--iti-flag-offset:-3760px}.iti__wf{--iti-flag-offset:-3776px}.iti__ws{--iti-flag-offset:-3792px}.iti__xk{--iti-flag-offset:-3808px}.iti__ye{--iti-flag-offset:-3824px}.iti__yt{--iti-flag-offset:-3840px}.iti__za{--iti-flag-offset:-3856px}.iti__zm{--iti-flag-offset:-3872px}.iti__zw{--iti-flag-offset:-3888px}.iti__globe{background-image:var(--iti-path-globe-1x);background-size:contain;background-position:right;box-shadow:none;height:19px}@media (min-resolution:2x){.iti__flag{background-image:var(--iti-path-flags-2x)}.iti__globe{background-image:var(--iti-path-globe-2x)}}

/* ===== primitives.css ===== */
/* ============================================================================
   Layout primitives. These own their own gutters, which is why a stray grid
   row can never again widen the document (LSI-12 cause [2]).
   ========================================================================== */

/* Measured rule: content is 85% of the viewport, capped at 1340px. */
.container {
  width: var(--container-ratio);
  max-width: var(--container-max);
  margin-inline: auto;
}

/* G-6 — mobile container model.
   Live mobile does NOT inset the container. Every content row is full-bleed and
   the horizontal inset lives on the row as 20px padding:
     .row (100vw, padding: 0 20px) > .col-xs-12
   Measured live: content 350 @390 and 728 @768. Slider/hero rows carry 0 padding
   instead — reproduced via .bleed below.
   Boundary is 1024px to stay aligned with the navigation switch; 768–1023 is the
   tablet band we hold design freedom over (R4), and this keeps 768 matching the
   live mobile measurement exactly. */
@media (max-width: 1023px) {
  .container { width: auto; max-width: none; margin-inline: 0; }
}


/* Full-bleed band with its own inner padding (live .fullWidthWidgetWrapper: 80px).
   Below 1024px live drops this entirely — the row is flush and the 20px inset
   comes from .content-col, so the two models do not stack. */
.bleed {
  width: 100%;
  padding-inline: var(--bleed-pad);
}
/* .bleed's desktop inner padding is dropped below 1024 — the mobile gutter rule
   above then supplies the same 20px every other container gets. */

/* A section in the widget stack. Live default is 30px top and bottom, but each
   widget's measured padding is passed in as --pt/--pb so vertical rhythm is
   data-driven rather than guessed.

   G-1b — centring model. Live nests:
     .content_wrapper (center) > .widget-wrapper (center)
       > .col-* (left) > .text-content-wrapper (left)
   The wrapper centre is almost entirely cancelled by that reset, and after G-1
   restored the authored inline `text-align` the centring is carried by the
   content itself. Measured A/B at 1440: a blanket `text-align: center` here
   produced 76 real mismatches, omitting it produced 45 — so it is omitted and
   the residual centring is applied to the specific elements live centres
   (see .section > media/actions rules below). */
.section { padding-top: var(--pt, var(--sp-5)); padding-bottom: var(--pb, var(--sp-5)); }

/* G-2 — the content column. Live puts Bootstrap's 15px gutters on .col-* inside
   .container; our DOM collapses .col-* and .text-content-wrapper into one node,
   so the column primitive carries the gutter. Without it every text block was
   30px wider than live at every desktop width, changing every wrap point. */
.content-col {
  padding-inline: var(--gutter);
}
/* G-6: live mobile rows use a 20px inset, not Bootstrap's 15px gutter. Below 1024
   that inset is owned by the container primitives (see "Mobile gutter"). */
.section--flush      { padding-block: 0; }
.section--flush-top  { padding-top: 0; }
.section--flush-btm  { padding-bottom: 0; }
.section--tight      { padding-block: var(--sp-3); }
.section--roomy      { padding-block: var(--sp-7); }

/* A painted section CONTAINS its content's margins.

   Without this a child's edge margin collapses straight out through a section
   whose padding is 0 on that side, and the escaped strip paints in the PAGE's
   colour rather than the band's. The PRESS COVERAGE heading is the case that
   surfaced it: its h2 carries `margin-bottom: 10px` (deliberate — it separates
   the heading from the logo row inside the band) and its captured MOBILE padding
   is 0/0, so below 1024 that 10px fell outside the grey and drew a white stripe
   across the middle of the band on every page carrying it. Desktop was fine only
   because the same section has 15px of bottom padding there for the margin to sit
   in.

   `flow-root` establishes a block formatting context, so the margin stays inside
   the colour it belongs to. Measured across 40 pages at 390 and 1440: document
   height changes by 0px everywhere — the strip does not move, it just gets
   painted the right colour. */
.section--band,
.section--warm,
.section--black,
.section--brand,
.section--surface { display: flow-root; }

/* Any authored surface the four named classes below do not cover. The colour rides
   in on `--section-bg` from the widget's captured background, so a background that is
   not one of the four can no longer be dropped on the floor — it used to be, on 14
   widgets across 10 pages. Colour only: the named classes exist because they carry
   type inversion too. */
.section--surface { background: var(--section-bg); }

.section--warm  { background: var(--c-surface-warm); }
.section--black { background: var(--c-surface-black); color: var(--c-on-dark); }
.section--band  { background: var(--c-surface-band); }
.section--brand { background: var(--c-brand); color: var(--c-on-dark); }

/* G-7 — invert type only for content sitting DIRECTLY on the dark ground.
   Cards inside these bands have their own white background and keep dark text:
   sampling the live pixels shows the band is black (0,0,0) with white headings,
   while the cards within it are white (255,255,255) with #4B5761 body copy.
   The previous blanket rule whitened card interiors too (40 measured mismatches). */
.section--black > .container > .content-col :where(h1,h2,h3,h4,h5,h6,p,li,td,th),
.section--black > .bleed > .content-col :where(h1,h2,h3,h4,h5,h6,p,li,td,th),
.section--black > .container > .richtext :where(h1,h2,h3,h4,h5,h6,p,li),
.section--black > .bleed > .richtext :where(h1,h2,h3,h4,h5,h6,p,li),
.section--brand > .container > .content-col :where(h1,h2,h3,h4,h5,h6,p,li,td,th),
.section--brand > .bleed > .content-col :where(h1,h2,h3,h4,h5,h6,p,li,td,th) { color: var(--c-on-dark); }

/* ---- Grid ---------------------------------------------------------------- */
.grid { display: grid; gap: var(--sp-5); }
/* Live Bootstrap columns have no gap — each column carries its own 20px padding. */
.grid--gapless { gap: 0; }
.grid--2 { grid-template-columns: repeat(2, 1fr); }
.grid--3 { grid-template-columns: repeat(3, 1fr); }
.grid--4 { grid-template-columns: repeat(4, 1fr); }

@media (max-width: 1023px) {
  .grid--3, .grid--4 { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 767px) {
  .grid--2, .grid--3, .grid--4 { grid-template-columns: 1fr; }
}

/* Healing Therapies — measured 67% at 10 viewports (1025→1920), capped at 1340px
   (cap engages at a 2000px viewport). */
.container--therapy { width: var(--container-therapy); max-width: var(--container-max); margin-inline: auto; }

/* Enquiry form and the card carousels — measured exactly 80.00% at every width
   including 2560, with NO max-width. */
.container--wide { width: var(--container-wide); max-width: none; margin-inline: auto; }

@media (max-width: 1023px) {
  .container--therapy,
  .container--wide { width: auto; max-width: none; margin-inline: 0; }
}

/* 2-column listing cards (`wt2imgtop-2tbbot`) — live puts EVERY instance of this
   widget in a 70% container, max 1340: 1008 @1440, 896 @1280, 1340 (capped) @1920,
   measured with zero exceptions across all its mod variants (plain health-guide
   listings, healthprogram, twocard-blogs, newoffer, hideLeftBox, card-flip).
   A modifier on `.container`, not a replacement for it, so the `.container`-keyed
   selectors (section text colours, the article inset) keep matching. Guarded to
   ≥1024 so the G-6 mobile full-bleed model needs no counter-rule. */
@media (min-width: 1024px) {
  .container.container--cards { width: var(--container-cards); }
  /* Live also insets the cards 30px inside that container on BOTH sides, so the
     row is 1008 but the two cards are 444 with a 60px gap, not 474. Measured at
     30/30 with zero exceptions across all five mod variants at 1440 and 1280 —
     the same inset the 3-column row uses. Setting the width alone left every card
     on the site 30px too wide. */
  .container.container--cards { padding-inline: 30px; }
}

/* Narrower container used by the live `two-col` widgets — measured 65% of the
   viewport (832 @1280, 936 @1440, 1248 @1920). Below 1024px live drops it to
   full-bleed, same as .container (G-6). */
.container--narrow { width: var(--container-narrow); max-width: var(--container-max); margin-inline: auto; }
@media (max-width: 1023px) {
  .container--narrow { width: auto; max-width: none; margin-inline: 0; }
}

/* The gallery's filter bar, and only that — 96% capped at 1340 (1229 @1280, 1340
   @1440 and @1920). Wider than .container on purpose: the bar's wrap points are a
   function of this width, and at .container's 85% the ninth pill drops to row two
   where live keeps it on row one. */
.container--pills { width: var(--container-pills); max-width: var(--container-max); margin-inline: auto; }
@media (max-width: 1023px) {
  .container--pills { width: auto; max-width: none; margin-inline: 0; }
}

/* ---- Mobile gutter (<=1023) --------------------------------------------
   One rule owns the 20px inset. It used to live only on .content-col, so any
   widget whose content sat straight in a container — hotel-info, the offerings
   rows, the enquiry form, the accordion, reviews, the press logos — rendered edge
   to edge, while others landed on 15px, 7.5px or 2.5px.

   Declared here, after every container primitive and after .content-col, so
   source order resolves it: the containers own the inset and the column drops
   its own 15px rather than stacking to 35px. `.bleed` likewise drops its 80px
   desktop inset here rather than in a block that the base rule would override. */
@media (max-width: 1023px) {
  .container,
  .container--wide,
  .container--narrow,
  .container--therapy,
  .container--pills,
  .bleed { padding-inline: var(--sp-4); }
  .content-col { padding-inline: 0; }
}

/* Media beside text. Reverses source order for the right-image variant without
   reordering the DOM, so reading order stays correct.
   Live stacks these at 1023 and below — measured img 728×409 @768, 350×197 @390. */
.media-side { display: grid; grid-template-columns: 1fr 1fr; align-items: stretch; }
.media-side--right > :first-child { order: 2; }
@media (max-width: 1023px) {
  .media-side { grid-template-columns: 1fr; }
  .media-side--right > :first-child { order: 0; }
}

.stack > * + * { margin-top: var(--sp-4); }

/* ---- Rich text ----------------------------------------------------------
   G-1b: live sets text-align:left on .text-content-wrapper — every authored
   text block resets the wrapper's centre. Elements outside a text block
   (icons, buttons, standalone media) keep the inherited centre. Authored
   inline `text-align` (restored by G-1) then overrides per block. */
.richtext { text-align: left; }

/* Authored copy contains raw URLs, and a URL is one unbreakable word. It does not
   widen its block — the block keeps its measure and the text simply paints past it
   — so the page gains horizontal scroll while every element still reports a rect
   inside the viewport, which is why this hid from the overflow checks for so long.

   Measured on health-guide/valuka-sweda at 423: an <li> holding a 549px reference
   link inside a 308px column, taking the document to 621. The same shape accounts
   for most of the pages logged as "authored content" overflow in
   docs/PROJECT_STATE.md §10o.

   `break-word`, not `anywhere`: it only breaks a word that cannot fit on a line of
   its own, so ordinary prose keeps live's wrap points and only the runaway URL is
   touched. */
.richtext { overflow-wrap: break-word; }

/* An authored embed carries the width it was pasted with — press.html holds a
   656px iframe, which took that page to 676 at a 390 viewport. Height is left
   alone: these are a mix of video and social embeds with their own aspect
   ratios, and capping the width is what the overflow needs. */
.richtext iframe { max-width: 100%; }

/* A wide table the CMS did NOT wrap. `.table-responsive` below is the authored
   wrapper and it already scrolls, but food-menu's table is a direct child of the
   rich text with no wrapper at all — 481px in a 350px column, taking the page to
   501. Matching on `> table` reaches exactly the unwrapped ones and cannot touch
   a wrapped table, whose parent is `.table-responsive`.
   `display: block` is what makes a table scrollable; it is confined to the widths
   where the table cannot fit, so the desktop keeps real table layout. */
@media (max-width: 1023px) {
  .richtext > table {
    display: block;
    max-width: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
  }
}
.richtext > :last-child { margin-bottom: 0; }
/* Live is NOT uniform here: 11 of 14 homepage h2 measure margin-bottom 0, and only
   the three tail widgets (PRESS COVERAGE, CONNECT WITH US, STAY INFORMED) carry
   10px. Those are set per-component in widgets/home.css. The 15px here was a
   replica invention that also masked the stripped spacers. */
.richtext h2 { margin-bottom: 0; }
/* C-23: live measures h3 margin-bottom 0 on all 57 homepage instances. The 10px
   here was a replica compensation masking the stripped <p>&nbsp;</p> spacer. */
.richtext h3 { margin-bottom: 0; }
/* Live rich-text paragraph rhythm is top-side: margin 15px 0 0 (62 instances).
   Review cards keep their own 5px/5px rule — they are not .richtext. */
.richtext p { margin: 15px 0 0; }
/* Live gives the FIRST paragraph of a rich-text block no top margin — measured on
   the VIEW ALL widget, where the spacer contributes 19.6px and the block totals
   48.6px. Card bodies override this below: there live's authored (hidden) <p></p>
   holds first-child, so the visible spacer keeps its 15px. */
.richtext > p:first-child { margin-top: 0; }
.richtext ul { padding-left: var(--sp-5); }
.richtext li { margin-bottom: var(--sp-1); }
/* ---- H1 CASE ------------------------------------------------------------
   Every page headline is upper case. 218 of the 288 already were, in the copy itself;
   this covers the other 70 without rewriting them.

   `text-transform`, not a text transform in the data, on purpose: it is presentational,
   so a screen reader still announces the authored sentence rather than spelling out an
   acronym-looking string, and the JSON-LD and <title> — which take the same copy — are
   untouched. h2-h6 are Title Cased in the data instead (tools/title-case.mjs), because
   there is no CSS equivalent for that. */
h1 { text-transform: uppercase; }
/* …except a plural `s` on an acronym, which tools/title-case.mjs wraps. "FAQs" is the
   plural of FAQ; "FAQS" reads as a five-letter acronym. The rest of the headline still
   upper-cases — only this one letter opts out. */
h1 .keep-case { text-transform: lowercase; }

/* ---- Heading rhythm, every page -----------------------------------------
   Live gives a rich-text heading margin 0 on both sides, which is fine for a
   heading that OPENS a widget — the widget's own padding is the space above it —
   and wrong for one that follows copy inside the same widget. Measured on
   naturopathy/colon-hydrotherapy: every mid-widget h2/h3 has margin-top 0, so the
   heading sits hard against the last line of the paragraph above and reads as part
   of it. That is the "spacing above headers" defect, and it is site-wide.

   Three rules, in this order:
     * a heading that is not the first thing in its block takes 30px above;
     * a heading DIRECTLY after another heading takes 5px instead — those pairs are
       a title with its own subtitle (CONNECT WITH US over its phone number), not
       two sections;
     * a heading with something after it takes 10px below, which is the value live
       itself uses on the three homepage tail widgets.

   `.page-article` in widgets/inner.css raises the first of these to 40 for long-form
   articles and is more specific, so it still wins there. Anything a component sets
   for itself in widgets/*.css also still wins: those load after this file — which is
   how the media-side EYEBROW keeps its deliberate one-line gap (19.6px, live-measured)
   and how the three homepage tail widgets keep their own 10px.

   WHAT IT COSTS, measured by neutralising these three rules in the browser and
   comparing document height (1440 / 390):
       homepage           +174 / +448px   (1.7% / 3.6%)
       contact-us         +100 / +190px   (3.2% / 4.4%)
       ayurveda/abhyangam  +70 / +110px   (3.0% / 3.3%)
       rooms-suites        +65 / +135px   (1.9% / 2.4%)
       wellness-programs   +60 / +280px   (0.7% / 2.0%)
       gallery             +10 /  +10px   (0.1% / 0.4%)
   The homepage is inside that set deliberately: the defect is on it too. Note that it
   is pixel-frozen against reference/home-golden/, so that baseline needs re-blessing —
   those files are already showing as deleted in git status, so there is nothing to
   compare against right now either way. */
/* TWO CONTAINERS, ANY DEPTH.
   `.richtext` is this build's wrapper for an authored block; `.text-content-wrapper`
   is LIVE's own, and it carries the prose on the four curative-therapies pages, press
   and part of the homepage — 55 elements over 6 pages that a `.richtext`-only rule
   never reached. Both are listed everywhere below.

   And the rules reach any DEPTH, not just direct children. The CMS nests content
   inside wrapper divs, and a rule written `>` stops at the wrapper: measured on
   curative-therapies/naturopathy, every "10 Minutes | Price: $90" line sat hard
   against the heading above it because both were one level further in than the
   selector looked.

   A WRAPPER (a div or p that has a block child) is given no margin of its own — its
   children carry the rhythm, and a margin on both would double the gap. A LEAF (one
   with no block child) is the thing that holds text, so it takes the paragraph gap.
   That distinction is what `:has()` is for. */
:is(.richtext, .text-content-wrapper) :is(div, p):has(> :is(div, p, h1, h2, h3, h4, h5, h6, ul, ol, table, blockquote)) {
  margin-block: 0;
}
:is(.richtext, .text-content-wrapper) :is(div, p):not(:has(> :is(div, p, h1, h2, h3, h4, h5, h6, ul, ol, table, blockquote))) {
  margin-top: var(--sp-4);
}
:is(.richtext, .text-content-wrapper) :is(h2, h3, h4, h5, h6) {
  margin-top: var(--sp-5);
  margin-bottom: var(--sp-2);
}
:is(.richtext, .text-content-wrapper) :is(h1, h2, h3, h4, h5, h6) + :is(h2, h3, h4, h5, h6) {
  margin-top: var(--sp-1);
}
:is(.richtext, .text-content-wrapper) :is(ul, ol) { margin-top: var(--sp-4); }

/* The first painted block in a container opens the widget, and the widget's own
   padding is the space above it. Two levels, because that is as deep as the CMS
   nests before content appears. */
:is(.richtext, .text-content-wrapper) > :first-child,
:is(.richtext, .text-content-wrapper) > :first-child > :first-child { margin-top: 0; }

/* AN AUTHORED SPACER BLOCK — `<p>&nbsp;</p>`, tagged by tools/article-html.mjs.
   Live spaces its cards with these: one above the title, one below. The spacer's own
   line box IS the gap (measured 20px), so it must not also take the paragraph rhythm,
   and neither must the block after it — otherwise the two stack and live's 20/20 comes
   out 50/60 (measured on curative-therapies before this).
   Only the spacer and its successor are exempted. Exempting whole cards instead put
   184 zero-gap pairs back on the therapy pages, which carry no spacers and depend on
   the rhythm. */
:is(.richtext, .text-content-wrapper) .cms-spacer { margin-block: 0; }
:is(.richtext, .text-content-wrapper) .cms-spacer + * { margin-top: 0; }
/* …and the block BEFORE one gives up its bottom margin for the same reason: with the
   heading's 10px still in place the card title sat 30px off its copy against live's
   20 (measured on curative-therapies). The spacer is the gap, on both sides of it. */
:is(.richtext, .text-content-wrapper) :is(h1, h2, h3, h4, h5, h6, div, p):has(+ .cms-spacer) {
  margin-bottom: 0;
}

/* A CARD TITLE THAT OPENS THE CARD still needs the gap.
   `> :first-child { margin-top: 0 }` above is right for a text widget, whose section
   padding is the space above its first block. A card has none — live's wrapper is
   `padding: 0 14px 60px`, no top — and it relies on an authored spacer instead. 39 of
   the 48 flush titles got that spacer back; these 9 never had one, so the gap has to
   come from here. Measured across all 425 card titles on the site: 352 sit in live's
   12-21px band, 64 higher (the 25px newoffer variant), and these were the only ones
   left at 0. 20px is live's figure. */
.media-top__col :is(.richtext, .text-content-wrapper) > :is(h1, h2, h3, h4, h5, h6):first-child {
  margin-top: var(--sp-4);
}

/* G-3: live rich-text links carry no underline (measured text-decoration: none). */
.richtext a { text-decoration: none; }

/* Star ratings are authored content (`<div class="stars">` inside the review
   cards, 16 pages). Live gives the block `margin-top: 8px`, so the rating clears
   the copy above it; nothing styled it here, so it sat hard against the last line
   of the review — measured gap 0 against live's 8 at both 1440 and 390. */
.stars { margin-top: 8px; }

/* ---- Scrollbars ---------------------------------------------------------
   Live does NOT restyle the page scrollbar; it styles the two scrollers that are
   part of the design, and hides the ones that would otherwise show through a
   slider. Taken verbatim from live's own rules:

     .table-responsive::-webkit-scrollbar        { height:5px; background:#ddd }
     .table-responsive::-webkit-scrollbar-track  { inset shadow; background:#f5f5f5 }
     .table-responsive::-webkit-scrollbar-thumb  { background:#294c4b }
     …review body::-webkit-scrollbar             { width:5px; background:#4b5761 }

   `scrollbar-width`/`scrollbar-color` are added alongside so Firefox gets the
   same treatment — live has only the WebKit half, which leaves Firefox with the
   default bar on the very tables this is meant to tidy.
   ------------------------------------------------------------------------ */
.table-responsive,
.review__body {
  scrollbar-width: thin;
  scrollbar-color: #294C4B #F5F5F5;
}
.table-responsive::-webkit-scrollbar { height: 5px; background-color: #DDDDDD; }
.review__body::-webkit-scrollbar { width: 5px; background-color: #4B5761; }
.table-responsive::-webkit-scrollbar-track,
.review__body::-webkit-scrollbar-track {
  -webkit-box-shadow: inset 0 0 6px rgba(0, 0, 0, .3);
  background-color: #F5F5F5;
}
.table-responsive::-webkit-scrollbar-thumb,
.review__body::-webkit-scrollbar-thumb { background-color: #294C4B; }

/* ---- Buttons ----------------------------------------------------------- */
.btn {
  display: inline-block;
  padding: var(--sp-2) var(--sp-4);
  font-size: var(--fs-md);
  font-weight: var(--fw-regular);
  line-height: var(--lh-button);    /* G-5: live measures 13px */
  letter-spacing: var(--ls-default);
  text-align: center;
  text-decoration: none;
  border: 1px solid transparent;
  border-radius: var(--radius-0);
  transition: background-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease);
}
.btn--primary { background: var(--c-brand); color: var(--c-on-dark); }
.btn--primary:hover { background: var(--c-brand-dark); color: var(--c-on-dark); }
.btn--ghost { background: transparent; color: var(--c-on-dark); border-color: var(--c-on-dark); }
.btn--ghost:hover { background: var(--c-on-dark); color: var(--c-brand); }
.btn--green { background: var(--c-green); color: var(--c-on-dark); }
.btn--green:hover { background: var(--c-sage); color: var(--c-on-dark); }
.btn--block { display: block; width: 100%; }

/* ---- Tables ------------------------------------------------------------
   `.table-responsive` is the authored wrapper the CMS emits around wide tables;
   live relies on Bootstrap's rule to scroll it rather than let it widen the
   document. Reproducing that mechanism, not patching the symptom. */
.table-scroll,
.table-responsive { overflow-x: auto; -webkit-overflow-scrolling: touch; max-width: 100%; }
.table { width: 100%; }
/* Padding is a property of every table cell; the RULE is not. Bootstrap — and
   live — draw cell borders only when the author asked for them with
   `table-bordered`, and the replica put them on every `.table` instead. Of the
   site's 73 authored tables 72 carry `table-bordered` and got their border twice
   over (this rule plus the more specific one in home.css); the one that does not
   is the ALL-INCLUSIVE EXPERIENCE list on rates-wellness-menu, which live leaves
   completely bare and the replica drew as a 4x2 grid of boxes. The widget even
   carries a `removeborder` modifier saying so.
   Header cells are named explicitly because home.css's bordered rule reaches
   `> tbody > tr > th` only, and one authored table uses a real `<thead>`. */
.table th, .table td { padding: var(--sp-2) var(--sp-3); }
.table-bordered th, .table-bordered td { border: 1px solid var(--c-hairline); }

/* ---- Programme inclusions: matrix on a desktop, cards on a phone ---------
   /wellness-program-inclusions.html compares 15 programmes across 16 inclusion
   rows. At 1440 that matrix is the right shape for the question. At 390 it is
   1,888px wide: the two label columns plus ONE programme are visible, the guest
   scrolls sideways through fourteen more, and the row labels leave the screen as
   soon as they do.

   The card view is generated at build time from the table itself
   (tools/inclusions-cards.mjs), so the table stays the single source of truth and
   the two cannot drift. Both are in the DOM; `display: none` hides one from the
   accessibility tree as well as the screen, so neither is announced twice.

   The cut is 767/768 — the same width every other stacked layout on the site uses. */
.pkg-cards { display: none; }

@media (max-width: 767px) {
  .pkg-cards { display: grid; gap: var(--sp-4); text-align: left; }
  /* The matrix is hidden with its scroll wrapper, or the wrapper leaves a
     30px-tall scrollbar strip above the cards with nothing inside it. */
  .pkg-cards ~ .table-responsive,
  .table-responsive:has(+ .pkg-cards) { display: none; }

  .pkg-card {
    background: #FCFCFC;
    border: 1px solid rgba(50, 68, 84, .3);
    padding: var(--sp-3) var(--sp-3) var(--sp-4);
  }
  .pkg-card__name {
    margin: 0 0 var(--sp-3);
    padding-bottom: var(--sp-2);
    border-bottom: 1px solid rgba(50, 68, 84, .3);
    color: var(--c-navy);
    font-size: var(--fs-md);
    line-height: var(--lh-heading, 1.3);
  }
  .pkg-card__list { margin: 0; padding: 0; list-style: none; }
  .pkg-card__list > li {
    position: relative;
    margin: 0 0 var(--sp-2);
    padding-left: 26px;
    font-size: var(--fs-sm);
    line-height: var(--lh-table);
  }
  .pkg-card__list > li:last-child { margin-bottom: 0; }
  /* The same green tick the matrix uses, drawn from the icon font already loaded
     rather than a second asset. */
  .pkg-card__list > li::before {
    content: "\f00c";
    font-family: "Font Awesome 5 Pro", "Font Awesome 5 Free", "FontAwesome";
    font-weight: 900;
    position: absolute;
    left: 0;
    top: 1px;
    color: #69A770;
  }
  .pkg-card__list--none { padding-left: 0; }
  .pkg-card__list--none::before { content: none; }
  /* The qualifier that comes with a choice ("Select choice of 1 - ...") sits
     under its own heading, one step down in weight, so the inclusion reads first
     and the condition second. */
  .pkg-card__note { display: block; margin-top: 2px; color: var(--c-ink-muted, #5A6B7B); }
}

/* The ALL-INCLUSIVE EXPERIENCE list on rates-wellness-menu is a LAYOUT table, not
   a data one: two columns, no header cell anywhere, and every cell holding a
   single `<ul><li>` of prose. It is the one bare `.table` on the site — the other
   72 all carry `table-bordered` — so `:not(.table-bordered)` names it exactly, and
   names it the same way the note above already does.

   Two columns of sentence-long bullets inside a 350px phone leaves ~160px a
   column: "State-of-the-art fitness center and private locker rooms with
   complimentary shower amenities" wraps to eleven lines against its neighbour's
   four, and the row's height is set by the longer one, so the table reads as
   ragged columns separated by empty space. It has no scrollbar to escape through
   either, because at 350px it is not wide enough to trigger `.table-responsive`.

   Stacking is the honest fix: with no headers there is no row/column relationship
   to preserve, so one bullet per full-width row loses nothing and reads properly.
   Bounded at 767, the same width `.grid--2` collapses at — above it the two
   columns have the room they were authored for. Empty cells are dropped rather
   than left as blank rows: the 4x2 grid holds 7 bullets, so one cell is empty. */
@media (max-width: 767px) {
  .table:not(.table-bordered),
  .table:not(.table-bordered) tbody,
  .table:not(.table-bordered) tr,
  .table:not(.table-bordered) td { display: block; width: auto; }
  .table:not(.table-bordered) td:empty { display: none; }
  .table:not(.table-bordered) td { padding: 0; }
  .table:not(.table-bordered) ul { margin: 0; }
}

/* …EXCEPT the plain `table table-bordered` tables, which live draws with NO cell
   rules at all and 8px cells. Measured on live, per cell rather than from one
   sample:

     rooms-suites/*      cell border 0px, padding 8px   (the amenities grid)
     food-menu           cell border 0px, padding 8px
     homepage            cell border 1px #d3d3d3, padding 5px   `table-striped table-condensed`
     wellness-program-…  cell border 1px #d3d3d3, padding 0     `table-bordered` alone

   So the class COMBINATION is what live keys on, not `table-bordered` by itself:
   the bare `table table-bordered` pair is bare-celled, and the variants that add
   `table-striped`/`table-condensed`, or drop `.table`, keep their rules. Scoped
   the same way here so the homepage and the inclusions matrix are untouched.

   This is the QC note "table below the one image slider styling is different
   than the live" on the rooms-suites pages: ours drew a grey box round every
   cell at 10px, which read as a spreadsheet against live's open grid. */
/* …and `table-autowidth` is a variant too. Live draws cell borders on
   `table table-autowidth table-bordered` (measured 1px rgb(211,211,211) on the
   introductory-package rates grid); only the BARE pair is border-less. Without
   this guard the rule reached 10 rates tables and stripped the grid off them —
   the outline that "used to be there". Verified against live across all six
   class combinations the build produces:
     table table-bordered ................................. no cell border
     table table-autowidth table-bordered ................. 1px
     table table-autowidth table-bordered condensed striped 1px
     table table-bordered table-striped ................... 1px
     table-bordered (no .table) ........................... 1px
     table ................................................ no cell border */
/* SUPERSEDED — every table now draws its grid.
   The rule that used to sit here stripped cell borders off the bare
   `table table-bordered` pair because live does, and the six class combinations
   above record which of live's tables draw a grid and which do not. That
   distinction is live's, it is not consistent, and a borderless data table is
   harder to read than a bordered one: on health-guide/acupuncture-for-anxiety the
   two-column Q&A read as loose prose in two ragged stacks.
   Every table in rich text now gets the same 1px grid live already draws on ten of
   them, so a reader never has to work out which kind of table they are looking at.
   Padding is 8px 10px — live's 8px vertical, and the scale's 10px across, which is
   what stops the text touching the new rule. */
.richtext table :is(th, td),
.table :is(th, td),
table.table-bordered :is(th, td) {
  /* #DDD, not `--c-rule`: it is the colour live already paints on 20 of the site's
     tables and the one widgets/home.css sets for `.table-bordered`, so the tables
     this rule newly reaches match the ones that were already bordered. `--c-rule`
     (#BBBDBF) is a darker grey and showed up on exactly one table — a fourth border
     colour on a page nobody asked to change. The per-variant colours home.css
     measures from live stay as they are: #D3D3D3 on the autowidth grids, and black
     on the rates grid, where grey vanishes into the green band. */
  border: 1px solid #DDD;
  padding: 8px 10px;
}
/* …EXCEPT a LAYOUT table. `w-mod-removeborder` is live's own signal that a table is
   scaffolding rather than data, and it carries exactly one thing on this site: the
   ALL-INCLUSIVE EXPERIENCE list on rates-wellness-menu, a 4x2 grid of cells each
   holding a single bullet. Live leaves it completely bare; the grid rule above drew
   eight boxes round a bullet list. Same call as the 40 one-row event tables on
   yo1-events — a grid belongs on tabular data, and this is a list. */
.w-mod-removeborder table :is(th, td) {
  border: 0;
  padding: 0 var(--sp-3) 0 0;
}

/* A CELL THAT SPANS ROWS CENTRES AGAINST ITS SPAN.
   `.richtext td` sets `vertical-align: top` (live's value, right for a normal cell),
   which pinned "Room Type" to the first of the three header rows it now covers — the
   label sat at the top of a 111px cell with 80px of blank beneath it. Merging a cell
   is a statement that it belongs to all the rows it covers, so it sits in the middle
   of them. Applies to the row-label cells in the data body too, which span a package's
   three room types. */
:is(.richtext, .text-content-wrapper) table :is(td, th)[rowspan]:not([rowspan="1"]) {
  vertical-align: middle;
}
/* The header band keeps its own padding below; it is repeated here only because the
   band rule sets `padding` wholesale and would otherwise lose the horizontal 10. */

/* A LIST ITEM reads left even in a centred cell. The amenities cells are authored
   `text-align: center` and hold a <ul>; centring reaches the <li> too, so the text
   centres inside the item while its bullet stays put at the list's content edge —
   every marker orphaned out to the left with a gap before its label.

   Live keeps the cell and the list centred and sets the ITEM to left, which is
   what pairs each bullet with its text. Measured on live: cell center, ul center
   padding-left 33px, li LEFT.

   Scoped to table cells, so an authored centred list anywhere else is untouched. */
.table :is(td, th) li { text-align: left; }
.table caption { text-align: left; padding-bottom: var(--sp-2); font-weight: var(--fw-bold); }

/* Table header band.

   Six widgets across six pages author their header row as WHITE text —
   `<span style="color: #ffffff">` — and rely on the stylesheet to put a fill
   behind it. The replica never did, so those header rows rendered as white on
   white: a blank band above the table on the therapies tables, the three
   rooms-suites amenity tables and the 17-row inclusions matrix.

   Matched on the authored white text itself rather than on a page or a modifier,
   because one of the six (rooms-suites) carries no modifier at all, and scoped to
   the FIRST row because the inclusions matrix also uses white spans as invisible
   spacers inside its body cells — those must stay invisible.

   Live's fill is rgb(108,143,125) on five of the six; the inclusions matrix uses
   navy and takes it from its own modifier in inner.css. Padding is the scale's
   10px rather than live's Bootstrap-default 8px — 2px, and worth less than an
   off-scale value. (Body cells sit at 4px 10px from an older rule and differ from
   live's 8px on every table; that is pre-existing and left alone here.) */
.richtext table tr:first-child :is(td, th):has(span[style*="#ffffff" i]) {
  background: rgb(108, 143, 125);
  padding: 8px var(--sp-2);
}

/* A TABLE CELL IS TABULAR CONTENT, NOT PROSE — nothing inside one takes the
   paragraph or heading rhythm.
   Live sets the list and its items to zero margin, and the replica was applying the
   article rhythm (`li` 5px, `ul` 20px) inside the cell, which spaced the therapies
   apart and pushed that table 44px taller than live's.
   The same trap caught the depth-independent rhythm added above: the CMS wraps each
   cell's content in a `<div>`, so every cell picked up a 20px top margin. Measured on
   wellness-programs/day-pass-yoga-meditation-schedule — every row 57px instead of 37,
   the time column pushed off centre, and rows whose content was bare text (no div)
   left at 40px, so the schedule came out visibly uneven. Cells are laid out by the
   table; their padding is the cell's own. */
.richtext table :is(ul, ol),
.richtext table li { margin-block: 0; }
:is(.richtext, .text-content-wrapper) :is(td, th) :is(div, p, h1, h2, h3, h4, h5, h6, ul, ol) {
  margin-block: 0;
}

/* A TWO-COLUMN TABLE ON A PHONE. The CMS authors these as `width: 20%` / `80%`, which
   on a 350px table leaves the first column 76px — and the schedule tables put a time
   range in it. "10:00 AM - 6:00 PM" measures 132px at the site's 14px, so every time
   broke over three or four lines while the column beside it had room to spare
   (measured on wellness-programs/day-pass-yoga-meditation-schedule).

   Below 768 the first column takes 40% and the cells drop to 13px, where the same
   string is 123px against the 130px of content the column then has — one line. Two
   columns only: a wider table on a phone scrolls inside `.table-responsive` and its
   proportions are its own business.

   Scoped to `table-bordered`, which 91 of the site's 92 tables carry. The one that
   does not is the ALL-INCLUSIVE EXPERIENCE layout table, which stacks to one column
   on a phone (see the Tables block above) — this rule was pinning its first cell to
   40% with `!important` even after it stacked, so alternate bullets came out 140px
   wide and wrapped to eleven lines while the ones beside them ran the full 350. */
@media (max-width: 767px) {
  .richtext table.table-bordered tr:has(> :nth-child(2)):not(:has(> :nth-child(3))) > :first-child {
    width: 40% !important;
  }
  .richtext table.table-bordered tr:has(> :nth-child(2)):not(:has(> :nth-child(3))) :is(td, th) {
    font-size: 13px;
  }
}

/* ---- Utility ----------------------------------------------------------- */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}
.text-center { text-align: center; }
.text-upper  { text-transform: uppercase; }

/* IN-PAGE ANCHORS.

   The header is `position: fixed` on desktop and `sticky` below 1024, so a fragment
   jump lands its target at y=0 and the header sits on top of it. On the FAQ page that
   put every category heading roughly 50px up behind a 90px header — the reader arrived
   with the heading hidden and the first questions already on screen, which reads as
   landing in the middle of the section.

   `scroll-margin-top` is the mechanism for exactly this. Applied to anything in `main`
   carrying an id, so the FAQ categories, the food-menu courses, the terms-and-
   conditions clauses and the skip link all stop in the same place. Harmless on the
   handful of form fields that also have ids — if a browser scrolls to a field on
   focus or validation, keeping it out from under the header is what you want anyway. */
main[id],
main [id] { scroll-margin-top: var(--anchor-offset); }

/* Skip link — the live site has none (LSI-45). */
.skip-link {
  position: absolute; left: var(--sp-3); top: var(--sp-3);
  z-index: calc(var(--z-header) + 1);
  padding: var(--sp-2) var(--sp-4);
  background: var(--c-brand); color: var(--c-on-dark);
  transform: translateY(-200%);
  transition: transform var(--dur-fast) var(--ease);
}
.skip-link:focus { transform: translateY(0); color: var(--c-on-dark); }

/* ===== global/header.css ===== */
/* ============================================================================
   Header: one 90px fixed band. Measured from the live site —
   logo 123×60 at (34,18) · nav centred · phone + BOOK NOW stacked at the right
   · promo bar 55px directly below on #0E273F.
   ========================================================================== */
.site-header {
  position: fixed;
  inset: 0 0 auto;
  height: var(--header-h);
  background: var(--c-surface);
  z-index: var(--z-header);
}

.header-bar {
  position: relative;
  height: var(--header-h);
  display: flex;
  align-items: center;
}

/* ---- Brand -------------------------------------------------------------- */
.brand {
  position: absolute;
  left: 34px;
  top: 18px;
  display: block;
  line-height: 0;
}
.brand img {
  width: 123px;
  height: 60px;
  max-width: var(--logo-max-w);
  object-fit: contain;
}

/* ---- Utility cluster (phone + booking), right-aligned, stacked ---------- */
/* Live measures the cluster's right edge 40px inside the viewport (1400 at 1440,
   1880 at 1920) with the phone at top 5px and the BOOK NOW button immediately
   under it at top 25px — a top-anchored stack, not a stack centred in the 90px
   band. Live positions both absolutely against the header; same mechanism here. */
.utility-bar {
  position: absolute;
  right: 40px;
  top: 5px;
  height: auto;
  display: flex;
  align-items: flex-start;
  z-index: var(--z-raised);
}
.utility-bar__inner {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0;              /* live: phone bottom 24.6, button top 25.0 */
}
/* Intentional bold link variant — live measures 14px / 19.6px / weight 700 /
   letter-spacing 0.1px / #586066. Set explicitly so the G-3 global reset
   (400 / 0.2px / sage) does not flatten it. */
.utility-bar__phone {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-link);
  color: var(--c-link-utility);
  white-space: nowrap;
}
.utility-bar__phone:hover { color: var(--c-brand); }

/* The handset points the wrong way round. Font Awesome's `fa-phone` draws the
   earpiece at the top-left; the face this build loads draws it mirrored, so beside
   left-to-right text the receiver reads backwards. Mirrored on the X axis to put it
   back — `display: inline-block` so the transform applies without shifting the
   glyph's baseline against the number next to it.

   Every instance, so the handset points the same way everywhere: the utility bar
   above BOOK NOW, the footer's contact link and the mobile action bar. `footer` as
   the scope rather than `.footer-col` — src/_includes/global/footer-mobile.njk is a
   second footer component with its own markup and no shared class, so a class-based
   selector would miss it if it is ever included (it is not, today). One decision
   about one glyph, so one rule. */
.utility-bar__phone i,
footer .fa-phone,
.mobile-action--call .fa-phone {
  display: inline-block;
  transform: scaleX(-1);
}

/* Live BOOK NOW: 122×35, 13px/13px, letter-spacing 1.05px, uppercase */
.booking-cta {
  min-width: 122px;
  padding: var(--sp-2) var(--sp-4);
  font-size: var(--fs-xs);
  line-height: var(--lh-button);
  letter-spacing: 1.05px;
  border-color: var(--c-brand);
}

/* ---- Promo bar ---------------------------------------------------------- */
.promo-bar {
  position: relative;
  z-index: var(--z-promo);
  /* No margin-top: the strip now sits inside the fixed header and overflows its
     90px box, exactly as live does. Flow space for header + promo is reserved on
     .outer-page below, which is live's mechanism (padding-top on the hero
     container) expressed against our own wrapper. */
  min-height: var(--promo-h);
  padding-inline: var(--gutter);
  /* --promo-h is 55, which is what this strip measures once it has settled on a
     desktop width. At 390 the message wraps to three lines and the strip ends up
     75px — but it starts at 45 and grows as the webfont swaps in, shoving the
     whole page down once. That single reflow was the largest layout shift on the
     site (0.25 of a 0.25 CLS on the mobile homepage). Reserving the settled
     height stops the jump; the settled layout is unchanged, because 75px is what
     it was already ending up at. */
  background: var(--c-navy-band);
  color: var(--c-on-dark);
  display: flex;
  align-items: center;
  justify-content: center;
}
.promo-bar__inner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  flex-wrap: wrap;
  padding-block: var(--sp-2);
}
.promo-bar__text {
  font-size: var(--fs-md);
  text-transform: uppercase;
  text-align: center;
  color: var(--c-on-dark);
}
.promo-bar__cta {
  background: transparent;
  color: var(--c-sage);
  border-color: transparent;
  font-size: var(--fs-md);
  text-transform: uppercase;
  padding: var(--sp-1) var(--sp-2);
}
.promo-bar__cta:hover { color: var(--c-on-dark); background: transparent; }

/* ---- Below 1024px: burger appears, utility cluster simplifies ----------- */
.nav-toggle { display: none; }

@media (max-width: 1023px) {
  .site-header { height: auto; position: sticky; }
  .header-bar {
    height: 70px;
    flex-wrap: nowrap;
    padding-inline: var(--gutter);
    gap: var(--sp-3);
  }
  .brand { position: static; left: auto; top: auto; order: 1; flex: 0 0 auto; }
  .brand img { width: 100px; height: 49px; }

  .utility-bar {
    position: static;
    right: auto;
    height: auto;
    order: 2;
    margin-left: auto;
    flex: 0 0 auto;
  }
  .utility-bar__inner { flex-direction: row; align-items: center; gap: var(--sp-3); }
  .utility-bar__phone span { display: none; }
  .utility-bar__phone i { font-size: var(--fs-md); }

  .nav-toggle {
    order: 3;
    display: inline-flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: var(--sp-1);
    color: var(--c-navy);
  }
  .nav-toggle__bars { display: block; width: 22px; }
  .nav-toggle__bars span {
    display: block; height: 2px; margin: 4px 0;
    background: currentColor; border-radius: 1px;
    transition: transform var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
  }
  .nav-toggle__label { font-size: var(--fs-3xs); text-transform: uppercase; letter-spacing: .5px; }

  .nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
  .nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(2) { opacity: 0; }
  .nav-toggle[aria-expanded="true"] .nav-toggle__bars span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }

  /* .promo-bar margin-top is no longer set; the sticky header carries it here. */
  /* The strip settles at 75px here — the message wraps to three lines at this
     width — so the reserve is raised to match. Declared on :root because a
     custom property needs a rule to live in. */
  :root { --promo-h: 75px; }
}

@media (max-width: 767px) {
  .promo-bar__text { font-size: var(--fs-xs); }
  .promo-bar__cta  { font-size: var(--fs-xs); }
}

/* Small phones: the booking CTA's 122px min-width plus 20px padding is the one
   rigid element in the header row and pushed the document 18px wide at 320px.
   Let it size to its label instead of a fixed minimum. */
@media (max-width: 479px) {
  .header-bar { gap: var(--sp-2); }
  .brand img { width: 84px; height: 41px; }
  .booking-cta {
    min-width: 0;
    padding-inline: var(--sp-2);
    font-size: var(--fs-2xs);
    letter-spacing: .6px;
  }
  .utility-bar__inner { gap: var(--sp-2); }
}

/* ---- Promo bar CTA: live is a bordered "LEARN MORE" button --------------- */
.promo-bar__cta {
  border: 1px solid rgba(255, 255, 255, .55);
  color: var(--c-on-dark);
  padding: var(--sp-1) var(--sp-3);
  font-size: var(--fs-md);
  text-transform: uppercase;
}
.promo-bar__cta:hover { background: var(--c-on-dark); color: var(--c-navy-band); }

/* ---- Nav caret --------------------------------------------------------- */
.nav-caret { margin-left: 5px; font-size: 11px; }
@media (max-width: 1023px) { .nav-caret { display: none; } }

/* ---- Offers toaster — live's `.promotions-wrapper` -----------------------
   A cross-fading slideshow pinned at the left, open by default. All values
   measured on live at 1440; see the note in global/side-offers.njk. */
.side-offers {
  position: fixed;
  left: 0;
  top: 150px;
  z-index: var(--z-promo);
}

/* The tab column: close at the top, gift mark, then the vertical label. */
.side-offers__tab {
  position: relative;
  z-index: 2;                    /* the panel slides BEHIND the tab, as live's does */
  width: 48px;
  /* Live measures tab and panel at the SAME 178; ours ran to 184 off its own
     content and overhung the panel by 6px. */
  height: 178px;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-top: 15px;
  background: #203B3B;
  color: var(--c-on-dark);
  overflow: hidden;
}
.side-offers__close,
.side-offers__btn {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 48px;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font-size: 12px;
  cursor: pointer;
}
/* Live rules the close off from the gift mark and the label below it: a 1px
   rgb(38,70,70) line across the tab at the close's bottom edge (sampled on live
   at y39 of the tab, against the tab's own rgb(32,59,59)). */
.side-offers__close {
  height: 25px;
  justify-content: center;
  border-bottom: 1px solid rgb(38, 70, 70);
}
.side-offers__btn { gap: 8px; padding-block: 6px 0; letter-spacing: .6px; text-transform: uppercase; }
.side-offers__label { writing-mode: vertical-rl; transform: rotate(180deg); white-space: nowrap; }
.side-offers__close:focus-visible,
.side-offers__btn:focus-visible { outline: 2px solid var(--c-on-dark); outline-offset: -3px; }

/* Drawn, like the other marks in this build, so it cannot depend on whichever
   glyph the icon font resolves. */
.side-offers__x {
  position: relative;
  width: 12px;
  height: 12px;
}
.side-offers__x::before,
.side-offers__x::after {
  content: "";
  position: absolute;
  top: 5px;
  left: 0;
  width: 12px;
  height: 2px;
  background: currentColor;
}
.side-offers__x::before { transform: rotate(45deg); }
.side-offers__x::after  { transform: rotate(-45deg); }

/* Live slides this sheet out rather than hiding it: `.promo-details-wrap` carries
   `transition: all .8s ease-in-out` and animates left 0 -> -400 (sampled across
   the curve). Absolutely placed beside the tab so the collapsed state occupies no
   layout width and cannot cover the page behind it. */
.side-offers__panel {
  position: absolute;
  left: 48px;
  top: 0;
  width: 350px;
  min-height: 178px;
  padding: 10px 15px 0;
  background: rgba(41, 76, 75, .7);
  transition: transform .8s ease-in-out;
}
.side-offers.is-collapsed .side-offers__panel {
  /* 48 + 350: far enough left that its right edge clears the viewport. */
  transform: translateX(-398px);
  pointer-events: none;
}
/* The markup ships the panel OPEN so it is still there with no JS. That means the
   first painted frame is open, and the script's initial close would animate out
   of it — a slide nobody asked for on every load. This kills the transition for
   that one frame only; the script drops the class immediately afterwards. */
.side-offers.is-instant .side-offers__panel { transition: none; }

.side-offers__slides {
  position: relative;
  height: 110px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.side-offers__slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity .5s ease;
}
.side-offers__slide.is-active { opacity: 1; }
/* The site's global link hover (--c-link-hover, brand teal) was reaching in here
   and repainting the copy dark teal ON the dark teal panel — the text simply
   disappeared under the pointer. Live keeps this text white on hover, with no
   underline, so the panel holds its colour. The heading and body inherit from
   the anchor, so pinning it here covers all three. */
.side-offers__link { display: block; color: var(--c-on-dark); text-decoration: none; }
.side-offers__link:hover,
.side-offers__link:focus-visible { color: var(--c-on-dark); text-decoration: none; }
.side-offers__link:focus-visible { outline: 2px solid var(--c-on-dark); outline-offset: 2px; }
/* live sets the heading in caps via CSS — its DOM text is title case. */
.side-offers__heading { display: block; font-size: 13px; margin-bottom: 20px; text-transform: uppercase; }
.side-offers__para { display: block; font-size: 14px; line-height: var(--lh-body); }

/* Live's `.promo-control` — a 31px row across the whole panel width, ruled off
   from the copy above it. The rule runs edge to edge, so the row cancels the
   panel's 15px inset and re-applies it to its own contents. */
.side-offers__foot {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 31px;
  padding-inline: 15px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-top: 1px solid rgba(41, 76, 75, .7);
}
.side-offers__count {
  margin: 0;
  font-size: 14px;
  color: var(--c-on-dark);
}

/* Live's control row — prev / pause / next, 30x30, right-aligned to the panel's
   own 15px inset, on the same baseline as the counter. */
.side-offers__controls {
  display: flex;
  gap: 0;
}
.side-offers__ctrl {
  width: 30px;
  height: 30px;
  display: grid;
  place-items: center;
  padding: 0;
  border: 0;
  background: none;
  color: var(--c-on-dark);
  cursor: pointer;
}
.side-offers__ctrl:focus-visible { outline: 2px solid var(--c-on-dark); outline-offset: -2px; }

.side-offers__chev {
  width: 8px;
  height: 8px;
  border: solid currentColor;
  border-width: 2px 2px 0 0;
}
.side-offers__ctrl--prev .side-offers__chev { transform: rotate(-135deg); margin-left: 3px; }
.side-offers__ctrl--next .side-offers__chev { transform: rotate(45deg); margin-right: 3px; }

/* Two bars while playing; a triangle once paused — live swaps the same pair. */
.side-offers__pause {
  width: 10px;
  height: 11px;
  border-left: 3px solid currentColor;
  border-right: 3px solid currentColor;
}
.side-offers__ctrl--play[aria-pressed="true"] .side-offers__pause {
  width: 0;
  height: 0;
  border: 6px solid transparent;
  border-left: 10px solid currentColor;
  border-right: 0;
}

/* Collapsed, the control is no longer a close — live swaps the mark for a
   right-pointing chevron, i.e. "open me again". Same control, same slot; only
   the mark and the label change. */
.side-offers.is-collapsed .side-offers__x::before,
.side-offers.is-collapsed .side-offers__x::after { display: none; }
.side-offers.is-collapsed .side-offers__x {
  width: 8px;
  height: 8px;
  border: solid currentColor;
  border-width: 2px 2px 0 0;
  transform: rotate(45deg);
  margin-right: 3px;
}

@media (prefers-reduced-motion: reduce) {
  .side-offers__slide,
  .side-offers__panel { transition: none; }
}
/* Not shown below 768 — client's instruction, matching the live site as served to
   a phone. (Fetching www.yo1.com at a 390 viewport from a desktop browser still
   returns the desktop page, toaster and all, which is why an earlier measurement
   here read it as present on mobile; live serves phones a different template.) */
@media (max-width: 767px) {
  .side-offers { display: none; }
}


/* ---- Flow reservation for the out-of-flow header + promo -----------------
   At >=1024 the header is fixed, so neither it nor the promo occupies flow.
   Live reserves the combined 145px with padding-top on its hero container; the
   same reservation lives on our page wrapper. Below 1024 the header is sticky —
   it stays in flow with the promo inside it — so no reservation is needed. */
.outer-page { padding-top: calc(var(--header-h) + var(--promo-h)); }
@media (max-width: 1023px) {
  .outer-page { padding-top: 0; }
}

/* ===== global/nav.css ===== */
/* ============================================================================
   Primary navigation — ONE component for every viewport.
   ≥1024px: horizontal bar, hover + focus dropdowns, overflow items move into ☰
   <1024px: disclosure panel with keyboard-operable submenus
   Live measurements: nav link 13px/42px, weight 700, uppercase, colour #042441,
   item padding 0 10px, link padding 22.5px 16px 22.5px 11px, 3px bottom border.
   ========================================================================== */
/* min-width:0 lets the flex item shrink to the header width, so the overflow
   measurement in nav.js reflects real available space rather than the
   already-overflowed content width. */
.primary-nav { flex: 1 1 auto; min-width: 0; }

.nav-list {
  display: flex;
  flex-wrap: nowrap;
  justify-content: center;
  align-items: stretch;
  margin: 0;
  padding: 0 180px;         /* clears the absolute logo and the right-hand cluster */
  list-style: none;
  justify-content: flex-start;
}
/* The clip that stops un-measured items spilling must only apply BEFORE nav.js has
   decided what fits. Left on permanently it also clips the dropdowns, which open
   downward out of this box — they were rendering correctly (250x107, visibility
   visible) and simply never appearing. CSS cannot do overflow-x:hidden with
   overflow-y:visible, so the clip is scoped to the pre-measured state instead. */
.nav-list:not(.is-measured) { overflow: hidden; }
/* Centre only once JS has decided what fits; before that, left-align so the
   first items are never clipped off-screen. */
.nav-list.is-measured { justify-content: center; }

.nav-item { position: relative; padding-inline: var(--sp-2); display: flex; align-items: center; }

.nav-link {
  display: block;
  padding: 22.5px 16px 22.5px 11px;
  font-size: var(--fs-xs);
  font-weight: var(--fw-bold);
  line-height: var(--lh-nav);        /* G-5: live measures 42px */
  letter-spacing: var(--ls-default);
  text-transform: uppercase;         /* G-4: live nav links ARE uppercase — keep */
  text-align: center;
  color: var(--c-navy);
  white-space: nowrap;
  /* The indicator is a BAR that grows, not a border that appears — see below.
     The transparent 3px border stays: live carries it too, and it is what makes
     the link 90px tall and leaves the air beneath the bar. Dropping it shrank the
     link to 87 and pulled the bar down against the header edge. */
  position: relative;
  border-bottom: 3px solid transparent;
  transition: color var(--dur-fast) var(--ease);
}

/* Live's nav indicator: a 2px rgb(4,36,65) rule pinned 10px above the link's
   bottom edge, whose WIDTH animates 0 -> the full link width over 0.5s
   (measured `transition: width 0.5s`, 0px at rest, 114.734px hovered on a 115px
   link). The replica snapped a 3px teal border-bottom on instead — the right
   idea, the wrong shape, colour and timing, and with no growth at all. */
.nav-link::before {
  content: "";
  position: absolute;
  left: 0;
  bottom: 10px;
  width: 0;
  height: 2px;
  background: var(--c-navy);
  transition: width .5s var(--ease, ease);
}
.nav-item:hover > .nav-link::before,
.nav-item:focus-within > .nav-link::before,
.nav-link:hover::before,
.nav-link:focus-visible::before { width: 100%; }

/* "You are here." Live does not mark the current page at all — its dropdown grey
   is hover only, verified on /rooms-suites.html where the matching item computes a
   transparent background. This is the client's addition, built from the states the
   nav already owns: the top-level bar sits out at full width, and the current
   submenu row keeps the same grey a hover would give it. */
.nav-item.is-current > .nav-link::before { width: 100%; }
.nav-submenu > li.is-current { background-color: #D3D3D3; }

@media (prefers-reduced-motion: reduce) {
  .nav-link::before { transition: none; }
}
/* Live does not recolour the label on hover — it stays rgb(4,36,65) and only the
   bar grows. Ours shifted it to brand teal at the same time. */
.nav-item:hover > .nav-link,
.nav-item:focus-within > .nav-link { color: var(--c-navy); }

/* ---- Dropdowns --------------------------------------------------------- */
.nav-submenu {
  position: absolute;
  top: 100%;
  left: 0;
  min-width: 250px;
  margin: 0;
  padding: var(--sp-1) 0;
  list-style: none;
  background: var(--c-surface);
  border: 1px solid rgba(0, 0, 0, .15);
  /* live: 0 6px 12px rgba(0,0,0,.176) — a soft drop, not the replica's tight 2px */
  box-shadow: 0 6px 12px rgba(0, 0, 0, .176);
  z-index: var(--z-header);
  /* Hidden via visibility (not display) so focus can still be managed, but
     pointer-events off and inert-like so it is not reachable while closed. */
  opacity: 0;
  visibility: hidden;
  transform: translateY(4px);
  transition: opacity var(--dur-fast) var(--ease), transform var(--dur-fast) var(--ease), visibility var(--dur-fast);
}
/* Opens on hover, on keyboard focus anywhere inside, and on explicit toggle —
   the live site opens on :hover only, leaving 44 links mouse-only (LSI-11b). */
.nav-item:hover > .nav-submenu,
.nav-item:focus-within > .nav-submenu,
.nav-item.is-open > .nav-submenu {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}
/* Live's dropdown rows measure 44px, not 32: the item carries `padding: 0 10px`
   and the link `4px 20px` over a 36px line box. The replica's 6px/19.6px rows
   packed twenty programmes into a 328px panel against live's 452 — the "dropdown
   looks broken" on the QC sheet. Reproduced as padding on the link so the row
   height is the measured 44 without touching type. */
.nav-submenu > li { padding-inline: 10px; }
.nav-submenu a {
  display: block;
  padding: 12px var(--sp-4);
  font-size: var(--fs-sm);
  font-weight: var(--fw-regular);
  line-height: var(--lh-body);
  text-transform: none;
  white-space: nowrap;
  color: var(--c-ink);
}
/* Live greys the ROW and leaves the link alone: `li:hover` paints
   rgb(211,211,211) across the full 396px row while the anchor stays transparent
   and keeps its colour. Ours painted a second, lighter background on the anchor
   itself (--c-surface-warm), which read as a cream band sitting inside the grey
   row, and shifted the label to brand teal at the same time. Only the row now. */
.nav-submenu a:hover { background: none; }

/* Keep the last dropdowns inside the viewport. */
.nav-item:nth-last-child(-n+3) > .nav-submenu { left: auto; right: 0; }

/* ---- The ☰ overflow item (reproduces li.moreOptions) ------------------- */
.nav-more-trigger {
  display: block;
  padding: 22.5px 12px;
  font-size: var(--fs-md);
  color: var(--c-navy);
  border-bottom: 3px solid transparent;
}
/* The mobile-only copy of the ☰ entries. Hidden here, where the ☰ itself shows
   them; revealed in the drawer block below, where the ☰ is not rendered. */
.nav-item--only-mobile { display: none; }
.nav-item--more:hover > .nav-more-trigger,
.nav-item--more:focus-within > .nav-more-trigger { color: var(--c-brand); }
.nav-submenu--more { right: 0; left: auto; }

/* The expand button is a desktop no-op; it exists for the mobile disclosure. */
.nav-expand { display: none; }

/* ============================ < 1024px ==================================== */
@media (max-width: 1023px) {
  .primary-nav {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    max-height: calc(100vh - 70px);
    overflow-y: auto;
    background: var(--c-surface);
    border-top: 1px solid var(--c-hairline);
    box-shadow: var(--shadow-md);
    display: none;
    z-index: var(--z-header);
  }
  .primary-nav.is-open { display: block; }

  .nav-list { flex-direction: column; padding: 0; }

  .nav-item {
    display: block;
    padding: 0;
    border-bottom: 1px solid var(--c-hairline);
  }
  /* The 56px reserve keeps the parent row's label clear of the absolutely
     positioned `.nav-expand` chevron. It belongs to THAT ROW, not to the whole
     item: the chevron is 52px tall and only ever covers the first line, but the
     padding sat on the item, so the open submenu was laid out inside it too and
     came out 334px wide in a 390px drawer — a dead strip down the right of every
     expanded menu. `position: relative` stays here, since the chevron is
     positioned against the item. */
  .nav-item.has-children { position: relative; }
  .nav-item.has-children > .nav-link { padding-right: 56px; }

  .nav-link {
    padding: var(--sp-3) var(--gutter);
    text-align: left;
    border-bottom: 0;
    white-space: normal;
  }
  .nav-item:hover > .nav-link { border-bottom-color: transparent; }

  /* The nav indicator is a DESKTOP BAR affordance and must not reach the drawer.
     `.nav-link::before` is a 2px navy (#042441) rule that animates its width from
     0 to 100% over half a second — right for a 115px bar item on hover, wrong for
     a drawer row, where the link is the full width of the phone. Tapping the
     burger puts focus inside the list, `:focus-within` fires, and a 390px blue
     line sweeps across the menu out of nowhere. There is no hover on a phone for
     it to be the feedback for, and `.is-current` made it permanent on every inner
     page's own menu entry.
     The current page still needs marking, so the drawer uses the marker it
     already owns — the same #D3D3D3 a current submenu row takes — rather than
     inventing a second visual language for one breakpoint. */
  .nav-link::before { display: none; }
  .nav-item.is-current > .nav-link { background-color: #D3D3D3; }

  /* Named, focusable submenu toggle — the live mobile nav uses <button></button>
     with no content at all (LSI-11d). */
  .nav-expand {
    display: flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: 0;
    right: 0;
    width: 56px;
    height: 52px;
    color: var(--c-ink);
  }
  .nav-expand i { transition: transform var(--dur-fast) var(--ease); }
  .nav-expand[aria-expanded="true"] i { transform: rotate(180deg); }

  .nav-submenu {
    position: static;
    min-width: 0;
    border: 0;
    box-shadow: none;
    background: var(--c-surface-warm);
    opacity: 1;
    visibility: hidden;
    transform: none;
    transition: none;
    display: none;
  }
  /* On mobile only an explicit toggle opens a submenu — hover/focus-within must
     not, or tabbing through would spring panels open unpredictably. */
  .nav-item:hover > .nav-submenu,
  .nav-item:focus-within > .nav-submenu { display: none; visibility: hidden; }
  .nav-item.is-open > .nav-submenu { display: block; visibility: visible; }

  /* Drawer submenu rows are TOUCH targets. 10px of padding on a 20px line gave a
     40px row — under the 44px minimum both Apple and Google publish, and 18 of the
     19 Wellness Programs rows sat at exactly that. `min-height` rather than more
     padding so a row that wraps to two lines grows instead of gaining dead space,
     and flex so the label is optically centred in the taller box. The `Popular`
     chip is an `::after` on the same anchor, so it becomes a second flex item and
     keeps sitting beside the label rather than under it. */
  .nav-submenu a {
    padding: var(--sp-2) var(--gutter) var(--sp-2) var(--sp-5);
    white-space: normal;
    min-height: 44px;
    display: flex;
    align-items: center;
    gap: 8px;
  }

  /* Everything else in the ☰ list is a duplicate of a bar item down here, so the
     ☰ itself is redundant — but the entries authored into `nav.more` are not
     duplicates (Contact exists only there), so they are emitted separately and
     revealed here. See global/nav.njk. */
  .nav-item--more { display: none !important; }
  .nav-item--only-mobile { display: block; }
}

/* ---- Dropdown: live's treatment ----------------------------------------
   Reproduced from live's stylesheet:
       .dropdown-menu > li > a                 { text-transform: uppercase !important }
       li.primary:nth-child(2) .dropdown-menu  { column-count: 2 }
       li:nth-child(2).primary:hover ul        { width: max-content }
       li.primary .dropdown-menu li:hover      { background-color: lightgray }
       .dropdown-menu li a.popular:after       { content:"Popular"; background:#0E273F;
                                                 color:#fff; font-size:10px;
                                                 margin-left:10px; padding:1px 6px }

   The column count is driven by the nav DATA (`columns: 2`) rather than live's
   positional `:nth-child(2)`, so it follows the Wellness Programs group even if the
   menu order changes.

   The badge is a CSS `content` string exactly as live does it, which keeps "Popular"
   out of the link's accessible name — a screen reader announces the programme name,
   not "Introductory Package Popular". */
.nav-submenu > li > a { text-transform: uppercase; }
.nav-submenu > li:hover { background-color: #D3D3D3; }

/* TWO COLUMNS, FILLED ACROSS.

   The group holds 19 items. `column-count: 2` fills greedily — first column to
   capacity, remainder into the second — which gave 10 items against 9, a 43px step at
   the foot of the flyout. A grid filled ACROSS rows distributes the remainder instead:
   19 items over two columns is 10/9 by count but the rows are paired, so the two
   columns end level except for the single trailing item.

   Reading order becomes left-to-right; the list is not alphabetical or otherwise
   ordered, so nothing is lost by it. 820px is live's own flyout width. */
.nav-submenu--2col {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: 40px;
  align-content: start;
  width: max-content;
  max-width: min(820px, calc(100vw - 40px));
}
.nav-submenu--2col > li { break-inside: avoid; }

/* The two columns are a DESKTOP flyout treatment. Below 1024 the nav is a drawer
   the width of the screen, and the same rule split the Wellness Programs list
   into two ~150px columns read side by side, wrapping most items onto three
   lines and pushing the "Popular" badge onto its own line. In the drawer the
   list is one column, top to bottom. */
@media (max-width: 1023px) {
  .nav-submenu--2col {
    /* `none`, NOT `block`. This rule used to say `block`, and that single word
       held the Wellness Programs submenu permanently OPEN in the drawer: the
       collapsed default is `.nav-submenu { display: none }` at (0,1,0), and
       `.nav-submenu--2col` at (0,2,0) outranks it, so the one submenu carrying
       the 2-col modifier never collapsed. It stayed `visibility: hidden`, so it
       was invisible — but invisible is not gone: it held 782px of layout, which
       is the blank gap under WELLNESS PROGRAMS, and it pushed Experiences,
       Special Offers, Gallery, Health Guide, Curative Therapies, Groups and
       Retreats, Location and Contact that same 782px down, off the first screen
       and past the end of the drawer's scroll on a 390x844 phone.
       Neutralising the desktop grid must reset the grid PROPERTIES and leave the
       open/closed state to the toggle: `.nav-item.is-open > .nav-submenu` is
       (0,3,0) and still wins when the item is actually opened. */
    display: none;
    grid-template-columns: none;
    column-gap: normal;
    width: auto;
    max-width: none;
  }
}

.nav-submenu a.popular::after {
  content: "Popular";
  display: inline-block;
  background-color: #0E273F;
  color: #fff;
  font-size: 10px;
  line-height: 1.6;
  margin-left: 10px;
  padding: 1px 6px;
  text-transform: uppercase;
  vertical-align: middle;
}

/* ===== global/footer.css ===== */
/* ============================================================================
   Footer. Reproduces the live RENDERED footer (the live site swaps its
   server-side footer for this one via JS — see LSI-02b; ours is server-rendered).
   Measured: bg #F5F5F5 · base 12.25px→12px · inner width 1360 at 40px inset
   · top row h91 with 1px bottom rule · mid row 35px block padding
   · connect heading 18px/700 #89B59E · bottom row 20px padding, 1px top rule.
   ========================================================================== */
/* 3h — visual parity only. Structure, links, social icons, awards and background
   are already verified correct (30/30 links, 9/9 social, 3/3 awards) and the
   improved semantic structure is kept per decision C-16.
   Live measurements @1440: footer 1440×458 · base 12.25px · top row 1360×91 @x40
   · mid row 1360×261 pad 35px 0 · policies 1390×45 pad 0 0 25px · bottom 1356×61
   pad 20px 0 · top-row link 20px/20px · column link 12.25px · social anchor 35×17
   with a 35×35 sage icon · award 108×108. */
/* I-06 / I-07 / I-08 — RESTORED, by client decision (reverses C-1).
   These three were built and measured, then removed when the client asked the
   inner pages to follow the HOMEPAGE footer instead. The client has now asked for
   live's footer, so they are back, and applied everywhere rather than scoped —
   live serves the same dark footer on every page including the homepage.

   I-06 is a measurement trap worth keeping in view: probing `#footer` reports
   `rgb(245,245,245)`, because that is the light outer SHELL. Live's
   `.custom-footer` sits inside it and covers it completely at 1440x458 with
   #0A151E. Reading the shell is what produced the light footer in the first
   place, and it is what a naive re-measure will produce again.

   Text colours are live's own, per region, sampled on the dark ground:
       top-row links      #6C8F7D  20px/20px
       column links       #7A8894  12.25px/19.6px
       address, copyright #64696E  12.25px
       policy links       #6C8F7D  12.25px
       connect heading    #89B59E  18px/19.8px 700
   Nothing is lightened for contrast beyond what live states — the values below
   are what live paints. */
.site-footer {
  background: var(--c-near-black);   /* #0A151E — live's .custom-footer */
  font-size: 12.25px;                /* live: 0.875em of the 14px base */
  color: var(--c-ink-muted);
}
.site-footer .container { width: auto; max-width: none; margin-inline: 40px; }

.site-footer a { color: inherit; font-weight: var(--fw-regular); }
.site-footer a:hover { color: var(--c-brand); }

/* ---- Top row ------------------------------------------------------------ */
.footer-top__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-5);
  min-height: 91px;                    /* live: 1360×91 */
  border-bottom: 1px solid rgba(142, 126, 126, .44);
}
.footer-top__nav ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-6);
  margin: 0;
  padding: 0;
  list-style: none;
}
.footer-top__nav a {
  font-size: 20px;                     /* live: 20px, not the 21px token */
  line-height: var(--lh-footer-link);  /* G-5: 20px */
  color: var(--c-sage);
  display: inline-block;
  padding-block: 35px;                 /* live top-row link is 113×98 */
}
.footer-top__nav a:hover { color: var(--c-brand); }

/* ---- Mid row ------------------------------------------------------------ */
.footer-mid__inner {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--sp-5);
  padding-block: 35px;           /* live: 35px */
  min-height: 261px;             /* live mid row height */
}
.footer-col { padding-inline: var(--gutter); }

.footer-col__heading {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  line-height: 1.1;
  color: var(--c-sage);
}
.footer-col__heading--alt { color: var(--c-heading-alt); }
/* Live leaves 23px between this heading and the first social disc; ours had 10,
   so the row read as stuck to the heading. Measured at 1440 on the live footer:
   heading bottom 607, disc top 630. */
.footer-col--connect .footer-col__heading { margin-bottom: 23px; }

.footer-col__links { margin: 0 0 var(--sp-2); padding: var(--sp-1) 0 0; list-style: none; }
.footer-col__links li { margin-bottom: 4px; }
.footer-col__links a { font-size: 12.25px; line-height: 17px; }   /* live: 62×17 */

/* I-08 — the three badges are ONE row on live. The column is too narrow for them
   at their natural size, so wrapping put the third on its own line; `nowrap` with
   a 12px gap keeps live's single 108px-tall row. */
.footer-col--awards {
  display: flex;
  flex-wrap: nowrap;
  align-items: flex-start;
  gap: 12px;
}
.footer-col--awards img { width: auto; height: 108px; object-fit: contain; }   /* live: 108×108 */

/* Below 768 the row has to fit the screen rather than hold 108px. Three fixed
   badges plus two 12px gaps need 348px, so with the 20px gutters they ran off a
   320px viewport and took the document to 364. They now share the width in equal
   thirds and keep their aspect — still live's single row, just scaled to what is
   there. `min-width: 0` because a flex item will not shrink below its content
   without it. */
@media (max-width: 767px) {
  .footer-col--awards img {
    height: auto;
    width: auto;
    min-width: 0;
    max-width: calc((100% - 24px) / 3);
  }
}

/* ---- Social list ------------------------------------------------------- */
.social-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-3);
  margin: 0;
  padding: 0;
  list-style: none;
}
/* Live: the anchor is 35×17 and the icon glyph inside is a 35×35 sage box at
   19px/300 — the colour sits on the icon, not the link. */
.social-list a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 35px;
  /* Live's anchor is 35x17 with the disc overflowing it, which works there
     because its rows are laid out individually. Here the anchors ARE the flex
     items, so a 17px box painting a 35px disc overflowed 9px top and bottom and
     ate the 15px row gap down to -3 — the rows read as touching, and at 1024
     they measured 1px apart. The box now matches what it paints, so the gap is
     the gap. Horizontal spacing is unchanged (the width is still 35). */
  height: 35px;
  font-size: 12.25px;
  color: var(--c-ink-muted);
}
/* I-07 — live puts the sage on `background-color` and white on `color`: a 35px
   filled disc with a white glyph. The previous rule had sage on `color` with no
   background at all, which is why these read as flat green glyphs. */
.social-list a i {
  width: 35px;
  height: 35px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 19px;
  font-weight: var(--fw-light);
  background: var(--c-sage);
  color: var(--c-on-dark);
  border-radius: 50%;
  transition: background var(--dur-fast) var(--ease);
}
.social-list a:hover i { background: var(--c-brand); color: var(--c-on-dark); }

/* ---- Policy row -------------------------------------------------------- */
.footer-policies__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--sp-2) 0;
  margin: 0;
  padding: 0 0 25px;             /* live: padding 0 0 25px */
  list-style: none;
}
.footer-policies__list a { font-size: 12.25px; line-height: 20px; padding-inline: var(--gutter); }  /* live: 103×20, pad 0 15px */

/* ---- Bottom row -------------------------------------------------------- */
.footer-bottom__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  flex-wrap: wrap;
  padding-block: var(--sp-4);
  border-top: 1px solid rgba(142, 126, 126, .44);
  color: var(--c-ink-grey);
}
.footer-bottom__inner p { margin: 0; font-size: 12.25px; line-height: var(--lh-body); }

/* ============================ responsive ================================= */
@media (max-width: 1023px) {
  .site-footer .container { margin-inline: 0; }   /* mobile gutter: the shared container padding is the inset */
  .footer-mid__inner { grid-template-columns: repeat(2, 1fr); }
  /* Stacked, the desktop link padding (35px top and bottom) plus the 30px column
     gap left a large hole between the link row and the button. Both come down
     here — the row still reads as two bands, just without the dead space. */
  .footer-top__inner {
    flex-direction: column;
    align-items: flex-start;
    gap: var(--sp-3);
    padding-block: var(--sp-4);
    min-height: 0;
  }
  .footer-top__nav ul { gap: var(--sp-4); }
  .footer-top__nav a { font-size: var(--fs-lg); padding-block: 8px; }
}

@media (max-width: 767px) {
  .footer-mid__inner { grid-template-columns: 1fr; gap: var(--sp-5); padding-block: var(--sp-5); }
  .footer-bottom__inner { flex-direction: column; align-items: flex-start; gap: var(--sp-1); }
  .footer-policies__list { gap: var(--sp-1) var(--sp-4); }
}


/* Mobile gutter (<=1023): the footer's .container takes the shared 20px padding
   like every other container, so its own desktop margin and the columns' 15px
   gutters are dropped here rather than stacking on top of it. */
@media (max-width: 1023px) {
  .footer-col,
  .footer-policies__list a { padding-inline: 0; }
}

/* ---- Footer top row on phones ------------------------------------------
   REVERSED at the client's request: this row used to be `display: none` below
   768, dropped because it broke onto three ragged lines (and live's phone footer
   carries none of it either). The client now wants the links and "Book Your
   Stay" on mobile too, so it is shown — with the tightened stacked spacing set
   in the 1023 block above, which is what stops it reading as three ragged lines.

   Note for the record: the mobile golden was re-blessed for the ORIGINAL hide, so
   this reversal moves the homepage's mobile height again. */
@media (max-width: 767px) {
  .footer-top__nav ul { gap: var(--sp-3) var(--sp-4); }

  /* The back-to-top tab is a fixed 41px band centred on the bottom edge below
     768, so without a run-off the last footer lines sit underneath it — the
     copyright and reservations numbers were being covered. Live pads its mobile
     footer by 50px for exactly this (measured: live footer padding-bottom 50px
     at 390 and 0 at 1440, its lowest text ending 11px clear of the tab). */
  .site-footer { padding-bottom: 50px; }
}

/* ---- Footer "Book Your Stay" button -------------------------------------
   `.site-footer a { color: inherit }` was winning over the button's own colour, so
   the label rendered in the footer's muted #7A8894 on the #294C4B fill — a 2.6:1
   contrast ratio, which fails WCAG 2.2 AA (1.4.3 needs 4.5:1) and reads as barely
   visible. Live sets this button's text to white outright:
       .top-footer button { color: white; background-color:#0a151e; border:.5px solid #fff }
   White on #294C4B measures 9.6:1. */
/* Live's footer CTA is a GHOST button, not a filled one: it sits on the footer's
   own rgb(10,21,30) with a 1px white outline and white label, 7px 15px — measured
   128x29 at 1440. Ours was the brand-teal fill, which is the difference in colour
   the client flagged. Hover fills it so the control still responds. */
.site-footer .footer-top__inner .btn--primary {
  background: #0A151E;
  border: 1px solid #fff;
  padding: 7px 15px;
}
.site-footer .footer-top__inner .btn--primary:hover,
.site-footer .footer-top__inner .btn--primary:focus-visible { background: var(--c-brand); }
.site-footer .footer-top__inner .btn--primary,
.site-footer .footer-top__inner .btn--primary:hover,
.site-footer .footer-top__inner .btn--primary:focus-visible { color: #fff; }

/* The one social icon drawn as an SVG rather than an icon-font glyph (TikTok —
   see footer.njk). Sized and filled by the same rules as its siblings so the row
   stays uniform. */
.social-list__svg { line-height: 0; }
.social-list__svg svg { display: block; width: 19px; height: 19px; }

/* ===== global/back-top.css ===== */
/* ============================================================================
   BACK TO TOP — live's `<p id="back-top"><a href="#top"><span class="fa
   fa-angle-up"></span></a></p>`, which the replica did not have at all.

   Measured on live at 1920, 1440 and 390 — identical at all three:
     container  40x40, fixed, right 10, bottom 55, z-index 9999
     disc       the SPAN: 40x40, #294C4B, border-radius 50%, glyph #F5F5F5
     behaviour  display:none until the page is scrolled past 200px, then block

   Hidden by default, so with no JS it never appears — which is exactly what live
   does, since its own reveal is script-driven too. The chevron is drawn rather
   than set from an icon font, like the gallery, lightbox and story chevrons.
   ========================================================================== */
.back-top {
  position: fixed;
  right: 10px;
  bottom: 55px;
  z-index: 9999;
  width: 40px;
  height: 40px;
  display: none;
  place-items: center;
  border-radius: 50%;
  background: var(--c-brand);
  color: #F5F5F5;
  text-decoration: none;
  transition: background var(--dur-fast, 150ms) var(--ease, ease);
}
.back-top.is-visible { display: grid; }
/* The site's global link hover (--c-link-hover, brand teal) was winning over the
   disc's own colour, and the chevron is drawn with `currentColor` — so on hover
   it turned dark teal on a dark disc and vanished. Same fault the offers toaster
   had. The glyph stays light; only the disc darkens. */
.back-top:hover,
.back-top:focus-visible { background: var(--c-brand-dark); color: #F5F5F5; }
.back-top:focus-visible { outline: 2px solid var(--c-brand); outline-offset: 3px; }

.back-top__chev {
  width: 12px;
  height: 12px;
  border: solid currentColor;
  border-width: 2px 0 0 2px;
  transform: rotate(45deg);
  /* the glyph's optical centre sits above the box centre once rotated */
  margin-top: 4px;
}

@media (prefers-reduced-motion: reduce) {
  .back-top { transition: none; }
}

/* ---------------------------------------------------------------------------
   CLEARING THE COOKIE LAUNCHER

   Adopt parks its 60x60 disc at right:20 / bottom:20, so it owns the strip from
   20px to 80px off the bottom — straight through the 55px this button sits at.
   Live collides with it too (measured on an inner page at 1440: back-top
   1390,805 vs adopt 1360,820, a 30x25 overlap), so this is one of the few
   places the replica deliberately does NOT copy live. Lifted to 90px: clear of
   the launcher's 80px top edge with 10px to spare, still on live's right:10
   column, so the two stack instead of sitting on top of each other.
   ------------------------------------------------------------------------- */
.back-top { bottom: 90px; }

/* Below 768 live drops the disc entirely and shows a centred tab flush to the
   bottom edge instead, which is why live has no collision on phones:
     p#back-top  fixed, left 50%, bottom 0, 60x41   (transform centres it)
     a           fills it, #294C4B, radius 20px 20px 0 0
   Copied as measured — it clears the cookie disc on the right and the UserWay
   button on the left on its own, so no lift is needed here. */
@media (max-width: 767px) {
  .back-top {
    right: auto;
    left: 50%;
    bottom: 0;
    transform: translateX(-50%);
    width: 60px;
    height: 41px;
    border-radius: 20px 20px 0 0;
  }
  /* the tab is shallower than the disc, so the chevron needs less optical lift */
  .back-top__chev { margin-top: 3px; }
}

/* ===== global/forms.css ===== */
/* ============================================================================
   Forms. Enquiry + newsletter. Labels are real <label> elements (the live
   newsletter field has only a placeholder — LSI-11).
   ========================================================================== */
.form { display: grid; gap: var(--sp-3); }

.form-field { display: grid; gap: var(--sp-1); }

.form-label {
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-default);
  color: var(--c-ink);
}

/* C-30. Live measures both of its form fields at 6px vertical padding and a 20px
   line-height; with the 1px border that gives the enquiry input its 34px box
   (6 + 20 + 6 + 2). The replica's 10px padding and inherited 19.6px line-height
   produced 41.59px. The newsletter field pins its own explicit height, so it is
   unaffected. Horizontal padding is unchanged. */
.form-control {
  width: 100%;
  padding: 6px var(--sp-3);
  line-height: 20px;
  font-size: var(--fs-sm);
  color: var(--c-ink);
  background: var(--c-surface);
  border: 1px solid var(--c-rule);
  border-radius: var(--radius-0);
  box-shadow: var(--shadow-input);
  transition: border-color var(--dur-fast) var(--ease);
}
.form-control:focus { border-color: var(--c-brand); }
.form-control::placeholder { color: var(--c-ink-muted); }
.form-control[aria-invalid="true"] { border-color: #B4392F; }

.form-error {
  font-size: var(--fs-2xs);
  color: #B4392F;
  min-height: 1em;
}

.form-row {
  display: grid;
  grid-template-columns: repeat(3, 1fr) auto;
  gap: var(--sp-3);
  align-items: end;
}
/* An empty validation slot must not reserve space. Each labelled field carried an
   empty .form-error (12px) plus the 5px field gap, inflating the control row from
   66.2px to 83.2px; `align-items: end` then aligned CONTACT ME to the bottom of
   that reserve, 17px below the inputs. Live has no such reserve and bottom-aligns
   the button with the input row. Hiding it when empty restores that without any
   offset on the button itself. */
.form-error:empty { display: none; }

/* Error messages must not change a field's height.
   The row bottom-aligns its columns (so CONTACT ME sits level with the inputs),
   which means a field that grows an error line pushes the error-free fields DOWN
   to meet it — the labels stopped lining up the moment one field failed and the
   others did not. Taking the message out of flow fixes the alignment, and adding
   the same reserve to every field in a row that has any error keeps the message
   from landing on whatever follows. Both parts are needed: out of flow alone
   would overlap the next field once the row stacks at narrow widths. */
.form-row .form-field { position: relative; }
.form-row .form-error {
  position: absolute;
  /* anchored to the BOTTOM of the reserve below, not to `top: 100%` — that
     resolves past the padding box, so the reserve pushed the message down ahead
     of it instead of making room, and once the row stacked the message landed on
     the next field's label */
  bottom: 0;
  left: 0;
  right: 0;
}
.form-row:has(.form-error:not(:empty)) .form-field { padding-bottom: 18px; }
/* Scoped to `.form-row` on purpose. Taking the message out of flow exists to stop
   a HORIZONTAL row's columns shifting when one of them grows; the page forms and
   the newsletter stack their fields, where a message in normal flow is both
   correct and simpler — and where an absolute one had nothing to anchor to, so it
   rendered somewhere else entirely and read as "no error shown at all". */
.page-form .form-error { display: block; margin-top: 4px; }
@media (max-width: 1023px) { .form-row { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 767px)  { .form-row { grid-template-columns: 1fr; } }

/* Newsletter layout lives with the section that owns it — widgets/home.css,
   under "STAY INFORMED, SUBSCRIBE NOW". Only the shared field/validation
   styling above applies here. */


/* C-31. Live's field column is: label (mb 5px) + a wrapper holding the input,
   and the input itself carries `margin: 8px 0`. That single value produces BOTH
   figures the audit recorded as separate gaps — the 13px label→input gap is the
   label's 5px plus the input's 8px top margin, and the 24px below the input is the
   input's 8px bottom margin plus the form-group's existing 15px. So one rule, not
   two. Scoped to the enquiry form: the newsletter field pins its own box. */
.enquiry-form .form-control { margin-block: 8px; }
/* The submit column holds no control, so with `align-items: end` its margin box
   would align to the row's bottom edge and sit 8px under the inputs. Giving it the
   same 8px keeps CONTACT ME level with the input bottom (C-29). Live reaches the
   same result by positioning its button group absolutely. */
.enquiry-form .form-row > .form-field:last-child { margin-bottom: 8px; }
/* Below 1024, two things that only make sense while the row IS a row are dropped,
   so the cream band's bottom matches its top (the client's report: the GET IN
   TOUCH band read far deeper below CONTACT ME than above the heading).

   1. The 8px above: with the fields stacked there is no input bottom to align to,
      so it is only trailing space under the button.
   2. The empty status line: `.form-status` holds nothing until the form is
      submitted, but an empty <p> still takes a line box AND the rich-text 15px
      top margin — ~39px reserved for a message that is not there yet. It returns
      the moment it has text, so the feedback behaviour is unchanged.

   MOBILE ONLY, deliberately. Both are defensible at every width, but live's
   desktop leaves 61px under that button and ours leaves 47; removing these there
   took it to 8 and the button ended up against the band edge. Desktop keeps what
   it measured. Closing the desktop 47-vs-61 gap is a separate question.

   `.for-inner` for the same reason the band floor carries it: the homepage runs
   this form too, and unscoped these took it 53px shorter at 390. The homepage is
   not part of an article-page spacing fix. */
@media (max-width: 1023px) {
  .for-inner .enquiry-form .form-row > .form-field:last-child { margin-bottom: 0; }
  .for-inner .form-status:empty { display: none; }
}

/* 3f — enquiry submit. Live: 130×29, background #294C4B, padding 7px 15px,
   13px/13px, weight 400. The form itself sits in the 80% container (.container--wide),
   so the button aligns from the grid rather than from any offset. */
.enquiry-form .btn {
  padding: 7px var(--sp-3);
  font-size: var(--fs-xs);
  line-height: var(--lh-button);
  letter-spacing: var(--ls-default);
}

/* ============================================================================
   PER-PAGE ENQUIRY FORM — the longer, bespoke form on four pages.

   See src/_includes/global/page-form.njk for which pages and why the field specs
   are recovered rather than captured. Layout measured off live at 1440 on
   corporate-retreats-catskills:

       label column   185 wide, 14px/19.6 700 rgb(75,87,97)
       control        554 wide starting at x=516, so a 185 + control grid
       input          34 tall, 1px solid #CCC, radius 4, padding 6px 12px, 14px
       textarea       114 tall
       submit         554 wide, 29 tall, #294C4B on white, 13px, padding 7px 15px
       row pitch      49px

   The site's other form draws underline controls; this one is live's bordered
   box, which is what these four pages actually show.
   ========================================================================== */
/* The field group is 739 wide — 185 label + 554 control with NO gap between
   them: live's label starts at 331 and its input at 516, and 331 + 185 = 516.
   Centred rather than left-aligned, which puts it within 19px of live's own
   position without borrowing its container. */
/* `.form` is `display:grid; gap:15px`, and its gap stacks on top of the row's own
   margin — that is where a 49px pitch became 64. The rhythm belongs to the row
   here, so the parent gap is closed. */
.page-form { max-width: 739px; margin-inline: auto; gap: 0; }
.page-form__row {
  display: grid;
  grid-template-columns: 185px minmax(0, 554px);
  align-items: start;
  margin-bottom: 15px;                 /* live's row pitch is 49 = 34 control + 15 */
}
.page-form__label {
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-default);
  color: var(--c-ink);
  padding-top: 7px;                    /* optical centring against the 34px control */
}
.page-form .form-control {
  width: 100%;
  height: 34px;
  padding: 6px 12px;
  border: 1px solid #CCC;
  border-radius: 4px;
  background: var(--c-surface);
  font-family: var(--ff-body);
  font-size: var(--fs-sm);
  color: var(--c-ink);
}
.page-form textarea.form-control { height: 114px; resize: vertical; }
.page-form .form-control:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 1px;
}
.page-form__submit {
  width: 100%;
  padding: 7px 15px;
  font-size: var(--fs-xs);
  line-height: var(--lh-button);
}
/* The consent row on contact-us puts its checkbox beside a long sentence, so the
   label reads as the sentence rather than as a column heading. */
.page-form__row--consent { grid-template-columns: 185px auto; align-items: center; }
.page-form__row--consent .page-form__label { padding-top: 0; }
.page-form__check { width: 18px; height: 18px; flex: none; }
/* The tick was the browser's own blue. `accent-color` recolours the native
   control rather than replacing it, so the box keeps the platform's focus and
   hover behaviour and its own tick glyph — nothing to rebuild with a pseudo
   element. Set on every checkbox and radio in a form, not just this one, so the
   next one added does not come back blue. (There is exactly one in the build
   today: the consent box on contact-us.) */
.form :is(input[type="checkbox"], input[type="radio"]) { accent-color: var(--c-brand); }
.form :is(input[type="checkbox"], input[type="radio"]):focus-visible {
  outline: 2px solid var(--c-brand);
  outline-offset: 2px;
}
/* The box and its agreement text read as one control, as they do on live, where
   both sit inside a single <label> in the field column. Top-aligned rather than
   centred so the box stays level with the FIRST line once the text wraps. */
.page-form__consent {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  color: var(--c-ink);
  cursor: pointer;
}

/* Below 1024 the label column cannot hold 185px beside a control, so the rows
   stack — the same shape the site's other form takes at these widths. */
/* The status line is a direct child of the form, so it started at the form's left
   edge — under the LABEL column, a column away from the fields it is talking
   about and from the Submit button above it. Indented by the label column's own
   width so it lines up with the controls. Live leaves its own message at the left
   edge (measured x=331, the same as its labels); this is a deliberate improvement,
   not a copy of it. */
.page-form .form-status { margin-left: 185px; }

@media (max-width: 1023px) {
  .page-form__row,
  .page-form__row--consent {
    grid-template-columns: 1fr;
    gap: 6px;
    margin-bottom: 14px;
  }
  .page-form__label { padding-top: 0; }
  .page-form__row--submit .page-form__label { display: none; }
  /* the label column is gone once the rows stack, so the indent would be a stray
     inset */
  .page-form .form-status { margin-left: 0; }
}

/* ===== global/simplotel-form.css ===== */
/* ============================================================================
   FORM INTEGRATION — the two pieces of chrome the Simplotel wiring brings with
   it: the country-code widget wrapped around each phone field, and the modal
   holding the reCAPTCHA checkbox.

   The brief was that the form layout must not change, so nothing here restyles
   a field. intl-tel-input wraps the input in a `.iti` element, which is the only
   structural change — these rules make that wrapper transparent to the layout so
   every measured box stays where it was.
   ========================================================================== */

.iti { display: block; width: 100%; }

/* The spacing lived on the input; with the wrapper in between, it has to move up
   a level or the field's outer box shifts by 8px top and bottom. */
.enquiry-form .iti { margin-block: 8px; }
.enquiry-form .iti .form-control { margin-block: 0; }

/* The country button sits inside the field's own border, so the input needs to
   start clear of it. iti's stylesheet handles this on its own control; this only
   restores our focus ring, which our .form-control rule paints on the input. */
.iti .form-control { width: 100%; }

/* ---- reCAPTCHA modal ------------------------------------------------------
   Live uses Bootstrap's `.modal`; this is the same behaviour without the
   dependency. Hidden with the `hidden` attribute rather than a class so it is
   out of the accessibility tree when closed, and so it stays closed with no JS —
   which matters, because the send is script-driven anyway. */
.sc-modal {
  position: fixed;
  inset: 0;
  z-index: 10000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 16px;
  background: rgba(0, 0, 0, .5);
}
/* beats `.sc-modal`'s own display, which the UA's [hidden] rule would lose to */
.sc-modal[hidden] { display: none; }

.sc-modal__dialog {
  width: 100%;
  max-width: 360px;
  background: #FFF;
  border-radius: 4px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, .3);
}
.sc-modal__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  border-bottom: 1px solid #EEE;
}
.sc-modal__title {
  margin: 0;
  font-size: var(--fs-base, 16px);
  font-weight: 600;
  color: var(--c-ink, #32383E);
}
.sc-modal__close {
  padding: 0 4px;
  border: 0;
  background: none;
  font-size: 24px;
  line-height: 1;
  color: #888;
  cursor: pointer;
}
.sc-modal__close:hover,
.sc-modal__close:focus-visible { color: var(--c-ink, #32383E); }
.sc-modal__body {
  display: flex;
  justify-content: center;
  padding: 16px;
}

/* the whole form dims while the request is in flight */
.form.is-busy { opacity: .55; pointer-events: none; transition: opacity .2s; }

.form-status.is-error { color: #B4392F; }
.form-status.is-success { color: #2E7D32; }

/* ===== global/third-party.css ===== */
/* Overrides for the two vendor widgets copied from live: UserWay paints the
   accessibility launcher bottom-left, Adopt the cookie launcher bottom-right.
   Both position themselves, so the only thing here is the one nudge live makes
   on phones, from its own stylesheet:

     .mobile #adopt-controller-button { right: 8px !important; }

   Live gets `.mobile` from server-side UA detection; we key off the same
   767px breakpoint the rest of the phone rules use. `!important` is live's —
   and it is needed, because Adopt writes right:20px onto the element itself. */
@media (max-width: 767px) {
  #adopt-controller-button { right: 8px !important; }
}

/* ===== global/mobile-actions.css ===== */
/* ============================================================================
   MOBILE ACTION BUTTONS — live's two floating controls at the right edge, both
   phone-only (absent from live's desktop DOM). Measured on live at 390:

     #phoneHeader        fixed, right 15, bottom 140, 56x56, #294C4B, radius 50%
                         z 1000, shadow rgba(0,0,0,.4) 2px 2px 6px 0
                         glyph .fa-phone 26px #FFF, always shown
     a.fab_book_button   fixed, right 15, bottom 80, 56x56 — the disc is on the
                         <i> rather than the anchor, glyph .fa-calendar 20px #FFF,
                         material shadow, revealed past 200px of scroll (stepped
                         on live: none at 0/50/100/150, block from 200 — the same
                         threshold the back-to-top button uses)

   The stack that results, off the bottom edge: cookie launcher 20-80, book
   80-136, call 140-196, with the back-to-top tab centred away from all three.

   NOT named `.fab` — FontAwesome owns that class for its brands family, and
   putting it on a button would re-point the element's font.
   ========================================================================== */
.mobile-action {
  position: fixed;
  right: 15px;
  z-index: 1000;
  display: none;
  place-items: center;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: var(--c-brand);
  color: #FFF;
  text-decoration: none;
}
@media (max-width: 767px) {
  .mobile-action--call { display: grid; }
  .mobile-action--book.is-visible { display: grid; }
}

.mobile-action--call {
  bottom: 140px;
  font-size: 26px;
  box-shadow: 2px 2px 6px 0 rgba(0, 0, 0, .4);
}
.mobile-action--book {
  bottom: 80px;
  font-size: 20px;
  box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14),
              0 1px 5px 0 rgba(0, 0, 0, .12),
              0 3px 1px -2px rgba(0, 0, 0, .2);
}

/* The site's global link hover (brand teal on brand teal) blanks white-on-dark
   controls — the same fault the offers toaster and the back-to-top button had.
   The glyph stays white; only the disc darkens. */
.mobile-action:hover,
.mobile-action:focus-visible { background: var(--c-brand-dark); color: #FFF; }
.mobile-action:focus-visible { outline: 2px solid var(--c-brand); outline-offset: 3px; }

/* ===== global/popup.css ===== */
/* ============================================================================
   PROMOTIONAL POPUP — ported from live's custom-plugin script, which builds the
   whole thing in JS and ships its CSS in an inline <style>. Values are live's;
   only the button colour differs (see below).
   ========================================================================== */
#customPopupOverlay {
  position: fixed;
  inset: 0;
  z-index: 9999;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, .6);
  animation: popupFadeIn .3s ease-in-out;
}
#customPopupBox {
  position: relative;
  padding: 0;
  overflow: hidden;
  overflow-y: auto;
  border-radius: 16px;
  background: #FFF;
  box-shadow: 0 12px 24px rgba(0, 0, 0, .2);
  animation: popupScaleIn .3s ease-in-out;
}
.popupCloseBtn {
  position: absolute;
  top: 12px;
  right: 16px;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 30px;
  height: 30px;
  padding: 0;
  border: 1px solid #000;
  border-radius: 30px;
  background: #FFF;
  color: #000;
  font-size: 24px;
  line-height: 10px;
  cursor: pointer;
  outline-offset: 2px;
  transition: all 250ms ease;
}
.popupCloseBtn:is(:hover, :focus, :active) { background: #000; color: #FFF; }
.popupCloseBtn i { padding-right: .5px; font-size: 18px; font-weight: 400; }

.popupMedia :is(img, iframe, video) {
  display: block;
  width: 100%;
  max-width: 550px;
  max-height: calc(100vh - 60px);
  object-fit: cover;
}

.popupContent { padding: 20px; text-align: center; }
/* Live's button is Bootstrap's default blue (#007bff). YO1's own config leaves
   buttonText empty, so nothing renders it today — kept for the day it is filled
   in, in the site's own colour rather than a framework default. */
.popupBtn {
  display: inline-block;
  margin-top: 16px;
  padding: 10px 20px;
  border-radius: 8px;
  background: var(--c-brand);
  color: var(--c-on-dark, #FFF);
  font-weight: bold;
  text-decoration: none;
  transition: background .3s;
}
.popupBtn:hover { background: var(--c-brand-dark); }
.popupBtn-disabled { opacity: .6; cursor: not-allowed; }
.popupSocial { margin-top: 8px; font-size: .9em; color: #333; }

@keyframes popupFadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes popupScaleIn {
  from { transform: scale(.9); opacity: 0; }
  to { transform: scale(1); opacity: 1; }
}

@media (max-width: 1200px) {
  .popupCloseBtn { top: 10px; right: 10px; width: 24px; height: 24px; }
}
@media (max-width: 768px) {
  .popupCloseBtn { top: 5px; right: 5px; width: 20px; height: 20px; }
  .popupCloseBtn i { padding-right: 0; padding-left: .5px; font-size: 14px; }
  /* live scopes these with its `.mobile` body class; dvh keeps the box clear of
     a phone's collapsing address bar */
  .popupMedia :is(img, iframe, video) {
    max-width: min(calc(100vw - 40px), 550px);
    max-height: calc(100dvh - 40px);
  }
}
@media (prefers-reduced-motion: reduce) {
  #customPopupOverlay, #customPopupBox { animation: none; }
}

/* ===== widgets/location.css ===== */
/* ============================================================================
   LOCATION PANEL — tabbed places list beside the map, on the location page.

   Layout follows the reference the client supplied
   (lamarvella.com/location.html): a dark panel on the left, map on the right,
   tabs across the panel, distance rows beneath, a full-width directions button
   at the foot. The reference's own palette is gold on near-black; this uses
   YO1's brand instead — the layout was the brief, not the identity. The accent
   is --c-heading-alt (#89B59E), which the site already uses for headings inside
   dark blocks, rather than a gold invented for this component.
   ========================================================================== */
/* The widget sat flush against the copy above it and the press band below. The
   captured padding for this widget is 0/0, so the breathing room is stated here
   rather than fought with `--pt`/`--pb`. */
.section.section--location { padding-block: 50px 60px; }
@media (max-width: 767px) { .section.section--location { padding-block: 30px 36px; } }
/* the plain map's 490px height rule must not reach this one */
.section--location .loc__map { height: auto; }

.loc {
  display: grid;
  grid-template-columns: 1fr 1.3fr;   /* reference proportion */
  border: 1px solid var(--c-rule);
  overflow: hidden;
}

.loc__panel {
  display: flex;
  flex-direction: column;
  /* A grid item will not shrink below its content's min-content width, and the
     nowrap tab strip makes that 419px. Without this the panel forced the whole
     grid wider than the viewport, `.loc`'s overflow:hidden clipped the last tab,
     and the strip never overflowed ITSELF — so there was nothing to scroll and
     "Nearby Attractions" could not be reached at all. */
  min-width: 0;
  background: var(--c-brand-dark, #1B3534);
  color: var(--c-on-dark, #FFF);
}

.loc__head { padding: 28px 32px 20px; border-bottom: 1px solid rgba(255, 255, 255, .08); }
.loc__name {
  margin: 0 0 4px;
  font-size: var(--fs-lg, 20px);
  font-weight: var(--fw-bold);
  line-height: 1.3;
}
.loc__place {
  margin: 0;
  font-size: var(--fs-2xs, 11px);
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--c-heading-alt);
}

/* One row, always. The labels are long enough to wrap to two and three lines in
   a 500px panel, which gave a tall ragged strip; they stay on one line and the
   strip scrolls sideways instead, at every width. Scroll-snap so a flick lands
   on a tab rather than between two. */
.loc__tabs {
  display: flex;
  background: rgba(255, 255, 255, .04);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;                 /* the tabs are their own affordance */
  min-width: 0;                          /* same floor, one level down */
}
.loc__tabs::-webkit-scrollbar { display: none; }
.loc__tab {
  /* grow to share the strip when the three fit, never shrink below their own
     text — so the strip scrolls only where it genuinely has to */
  flex: 1 0 auto;
  white-space: nowrap;
  scroll-snap-align: start;
  padding: 14px 16px;
  border: 0;
  border-bottom: 2px solid transparent;
  background: none;
  color: rgba(255, 255, 255, .72);
  font: inherit;
  font-size: var(--fs-2xs, 11px);
  letter-spacing: .14em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color .3s, border-color .3s, background .3s;
}
.loc__tab:hover { color: #FFF; }
.loc__tab.is-active {
  color: var(--c-heading-alt);
  border-bottom-color: var(--c-heading-alt);
  background: rgba(255, 255, 255, .05);
}
.loc__tab:focus-visible { outline: 2px solid var(--c-heading-alt); outline-offset: -2px; }

.loc__list { display: none; padding: 12px 32px; }
.loc__list.is-active { display: block; }
.loc__list[hidden] { display: none; }

.loc__row {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  padding: 14px 0;
  border-bottom: 1px solid rgba(255, 255, 255, .07);
  color: inherit;
  text-decoration: none;
  transition: padding-left .3s;
}
.loc__row:last-child { border-bottom: 0; }
/* The site's global link hover (brand teal) was winning here, turning white rows
   into #294C4B on a #203B3B panel — about 1.05:1, effectively invisible. The
   same rule has already had to be beaten off the offers toaster, the nav
   submenu, the back-to-top button and the mobile action buttons; anything
   white-on-dark needs its hover stated outright. The row keeps its white text
   and the name picks up the accent, so hover still reads as a change. */
.loc__row:hover { padding-left: 6px; color: #FFF; }
.loc__row:hover .loc__label { color: var(--c-heading-alt); }
.loc__row:hover .loc__addr { color: rgba(255, 255, 255, .75); }
.loc__row:focus-visible { outline: 2px solid var(--c-heading-alt); outline-offset: 2px; }
/* the row the map is currently showing */
.loc__row.is-current { padding-left: 6px; }
.loc__row.is-current .loc__label { color: var(--c-heading-alt); }

.loc__icon {
  display: grid;
  place-items: center;
  flex: none;
  width: 30px;
  height: 30px;
  border: 1px solid rgba(255, 255, 255, .2);
  color: var(--c-heading-alt);
  font-size: 11px;
}
.loc__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.loc__label { font-size: var(--fs-sm, 14px); }
.loc__dist {
  font-size: var(--fs-xs, 12px);
  letter-spacing: .06em;
  color: var(--c-heading-alt);
}
.loc__addr { font-size: var(--fs-2xs, 11px); color: rgba(255, 255, 255, .6); line-height: 1.5; }

.loc__dir {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin: auto 32px 28px;      /* `auto` top pins it to the panel foot */
  padding: 14px;
  background: var(--c-heading-alt);
  color: var(--c-brand-dark, #1B3534);
  font-size: var(--fs-xs, 12px);
  font-weight: var(--fw-bold);
  letter-spacing: .18em;
  text-transform: uppercase;
  text-decoration: none;
  transition: filter .3s;
}
/* same trap on the button: its dark label was being repainted brand teal */
.loc__dir:hover { filter: brightness(1.08); color: var(--c-brand-dark); }
.loc__dir:focus-visible { outline: 2px solid #FFF; outline-offset: 2px; }

.loc__map { display: block; width: 100%; min-height: 560px; border: 0; }

@media (max-width: 1023px) {
  .loc { grid-template-columns: 1fr; }
  /* map first on a phone: it is the thing people came for, and the panel is
     tall enough to push it off the screen otherwise */
  .loc__map { grid-row: 1; min-height: 340px; }
  .loc__head, .loc__list { padding-inline: 20px; }
  .loc__dir { margin-inline: 20px; }
}
@media (max-width: 480px) {
  .loc__tab { letter-spacing: .08em; padding: 12px 14px; }
}
@media (prefers-reduced-motion: reduce) {
  .loc__row, .loc__tab, .loc__dir { transition: none; }
}

/* ===== widgets/hero.css ===== */
/* Hero: 735px video band. Header (90) + promo (55) + hero (735) = 880, matching live. */
.hero {
  position: relative;
  height: var(--hero-h);
  overflow: hidden;
  background: var(--c-near-black);
}
.hero__video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}
/* Live: play/pause + mute, centred at the bottom of the hero. */
.hero__controls {
  position: absolute;
  left: 50%;
  translate: -50% 0;
  bottom: var(--sp-3);
  display: flex;
  gap: var(--sp-1);
}
.hero__btn {
  width: 34px; height: 30px;
  display: inline-flex; align-items: center; justify-content: center;
  color: var(--c-on-dark);
  background: var(--c-overlay-50);
  transition: background-color var(--dur-fast) var(--ease);
}
.hero__btn:hover { background: var(--c-brand); }

@media (max-width: 1023px) { .hero { height: 480px; } }
@media (max-width: 767px)  { .hero { height: 340px; } }
@media (max-width: 400px)  { .hero { height: 300px; } }

/* ===== widgets/carousel.css ===== */
/* ============================================================================
   ONE carousel component, replacing JSSOR + Slick + Bootstrap carousel.
   Scroll-snap based: works with no JS, keyboard-navigable, and off-screen slides
   are not focus traps (the live carousels put focusable links inside
   aria-hidden containers — LSI-09).
   ========================================================================== */
.carousel { position: relative; }

.carousel__viewport { overflow: hidden; }

.carousel__track {
  display: flex;
  /* No flex gap — spacing is the slide's own margin-right, so the pitch
     (slide + gap) tiles the viewport exactly three times, as live does. */
  gap: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;
  scrollbar-width: none;
}
.carousel__track::-webkit-scrollbar { display: none; }

/* Live tiles the viewport by PITCH, not by a centre peek:
     pitch = viewport / 3      slide = pitch - 40      gap = 40 (slide margin-right)
   Verified exact at four widths:
     1920: 1536/3 = 512 → slide 472 ✓    1440: 1152/3 = 384 → slide 344 ✓
     1280: 1024/3 = 341 → slide 302 ✓    1024:  819/3 = 273 → slide 233 ✓
   Three pitches fill the viewport exactly, so exactly three slides show and no
   fourth card is exposed. The earlier "(container - 120)/3" model produced the
   right slide WIDTH but no gap, leaving 120px spare and revealing a 4th card. */
.carousel { --carousel-gap: 40px; }
/* The gap is split EVENLY across the two sides, which is what live does
   (measured margin-left: 20px; margin-right: 20px on every .slick-slide).
   Pitch and slide width are unchanged — 20+20 still tiles at slide+40 — but the
   row now sits 20px inside the viewport on both edges instead of flush-left with
   a 40px hole on the right. Putting the whole gap on margin-right shifted every
   slide 20px left of live at all four desktop widths.
   scroll-margin-left grows the snap area to the margin box so `start` snapping
   still lands the card at viewport + 20, not viewport + 0. */
.carousel__slide {
  flex: 0 0 calc(100% / var(--per-view) - var(--carousel-gap));
  margin-inline: calc(var(--carousel-gap) / 2);
  /* Live puts 20px on ALL FOUR sides of every .slick-slide. The horizontal half
     was restored in C-18; the vertical half is the same authored margin and adds
     40px to each carousel section's height. Fixed 20px, not half the gap: the
     narrow-viewport gap changes (20px, 15px) are horizontal pitch only. */
  margin-block: 20px;
  scroll-margin-left: calc(var(--carousel-gap) / 2);
  scroll-snap-align: start;
  /* A flex item's automatic minimum size is its min-content width, which floors a
     narrow slide at whatever its longest word needs: at 768 the three ayurveda
     cards came out 188 / 182 / 165 instead of live's uniform 165. Slick writes an
     explicit pixel width on every slide, which is the same thing this says. */
  min-width: 0;
  --per-view: 3;
}

/* Arrows — NOT one shared implementation on live. Three measured variants:
     card sliders : 20×20, transparent, glyph 40px  #294C4B, sat in the outer margin
     reviews      : 20×20, transparent, glyph 20px  #333333, offset -25px
     hero (JSSOR) : 30×30, transparent, glyph 19.6px #FFFFFF
   None has a background chip — the replica's 40×40 rgba(0,0,0,.5) button was invented.
   One primitive + two variants, rather than three separate implementations. */
.carousel__btn {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
  width: 20px; height: 20px;
  display: inline-flex; align-items: center; justify-content: center;
  background: none;
  color: var(--c-brand);
  font-size: 40px;
  line-height: 40px;
  opacity: .75;              /* live: opacity 0.75 on the glyph */
  z-index: var(--z-raised);
  transition: color var(--dur-fast) var(--ease), opacity var(--dur-fast) var(--ease);
}
.carousel__btn:hover { color: var(--c-brand-dark); opacity: 1; }
/* Live anchors the arrows to the full-width section, not the carousel container:
   prev is a constant 60px from the page's left edge and next 75px from the right,
   at every viewport. The positioning context is therefore the section. */
.carousel__btn--prev { left: 60px; }
.carousel__btn--next { right: 75px; }
/* An arrow that cannot move anything is not a control, it is furniture. The script
   marks both buttons `disabled` whenever every slide already fits, which is exactly
   the state the client asked about on dining-at-yo1: three cards, all on screen at
   ≥1024, two arrows that do nothing. Below 1024 the same three cards overflow, the
   script enables the arrows, and they come back — so the behaviour follows whether
   there is anything to scroll rather than a guessed breakpoint, and stays right for
   any card count.

   HELD BACK FROM THE HOMEPAGE for the same reason as the card CTA above: its
   three-card offer slider is in this state too, and the page is pixel-frozen. The
   `:not()` is a hold, not a design distinction — remove it with the other one when
   the golden is re-blessed. */
.section:not(.w-mod-health-guide-slider) .carousel__btn[disabled] { display: none; }
.carousel__btn[disabled] { opacity: .35; cursor: default; }

/* Reviews variant — smaller glyph, neutral colour, tighter offset. */
.reviews .carousel__btn { font-size: 20px; color: #333333; }
.reviews .carousel__btn--prev { left: -25px; }
.reviews .carousel__btn--next { right: -25px; }
/* Reviews sit in the 85% container and are positioned against it, not the page. */
.reviews .carousel { position: relative; }

@media (max-width: 1279px) { .carousel__slide { --per-view: 3; } }

/* Below 1024 the bleed/container inset is removed (G-6), so the arrows must sit
   inside the track — at their desktop -30px offset they pushed the document 30px
   wider than the viewport. */
/* Below 1024 there is no page margin for the arrows to sit in (the container is
   full-bleed), so a 40px glyph in a 20px box would push the document ~10px wider.
   Give the button a box that actually contains its glyph and inset it. */
@media (max-width: 1023px) {
  /* gap declared on the carousel so the track's column-gap and the slide's
     flex-basis read the same value; --per-view stays on the slide. */
  .carousel { --carousel-gap: 20px; }
  .carousel__slide { --per-view: 2; }
  .carousel__btn,
  .reviews .carousel__btn { width: 32px; height: 32px; font-size: 28px; line-height: 32px; }
  .carousel__btn--prev,
  .reviews .carousel__btn--prev { left: 4px; }
  .carousel__btn--next,
  .reviews .carousel__btn--next { right: 4px; }
}
@media (max-width: 767px) {
  .carousel { --carousel-gap: 15px; }
  .carousel__slide { --per-view: 1.2; }

  /* REVIEWS ARROWS, quieter on a phone. At 1.2 slides per view the card fills the
     screen and the arrows sit ON the review copy rather than beside it — at 28px and
     .75 opacity in #333 they read as two dark buttons stamped over the text (measured
     overlapping the third line of the first card). Smaller and lighter so they still
     say "there is more" without competing with the review; full strength on press, so
     the target itself is unchanged. */
  .reviews .carousel__btn { font-size: 22px; opacity: .4; }
  .reviews .carousel__btn:hover,
  .reviews .carousel__btn:focus-visible,
  .reviews .carousel__btn:active { opacity: .9; }
}

/* Card sliders do not step down to two-up at 768. Live keeps three cards and the
   full 40px pitch all the way to 768 — measured on the ayurveda programme slider:
   viewport 615 = 3 x 205, slide 165 — where the replica showed two 344px cards and
   a very different row. The generic 1023 step above is right for the other
   carousels, so this restores the card slider's own geometry rather than moving
   the shared breakpoint. The review slider keeps the generic behaviour. */
@media (min-width: 768px) and (max-width: 1023px) {
  .section:not(.w-mod-yo1-text-review-slider) .carousel--cards { --carousel-gap: 40px; }
  .section:not(.w-mod-yo1-text-review-slider) .carousel--cards .carousel__slide { --per-view: 3; }
  /* G-6 takes every container full-bleed below 1024, but live holds this widget's
     carousel at 80% of the viewport until 768 (615 at 768, 819 at 1024, 1152 at
     1440 — the same 80% at all three) and only goes edge-to-edge on phones. The
     three-up pitch above is measured against that 615, so the two travel together. */
  .section:not(.w-mod-yo1-text-review-slider) .container--wide:has(> .carousel--cards) {
    width: var(--container-wide);
    margin-inline: auto;
    /* G-6 also puts a 20px gutter on the mobile container; the 80% width already
       is the inset here, and leaving it on measured a 574 viewport against live's
       615. */
    padding-inline: 0;
  }
}


/* ---- Card ---------------------------------------------------------------
   Live: slide 344×464 with a WHITE background; image 342×192 (16:9, 1px inset);
   text wrapper 342 wide with 0 20px padding; h3 21px/700; CTA a filled button. */
.card {
  display: flex;
  flex-direction: column;
  background: var(--c-surface);
  /* Live draws a real edge here: 1px solid rgba(192,187,187,.7) on white,
     measured identically on the ayurveda programme slider and on the homepage's
     health-guide slider — both are the same `wt-image-text-slider` widget. The
     replica declared the border transparent, so the cards had no edge at all and
     dissolved into the pale band behind them. Kept as a 1px border either way, so
     nothing moves when the colour changes. */
  border: 1px solid rgba(192, 187, 187, .7);
}
/* The text-review slider is the one card slider live leaves borderless (measured
   408x270, transparent ground). It keeps the invisible border. */
.w-mod-yo1-text-review-slider .card { border-color: transparent; }
.card__media { margin-bottom: 0; }
.card__media img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; display: block; }
.card__body {
  font-size: var(--fs-sm);
  padding: 0 var(--sp-4);
  color: var(--c-ink);
}
/* Air between the photograph and the card title — a client request, not live's
   geometry: live butts the h3 straight onto the image (h3 top 5523 = image bottom
   5523, measured), which is why the replica had no gap either. 15px is the card's
   own rhythm — the same value C-23 restores for a card that opens with a spacer
   paragraph — and it grows each card by 15px.

   HELD BACK FROM THE HOMEPAGE, like the CTA size and the pinned foot: its sliders
   carry the same cards and the page is pixel-frozen against the golden. The review
   slider is excluded because live gives it no card padding at all. */
.section:not(.w-mod-health-guide-slider):not(.w-mod-yo1-text-review-slider) .card__body {
  padding-top: var(--sp-3);
}

/* Live's theme default for a card title is #32383E — the same value DS-13 records
   for inner-page h3 — not the #4B5761 body ink the replica inherited. */
.card__body h2, .card__body h3 { color: #32383E; }
.card__body h2, .card__body h3 {
  font-size: var(--fs-xl);
  /* Same C-23 compensation as .richtext h3 — live measures 0 here too. Leaving it
     in this scope made the restored spacer additive (body 256.9 vs live 246.9). */
  margin-bottom: 0;
}
/* C-23: in card sliders live renders the authored spacer paragraphs as
   inline-block (measured), so two consecutive spacers share ONE line box —
   34.6px for the pair, not 69.2px. Card copy is authored as <div>, so every <p>
   in a card body is a spacer (verified: 0 non-blank <p> across all 34 slides).
   Outside card sliders live keeps paragraphs block, so this stays card-scoped. */
.card__body p { display: inline-block; }
/* On live the leading authored <p></p> (display:none) occupies first-child, so the
   visible spacer keeps the 15px rhythm margin and the image→heading gap measures
   34.6px. We strip that empty paragraph, so re-assert the measured value here. */
.card__body > p:first-child { margin-top: 15px; }

/* ---- Card CTA -----------------------------------------------------------
   SHARED, and keyed on the authored class exactly as live is. Live does not
   style "every anchor in a card": it styles a.btn.txt-wdgt-btn.btn-default as a
   filled button (105×29, #294C4B, 7px 15px, 13px/13px, white) and leaves every
   other anchor as inline text (#6C8F7D, no box). Styling `.card__body a` turned
   two authored inline links into filled blocks:
     - Health Guide, where the <a> wraps the card TITLE  -> title in a filled box
     - Special Offers, where "Know More" sits inside the description sentence
   Both are the same defect, fixed once here rather than per variant. */
.card__body a.btn {
  display: inline-block;
  /* No margin-top: live measures 0. The 10px here was a replica compensation for
     the stripped <p>&nbsp;</p> spacer (C-23). */
  padding: 7px var(--sp-3);
  background: var(--c-brand);
  color: var(--c-on-dark);
  font-size: var(--fs-xs);
  line-height: var(--lh-button);
  letter-spacing: var(--ls-default);
}
.card__body a.btn:hover { background: var(--c-brand-dark); color: var(--c-on-dark); }

/* The card CTA is not the inner-page CTA. Live measures 105x29 inside a card —
   13px/13px, 7px 15px, 1px border in the fill colour — against 123x29 for the same
   authored class outside one on the same page. `.richtext a.btn.txt-wdgt-btn`
   (DS-04, 12px/12px 20px/42px) was winning here purely on specificity, because a
   card body IS a richtext, so every card button rendered 13px too tall.

   HELD BACK FROM THE HOMEPAGE for the same reason as the pinned foot below: its
   cards keep the CTA in flow, so shrinking it moves the page and the golden is
   frozen. Remove the `:not()` when the golden is re-blessed. */
.section:not(.w-mod-health-guide-slider) .card__body.richtext a.btn.txt-wdgt-btn {
  font-size: var(--fs-xs);
  line-height: var(--lh-button);
  padding: 7px var(--sp-3);
  border: 1px solid var(--c-brand);
}

/* Non-button links keep the global link treatment (G-3: #6C8F7D) and stay in the
   text flow. Live measures display:inline, no background, no padding. */
.card__body a:not(.btn) { display: inline; padding: 0; background: none; }

/* ---- Health Guide variant ----------------------------------------------
   The only presentational difference live gives this variant is the "Popular"
   badge: 56×15, #0E273F, 10px/11px, 700, padding 1px 6px 3px,
   margin 0 0 2.78125px 10px, letter-spacing .5px, vertical-align middle.
   Scoped to the variant so a leading <span> in any other card heading is not
   swept into a badge by position alone. */
.card--guide .card__body h2 > span:first-child,
.card--guide .card__body h3 > span:first-child {
  display: inline-block;
  padding: 1px 6px 3px;
  margin: 0 0 2.78125px 10px;
  background: var(--c-navy-band);
  color: var(--c-on-dark);
  font-size: var(--fs-3xs);
  font-weight: var(--fw-bold);
  line-height: 1.1;
  letter-spacing: .5px;
  vertical-align: middle;
}

/* Live glyphs: Font Awesome Pro arrow-circle (U+F0A8 / U+F0A9) at weight 100. */
.carousel__btn i { font-weight: 100; font-size: inherit; line-height: inherit; }


/* ---- Mobile gutter (<=1023) --------------------------------------------
   On desktop the slide's own left/right margins carry the spacing AND inset the
   row 20px inside the viewport, which is live's model. Below 1024 the container
   already supplies the 20px, so a leading margin would push the first card to
   27.5px while every other widget sits on 20px. Switching to a track column-gap
   keeps the pitch identical (basis + gap, exactly as before) but drops the
   leading and trailing half-margins, so the first and last cards land on the
   same 20px edge as the rest of the page. carousel.js reads column-gap as well
   as the margins, so the step calculation is unchanged. */
@media (max-width: 1023px) {
  .carousel__track { column-gap: var(--carousel-gap); }
  .carousel__slide { margin-inline: 0; scroll-margin-left: 0; }
}

/* ==========================================================================
   TEXT-REVIEW SLIDER — live `simp-custom-yo1-text-review-slider`, 16 programme
   pages. Three quoted guest reviews on white cards.

   Measured on live at 1440 (introductory-package):
       widget            1224 wide (the page's own container, x=108)
       slide             376 x 203.6 · padding 20px · margin 10px 16px
                         background #fff · radius 6px
                         shadow rgba(50,50,50,.15) 0 4px 13px -2px
       h3                21px / 23.1 / 700 rgb(50,56,62)
       body              14px / 19.6
       image block       display:none
       arrows / dots     none — all three cards fit
   The replica had the generic card slider here: no padding, square corners, no
   shadow, the 80% carousel viewport, and (on two pages) the stray CMS image
   rendered as a photo. The card is the component's whole visual identity, so it
   is spelled out rather than inherited.
   ========================================================================== */
.w-mod-yo1-text-review-slider .carousel__slide {
  padding: var(--sp-4);
  margin-block: 10px;
  margin-inline: 16px;
  background: #fff;
  border-radius: 6px;
  box-shadow: rgba(50, 50, 50, .15) 0 4px 13px -2px;
}
/* The slide's own padding replaces the generic card body inset, so the copy is
   not double-padded. */
.w-mod-yo1-text-review-slider .card__body { padding: 0; }

/* Live's pitch is slide + 32px (16px each side), not the 40px card-slider gap. */
.w-mod-yo1-text-review-slider .carousel { --carousel-gap: 32px; }

/* No arrows on this widget, at any width — by client decision.

   Above 1024 there is nothing for them to do: all three reviews are on screen and
   live draws none either. Below it the row scrolls, but a swipe is the gesture on
   touch and the next card's peek already says so, so the client asked for them
   gone rather than sitting over the cards. The row stays reachable without them:
   scroll-snap for touch and trackpad, and the script's Arrow-key handler for the
   keyboard.

   Not generalised to `.carousel__btn[disabled]` site-wide, tempting as that is —
   the homepage's three-card offer slider draws the same dead pair and is under the
   content freeze, so it is reported in the docs rather than silently changed. */
.w-mod-yo1-text-review-slider .carousel__btn { display: none; }

/* ==========================================================================
   CARD CTA — pinned to the card foot, on one line across the row.

   The Read More buttons sat wherever each card's copy happened to end, so a row
   of cards showed them at four different heights: measured spreads of 27px on
   rooms-suites and 23 / 46 / 20px across the homepage's three sliders. Live is no
   better — its spread on the same rooms-suites row is 59px — so this is a design
   decision, not a fidelity one. Cards in a row are equal height already (the
   slides stretch), so the only thing stopping the CTAs from lining up was that
   they were still in the text flow.

   Absolute, rather than flex with `margin-top:auto`, because the authored button
   sits two `contenteditable` wrappers deep inside the copy div and is not a child
   of the card body. That is the same reason the programme cards pin theirs, and
   `display:contents` on the wrappers would turn every sibling into a flex item
   and change their margin behaviour.

   The reserve is 20px, not the height of the button. Live's own tallest card keeps
   its CTA IN the copy flow, landing it in the trailing `<br>` whitespace every one
   of these cards is authored with, and then aligns the shorter cards' buttons to
   that same line — so the row is only ever as tall as the tallest copy plus a
   little air, never copy PLUS a button. Reserving the button's full height on top
   (the earlier 93px, sized for DS-04's 43px inner-page CTA) made every card in the
   row 59px taller than live. Measured: live 610 = 2px border + 192px image + 415px
   tallest copy + 1; ours now reproduces that.

   HELD BACK FROM THE HOMEPAGE, pending sign-off. Its three sliders carry 25 of the
   43 buttoned cards and have the same defect (spreads of 23 / 46 / 20px), so this
   SHOULD reach them. It cannot yet: the homepage is pixel-frozen against a golden
   and the fix grows it 153px (51px per slider, all of it this foot). Re-blessing
   that golden is a baseline change for the client to authorise, not something to
   do quietly in a CSS pass. `health-guide-slider` is homepage-only, so the
   exclusion below is a hold, not a design distinction — DELETE IT, and re-bless,
   the moment the change is approved. Until then the homepage's CTAs stay ragged
   while every other slider's line up, which is a known, temporary inconsistency. */
.section:not(.w-mod-health-guide-slider) .card:has(a.btn.txt-wdgt-btn) { position: relative; }
.section:not(.w-mod-health-guide-slider) .card:has(a.btn.txt-wdgt-btn) .card__body { padding-bottom: var(--sp-4); }
.section:not(.w-mod-health-guide-slider) .card .txt-wdgt-btn-wrapper {
  position: absolute;
  left: var(--sp-4);
  right: var(--sp-4);
  /* Live lands the anchor box 16px off the card's bottom edge (measured 594 of 610
     on all three ayurveda cards, which all share one line). The 30px here was part
     of the invented foot and sat the button 15px higher than live's. */
  bottom: var(--sp-3);
  margin: 0;
}


/* ============================================================================
   PLAIN IMAGE SLIDER — live's `wt-image-text-slider` in its nonFullWidthWidget
   shape: PAST RETREATS on groups-and-retreats, and the dining-at-yo1 row. Two
   widgets site-wide; the full-width shape (the bordered programme card) is
   unchanged.

   Measured at 1440 on both pages, identically:
     viewport 1224 (the 85% container)   slide 408, margin 0, padding 0 15px
     slide transparent, NO border        image 378 wide, its own height
     caption 378 wide, padding 0         h3 21px/700 #32383E, centred
   The image heights differ between the two pages — 378 (square) and 213 (16:9) —
   which is the proof that live forces no aspect ratio here. The replica's card
   treatment squashed the square crops into 16:9 and sized the row 344/384.
   ========================================================================== */
.carousel--plain { --carousel-gap: 0px; }
.carousel--plain .carousel__slide {
  margin-inline: 0;
  padding-inline: 15px;
}
.carousel--plain .card {
  background: none;
  border: 0;
}
/* The image keeps the ratio it was cropped at, so `aspect-ratio` comes off and
   the height goes back to auto. `object-fit` has nothing to do once the box is
   the image's own shape. */
.carousel--plain .card__media img {
  aspect-ratio: auto;
  height: auto;
  object-fit: fill;
}
.carousel--plain .card__body { padding: 0; }
.carousel--plain .card__body :is(h2, h3) { text-align: center; }
/* Live's arrows on this variant are the 20px glyph, not the card slider's 40px. */
.carousel--plain .carousel__btn { font-size: 20px; line-height: 20px; }

/* ---- wedding page gallery slider ---------------------------------------
   The fifth `simp-custom-*` skin on create-your-perfect-wedding-day-at-yo1,
   and the one widget on that page the generic card slider gets most wrong:
   live shows ONE big photograph centred with its neighbours peeking, no card
   chrome and no caption, and puts the arrows on white discs well inside the
   page. Measured on live at 1440:

     viewport   full-bleed 1440, padding-inline 30.3% (436.3px) — the peek
     slide      518 wide, margin 0 25px  -> pitch 568, one per view
     image      516x290, filling the slide
     caption    display: none
     arrows     a 62px white disc, prev at left 98, next at right 139

   The padding on the scroll container is what reveals the neighbours, which is
   the same thing slick's `slick-list` padding does on live.
   ------------------------------------------------------------------------ */
.w-mod-wedding-page-gallery-slider > .container--wide {
  width: 100%;
  max-width: none;
  margin-inline: 0;
  padding-inline: 0;
}
.w-mod-wedding-page-gallery-slider .carousel { --carousel-gap: 50px; }
/* The peek padding belongs on the TRACK, which is the scroll container — the
   viewport merely clips. Putting it on the viewport left the track 567 wide with
   its own overflow hidden, so the neighbouring slides never showed and the row
   read as one image on an empty white band. This is what slick's `.slick-list`
   padding does. */
.w-mod-wedding-page-gallery-slider .carousel__viewport { overflow: visible; }
.w-mod-wedding-page-gallery-slider .carousel__track {
  padding-inline: 30.3%;
  scroll-padding-inline: 30.3%;
}
.w-mod-wedding-page-gallery-slider .carousel__slide {
  --per-view: 1;
  margin-block: 0;
}
/* No card here — the photograph is the slide. */
.w-mod-wedding-page-gallery-slider .card {
  background: none;
  border: 0;
}
.w-mod-wedding-page-gallery-slider .card__body { display: none; }
/* Live's arrows are a white disc with the glyph centred on it, not the bare
   glyph the other sliders use. */
.w-mod-wedding-page-gallery-slider .carousel__btn {
  width: 62px;
  height: 62px;
  border-radius: 50%;
  background: var(--c-surface);
  opacity: 1;
}
.w-mod-wedding-page-gallery-slider .carousel__btn--prev { left: 98px; }
.w-mod-wedding-page-gallery-slider .carousel__btn--next { right: 139px; }

/* Live puts a plain chevron on the disc. The shared button carries an
   `fa-arrow-circle-*` glyph — a circle WITH an arrow in it — so on top of the
   white disc it read as two concentric rings. The glyph is dropped here and the
   chevron drawn, the same way the lightbox, gallery and story arrows are. */
.w-mod-wedding-page-gallery-slider .carousel__btn i { display: none; }
.w-mod-wedding-page-gallery-slider .carousel__btn::after {
  content: "";
  width: 14px;
  height: 14px;
  border: solid var(--c-brand);
  border-width: 0 0 3px 3px;
}
.w-mod-wedding-page-gallery-slider .carousel__btn--prev::after { transform: rotate(45deg); margin-left: 5px; }
.w-mod-wedding-page-gallery-slider .carousel__btn--next::after { transform: rotate(-135deg); margin-right: 5px; }

@media (max-width: 1023px) {
  /* The 30.3% peek would leave almost nothing of the slide on a phone. */
  .w-mod-wedding-page-gallery-slider .carousel__viewport {
    padding-inline: 10%;
    scroll-padding-inline: 10%;
  }
  .w-mod-wedding-page-gallery-slider .carousel { --carousel-gap: 20px; }
  .w-mod-wedding-page-gallery-slider .carousel__btn { width: 40px; height: 40px; }
  .w-mod-wedding-page-gallery-slider .carousel__btn--prev { left: 8px; }
  .w-mod-wedding-page-gallery-slider .carousel__btn--next { right: 8px; }
}

/* ===== widgets/faq.css ===== */
/* ============================================================================
   FAQ — THE shared disclosure component (Pass 5A).

   Replaces two implementations of one pattern:
     * the homepage `accordion` (7 rows, 1 page) — good architecture, looser skin
     * authored `w-mod-info` rows (129 rows, 14 pages) — good skin, rebuilt by JS

   The consolidation takes the homepage's ARCHITECTURE and the inner pages' SKIN, so
   the 129 rows that already carried the canonical look keep it and the 7 outliers
   adopt it. Page identity is no longer a design input.

   Canonical values, all measured from the 129-row implementation:
     question   20px / 22px / w400 / #000        (18px / 30px below 1024)
     row        ~35px, from a 12px trigger under-padding
     pitch      65px — 20px above the row, 10px below, matching the authored rhythm
     divider    1px solid grey, under the question
     chevron    fa-chevron-down, #414042, rotates 180° when open
     open       question and chevron both go #6CA773
     answer     14px / 19.6px / #4B5761, 20px below the divider
   Everything above is a `--faq-*` token in tokens.css so the two can never drift
   apart again.
   ========================================================================== */

/* Group wrapper. No max-width — the container owns the width. The gutter matches
   `.content-col`, which every text widget on the page carries, so the questions line
   up with the headings above them instead of overhanging them by 15px. Below 1024
   `.content-col` contributes nothing and `.container` supplies the 20px inset to both,
   so this is desktop-only. */
/* `[data-faq]` is on the widget's own wrapper (render.njk). It matters because four
   health-guide articles author a `<div class="faq">` around each question in their
   BODY copy — a shape that predates this component — and the gutter below reached
   those too, insetting one block of an article's prose by 15px against every
   paragraph around it. The attribute selector cannot match authored markup. */
.faq[data-faq] { }
@media (min-width: 1024px) {
  .faq[data-faq] { padding-inline: var(--gutter); }
}

/* CLOSING SPACE.

   The group always ends with 60px of VISIBLE air, whatever follows it.

   Each item carries 20/10 of its own, so the last one alone leaves 10px. Where the
   next section is transparent its 30px top padding reads as part of the gap, and 10 +
   20 + 30 gives the 60. Where the next section PAINTS a background — the cream GET IN
   TOUCH band on the FAQ page — that 30px sits inside the band and stops separating
   anything, so the same 20px here yields only 30px of visible white. The second rule
   adds the swallowed 30 back.

   Worth saying why the FAQ gets this when a plain text section against the same band
   does not: 30px is the site norm at that boundary (measured, 77 of 95 sampled), and a
   text section ends in a line of type. This group ends in a DRAWN RULE, and a hard
   horizontal line 30px above the hard horizontal edge of a colour band reads as a
   mistake rather than as spacing. The extra room is for the divider, not a claim that
   the site norm is wrong. */
.section--faq { padding-bottom: var(--sp-4); }
.section--faq:has(+ .section--warm),
.section--faq:has(+ .section--band),
.section--faq:has(+ .section--surface),
.section--faq:has(+ .section--black),
.section--faq:has(+ .section--brand) {
  padding-bottom: calc(var(--sp-4) + var(--sp-5));
}

/* The authored rows each sat in their own <section> with 20px/10px padding. That
   rhythm now belongs to the item, so the group's section is flush and the 65px pitch
   is preserved exactly. */
.faq__item { padding-block: var(--faq-item-pt) var(--faq-item-pb); }

.faq__heading {
  margin: 0;
  font-size: var(--faq-q-size);
  font-weight: var(--faq-q-weight);
  line-height: var(--faq-q-lh);
}

.faq__toggle {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--sp-4);
  width: 100%;
  /* Under-padding only: the divider sits tight under the question, as the 129-row
     implementation does. The breathing room above and below is the item's. */
  padding: 0 0 var(--faq-q-pb);
  background: none;
  border: 0;
  border-bottom: var(--faq-line);
  cursor: pointer;
  text-align: left;
  white-space: normal;
  font-family: var(--ff-body);
  font-size: var(--faq-q-size);
  line-height: var(--faq-q-lh);
  font-weight: var(--faq-q-weight);
  color: var(--faq-q-color);
  transition: color var(--dur-fast) var(--ease);
}
.faq__toggle:hover { color: var(--faq-open); }

.faq__toggle i {
  flex: 0 0 auto;
  font-size: var(--faq-icon-size);
  color: var(--faq-icon);
  transition: transform var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease);
}
/* Open state. The colour is set on BOTH the button and the icon: the icon carries its
   own `color`, so inheriting from the opened button is not enough — that is exactly
   why the old inner-page chevron stayed grey while the homepage's turned green. */
.faq__toggle[aria-expanded="true"] { color: var(--faq-open); }
.faq__toggle[aria-expanded="true"] i { color: var(--faq-open); transform: rotate(180deg); }

/* Collapsed in the HTML, not by JavaScript — so it holds before the script runs and
   without it. The old inner-page rows showed every answer until faq-info.js had run. */
.faq__panel {
  display: none;
  padding-top: var(--faq-panel-pt);
  font-size: var(--fs-sm);
  line-height: var(--lh-body);
  color: var(--c-ink);
}
.faq__panel.is-open { display: block; }
/* Answers carry authored markup on 7 of the 129 rows — links, lists, <br>. Keep the
   block rhythm inside the panel from doubling up at its edges. */
.faq__panel > :first-child { margin-top: 0; }
.faq__panel > :last-child { margin-bottom: 0; }

/* VIEW ALL — homepage only, content-driven. Live: 88×29, 13px/400 white on #294C4B. */
.faq__more { margin: var(--sp-6) 0 0; text-align: center; }
.faq__viewall {
  font-size: var(--fs-xs);
  font-weight: var(--fw-regular);
  line-height: 1.15;
  letter-spacing: var(--ls-default);
  text-transform: none;
  padding: 7px var(--sp-3);
  border-color: var(--c-brand);
}

@media (max-width: 1023px) {
  .faq__item { padding-block: var(--faq-item-pt-m) var(--faq-item-pb-m); }
  .faq__heading,
  .faq__toggle { font-size: var(--faq-q-size-m); line-height: var(--faq-q-lh-m); }
}

/* ===== widgets/home.css ===== */
/* ============================================================================
   Homepage widget styling. Values measured from the live rendered page.
   ========================================================================== */

/* Spacer widgets on the live site are empty 61px text blocks (11 of them on the
   homepage). Here they are one element driven by a token instead of DOM noise. */
.w-spacer { height: calc(var(--n, 1) * 61px); }
@media (max-width: 767px) { .w-spacer { height: calc(var(--n, 1) * 30px); } }

/* A spacer between two PAINTED sections cuts a white stripe across the seam.
   `.w-spacer` carries no background of its own, so it paints in the page's colour;
   sandwiched between two coloured bands that reads as an unintended white band —
   61px of it above 767, 30px below. Swept all 299 pages at BOTH 1440 and 390 —
   painted adjacency can differ by breakpoint, so one viewport would not have
   settled it — and the same two seams come back at each: the homepage only,
   twice, and both times under a black slider —

     black wellness carousel  → spacer → warm  "Get in Touch"   (user report)
     black health-guide slider → spacer → band  "Press Coverage"

   — so the rule is written as a property of the SEAM, not patched onto those two
   widgets. The spacer takes the colour of the band ABOVE it, which reads as that
   band simply ending with its own breathing room. Chosen over collapsing the
   spacer to zero because painting moves nothing: the document height is identical
   before and after, so no other measurement in the project shifts.

   `--surface` is deliberately absent from the "band above" list: it paints with
   `var(--section-bg)`, set per widget on the section itself, and a custom property
   does not reach a sibling. No page currently puts a spacer under one. */
.section--band  + .w-spacer:has(+ :is(.section--band, .section--warm, .section--black, .section--brand, .section--surface)) { background: var(--c-surface-band); }
.section--warm  + .w-spacer:has(+ :is(.section--band, .section--warm, .section--black, .section--brand, .section--surface)) { background: var(--c-surface-warm); }
.section--black + .w-spacer:has(+ :is(.section--band, .section--warm, .section--black, .section--brand, .section--surface)) { background: var(--c-surface-black); }
.section--brand + .w-spacer:has(+ :is(.section--band, .section--warm, .section--black, .section--brand, .section--surface)) { background: var(--c-brand); }

/* ---- Section headers on a black band (simp-custom-special-offers-header) ---
   G-4: no text-transform. Live computes `none` here — the copy is already
   uppercase in the authored content, so transforming it was both redundant and
   wrong wherever the source is not uppercase. */

.w-mod-special-offers-header,
.w-mod-margin-bottomZero { margin-bottom: 0; }
/* Vertical padding comes from the measured --pt/--pb on each section, so the
   live spacing modifiers must not add their own. */
.w-mod-fontsize .richtext { font-size: var(--fs-sm); }
/* DS-19: `.w-mod-align3text .w-col { text-align: center }` also sat here and was
   dead — the same selector is re-declared as `justify` further down this file, and
   the later one wins. Only the live-measured `justify` remains. */
.w-mod-strong .w-col strong { display: block; margin-bottom: var(--sp-2); }

/* ---- 3-up icon+text columns (wt3tb) -----------------------------------
   Live: section 1440×191 · column 427×191, padding 5px 20px 10px · icon 50×50
   inline · heading 18px/19.8 w700 centred · body 14px/19.6 justified.

   Those numbers were measured on the HOMEPAGE's two icon columns and applied to
   all 66 text-2/text-3 widgets. Live only uses them there: every other instance
   (food-menu, contact-us, the therapy pages, thank-you, and all 16 wellnesspack
   lists) is `text-align:left` with a 15px column gutter. Justifying a ~370px
   column stretches the word spacing badly — visible on the introductory-package
   INCLUSIONS list — so the base is now left with the site's gutter, and the
   homepage's measured treatment rides on its own `align3text` modifier below. */
.w-col { padding: 5px var(--gutter) var(--sp-2); text-align: left; }
/* Live mobile stacks the 3-up columns full width; the row's 20px is the only
   horizontal inset, so the column must not add its own. */
@media (max-width: 1023px) { .w-col { padding-inline: 0; } }
.w-col img { width: 50px; height: 50px; object-fit: contain; display: inline; margin: 0; }
.w-col h2, .w-col h3 {
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  line-height: 1.1;
  text-align: center;
  margin-bottom: 0;
}
.w-col p { font-size: var(--fs-sm); line-height: var(--lh-body); margin-bottom: 0; }
/* The homepage's icon columns — the one place live justifies the body and pads
   the column to 20px. The padding is kept and the JUSTIFY IS NOT: it was the last
   justified text left on the site once the authored `text-align: justify` was taken
   out of the content everywhere (1913 declarations over 205 pages), and 27 elements
   keeping it here read as an oversight rather than a distinction. These columns are
   ~390px wide on the desktop, which is where justification does its worst work —
   no hyphenation, so the word spaces stretch instead.
   The desktop-only guard on the padding still matters: unqualified it out-specifies
   the mobile zero-inset rule above and grew the homepage 58px at 390. */
.w-mod-align3text .w-col { text-align: left; }
@media (min-width: 1024px) {
  .w-mod-align3text .w-col { padding-inline: var(--sp-4); }
}

/* ---- Section with a background image (bgImageContainer) ---------------- */
.section--bgimage {
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  color: var(--c-on-dark);
  padding-block: 50px;
  text-align: center;
  position: relative;
}
/* C-10: reproduce live's actual scrim.
   The replica had invented `::before { background: rgba(0,0,0,.5) }`. Live puts its
   overlay on `::after` instead — `rgba(2, 21, 36, .6)`, a dark navy at 60%, not
   black at 50%. An earlier probe read only `::before` (which live does define, but
   transparent) and wrongly concluded there was no overlay; removing it outright
   left the band at mean luminance 132.5 against live's 70.2. */
.section--bgimage::after {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(2, 21, 36, .6);
  pointer-events: none;
}
.section--bgimage > * { position: relative; z-index: 1; }
.section--bgimage :where(h1, h2, h3, p) { color: var(--c-on-dark); }
.w-mod-buttoncolor .btn { background: var(--c-brand); color: var(--c-on-dark); }
.w-mod-button-spacing .richtext a { margin-top: var(--sp-4); }

/* ---- media-side (imageDescriptionSide) -------------------------------
   Live: block 936×246 inside the 65% container. Each half is 468 with 15px
   Bootstrap gutters, giving a 438px content box — so the image lands at
   438×246 and the text wraps over exactly the same measure as the live site. */

/* DS-06: media-side was the only one of the 18 components whose branch never called
   pad(), so it emitted no `data-widget-id` and no `--pt`/`--pb`, and carried
   `section--flush` to hard-zero the block padding instead. It now reads its own
   captured values and still defaults to zero, so nothing that measures 0 moves. */
.section--media-side {
  padding-top: var(--pt, 0);
  padding-bottom: var(--pb, 0);
}
/* Below 1024 this component is flush, and that is a measurement, not an assumption:
   of the 198 media-side instances in the page data, all 186 that carry mobile values
   measure 0/0 and the remaining 12 carry none. So the mobile default is 0 rather
   than "fall back to the desktop value" — without this the 12 undated instances
   (5 on the homepage, 4 on thank-you-page, 3 on yo1-events) would inherit their
   desktop padding at 390 and diverge from the 186 that do not. */
@media (max-width: 1023px) {
  .section--media-side {
    padding-top: var(--ptm, 0);
    padding-bottom: var(--pbm, 0);
  }
}
.media-side__media,
.media-side__body { padding-inline: var(--gutter); }
.media-side__media { overflow: hidden; }
/* Live sets no aspect-ratio here — the image renders at its authored crop, so the
   four media rows are 16:9 (5000/2812, 1500/844, 1600/900) and the contact strip
   is 4:3 (652/489 → 329px at 1440, 446px at 1920, both matching live). The ratio
   is carried per-image from the transform chain; the fallback keeps the previous
   value for any image without one. The ≤767 override below still applies. */
.media-side__media img {
  width: 100%; height: auto; object-fit: cover;
  aspect-ratio: var(--img-ratio, 5000 / 2812);
}
/* C-24: live measures .text-content as display:block with its content top-aligned
   in the media column on all five media-side widgets — the heading sits at its
   natural offset (39.2px on the four rows, where an eyebrow line precedes it, and
   0 on the contact strip, which opens with the heading). The flex/justify-content
   :center here was a replica invention: it recentred the block against the image
   height, which stayed invisible only while the contact-strip image was the wrong
   height (C-25). No offset replaces it — the centring is simply removed. */
.media-side__body { display: block; }
/* DS-13: live gives this eyebrow two different colours — #042541 on the homepage
   (`.for-homepage .widget-wrapper.imageDescriptionSide h3`, its only deliberate
   treatment of the element) and #32383E on inner pages, where it just inherits the
   theme default. The replica rendered a third value, --c-ink, on both. One colour
   now, and it is the one live actually designs: --c-navy, which the token set
   already records as absorbing #042541. */
.media-side__body h3 {
  font-size: var(--fs-xl);
  line-height: 1.1;
  /* DS-13 chose ONE colour for this eyebrow and picked live's homepage value,
     --c-navy (#042541). Its own note records the other half: inner pages measure
     #32383E, the theme default. Verified again on yo1-events, where all 40 event
     titles are rgb(50,56,62) on live and were rendering navy here. So the colour
     is now per-context, which is what live actually does — the homepage keeps
     navy below, every inner page takes the theme default. */
  color: #32383E;
  /* C-32: live measures 0. The 5px here was invented spacing and put every media
     row's body copy 15px below live once the eyebrow margin is also accounted for. */
  margin-bottom: 0;
}
/* Live's only deliberate treatment of this element:
   `.for-homepage .widget-wrapper.imageDescriptionSide h3` = #042541. */
.for-homepage .media-side__body h3 { color: var(--c-navy); }

.media-side__body h2 {
  font-size: var(--fs-2xl);
  line-height: 1.1;
  /* Live measures 0 on BOOK YOUR HEALTH PROGRAMS, the only media-side h2. */
  margin-bottom: 0;
}
/* EYEBROW — the short lead-in line above a media-side heading.

   Live authored the gap between the two as a trailing `<br>&nbsp;` inside the eyebrow
   paragraph: literally a blank second line. That made the spacing a property of what a
   CMS author happened to type, and it was typed inconsistently — 10 of the site's 12
   eyebrows carried it and 2 did not, so the same "131 Rooms. Luxurious Accommodation."
   widget rendered spaced on the homepage and flush against its heading on Our Story
   and thank-you-page.

   The blank line is stripped in the data (tools/media-side-eyebrow.mjs) and the gap
   belongs to the component, at exactly the height it used to occupy: one body line.
   Measured before the change, the blank line contributed 19.59px on all 10 spaced
   rows, so the 10 are pixel-identical and the 2 gain the gap.

   Scoped to a FIRST-CHILD paragraph followed directly by a heading, which is the
   eyebrow shape. A body paragraph that merely precedes a heading — the contact strip
   on curative-therapies/acupuncture, four lines of copy above the phone number — is a
   different thing and keeps its current spacing. */
.media-side__body .richtext > p:first-child + :is(h1, h2, h3, h4) {
  margin-top: var(--lh-body);
}

/* C-32 / C-33: live measures media-side paragraphs at margin-top 0 AND
   margin-bottom 0. C-32 zeroed only the bottom, so the 15px top margin from
   `.richtext p` (primitives.css, added for live's rich-text rhythm elsewhere)
   still applied here — `.richtext > p:first-child` exempts only the eyebrow.
   CHEF-PREPARED is the one media-side widget with more than one paragraph, so it
   alone accumulated 2 × 15px. `margin-block: 0` covers both sides in the scope
   live measures as zero; `.richtext p` is unchanged everywhere else. */
.media-side__body p { font-size: var(--fs-sm); margin-block: 0; }
/* Stacked below 1024: live serves a 16:9 crop and the row's 20px inset
   (measured 728×409 @768, 350×197 @390). */
@media (max-width: 1023px) {
  /* mobile gutter: .container--narrow supplies the 20px now */
  .media-side__media,
  .media-side__body { padding-inline: 0; }
  .media-side__media img { aspect-ratio: 16 / 9; }
  .media-side__body { padding-top: var(--sp-4); padding-bottom: 0; }
}

/* ---- Video widget ------------------------------------------------------ */
/* `object-fit: cover`, as live sets on every one of its videos. A <video> defaults
   to `contain`, so wherever the box ratio and the source ratio disagree the poster
   letterboxed against the near-black background below — the black strip along the
   top of the poster on the QC sheet. Live's own video boxes are not 16/9 either
   (measured 682x351, ratio 1.94, on abhyangam), which is exactly the case that
   produced the bars. */
.w-video {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  background: var(--c-near-black);
}

/* ---- Press logos ------------------------------------------------------- */
/* Live keeps each logo at its authored width and lets the height fall out of the
   intrinsic ratio — CNN 85×40.5, NY Post 130×23, I Love NY 130×97.5 at every
   desktop width — with the three centred on a common vertical axis and 22.5px
   between them. Normalising them all to height:50px was the replica's own rule. */
.press-logos {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  /* Live's logo row is a flex container whose gap tracks the container width at
     1.953125% with a 20px floor. Measured gaps: 1920→30, 1600→25, 1440→22.5,
     1280→20, 1024→20 (the floor). */
  gap: max(20px, 1.953125%);
  margin: 0; padding: 0; list-style: none;
}
.press-logos img { display: block; width: 130px; height: auto; }
.press-logos .CNN-img { width: 85px; }
/* The I-love-NY asset is padded by the CDN with a solid #eeeeee box
   (`w_200,h_150,c_pad,b_rgb:eeeeee` in its URL), so it renders as a pale rectangle on
   the band. Live hides that the same way the source does:
       .clearfix.widget-wrapper.press-coverage-logos .i-love-ny-img { mix-blend-mode: multiply }
   Multiply makes the #eeeeee padding disappear into any lighter ground while leaving
   the black artwork untouched. Applied site-wide, as live does — this is one of the
   logos on the homepage too. */
.press-logos .i-love-ny-img { mix-blend-mode: multiply; }
/* Live desaturates every press logo — `filter: grayscale(1)` on all three,
   measured on CNN, the New York Post and I Love NY, and it does NOT come back on
   hover (live reports grayscale(1) in both states). Ours were rendering in full
   colour, which is why the I Love NY heart was red here and black on live. */
.press-logos img { filter: grayscale(1); }

/* ---- Tables inside rich text (facilities grid) ------------------------- */
/* The table's own margin is ZERO on both sides, and the gap above it belongs to its
   wrapper. The 15px bottom margin this rule used to carry was the asymmetry reported
   on health-guide/a-holistic-detox-program-in-the-catskills: measured 20px above a
   table and 35 below it, and 55 where a heading followed — the table's 15 stacking on
   top of whatever the next block brought. Now the space above is the paragraph rhythm's
   20 and the space below is whatever follows asks for: 20 before a paragraph, 40 before
   a heading. Same rule as the lists, so a table sits in the flow like everything else. */
.richtext table { width: 100%; margin-block: 0; }
.richtext .table-responsive,
.richtext .table-scroll,
.richtext > table { margin-top: var(--sp-4); margin-bottom: 0; }
.richtext > :first-child:is(.table-responsive, .table-scroll, table) { margin-top: 0; }
.richtext td, .richtext th { padding: 4px var(--sp-2); vertical-align: top; line-height: var(--lh-table); }

/* ---- Newsletter band --------------------------------------------------- */
.newsletter-section { background: var(--c-surface-warm); }

/* ---- Reviews subheader -------------------------------------------------
   DS-19: `.section--flush { padding-block: 0 }` was declared here as well as in
   primitives.css, where it belongs. The duplicate is removed; the primitive is
   unchanged, so nothing about the cascade moves. */

/* ---- Tabs (replaces the bootstrap_imgSlider carousel) ------------------ */
/* C-36: live uses Bootstrap's gutter model here, not a grid gap — the inner
   wrapper carries 15px each side and so does each column, with the two columns
   tiling the remaining width edge to edge. Measured at 1440: wrapper content
   934.8, list outer 311.6 / content 281.6, panel outer 623.2 / content 593.2.
   A 30px grid gap gave the same OUTER columns but 30px wider content, which made
   the panel image 30px wider (and taller) and reflowed its copy. */
.tabs {
  display: grid;
  grid-template-columns: 1fr 2fr;
  gap: 0;
  padding-inline: var(--gutter);
  align-items: start;
}
.tabs__list, .tabs__panels { padding-inline: var(--gutter); }
.tabs__list { display: flex; flex-direction: column; }
.tabs__tab {
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--fs-sm);
  font-weight: var(--fw-bold);
  letter-spacing: var(--ls-default);
  text-align: left;
  color: var(--c-ink);
  border-left: 3px solid transparent;
  transition: color var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.tabs__tab:hover { color: var(--c-brand); }
.tabs__tab[aria-selected="true"] { color: var(--c-brand); border-left-color: var(--c-brand); }

.tabs__panel { display: none; }
.tabs__panel.is-active { display: block; }
.tabs__media img { width: 100%; aspect-ratio: 16 / 9; object-fit: cover; }
.tabs__body { padding-top: var(--sp-3); font-size: var(--fs-sm); }
.tabs__link { display: inline-block; margin-left: var(--sp-1); }  /* G-3: live has no underline */

@media (max-width: 1023px) {
  /* mobile gutter: .container--therapy supplies the 20px, so the desktop
     Bootstrap gutters would stack to 50px. */
  .tabs, .tabs__list, .tabs__panels { padding-inline: 0; }
}
@media (max-width: 767px) {
  .tabs { grid-template-columns: 1fr; }
  .tabs__list { flex-direction: row; overflow-x: auto; }
  .tabs__tab { border-left: 0; border-bottom: 3px solid transparent; white-space: nowrap; }
  .tabs__tab[aria-selected="true"] { border-bottom-color: var(--c-brand); }
}

/* ---- Hotel info block (live: 1199×91 @ y=880) --------------------------- */
/* Live: 60px above the h1, nothing below (block 880→971, h1 940→971).
   The nothing-below is a DEPARTURE now, on the client's instruction: the next section
   captures 0 top padding, so live's h1 sits directly on the paragraph beneath it with
   no separation at all. 30px is the site's own default section padding, so the headline
   gets the same air every other block boundary gets. */
.hotel-info { padding: 60px 0 var(--sp-5); text-align: center; }
.hotel-info__title {
  font-size: var(--fs-3xl);
  font-weight: var(--fw-bold);
  line-height: var(--lh-tight);
  color: var(--c-ink);
  margin: 0;
}
@media (max-width: 1023px) { .hotel-info { padding-top: var(--sp-5); } }
@media (max-width: 767px)  { .hotel-info__title { font-size: var(--fs-xl); } }

/* ---- Reviews ----------------------------------------------------------- */
/* ---- Reviews — live measured structure -----------------------------------
   card 388×250 (height/min/max all 250px), padding 15px, margin 10px, bg #F5F3EF
   title  356×20, max-height 40px, overflow hidden, 14px/20px w700
   rating 356×20
   body   356×90, height AND max-height 90px, overflow:auto — it SCROLLS, not clamped
   meta   name 14px/700 + date 12px
   No JS equal-height: the box is simply fixed. */
/* Live measures the Reviews subheader h2 at margin-bottom 0, like the other 10. */
.reviews__title { text-align: center; margin-bottom: 0; }
/* Live reviews carousel: list 1224, slide 408, card 388 (10px margin each side) —
   exactly 3 per view with NO peek, unlike the card sliders. */
.reviews .carousel { --carousel-gap: 20px; }   /* live pitch 408 = card 388 + 10px margin each side */
.reviews__track .review { --per-view: 3; }
.review {
  height: 250px;
  min-height: 250px;
  max-height: 250px;
  padding: var(--gutter);
  margin: var(--sp-2);
  background: var(--c-surface-warm);
  border: 1px solid transparent;
  text-align: left;
  display: flex;
  flex-direction: column;
}
.review__title {
  font-weight: var(--fw-bold);
  font-size: var(--fs-sm);
  line-height: var(--lh-review-title);
  max-height: 40px;
  overflow: hidden;
  margin: var(--sp-1) 0;
}
.review__rating { color: var(--c-green); margin: var(--sp-1) 0; line-height: var(--lh-body); }
.review__body {
  font-size: var(--fs-2xs);
  line-height: var(--lh-review-body);
  height: 90px;
  max-height: 90px;
  overflow: auto;
  margin: var(--sp-1) 0;
}
.review__meta { margin-top: auto; display: inline-block; }
.review__name { font-size: var(--fs-sm); font-weight: var(--fw-bold); line-height: var(--lh-body); margin: 0; }
/* Live carries the date in the markup but does not paint it: `.review_data_date`
   computes `visibility: hidden` on all 37 of them. `visibility`, not `display`,
   so the element keeps its box and the card still reserves that line — using
   `display: none` here would paint the same result but leave every review card
   shorter than live's. */
.review__date {
  font-size: var(--fs-2xs);
  line-height: 1.5;
  margin: 0;
  visibility: hidden;
}
@media (max-width: 1023px) { .reviews__track .review { --per-view: 2; } }
@media (max-width: 767px)  { .reviews__track .review { --per-view: 1.1; } }


/* Headings on the grey press band render at 700 on the live site, not the
   600 used elsewhere. C-15: the "Connect With Us" block does the same. */
/* C-15: live renders these two headings at 700 and black, not the 600/#4B5761
   the generic h2 uses. Both are measured on the live PRESS COVERAGE and
   CONNECT WITH US widgets. */
.section--band h2,
.w-mod-connect h2,
.connect h2 {
  font-weight: var(--fw-bold);
  color: var(--c-ink-black);
}
/* The only homepage h2 live gives a bottom margin — 10px, measured on PRESS
   COVERAGE and CONNECT WITH US (STAY INFORMED carries it on its own class).
   Written child-of-.richtext so it out-specifies `.richtext > :last-child`,
   which otherwise zeroes the PRESS heading (it is the only node in its block). */
.section--band .richtext > h2,
.connect .richtext > h2 { margin-bottom: 10px; }


/* Card-slider arrows are anchored to the full-width section (live: 60px from the
   page's left edge, 75px from the right), so the section is their containing block.

   The `position: static` MUST carry the same condition as the rule that positions
   the section. As two independent selectors they came apart the moment a card
   carousel sat in a container other than `--wide`: the section stopped being
   positioned, the carousel was still forced static, and with no positioned
   ancestor left the absolute arrows resolved against the document — landing
   `top: 50%` down the page, on top of whatever happened to be there. That is how
   the review slider's arrows ended up straddling the INCLUSIONS list. Written as
   one descendant chain so the pair cannot separate again; any card carousel
   outside that shape keeps the base `.carousel { position: relative }` and its
   arrows stay inside it. */
.section:has(> .container--wide > .carousel--cards) { position: relative; }
.section:has(> .container--wide > .carousel--cards) > .container--wide > .carousel--cards { position: static; }


/* ---- CONNECT WITH US, START YOUR JOURNEY -------------------------------
   Live measures (identical rule at 1920/1440/1280/1024):
     widget padding-inline 100px (fixed), col padding-inline 0
     text wrapper = 75% of the remaining box, centred
       1920 → 1290   1440 → 930   1280 → 810   1024 → 618
     phone line  #586066
     Book Your Stay  a.btn.btn-forlink.pull-center
       130.6×29, background #0A151E, white, 7px 15px, 13px/13px, centred        */
.connect > .container {
  width: 100%;
  max-width: none;
  padding-inline: 100px;
}
.connect .content-col { padding-inline: 0; }
.connect .richtext { width: 75%; margin-inline: auto; }
.connect .font-abold { color: var(--c-link-utility); }

.btn-forlink {
  padding: 7px var(--sp-3);
  background: var(--c-navy-deep);
  color: var(--c-on-dark);
  font-size: var(--fs-xs);
  line-height: var(--lh-button);
}
.btn-forlink:hover { background: var(--c-brand); color: var(--c-on-dark); }
/* Live utility: centres an inline-block inside a left-aligned rich-text block. */
.pull-center { display: block; width: fit-content; margin-inline: auto; }

@media (max-width: 1023px) {
  .connect > .container { padding-inline: var(--sp-4); }
  .connect .richtext { width: 100%; }
}

/* ---- STAY INFORMED, SUBSCRIBE NOW (C-17) -------------------------------
   Live: full-bleed band #E9EAEB (absorbed by --c-surface-band), heading block
   padded 40px 15px 0, h2 at 25px/500 in #294C4B — deliberately NOT the generic
   h2 (600/#4B5761) — then the supporting line at 14px/#4B5761, then a 20px row
   holding a 41.667%-wide block that carries a 100px right margin, the pair
   centred. Inside it the 260px field lane is centred on the form and the submit
   button is absolutely positioned at the lane's right edge less 8px, which is
   exactly how live places it (.button-holder is position:absolute).            */
.newsletter-section {
  background: var(--c-surface-band);
  padding-block: 0;
}
.newsletter-head {
  padding: 40px var(--gutter) 0;
  text-align: center;
}
.newsletter-title {
  margin-bottom: 10px;
  font-size: var(--fs-2xl);
  font-weight: var(--fw-medium);
  line-height: 27.5px;
  color: var(--c-brand);
}
.newsletter-sub { font-size: var(--fs-sm); color: var(--c-ink); }

.newsletter-row { display: flex; justify-content: center; padding: 20px 0; }
.newsletter-block {
  width: 41.6667%;
  margin-right: 100px;
  padding-inline: var(--gutter);
}

.newsletter {
  position: relative;
  display: block;
  text-align: center;
}
.newsletter__lane {
  display: block;
  width: 270px;             /* 260px field + the 10px live leaves to its right */
  margin: 0 auto 21px;      /* live .form-group margin — gives the 51px form box */
  padding-right: 10px;
  text-align: left;
}
.newsletter__lane .form-control {
  width: 100%;
  height: 30px;
  padding: 6px 12px;
  font-size: var(--fs-sm);
  line-height: 20px;
  color: #555555;
  background: var(--c-surface);
  border: 1px solid rgba(4, 37, 65, .79);
  border-radius: 0;
  box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
}
.newsletter__submit {
  position: absolute;
  top: 0;
  left: 50%;
  margin-left: 127px;       /* lane half-width (135) less live's -8px offset */
  padding: 7px 12px 8px;
  white-space: nowrap;      /* live lets it overrun the form box rather than wrap */
  background: var(--c-surface);
  color: var(--c-brand);
  border: 1px solid var(--c-brand);
  font-size: var(--fs-xs);
  line-height: var(--lh-button);
}
.newsletter__submit:hover { background: var(--c-brand); color: var(--c-on-dark); }
.newsletter .form-error,
.newsletter .form-status { display: block; text-align: center; }
/* Empty validation slots must not add a line box to the 51px form. */
.newsletter .form-error:empty,
.newsletter .form-status:empty { display: none; }

@media (max-width: 1023px) {
  .newsletter-row { padding-inline: var(--sp-4); }   /* mobile gutter */
  .newsletter-block { width: 100%; margin-right: 0; padding-inline: 0; }
  /* One row, as live: the field and Subscribe sit side by side at 390 too. The
     lane was `width: 100%` inside a WRAPPING flex row, so the button was pushed
     onto a line of its own and centred under the field — the "alignment is not
     good" below 1024. It flexes now instead, and the row no longer wraps.
     Unlike live, which keeps its own button at a fixed width and lets it run off
     the right edge of a 390 screen, the field gives way so both stay on screen. */
  .newsletter { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: center; gap: var(--sp-2); }
  /* The validation slots are children of the same flex container, so with the
     row layout they lined up BESIDE the field and the button instead of under
     them. Giving them a full-width basis puts them on their own line; the field
     and the button still share the first line because the lane can shrink to
     nothing (min-width: 0) and so never forces a wrap of its own. */
  .newsletter > .form-error,
  .newsletter > .form-status { flex: 0 0 100%; margin: 0; }
  /* margin-inline:auto would absorb the flex free space and shove the button
     to the far edge — the pair is centred by justify-content here instead. */
  .newsletter__lane {
    width: auto;
    flex: 1 1 auto;
    min-width: 0;              /* a flex item's min-content floor would re-wrap it */
    max-width: 260px;
    margin-inline: 0;
    padding-right: 0;
  }
  .newsletter__submit { position: static; margin-left: 0; }
}


/* ---- HEALING THERAPIES: live's per-widget rules -------------------------
   Live keys these two off the widget id in its own stylesheet:

     .homepage .content_wrapper .no-gutter .widget-wrapper.wt1tb[data-widget-id="2417194"]
        { padding-top: 4.5%; padding-bottom: 2%; }
     ... .widget-wrapper.wt1tb[data-widget-id="2120268"]
        { width: 67%; border-bottom: .5px solid #6c8f7d; margin-bottom: 15px; ... }

   C-34: the heading's padding is a PERCENTAGE of the container, verified
   4.497–4.500% / 1.997–2.000% at 1920/1440/1280/1024/768/390/320. Our pipeline
   captures computed pixels at one viewport, so it had frozen 65px/29px — correct
   only near 1440. Declaring padding-block here overrides `.section`'s
   `var(--pt)/var(--pb)` without touching the captured values or any other widget;
   this is the only widget of the 41 on this page that live sizes by percentage.
   C-35: the therapy slider's 15px bottom margin, same source rule. */
[data-widget-id="2417194"] { padding-block: 4.5% 2%; }
[data-widget-id="2120268"] { margin-bottom: 15px; }


/* ---- Mobile gutter: components outside the container primitives ---------
   The newsletter band and the carousels position their own children, so the
   20px is applied where it lands on the same visual edge as everywhere else. */
@media (max-width: 1023px) {
  .newsletter-head { padding-inline: var(--sp-4); }

}

/* ---- Bootstrap table classes used by authored content -------------------
   Live's facilities table carries `table table-striped table-bordered
   table-condensed`, and the replica styled none of them — so the row striping was
   missing entirely and the borders came from the generic `.richtext td` rule.
   Lifted verbatim from live's stylesheet:
       .table-striped   > tbody > tr:nth-of-type(odd) { background-color:#f9f9f9 }
       .table-bordered                                { border:1px solid #ddd }
       .table-bordered  > tbody > tr > td             { border:1px solid #ddd }
       .table-condensed > tbody > tr > td             { padding:5px }
   Note it is nth-of-type on the ROW, not a per-cell background. */
.richtext .table-striped > tbody > tr:nth-of-type(odd) { background-color: #F9F9F9; }

/* ---- Rates tables: live's green banding ---------------------------------
   The rates grids are not plain striped tables — live bands them by PACKAGE, in
   groups of three rows alternating two greens, and the replica fell back to
   Bootstrap's flat #F9F9F9 odd/even. Live's own rules:

     .simp-custom-health-rates-tab tr:is(:nth-child(6n+4),(6n+5),(6n+6)) { #E8F5E9 }
     .simp-custom-health-rates-tab tr:is(:nth-child(6n+7),(6n+8),(6n+9)) { #C8E6C9 }
     .simp-custom-wellness-destop  tr.color1 { #E8F5E9 }   tr.color2 { #C8E6C9 }

   The nth-child arithmetic is safe to copy because the DOM matches live's
   exactly here — one tbody, 49 rows, same order — verified before porting it.
   Live reaches for !important to beat `.table-striped`; the chain below simply
   outranks it instead. The banding starts at row 4 because rows 1-3 are the
   header block. */
.w-mod-health-rates-tab .richtext .table-striped > tbody > tr:is(:nth-child(6n+4), :nth-child(6n+5), :nth-child(6n+6)),
.w-mod-wellness-destop .richtext .table-striped > tbody > tr.color1 { background-color: #E8F5E9; }
.w-mod-health-rates-tab .richtext .table-striped > tbody > tr:is(:nth-child(6n+7), :nth-child(6n+8), :nth-child(6n+9)),
.w-mod-wellness-destop .richtext .table-striped > tbody > tr.color2 { background-color: #C8E6C9; }

/* "Popular" chip in front of the first seven programme names. Live has no such
   text in the markup — it is a pseudo-element hung off the rowspan that opens
   each three-row programme group, capped at the first 22 rows so the room-only
   rows underneath stay bare:
     .simp-custom-health-rates-tab tr:nth-of-type(-n+22) td[rowspan="3"] a:before
   Our rowspans are live's, so the same selector picks the same seven anchors -
   checked name for name at 1440 and 390. (Live's phone copy is a second widget
   at -n+21; it badges the same seven, so one rule covers both here.) */
.w-mod-health-rates-tab tr:nth-of-type(-n+22) td[rowspan="3"] a::before {
  content: "Popular";
  background-color: #0E273F;
  color: #FFF;
  font-size: 10px;
  letter-spacing: .5px;
  /* Bootstrap's inherited 1.42857 governs the chip on live (14.29px, not our
     20px), and the chip's painted height is that plus the 1px padding. */
  line-height: 1.42857;
  padding: 1px 6px;
  margin-right: 10px;
  text-decoration: none;
}
.richtext .table-bordered { border: 1px solid #DDD; }

/* Cell border colour, measured per variant on live — it is NOT one value:
     table table-bordered table-striped ............... rgb(0,0,0)
     table table-autowidth table-bordered ............. rgb(211,211,211)
     …autowidth …bordered …condensed …striped ......... rgb(211,211,211)
     table-bordered (no .table) ....................... rgb(211,211,211)
   Ours painted #DDD everywhere. On the green-banded rates grid a light grey line
   all but vanishes against the fill, which is the "border is missing" — live's
   is black there. */
.richtext .table.table-bordered.table-striped:not(.table-autowidth):not(.table-condensed) > tbody > tr > :is(td, th) {
  border-color: #000;
}
.richtext .table-autowidth.table-bordered > tbody > tr > :is(td, th),
.richtext .table-bordered:not(.table) > tbody > tr > :is(td, th) {
  border-color: #D3D3D3;
}
.richtext .table-bordered > tbody > tr > td,
.richtext .table-bordered > tbody > tr > th,
.richtext .table-bordered > thead > tr > td,
.richtext .table-bordered > thead > tr > th { border: 1px solid #DDD; }
.richtext .table-condensed > tbody > tr > td,
.richtext .table-condensed > tbody > tr > th { padding: 5px; }


/* ---- UNWIND AMIDST SERENITY: centre BOOK NOW -----------------------------
   Live right-aligns this button — its wrapper measures a 1047px left gap and 0
   on the right — and the alignment is an INLINE `style="text-align: right"` in
   the captured content, with `pull-right` floating the anchor. A deliberate
   departure from live, asked for on this widget alone, so it is keyed off the
   widget id the way the other per-widget corrections on this page are.

   `!important` is needed to beat the inline style; the float has to go too, or
   the anchor stays right regardless of how its wrapper aligns text. */
[data-widget-id="2417204"] .txt-wdgt-btn-wrapper { text-align: center !important; }
[data-widget-id="2417204"] .txt-wdgt-btn-wrapper .pull-right { float: none; }

/* ---- OUR OFFERINGS card descriptions ------------------------------------
   The six cards sit in two `w-mod-align3text` widgets, and four of the six carry an
   authored `<span style="color: #042441">` on their description — navy, which reads
   as black beside the two that inherit the body's grey (measured rgb(4,36,65) against
   rgb(75,87,97)). Same component, same role, two colours.

   `!important` is used here deliberately and is the only thing that works: the colour
   is an inline style attribute, which no selector can outrank. Scoped to the
   modifier — the only two widgets on the site that carry it — and to spans that
   actually declare that navy, so it cannot reach anything else. */
/* The value, not `inherit`: these spans sit inside a wrapper the CMS colours white
   for the image caption above, so inheriting turns the description white. */
.w-mod-align3text .richtext span[style*="#042441" i] { color: var(--c-ink) !important; }

/* ===== widgets/inner.css ===== */
/* ==========================================================================
   Site components — inner-page origin.

   WAS: "everything here is additive and scoped to markup the homepage does not
   emit, so it cannot repaint the homepage". That premise expired with Pass 4E. The
   homepage is no longer frozen, and holding shared components behind `.for-inner`
   was the single largest source of cross-page inconsistency in the audit: it gave
   the site two type scales, two button sizes and two rhythm systems.

   What lives here now:

     1. Components only inner pages emit — page title, hero carousel, gallery grid,
        the wedding page, the listing rows. Unscoped, because the homepage emits no
        such markup; the same situation `.text-align` and `.border-center` are in.
     2. Shared component rules that were inner-scoped by accident and are now
        site-wide: the mobile type scale (DS-02), the mobile rhythm mechanism
        (DS-03), the authored CTA button (DS-04), the media-side width variant
        (DS-01) and the FAQ disclosure row (DS-05).
     3. Three genuinely page-specific rules, still scoped and marked as such:
        `.page-article` (the article body measure), `.for-inner
        .section--bgimage` (DS-14, open) and the per-widget-id widths.

   DS-16 — this file should be split along that seam and renamed. Deferred: it is a
   move, not a fix, and Pass 4E was scoped to consistency changes only.

   tools/home-guard.mjs still pixel-diffs the built homepage on every run. It will
   now report the DELIBERATE homepage changes listed in docs/PROJECT_STATE.md; the
   golden was re-blessed once those were measured and accepted.
   ========================================================================== */

/* Live ships a global helper class used by authored inner-page headings:
       .text-align{text-align:center}
   The homepage never emits class="text-align" (it uses inline style="text-align:…"
   on every one of its 171 occurrences), so this rule is inert there. */
.text-align { text-align: center; }

/* Page-title band — live `.widget-wrapper.wtheader`.
   Measured on about-us/our-story at 1440: y=145, height 91, padding 30px block,
   transparent background, h1 28px/30.8px 600 0.2px rgb(75,87,97) centred.

   DS-22 — this rule used to declare `font-size: 28px` and `line-height: 30.8px`
   as well. Both were REDUNDANT on desktop: the base `h1` in reset.css already
   resolves to exactly those values (`--fs-3xl` 28px x `--lh-tight` 1.1 = 30.8px).
   Being redundant did not make them harmless — at (0,2,1) they out-specified the
   site's mobile type tier (`.richtext h1`, (0,1,1)), so this was the one h1 on the
   site that never entered the mobile scale. It stayed 28px at 390 and wrapped onto
   a second line while every comparable heading stepped down.

   The fix is to stop declaring what the base already says, so the cascade can do
   its job — not to raise the mobile rule, add a desktop media query, or reach for
   `!important`. What remains here is only what live measures as DIFFERENT from the
   base: weight 600 against the base's 700 (live's page-title h1 really is lighter
   than its other h1s), plus the explicit colour and letter-spacing. */
.section--page-title { padding-block: 30px; }
.section--page-title .richtext h1 {
  margin: 0;
  font-weight: 600;
  letter-spacing: 0.2px;
  color: rgb(75, 87, 97);
}

/* Full-bleed video widget — live `.simp-custom-videoFullwidth`.
   Live spans the whole viewport width with no container padding; the default
   .bleed (80px) + .content-col (15px) inset was holding it 95px off each edge. */
.w-mod-videoFullwidth .bleed,
.w-mod-videoFullwidth .content-col { padding-inline: 0; }
.w-mod-videoFullwidth .w-video { display: block; width: 100%; }

/* Live gives this video a FIXED height, not an aspect ratio: 810px at 1920, 1440,
   1280 and 1024 alike, with `object-fit: cover` doing the cropping. The base
   `.w-video` rule uses `aspect-ratio: 16/9`, which happens to agree at exactly
   1440 and diverges everywhere else — at 1920 ours stood 1080 tall against live's
   810, which is the "video is huge" on the QC sheet. Measuring at 1440 alone
   would never have shown it.

   Fixed to live's 810 for desktop. Below 1024 the base aspect ratio takes over
   again: live is non-responsive and holds 810 there too, but 810 of video on a
   390 screen is most of the viewport, and live serves its own `/m/` template at
   those widths rather than this one. */
@media (min-width: 1024px) {
  .w-mod-videoFullwidth .w-video {
    height: 810px;
    aspect-ratio: auto;
    object-fit: cover;
  }
}

/* ==========================================================================
   FOOTER — REVERTED, by client decision.

   Live's footer is dark (`.custom-footer { background:#0A151E }`) with sage social
   discs and a single row of award badges, and that treatment was built here and
   measured. The client asked for the inner pages to follow the HOMEPAGE footer
   instead, so the whole site is consistent with the page they froze.

   Cost, recorded so the decision is reversible: the inner pages now diverge from
   live's footer exactly as the homepage does. The original block is in git history
   and in docs/INNERPAGES.md I-06/I-07/I-08 with the measured values, so it can be
   restored in one edit if the client changes their mind.
   ========================================================================== */

/* ==========================================================================
   HERO CAROUSEL — live `.generic_slider.jssor-carousel-container`.
   JSSOR fade slider: one slide visible, cross-faded, 3000ms, arrows, no bullets.
   Measured @1440: widget 936 wide (65% narrow container), 30px block padding,
   slider 906x509.625 = 16:9 inside the column's 15px gutter.
   Not scoped to .for-inner — the homepage emits no [data-hero-carousel], so
   these rules cannot reach it. home-guard.mjs proves that on every run.
   ========================================================================== */
.hero-carousel { position: relative; }

.hero-carousel__track {
  position: relative;
  margin: 0;
  padding: 0;
  list-style: none;
  aspect-ratio: 16 / 9;
  overflow: hidden;
}
.hero-carousel__slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 480ms var(--ease, ease);
}
.hero-carousel__slide.is-active { opacity: 1; }
.hero-carousel__slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* No-JS: only the first slide is is-active in the markup, so the rest stay hidden
   and the page still shows an image. */

.hero-carousel__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;                    /* WCAG 2.5.8 target size */
  height: 44px;
  border: 0;
  border-radius: 50%;
  background: rgba(0, 0, 0, .35);
  color: #fff;
  font-size: 22px;
  cursor: pointer;
  transition: background var(--dur-fast, 150ms) var(--ease, ease);
}
.hero-carousel__nav:hover { background: rgba(0, 0, 0, .55); }
.hero-carousel__nav--prev { left: 10px; }
.hero-carousel__nav--next { right: 10px; }

@media (prefers-reduced-motion: reduce) {
  .hero-carousel__slide { transition: none; }
}

/* ==========================================================================
   MEDIA-TOP — live `wt<N>imgtop-<N>tbbot` (`two-col` / `three-col`).
   N columns, each an image above its own text block: the blog-listing and
   programme-listing card rows. Live's image is `.img-responsive.image-content`
   carrying its authored crop as an inline aspect-ratio.
   ========================================================================== */
.section--media-top .media-top__col { min-width: 0; }
.section--media-top .media-top__media {
  aspect-ratio: var(--img-ratio, 16 / 9);
  overflow: hidden;
  margin-bottom: var(--sp-3);
}
.section--media-top .media-top__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* ==========================================================================
   ORPHAN IMAGES — images live places beside `.text-content-wrapper` rather than
   inside it. A single one flows at container width; four or more become the
   gallery grid (live: `.col-sm-6.col-md-4`, i.e. 3-up on desktop, 2-up on
   tablet, with 157.5px-tall thumbnails).
   ========================================================================== */
.w-images img { max-width: 100%; height: auto; display: block; }
/* Live's gallery grid, measured:
       #gallery-pager .gallery_thumbnail_margin { float:left; margin:0 -2px 30px -2px }
       img.thumb-img  { height:157.5px }            (col-sm-6 col-md-4 -> 3-up)
       .gallery-header { margin:20px 0 30px !important }
   130 thumbnails over 44 rows at 157.5 + 30 = 187.5 per row is what makes live's
   gallery widget 8391px tall. */
/* The grid is FIXED, not fluid: thumbnails measure 280x157.5 at a 306px pitch and
   the block is centred, identical at 1440 and 1920 (checked on both captures — the
   first three thumbs start at 508/814/1120 at 1920 and the row is centred on 954
   against a viewport centre of 960). A fluid `repeat(3, 1fr)` stretched them to
   ~395px wide, so every thumbnail was cropped differently from live and sat in the
   wrong place, which is why the page scored 43.96 with all its content present. */
.w-images--grid {
  display: grid;
  grid-template-columns: repeat(3, 280px);
  column-gap: 26px;
  row-gap: 30px;
  justify-content: center;
}
/* The grid item is `.shot` — every thumbnail is wrapped in a link so the lightbox
   has something to open (widgets/render.njk). The sizing sits on whichever element
   is the item, because a grid that carries plain images elsewhere must keep
   working; today only the gallery renders this class, but the rule does not need
   to know that. */
.w-images--grid > :is(a, img) {
  width: 280px;
  height: 157.5px;
}
.w-images--grid img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.w-images--grid > .shot { display: block; position: relative; }
/* Written child-of-.richtext so it out-specifies `.richtext > :last-child`, which
   otherwise zeroes the 30px below the heading — the h1 is the ONLY node in its
   richtext here, because the grid's thumbnails are siblings of that block, not
   children of it. Live hits the same collision and answers it with `!important`
   (see the captured rule quoted above).

   Losing those 30px moved all 44 photo rows up by 30 and cost ~24 points of pixel
   score on the page, while the heading itself sat correctly — the kind of failure
   that reads as "the grid is misaligned" and is really a collapsed margin.

   Weight is deliberately left at the reset's 700. Live's computed style SAYS 600,
   but 600 is not among the faces it loads, so it paints the 700 it has: "All Photos"
   measures 107px of ink on live, which is what our 700 gives. Setting 600 here made
   it 99px — i.e. matching live's declaration and missing its rendering. */
.richtext > .gallery-header { margin: 20px 0 30px; text-align: center; }
@media (max-width: 1023px) {
  .w-images--grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 600px) {
  .w-images--grid { grid-template-columns: 1fr; }
}

/* Full-width media-side (live `imageDescriptionSide … fullWidthWidget`).
   The narrow variant is flush; this one carries its measured padding and lets the
   image run to the container edge, which is what makes live 590 tall where the
   narrow variant is 247. */
.section--media-side-full { padding-block: var(--pt, 30px) var(--pb, 30px); }
.section--media-side-full .bleed { padding-inline: 0; }

/* ==========================================================================
   ENQUIRY FORM — the underline treatment, inner pages only.

   HANDOFF.md §7.2 listed this as "the only open item waiting on a human", on the
   basis that live's fields are underline controls while the replica draws boxes,
   and that choosing between them was a design decision.

   It is not a design decision. Live's custom-plugin stylesheet states it outright:

     .simp-custom-bottom-form .form-group div.col-sm-12.col-md-6 input {
         width:100%; padding:2px!important; margin:8px 0; box-shadow:none;
         border:none; background:transparent; border-radius:0; border-bottom:2px solid }
     … :is(.simp-custom-bottom-form,.simp-custom-top-form) .iti .iti__countr {
         background:transparent; border:unset; border-bottom:2px solid #6B8F7D;
         border-radius:0; margin:0 }
     .simp-custom-bottom-form .form-group.col-sm-12 { width:25%; padding:0 }
     .simp-custom-bottom-form #form-group-submit { position:absolute; right:0; top:15px }
     .simp-custom-bottom-form #form-group-submit .formSubmitButton {
         color:white!important; background-color:#294c4b; width:130px!important }
     .simp-custom-bottom-form { padding-bottom:0!important; height:160px!important }

   The previous developer could not find it because it lives in the custom-plugin
   sheet rather than the theme CSS. Measured, not chosen.

   NOW SITE-WIDE, at the client's request. The homepage's form widget carries the same
   mods (bottom-form, fullwidthform, link-ThankuPage) and the same 160px height, so it
   had the identical defect; it was scoped away only while the homepage was frozen.
   ========================================================================== */
.enquiry-form .form-control {
  padding: 2px;
  margin: 8px 0;
  background: transparent;
  border: none;
  border-bottom: 2px solid #6B8F7D;
  border-radius: 0;
  box-shadow: none;
}
.enquiry-form .form-control:focus {
  border-bottom-color: #294C4B;
  outline: 2px solid transparent;   /* the visible focus cue is the thicker rule */
  outline-offset: 2px;
}
/* Keep a real focus ring for keyboard users — live has none, which is a WCAG
   2.4.7 failure we are not going to reproduce. */
.enquiry-form .form-control:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 2px;
}
.enquiry-form .form-control[aria-invalid="true"] { border-bottom-color: #B4392F; }

.enquiry-form .btn--primary {
  background-color: #294C4B;
  color: #fff;
  width: 130px;
}

/* ==========================================================================
   INNER-PAGE MEASURE — live `inline-home.css`:

       .inner-page .content_wrapper .widget-wrapper:not(:nth-child(1)) {
           padding-left: 10%; padding-right: 10%; }

   Every widget except the first gets a 10% inset on inner pages. The rule is
   `.inner-page`-scoped upstream, so the homepage (`.homepage`) never receives it —
   which is why the replica, built from the homepage, had no equivalent and rendered
   inner-page text 1194px wide against live's ~936px. Wider measure means fewer line
   wraps, which is where the missing section height was going.

   The percentage resolves against the section (viewport width), matching live's
   144px @1440, 192px @1920, 128px @1280.

   ARTICLE BODY MEASURE — now template-level (P0-1).

   Live delivered that rule inside a pasted block of custom CSS that also carried a
   `padding-top:115px`, nine rules naming 28 widget ids, and a `.for-homepage` rule.
   It reached 153 of the 160 health-guide articles and missed seven, so seven articles
   identical in template and composition to their siblings rendered 25 characters per
   line wider. The replica reproduced that by regex-testing the captured markup into an
   `insetWidgets` flag — a mechanism that recorded whether a string was present, not a
   design intent.

   The measure is KEPT: it is the only thing on the site holding long-form prose
   anywhere near a readable line length. What changed is how a page qualifies —
   `page-article`, derived from URL and composition in src/_data/page-type.mjs,
   applied to every article and to nothing else.

   CORRECTED (measured, four articles at 1440, first full line of the longest body
   paragraph): the inset gives 119–129 characters per line at 906px, against
   161–169 at 1194px without it. This comment previously said 84 against 109; no
   article reproduces those figures, and the difference matters — 84 sounds close to
   the comfortable 45–75 and 129 plainly is not. The inset is a real improvement
   that still leaves the measure about 1.7x too wide, which is the decision below.

   Applying it to EVERY inner page would still overshoot (policies/privacy-policy came
   out 803px taller than live when that was tried), which is why it is scoped to the
   article template rather than made global.

   The 10% value itself is inherited and is NOT re-derived here. If the intent is
   readability the number should come from a target characters-per-line — at the
   current 14px that is nearer 500–600px than today's 906, and raising the body to
   16px would put a 75-character line at about 670. Either way it is a type-scale
   decision rather than a formatting one, and it is still open — as is bringing the
   form and press bands onto the same measure on article pages.
   ========================================================================== */
/* `.media-side` is in the list because it is a container too — the media-side
   branch emits `.container--narrow.media-side` rather than `.container`, so it
   never matched and those rows kept the full 1224 while every text widget around
   them sat at 936. Measured on live at 1440 on
   health-guide/5-ways-to-bring-down-the-risk-of-seasonal-diseases: every widget on
   the page, media-side included, has a 936 measure at x=252 and a 438 image; ours
   ran 1224 wide with a 582 image, so the image/text rows were visibly broader than
   the article they sit in.

   The 10% resolves against the SECTION, not the container — 10% of 1440 is 144 a
   side, and 1224 - 288 = 936. That is why the same percentage lands both container
   flavours on live's number without either needing its own value.

   Desktop only. The inset buys READABILITY — it exists to pull an 109-character
   line back to 84 — and a phone line is already short, so below 1024 it stops
   being a measure and becomes only lost width: 10% of 390 is 39px a side against
   the site's standard 20px gutter, leaving article copy 312px wide where every
   other page gets 350. Gated at 1024 to match the G-6 container model, which is
   where `.container` itself stops being centred and goes full-bleed with that
   20px gutter. Below it an article now reads like every other page. */
@media (min-width: 1024px) {
  .page-article main > section:not(:first-child) > :is(.container, .media-side) {
    padding-inline: 10%;
  }
}

/* ==========================================================================
   ARTICLE BODY RHYTHM — the 164 health-guide posts, and nothing else.

   Live has no paragraph rhythm on these pages at all. Every paragraph is a `<div>`,
   `.richtext div` carries no margin, and the gaps are `<br> &nbsp;` runs typed into
   the copy — 4998 of them across the health-guide files. That is why the spacing is
   even on one article, doubled on the next, and absent on a third: measured at 390 on
   role-of-healthy-diet-in-longevity, two adjacent paragraphs render with a 0px gap.

   tools/article-html.mjs strips those runs and splits the double-break paragraphs
   into real blocks. These are the margins that replace them, so article spacing is
   four values instead of 171 hand-typed approximations.

   MEASURED BEFORE (1440 / 390):
       body copy      14px / 19.6px  (1.4 — tight for long-form at any measure)
       paragraph gap  0 from CSS, 0-24px from <br> runs
       h2 margin      0 top / 0 bottom
       list           20px below, 0 above, 30px indent
       alignment      justify on every paragraph and every <li>

   All values come from the existing --sp-* scale, which is derived from live's own
   spacing, so the rhythm is new but the numbers are not foreign to the site.

   THIS IS A DELIBERATE DEPARTURE FROM LIVE, on readability grounds — the same
   grounds on which the article measure above is kept. It changes article section
   heights, so health-guide pages score lower against the live pixel references in
   reference/qa/inner/ by design. Everything here is scoped to `.page-article`, so no
   other template can be reached.

   NOT changed, and still open: the 14px body size and the 906px measure it sits on —
   119–129 characters per line at 1440, measured, against a comfortable 45–75. The
   corrected figures are in the measure block above. That is a type-scale decision
   rather than a formatting fix, so it is flagged here and not taken.

   Also still open: 589 single `<br>` across 86 articles sit at a sentence boundary
   and read as paragraph breaks, but the same shape carries label:value list lines
   ("Antioxidants: Leafy greens are loaded with…"), so no rule separates the two
   safely. Those want an editor, not a selector.
   ========================================================================== */

/* Body copy. `li` sets its own line-height in reset.css, so it needs saying twice. */
.page-article .richtext,
.page-article .richtext li { line-height: 24px; }

/* Paragraph rhythm. One gap, top-side, matching the site's own paragraph token —
   `.richtext p { margin: 15px 0 0 }` is the homepage's measured value and stays. */
.page-article .richtext :is(div, p):not(:has(> :is(div, p, h1, h2, h3, h4, h5, h6, ul, ol, table, blockquote))) { margin-top: var(--sp-4); }
.page-article .richtext > :first-child,
.page-article .richtext > :first-child > :first-child { margin-top: 0; }

/* Section headings need to belong to the copy BELOW them, not float between two
   paragraphs — live gives them 0 on both sides, so they sat equidistant from the
   paragraph above and the one below and read as belonging to neither. 40 above and
   10 below, from the site's own scale: at the desktop h2's 25px that is 1.6x its size
   above and a quarter below, and at the mobile 20px, 2x and a half. An h3 is a
   sub-section, so it takes 30 rather than 40 — same rule, one step down. */
.page-article .richtext :is(h1, h2, h3, h4) { margin-top: var(--sp-6); }
.page-article .richtext :is(h2, h3, h4) { margin-bottom: var(--sp-2); }
.page-article .richtext :is(h3, .faq-q) { margin-top: var(--sp-5); }

/* Lists. Live puts the whole gap BELOW a list and none above it, which reads as
   though each list belongs to the paragraph after it rather than the one that
   introduces it. Symmetric instead: the list takes the paragraph gap above, and the
   paragraph after it supplies its own. 20px of indent rather than 30 — on a 350px
   phone column, 30px of it was 9% of the measure. */
.page-article .richtext :is(ul, ol) { margin: var(--sp-4) 0 0; padding-left: var(--sp-4); }
.page-article .richtext li { margin-bottom: var(--sp-2); }
.page-article .richtext li:last-child { margin-bottom: 0; }
.page-article .richtext li > :is(ul, ol) { margin-top: var(--sp-2); }

/* An FAQ question promoted out of `<strong>` by tools/article-html.mjs. It reads as
   the h3 it now is (18px) and keeps the tighter pairing with its own answer, so a
   question sits closer to its answer than to the question above it.

   Line-height is loosened from the heading token's 1.1 because these headings are
   SENTENCES, not labels. Measured across all 104: at 1440 only 12 wrap (92 sit on one
   line, where the value changes nothing), but at 390 the split is 16 / 54 / 30 / 4
   across one, two, three and four lines — so 88 of them wrap on a phone, and 1.1 sets
   those almost solid. 1.35 is still tighter than the body's 24/14. */
.page-article .richtext .faq-q {
  margin-top: var(--sp-5);
  margin-bottom: var(--sp-1);
  line-height: 1.35;
}

/* Authored `<div class="faq">` — four articles wrap each question in one. It holds a
   heading and a paragraph, so it is a block in the flow and takes the same gap. */
.page-article .richtext div.faq { margin-top: var(--sp-4); }
.page-article .richtext div.faq > :is(h3, h4) { margin: 0 0 var(--sp-1); }

/* ==========================================================================
   MEDIA-SIDE WIDTH — one component, one explicit variant (DS-01).

   Live's base widget is 85% wide (1224 @1440, max-width 1340); the authored
   `simp-custom-width-65` modifier narrows it to 65%:

       .simp-custom-width-65 { width: 65% !important }

   Live ALSO force-narrows every instance on the homepage with a page selector
   (`.homepage .widget-wrapper.imageDescriptionSide { width:65% !important }`), which
   is what produced the reported defect: the BOOK YOUR HEALTH PROGRAMS contact strip
   rendered 936px flush on the homepage and 1224px with 30px padding on the 79 other
   pages that carry it.

   That page rule is gone from both the markup and this stylesheet. The variant is
   driven only by the widget's own modifier, on every page, and the homepage rows
   that live means as 65% now say so in their data (tools/build-page-data.mjs).

   Padding is no longer declared here either — `.section--media-side` reads each
   widget's captured `--pt`/`--pb`, which already encode the `paddingTop-zero` and
   `paddingBottom-zero` modifiers (verified: 8 of 8 instances agree).
   ========================================================================== */
.section--media-side-wide > .media-side {
  width: 85%;
  max-width: 1340px;
  margin-inline: auto;
}
@media (max-width: 1023px) {
  .section--media-side-wide > .media-side { width: auto; max-width: none; }
}

/* ==========================================================================
   FAQ ROWS — REMOVED (Pass 5A).

   This file used to carry an 11-rule `w-mod-info` block reproducing live's
   `simp-custom-info` disclosure rows. Those rows are now the shared FAQ component:
   the authored HTML is parsed into structured items by tools/faq-items.mjs and
   rendered by the `faq` branch of render.njk, styled by widgets/faq.css.

   src/scripts/faq-info.js is deleted with it — its only job was to rebuild that DOM
   in the browser.
   ========================================================================== */

/* ==========================================================================
   MOBILE FOOTER — REMOVED, by the same client decision as the dark footer.

   Live serves a phone UA a different footer entirely (address / email / phone stack,
   flat social row, Simplotel credit) and it was built here as
   src/_includes/global/footer-mobile.njk, worth +7 points at 390 across every page.
   The homepage has no such footer, and the instruction was that inner pages follow
   the homepage footer — so it is not rendered.

   The component file is kept on disk, unused. Restoring it is two edits: re-add the
   include in base.njk and this stylesheet block. See docs/INNERPAGES.md I-18.
   ========================================================================== */

/* Mobile gallery — live's `/m/` template presents the gallery as a ONE-IMAGE
   carousel with prev/next arrows and a category dropdown, not a grid. Stacking all
   130 thumbnails one per row made our mobile gallery 25,991px tall against live's
   1,496px, which scored 1.41% union.

   Below 1024 the grid becomes a horizontal scroll-snap strip showing one image at a
   time. That matches live's presentation and its height, works without JavaScript,
   and stays operable by touch and keyboard (a scroll container is focusable and
   arrow-scrollable) rather than reproducing live's arrow-only widget. */
@media (max-width: 1023px) {
  .w-images--grid {
    display: flex;
    grid-template-columns: none;
    gap: 0;
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch;
    /* MUST reset the desktop grid's centring. A flex line that overflows a
       `justify-content: center` container overflows EQUALLY on both sides, and the
       left half is then unreachable — scrollLeft 0 is already past it. Measured at
       390: the first of the 130 photographs sat at x = -22555 with scrollWidth
       22925 instead of 45500, so the strip opened halfway through the gallery and
       65 photographs could not be reached at all. */
    justify-content: flex-start;
  }
  /* Same wrapping story as the desktop grid: the flex item is whichever element
     is the direct child, and the image inside a link fills it. */
  .w-images--grid > :is(a, img) {
    flex: 0 0 100%;
    scroll-snap-align: start;
    width: 100%;
    height: auto;
    aspect-ratio: 16 / 9;
  }
  .w-images--grid img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
}

/* A widget carrying only images and a column count lays them out side by side —
   live's `two-col`/`three-col` on an image-only widget. Stacking two 16:9 images
   full-width instead made each of these 595px tall against live's 387. */
.w-images--c2, .w-images--c3 { display: grid; gap: var(--sp-3, 15px); }
.w-images--c2 { grid-template-columns: repeat(2, 1fr); }
.w-images--c3 { grid-template-columns: repeat(3, 1fr); }
.w-images--c2 img, .w-images--c3 img { width: 100%; height: auto; }
@media (max-width: 767px) {
  .w-images--c2, .w-images--c3 { grid-template-columns: 1fr; }
}

/* Media column inside a `wt2tb` — live:
       .simp-custom-left-media  .left-box,  .simp-custom-right-media .right-box { width:55%; padding-right:40px !important }
       .simp-custom-left-media  .right-box, .simp-custom-right-media .left-box  { width:45% }
   The media sits on the named side; the replica rendered only the text column, so
   these rows measured 251px against live's 384. */
.w-col--media { min-width: 0; }
.w-col--media .w-video,
.w-col--media img { width: 100%; height: auto; display: block; }

@media (min-width: 768px) {
  .w-mod-left-media .grid--2,
  .w-mod-right-media .grid--2 { grid-template-columns: 55% 45%; }
  .w-mod-left-media .w-col--media { padding-right: 40px; }
  /* right-media puts the media second; the text column keeps the 55% side. */
  .w-mod-right-media .grid--2 { grid-template-columns: 45% 55%; }
  .w-mod-right-media .w-col--media { order: 2; padding-left: 40px; padding-right: 0; }
}

/* ==========================================================================
   WEDDING PAGE — live gives this one page a bespoke treatment. Reproduced from
   the custom-plugin sheet:

     .simp-custom-wedding-page-text { padding: 2.4% 18.65% 2.2% 18.65% }
       h1 { font-size:38px; line-height:60px; padding-bottom:3.3% }
       p  { font-size:18px; line-height:36px; text-align:center }
     .simp-custom-wedding-page-limg-rtext { padding: 30px 8.23% 7.8% 8.23% }
       .left-box { width:44.4% }  .left-box img { height:474px; object-fit:cover }
       .right-box{ width:54.6% }

   NOT reproduced: live also absolutely positions the text block as a white card
   overlapping the image, with a 4px #f2dfaf border and a shadow
   (`position:absolute; left:-29px; width:97.5%; box-shadow:4px 8px 12px #DBC79…`).
   That needs markup this component does not emit, and it is one page — recorded in
   docs/INNERPAGES.md §12 rather than half-built.
   ========================================================================== */
.w-mod-wedding-page-text { padding: 2.4% 18.65% 2.2%; }
.w-mod-wedding-page-text .richtext h1 {
  font-size: 38px;
  line-height: 60px;
  letter-spacing: 0;
  padding-bottom: 3.3%;
}
.w-mod-wedding-page-text .richtext p,
.w-mod-wedding-page-text .richtext div {
  font-size: 18px;
  line-height: 36px;
  letter-spacing: 0;
  text-align: center;
  margin-top: 0;
}

@media (min-width: 1024px) {
  .w-mod-wedding-page-limg-rtext,
  .w-mod-wedding-page-rimg-ltext { padding: 30px 8.23% 7.8%; }
  .w-mod-wedding-page-limg-rtext .media-side,
  .w-mod-wedding-page-rimg-ltext .media-side {
    grid-template-columns: 44.4% 54.6%;
    align-items: center;
  }
  .w-mod-wedding-page-rimg-ltext .media-side { grid-template-columns: 54.6% 44.4%; }
  .w-mod-wedding-page-limg-rtext .media-side__media,
  .w-mod-wedding-page-rimg-ltext .media-side__media { aspect-ratio: auto; }
  .w-mod-wedding-page-limg-rtext .media-side__media img,
  .w-mod-wedding-page-rimg-ltext .media-side__media img {
    height: 474px;
    object-fit: cover;
    width: 100%;
  }
  .w-mod-wedding-page-limg-rtext .media-side__body h2,
  .w-mod-wedding-page-rimg-ltext .media-side__body h2 {
    font-size: 30px;
    line-height: 60px;
    font-weight: 600;
    letter-spacing: 0;
  }
  .w-mod-wedding-page-limg-rtext .media-side__body,
  .w-mod-wedding-page-rimg-ltext .media-side__body {
    font-size: 18px;
    line-height: 36px;
    padding-top: 2.37%;
    letter-spacing: 0;
  }
}

/* `simp-custom-limg-rtext-heading` — the same treatment as wedding-page-text with a
   wider inset and no bottom padding:
       { padding: 2.4% 15.25% 0% 15.25% }  h1/h2 38/60, p 18/36 centred */
.w-mod-limg-rtext-heading { padding: 2.4% 15.25% 0; }
.w-mod-limg-rtext-heading .richtext h1,
.w-mod-limg-rtext-heading .richtext h2 {
  font-size: 38px;
  line-height: 60px;
  letter-spacing: 0;
  padding-bottom: 3.3%;
}
.w-mod-limg-rtext-heading .richtext p,
.w-mod-limg-rtext-heading .richtext div {
  font-size: 18px;
  line-height: 36px;
  letter-spacing: 0;
  text-align: center;
  margin-top: 0;
}

/* ==========================================================================
   MOBILE HEADER — REVERTED, by client decision.

   Live's phone template stacks the header in two rows (logo centred on its own band,
   then a control row with the hamburger, BOOK NOW and the phone) and keeps the promo
   strip on one line. That was built and measured here: it removed a constant ~50px
   offset from the top of every inner page at 390.

   The client asked for the inner-page navbar to match the HOMEPAGE at mobile, so both
   now use the homepage's single-row header and its wrapping promo strip. Like the
   footer, this means both diverge from live in the same way, which is the consistency
   that was asked for.

   Restoring is one block: see docs/INNERPAGES.md I-23 for the measured values
   (logo band 0-72, control row 72-124, promo strip 124-186 at 390).
   ========================================================================== */

/* Card-row width, measured per column count. The TWO-column width lives on the
   container primitive now — `.container--cards` in primitives.css, emitted by the
   media-top template — not here. A width rule for it did sit here, but a stray `}`
   left behind by the mobile-header revert above made the parser swallow exactly
   that one rule, so every browser rendered the cards at the 85% default while the
   stylesheet read 70%. The three-column variant runs the full 1440 rather than
   the 85% default. */
/* The THREE-column card row keeps the site's default container. It used to be
   forced to `width:100%`, on the claim that "the three-column variant runs the
   full 1440". Only one of the three plain c3 widgets does — and that one is
   `fullWidth` in the data, so it renders `.bleed` and gets its width from there.
   The other two (discover-your-dosha, rooms-suites) measure 1224 @1440 and 1088
   @1280 on live, i.e. the ordinary 85% container, and the override was stretching
   them edge-to-edge with their images bleeding off both sides.
   `.bleed` needed nothing: it is already 100% wide by definition. */

/* Card chrome on the two-column listing rows. Live draws a 1px rgb(219,219,227)
   border around each card with a rule under the image and under the title, and pads
   the title and body bands — worth ~88px of row height that the replica's bare
   image-over-text column did not have. Sampled from the live capture at 1440. */
/* One name for the 2-column gap, because the single-card rule below has to do
   arithmetic with it and a second literal would drift from this one.

   Declared on `.section--media-top`, not on `--c2`: the single-card centring rule
   keys on `w-mod-center`, which live also authors on a ONE-column row (special
   offers, below), and there `var(--card-gap)` resolved to nothing — an invalid
   `calc()` throws the whole `grid-template-columns` away, which is why that card
   rendered full width instead of centred. Only c1 and c2 rows carry `center` in
   the build, so nothing else changes shape. */
.section--media-top { --card-gap: 60px; }
.section--media-top--c2 .grid { gap: var(--card-gap); }

/* A 2-column row that renders only ONE card — live `hideLeftBox` + `center`, on
   the acupuncture, naturopathy and yoga guide pages. The card keeps a single
   column's width and sits in the middle of the row rather than in the left
   track.

   Measured on live at 1440: the card is 444 wide with 498px clear on BOTH sides
   of the 1440 viewport. Our grid is 948 (444 + 60 + 444) starting at x=246, so
   centring the card inside the grid puts it at 246 + (948 - 444) / 2 = 498 —
   live's number exactly, without positioning against the viewport.

   Collapsing to a single track rather than spanning the card across two: a
   spanned card would have to be given back its width by hand, and this way the
   width still comes from the same `(100% - gap) / 2` the two-card rows use.

   Keyed on `center`, which live authors on precisely these rows — NOT on "one
   card in a two-column row" generally. The other family of short rows
   (`remove-rightbox`, 8 rows on 4 pages) carries no `center` and live does not
   centre those.

   Bounded at 768 to match `.grid--2`, which is the thing being overridden: it is
   two columns down to 768 and one below that. Left unbounded the half-column
   width beat `.grid--2 { grid-template-columns: 1fr }` on specificity and the
   card kept shrinking with the viewport — 145px wide at 390, centred in a sea of
   white, while every other card row went full width. Below 768 this row now
   stacks like the rest. */
@media (min-width: 768px) {
  .section--media-top.w-mod-center .grid:has(> :only-child) {
    grid-template-columns: calc((100% - var(--card-gap)) / 2);
    justify-content: center;
  }
}

/* The same short row, authored on live as ONE column instead of two — the third
   Special Offers card ("Day Pass"), which live splits off into its own
   `wt1imgtop` widget below the two-card row rather than making that row a three.

   The template only asks for `.container--cards` when `cols == 2`, so this row sat
   in the ordinary 85% container while its two siblings sat in the 70% card
   container inset 30px on each side. The card was 1224 wide against their 444.

   Giving the row the card container makes the rule above resolve to the same
   `(100% - 60) / 2` = 444 the two-card row uses, centred in the same 1008 track —
   so all three cards are one width and one rhythm, and the geometry still comes
   from `--container-cards` rather than a literal repeated here.

   Bounded at 1024 to match `.container--cards` itself: below that live drops card
   rows to full bleed (G-6), and this row now drops with them. */
@media (min-width: 1024px) {
  .section--media-top--c1.w-mod-center > .container {
    width: var(--container-cards);
    max-width: var(--container-max);
    margin-inline: auto;
    padding-inline: 30px;
  }
}
/* Live's listing card, measured on health-guide at 1920 and 1440 (identical):
     card      background #FCFCFC, border 1px solid rgba(50,68,84,.3)
     media     border-bottom the same rgba(50,68,84,.3)
     wrapper   padding 0 15px 60px — NO top padding
     image -> heading 20, heading -> body 20, body -> Read More 19
   The replica had no card background at all (the QC sheet's "in the card
   background is missing"), a colder grey border, and 35/34 where live has 20/20:
   its 14px wrapper top padding stacked on top of the authored spacer paragraph,
   and the h4 added a 14px bottom margin on top of the authored spacer below it.
   Live leans on the authored spacers alone, so they are what supplies the 20s.

   The centred ONE-column row is included: it is a card in a row of cards (the
   third Special Offers card, sized to match its two siblings above), and a card
   without the chrome reads as a different component sitting under them. Only the
   three CHROME rules are shared — the title `min-height` rules below stay on `--c2`,
   because they exist to stop a one-line title stepping the row against a two-line
   one, and a single card has no row to step against. */
:is(.section--media-top--c2, .section--media-top--c1.w-mod-center) .media-top__col {
  background: #FCFCFC;
  border: 1px solid rgba(50, 68, 84, .3);
}
:is(.section--media-top--c2, .section--media-top--c1.w-mod-center) .media-top__media {
  margin-bottom: 0;
  border-bottom: 1px solid rgba(50, 68, 84, .3);
}
:is(.section--media-top--c2, .section--media-top--c1.w-mod-center) .media-top__col .richtext { padding: 0 14px 60px; }

/* Card titles that are an h3 rather than an h4 — the health-guide category pages
   author them that way (18px/19.8, no padding, 10px below). They need the same
   two-line reservation as the h4 titles above, for the same reason: measured on
   health-guide/acupuncture, a one-line title next to a two-line one stepped the row
   by 20px. The figure is just the two lines, because this variant carries no padding.

   `:not(.w-mod-twocard-blogs)` because THAT variant adds 5px under its title and so
   needs 44.6, not 39.6 — and it sets that for itself further down this file. Without
   the exclusion this selector is the more specific of the two and won at 39.6, which
   left the therapy cards stepping 4-5px (measured on curative-therapies/yoga). One
   figure per variant, each owned where the variant is defined. */
.section--media-top--c2:not(.w-mod-twocard-blogs) .media-top__col .richtext h3 {
  min-height: calc(19.8px * 2);
}

/* A c2 card title that OPENS the card takes the 20px above it.
   The two rules above set `margin: 0 -14px` on the title, which zeroed the general
   card-title rule in primitives.css — same specificity, and this file loads later. 39
   of the 48 flush card titles were fixed by restoring live's authored spacer; these 9,
   all on health-guide.html, never had one, so the gap has to be stated here.
   Measured across all 425 card titles on the site: 352 sit in live's 12-21px band and
   these were the only ones left at 0. */
.section--media-top--c2 .media-top__col .richtext > :is(h3, h4):first-child {
  margin-top: var(--sp-4);
}

/* Card-row rhythm below 1024. Every one of the 171 two-col card widgets captured
   0/0 mobile padding (live's /m/ template, whose stripped card treatment we no
   longer follow), so consecutive rows sat flush against each other while the cards
   INSIDE a row kept the 60px grid gap — at 390 the stack read 60, 0, 60, 0…, and
   in the 768–1023 band whole rows touched. Desktop rhythm is 60 (30px + 30px of
   section padding), the stacked grid gap is 60; this makes row-to-row the same 60.
   A margin, not padding — the DS-03 mobile rhythm rule owns padding via --ptm and
   would win on specificity. */
@media (max-width: 1023px) {
  .section--media-top--c2 + .section--media-top--c2 { margin-top: 60px; }
  /* …and the FIRST row of a listing, which follows the page hero rather than
     another card row and so matched neither the rule above nor any padding: its
     captured `--ptm` is 0 like all the rest, so the first card sat hard against
     the bottom of the hero image with no separation at all.
     30px, not the 60 above, because that is what desktop gives it — the row's own
     `padTop:30` clears the hero, while row-to-row there gets 30+30. Mirroring the
     two numbers keeps the narrow layout reading like the wide one. */
  .section:not(.section--media-top--c2) + .section--media-top--c2 { margin-top: 30px; }
}

/* A card CTA never touches the copy above it. The gap used to be authored content —
   a stray `&nbsp;` line on some special-offers cards and nothing on their siblings
   (stripped from the data, tools/cta-spacer.mjs) — or absent entirely wherever the
   button sits in flow, which below 768 includes the programme cards (the pinned
   foot is a ≥768 behaviour). One body line, supplied by the component, so the card
   that had the authored line is pixel-identical and the rest gain it. Deliberately
   placed BEFORE the variant rules so they still win where live is specific:
   healthprogram pins the wrapper at ≥768 (higher specificity, margin 0) and
   twocard-blogs sets live's measured 10px at ≥768 (equal specificity, later). */
.section--media-top--c2 .richtext .txt-wdgt-btn-wrapper { margin-top: var(--lh-body); }
/* Live's heading rule spans the FULL card width — its h4 carries negative side
   margins that cancel the wrapper's inset — and the gap under it comes from the
   authored spacer, not from a margin on the heading. */
.section--media-top--c2 .media-top__col .richtext h4 {
  margin: 0 -14px;
  padding: 0 14px 15px;
  border-bottom: 1px solid rgb(187, 189, 191);
  /* TWO LINES ALWAYS, so the rule under the title lands on the same line in every
     card of a row. The titles are blog headlines and their length is not controlled:
     measured across the site's 180 card rows, 249 titles fit one line, 139 take two
     and 2 take three — and 94 of those rows paired a one-line title with a two-line
     one, which stepped the divider and everything under it by 20px. Reserving the
     second line costs 20px on the shorter card and buys an aligned row.
     18px/19.8 x 2 lines + the 15px below = 54.6; `box-sizing: border-box` is global,
     so the padding is inside the figure. */
  min-height: calc((19.8px * 2) + 15px);
}

/* A TEXT BLOCK FLUSH UNDER THE FULL-WIDTH VIDEO.

   The room pages put the description in a text widget whose captured top padding is
   0, directly under the edge-to-edge video: measured on
   rooms-suites/lakeview-double-king-room, the copy starts 12px under the video frame
   and the whole block has 0 above and 12 below. Against a full-bleed band that reads
   as the caption having fallen off the video rather than being its own block.

   Floored at the site's own 30px section padding — `max()` so a widget that captured
   MORE than 30 keeps its own figure. Scoped to this one widget shape (5 sections
   across the 4 room pages and one other), NOT to every full-bleed band: there are 515
   of those and most of the pairings after them — the warm form band, the bgimage
   heroes — are flush on purpose. */
.w-mod-videoFullwidth + .section {
  padding-top: max(var(--pt, var(--sp-5)), var(--sp-5));
}

/* Map — live's `.widget-wrapper.location`, measured 490px tall at 1440. */
.section--map { padding-block: 0; }
.w-map { display: block; width: 100%; height: 490px; border: 0; }
@media (max-width: 767px) { .w-map { height: 320px; } }

/* `simp-custom-healthprogram` — the programme listing cards. Live centres the card
   body and pins its CTA to the card foot:
       .text-content-wrapper { margin-top:15px; border-bottom:0; text-align:center !important }
       …the button wrapper { position:absolute; bottom:20px }
   Pinning the CTA is what reserves the extra height, which is why live's rows measure
   684 against the replica's 603. Reproduced as centred text plus the reserved foot,
   rather than absolute positioning the component does not emit. */
.w-mod-healthprogram .media-top__col { position: relative; }
.w-mod-healthprogram .media-top__col .richtext { margin-top: 15px; }
/* Centring is a DESKTOP behaviour — live left-aligns these cards at 390. */
@media (min-width: 768px) {
  .w-mod-healthprogram .media-top__col .richtext,
  .w-mod-healthprogram .media-top__col .richtext h3,
  .w-mod-healthprogram .media-top__col .richtext h4 { text-align: center; }
}
/* The reserved CTA foot is a DESKTOP behaviour. Live's phone template drops it
   (`.mobile .simp-custom-healthprogram .descriptionMobileNoSpace`), and applying it at
   390 cost that page 8 points (43.44 -> 35.45) while gaining 16 on desktop.

   Live pins BOTH controls to that reserved foot — "Book Now" bottom-left and
   "READ MORE" bottom-right, on one row:
       .text-content-wrapper .txt-wdgt-btn-wrapper  { position:absolute; bottom:15px; left:15px }
       .text-content-wrapper h3 + div .internal-link { position:absolute; bottom:20px }
       … h3 + div + div .internal-link               { right:30px; bottom:20px }
   Our markup carries the same hooks: the READ MORE anchor is the `.internal-link`
   inside the div after the h3, and the button sits in `.txt-wdgt-btn-wrapper`. */
/* The gap between a card title and its copy — at EVERY width. The title had
   `margin: 0` and the paragraph started on the next line with nothing between
   them. Live spends 15px here (as padding under the heading), and the phone needs
   it at least as much as the desktop does: this sat inside the ≥768 block on the
   first cut and the reported card still had its title glued to the copy at 390.
   Padding, not margin, so it cannot collapse out of the card. */
.w-mod-healthprogram .media-top__col .richtext h3 { padding-bottom: var(--sp-3); }

@media (min-width: 768px) {
  /* The reserved foot must clear the pinned controls or the paragraph runs into
     them — and must not reserve room for a control the card does not have.
     Live reserves a flat 60px because its CTA is only 29px tall (15px offset + 29
     + 16px of air). The DS-04 CTA is 43px, so a card carrying one needs
     15 + 43 + 16 = 74. A card whose only control is the Know More link needs just
     the 60: the link sits at bottom 20 and is ~20 tall. Reserving 74 everywhere
     left 75px of dead white space under the copy on every link-only card — the
     bottom third of the card on weight-management. */
  .w-mod-healthprogram .media-top__col .richtext { padding: 0 var(--sp-3) 60px; }
  .w-mod-healthprogram .media-top__col:has(a.btn.txt-wdgt-btn) .richtext { padding-bottom: 74px; }

  /* Live's card title above 768: 21px/23.1 bold, pulled out to the card edges by
     a -15px side margin so the centring reads against the card rather than the
     text column, with 15px of air above it. */
  .w-mod-healthprogram .media-top__col .richtext h3 {
    font-size: 21px;
    line-height: 23.1px;
    font-weight: var(--fw-bold);
    margin: var(--sp-3) calc(-1 * var(--sp-3)) 0;
    padding-inline: var(--sp-3);
    /* Two lines reserved, so the copy under the title starts on the same line in
       every card of a row — programme names run to one line or two and the row
       stepped by 23px wherever they differed. Same reasoning as the c2 card titles
       above; the figure is this variant's own 23.1px line-height, twice. */
    min-height: calc(23.1px * 2);
  }

  .w-mod-healthprogram .media-top__col .richtext .txt-wdgt-btn-wrapper {
    position: absolute;
    bottom: 15px;
    left: 15px;
    margin: 0;
  }
  /* The card's trailing link, pinned to the foot. TWO authored shapes reach it:
     READ MORE sits inline at the end of the body div, and Know More sits in a div
     of its own after it — the replica only matched the first, so every Know More
     card left the link in flow above 75px of reserved emptiness. Live offsets the
     two differently (right 20 and right 30) for no reason anyone can defend; one
     value here, symmetric with the 20px bottom. */
  .w-mod-healthprogram .media-top__col .richtext h3 + div .internal-link,
  .w-mod-healthprogram .media-top__col .richtext h3 + div + div .internal-link {
    position: absolute;
    right: 20px;
    bottom: 20px;
  }
}

/* Banner sections must keep their MEASURED padding. `.section--bgimage` hard-codes
   `padding-block: 50px` (correct for the homepage's banner, which is what it was
   written for), and because home.css is imported after primitives.css it beats
   `.section { padding-top: var(--pt) }`. Inner-page banners measure 100px top and
   bottom, so every one of them came out ~100px short — seven on the food-menu page
   alone. Higher specificity here restores the measurement without touching the
   homepage's rule. */
.for-inner .section--bgimage {
  padding-top: var(--pt, 50px);
  padding-bottom: var(--pb, 50px);
}

/* ==========================================================================
   MOBILE PADDING — site-wide, below 1024 (DS-03).

   Widget padding was captured from the DESKTOP probe and applied at every width.
   Live's phone values are consistently smaller: a banner is 100px top and bottom on
   desktop and 50px on a phone; ordinary text widgets are 30px against 15px. Applying
   the desktop value at 390 inflated every single section on every page.

   `--ptm` / `--pbm` carry live's measured phone values (see tools/normalize-page.mjs).
   Sections that are deliberately flush are excluded so this cannot un-flush them, and
   any widget without a mobile value falls back to `--pt` exactly as before.
   `.section--media-side` is excluded for the same reason: it owns its own mobile
   default (0, measured across 186 instances) and used to be excluded implicitly by
   carrying `section--flush`.

   Widget 2417194 (HEALING THERAPIES) is excluded on the same principle — it is the
   one widget live sizes by PERCENTAGE rather than pixels (C-34, verified 4.5%/2%
   at every viewport from 1920 to 320), so it owns its block padding at every width.
   De-scoping this rule to the homepage caught it: at 390 the percentage collapsed to
   the frozen desktop pixels (17.55/7.80 -> 65/29) and the section grew 76px. The
   exclusion list is "sections that declare their own block padding", and it belongs
   on it.

   DS-03: both halves of this mechanism used to be inner-only — `pad()` emitted
   `--ptm`/`--pbm` only when `inner`, and this rule only matched `.for-inner`. The
   homepage was structurally outside the responsive rhythm system. Both gates are
   gone. Note what this does and does not fix: every page now runs the same rule,
   but the homepage carries no captured mobile values (0 of 31 widgets), so it falls
   back to `--pt` and its rendered mobile rhythm is unchanged. There is no valid
   source for those values — `/m/` is not a fidelity target and live's desktop
   template is non-responsive — so closing that last gap needs authored values, not
   code. Recorded in PROJECT_STATE.md as the next decision.
   ========================================================================== */
@media (max-width: 1023px) {
  .section:not(.section--flush):not(.section--flush-top):not(.section--flush-btm):not(.section--map):not(.section--media-side):not([data-widget-id="2417194"]) {
    padding-top: var(--ptm, var(--pt, var(--sp-5)));
    padding-bottom: var(--pbm, var(--pb, var(--sp-5)));
  }

  /* A PAINTED band gets a 20px floor on mobile. Its captured values are 10 or 15
     on 992 of the 1041 band widgets, and on a band that is too tight to read as a
     band: the colour change draws the eye straight to the edge, so a heading with
     10px above it looks stuck to the section before it. `GET IN TOUCH` and
     `PRESS COVERAGE` on the article template are the two the client flagged.

     A DEPARTURE from live, deliberately. Live's /m/ template gives both of those
     headings exactly 10px too, so this is not a defect being corrected — it is a
     spacing preference over live's own mobile, taken on the client's instruction.

     20px is --sp-4, the same unit as the mobile gutter, so the band's vertical
     breathing room matches its horizontal one. `max()` keeps it a FLOOR: the one
     band captured at 50/50 is untouched, as are the two at 20.

     The selector repeats the chain above and adds `:is(...)` because it has to —
     the captured values arrive as INLINE `--ptm`, which no stylesheet rule can
     override, so the padding itself has to be re-declared at a specificity above
     the rule directly above this one.

     `.for-inner` scopes it OFF the homepage. The homepage carries no captured
     mobile values at all (0 of 31 widgets), so it falls through to `--pt` — and a
     floor of 20 against its 15px bands silently grew the page 40px at 390, which
     the homepage guard caught. Whether the homepage should get the same treatment
     is a separate decision; it is not a side effect of an article-page fix. */
  .for-inner .section:not(.section--flush):not(.section--flush-top):not(.section--flush-btm):not(.section--map):not(.section--media-side):not([data-widget-id="2417194"]):is(.section--warm, .section--band, .section--black, .section--brand) {
    padding-top: max(20px, var(--ptm, var(--pt, var(--sp-5))));
    padding-bottom: max(20px, var(--pbm, var(--pb, var(--sp-5))));
  }

  /* That floor is space INSIDE the band. What was missing is space BEFORE it:
     `.section--media-side` is deliberately flush at mobile (measured — 186 of
     the 198 instances that carry mobile values are 0/0), so where one is
     followed by a band with its own background, the copy ran straight into the
     colour with no break. Live does not show this because its own press band is
     transparent at this width; ours is grey, so the seam is visible.

     Margin rather than padding, so the gap is OUTSIDE the coloured box — that is
     the white space the seam needs — and scoped to the pairing that causes it
     rather than to every band. */
  .for-inner .section--media-side + .section:is(.section--band, .section--warm, .section--black, .section--brand) {
    margin-top: 24px;
  }
}

/* ==========================================================================
   MOBILE TYPE SCALE — site-wide, below 1024 (DS-02).

   Measured across every captured mobile probe, and strikingly consistent:
       h1  18.2px / 27.3px  w600   (273 pages)
       h2  20px   / 30px    w600   (265 pages)
       h3  18px   / 27px    w400   (179 pages)
   against desktop's h2 at 25px/27.5px. The replica carried its desktop scale down to
   390, so every heading was oversized and wrapped onto more lines than live.

   DS-02: this was `.for-inner`-scoped, which left the HOMEPAGE on the full desktop
   scale all the way down to 320 — the same media-side eyebrow rendered 21px/700 on
   the homepage and 18px/400 on Our Story at 390. The scope was never a design
   decision; the homepage was simply frozen when the scale was measured. One scale
   now, every page.
   ========================================================================== */
@media (max-width: 1023px) {
  .richtext h1 { font-size: 18.2px; line-height: 27.3px; font-weight: 600; }
  .richtext h2 { font-size: 20px; line-height: 30px; font-weight: 600; }
  .richtext h3 { font-size: 18px; line-height: 27px; font-weight: 400; }
}

/* Phone treatment — the card KEEPS its chrome, by client decision.

   Live's phone template strips it: full-bleed image, no border, 25px text inset.
   An override block here copied that (and the 29-page /m/ diagnostic preferred it,
   59.80 vs 59.45), but /m/ is not a fidelity target and the client called the
   edge-to-edge look a defect ("Mobile it's touching edges"). Removing the block
   also closes a seam it created: 768–1023 never had the override, so the same card
   flipped from bordered-and-inset to full-bleed at 767→768. One treatment below
   1024 now — the standard 20px container inset, the card border, the 14px text
   padding — the same card as desktop, narrower. */

/* ==========================================================================
   AUTHORED CTA BUTTONS — live's `a.btn.txt-wdgt-btn`.

   The class is in the authored content and the replica emitted it, but nothing
   styled it, so every one of these rendered as a bare text link. Measured off the
   live capture: 96x36 box, fill rgb(41,76,75), white 12px Poppins.

   DS-04: this was `.for-inner`-scoped, so the homepage's 39 instances fell through
   to the generic `.btn` and rendered 7px 15px / 13px / 29px tall against every inner
   page's 12px 20px / 12px / 42px. Same authored class, two buttons. The scope is
   gone and the measured values are now the site's only treatment; the hex literals
   are replaced by the tokens that already hold them (DS-17, only where it supports
   this fix).
   ========================================================================== */
.richtext a.btn.txt-wdgt-btn {
  display: inline-block;
  background: var(--c-brand);
  color: var(--c-on-dark);
  font-size: var(--cta-fs);
  line-height: normal;
  padding: var(--cta-pad);
  border: 0;
  border-radius: var(--radius-0);
  text-decoration: none;
  transition: background var(--dur-fast, 150ms) var(--ease, ease);
}
.richtext a.btn.txt-wdgt-btn:hover,
.richtext a.btn.txt-wdgt-btn:focus-visible { background: var(--c-brand-dark); }
.richtext a.btn.txt-wdgt-btn .text { color: inherit; }

/* ==========================================================================
   `simp-custom-blog-3imgtext` — the related-posts row on 27 article pages. Live:
       .simp-custom-blog-3imgtext                        { padding:0 0 15px; display:flex }
       … .fullWidthWidgetWrapper                          { width:82%; display:flex }
       … .text-content                                    { margin:10px 30px; background:#fff }
       … .text-content-wrapper                            { background:#fff; padding:0 0 30px;
                                                            text-align:left }
       … .text-content-wrapper h4                         { border-bottom:1px solid rgba(230,235,241,.4);
                                                            padding-bottom:10px; padding-left:15px;
                                                            font-size:13px }
       … .text-content-wrapper p                          { margin-top:0 }
   ========================================================================== */
/* The card's APPEARANCE — white ground, ink text, the title rule and the 15px
   insets — applies at every width. Only the ROW's desktop layout is gated below.

   That split is the fix for a real defect. The whole block used to sit inside the
   `min-width: 768px` guard, so under 768 the card lost its white ground and sat
   straight on the section's black band — while the h4 kept the authored inline
   `<span style="color:#32383e">` it carries at every width. Dark grey on black:
   the title was unreadable on 27 article pages at phone widths.

   The guard exists because live keeps a SEPARATE `.mobile` template for this
   widget, and applying the desktop rules unguarded once cost the article template
   6.2 points at 390. That cost was measured against the /m/ capture, which the
   compare tool itself reports as "not a fidelity target" — and it is not a reason
   to ship an unreadable heading. The row's geometry stays behind the guard; only
   what makes the card legible comes out. */
.w-mod-blog-3imgtext .media-top__col {
  background: #fff;
  /* The card paints its own white ground, so it must own the text colour that
     works on it. This row sits on a BLACK band, and `.section--black` sets
     `color: #fff` on the section — which inherits straight through to the card.
     G-7 already stopped the heading-inversion rule from reaching card interiors,
     but the band's own `color` is inheritance, not a selector, so it arrived
     anyway: 81 paragraphs across 27 article pages rendered white on white. */
  color: var(--c-ink);
}
/* Live runs the photograph hard into the title — measured gap 0. The 15px comes
   from the shared `.media-top__media` margin, which the card rows that DO want a
   gap rely on; reset here rather than there. */
.w-mod-blog-3imgtext .media-top__media { margin-bottom: 0; }
.w-mod-blog-3imgtext .media-top__col .richtext {
  background: #fff;
  padding: 0 0 30px;
  text-align: left;
}
.w-mod-blog-3imgtext .media-top__col .richtext h4 {
  border-bottom: 1px solid rgba(230, 235, 241, .4);
  padding-bottom: 10px;
  padding-left: 15px;
  font-size: 13px;
  margin-top: 0;
  /* A DEPARTURE from live, on the client's instruction. Live sets no top padding
     here and its measured image-to-title gap is 0, so the title sits hard against
     the photograph. 15px to match the inset already on the left, so the title
     block is evenly spaced rather than given an arbitrary new number. */
  padding-top: 15px;
  /* Two lines reserved, so the rule under the title and the copy below it land on
     the same line across the row. These are blog headlines in a 3-up strip and most
     take two lines while some take one, which stepped the row by 14.3px. Figure is
     this variant's own 14.3px line-height twice, plus its 15/10 padding. */
  min-height: calc((14.3px * 2) + 15px + 10px);
}
/* The body copy is inset 15px like the title above it — live's `p` computes
   `padding: 0 15px`. Without it the paragraph sat flush to the card edge while
   the title was indented, so the two did not line up. */
.w-mod-blog-3imgtext .media-top__col .richtext p {
  margin-top: 0;
  padding-inline: 15px;
}

@media (min-width: 768px) {
  .w-mod-blog-3imgtext { padding: 0 0 15px; }
  .w-mod-blog-3imgtext > .container,
  .w-mod-blog-3imgtext > .bleed {
    width: 82%;
    max-width: none;
    margin-inline: auto;
    /* `.bleed` carries an 80px inner inset by default; live's
       `.fullWidthWidgetWrapper` here has padding 0. Measured on live at 1440 the
       wrapper is 1180.8 wide, which across three cards at `margin: 10px 30px`
       gives 333.6 each. Ours was 280.3 — the 82% was right and the 80px a side
       was eating 160 of it. That extra 53px per card is also why the titles
       wrapped to two lines where live keeps them on one. */
    padding-inline: 0;
  }
  /* Live puts the 30px on the CARD, not between cards, so the outer edges are inset
     too: 1180 - (6 x 30) = 1000 across three cards = 333 each. A 60px grid gap alone
     gives 353 and the row runs tall. */
  .w-mod-blog-3imgtext .grid { gap: 0; }
  /* Row layout only — the card's own appearance is declared above, unguarded, so
     it survives below 768 where this geometry does not apply. */
  .w-mod-blog-3imgtext .media-top__col {
    margin: 10px 30px;
    /* Live outlines the card: `border: 1px solid rgb(0,0,0)`, which is why its
       image sits inset by exactly 1px on all three sides. Invisible against the
       black band on the outer edges, but it is what separates the white card from
       the band where the two meet. */
    border: 1px solid #000;
  }
}

/* ==========================================================================
   ANCHOR NAV — live `simp-custom-anchorTag`, the jump links above the FAQ page.

   The authored markup runs the links together with no whitespace between them:

       <a href="#about-property"><span>PROPERTY</span></a><a href="#about-hospitality">…

   so without a rule they render as one unbroken string — PROPERTYHOSPITALITYWELLNESS…
   Live spaces them from the stylesheet, and this is that rule verbatim:

       .simp-custom-anchorTag a            { padding: 0 14px; display: inline-block }
       .simp-custom-anchorTag a:first-child{ padding-left: 0 }
       .simp-custom-anchorTag a:last-child { padding-right: 0 }

   28px between adjacent links, flush at both ends. `line-height` is added here and not
   in live because live hides this nav on a phone (`simp-custom-mobile-hide`) while we
   keep it: at 390 the eight links wrap onto several rows, which live never has to
   handle, and without it the wrapped rows sit too tight.
   ========================================================================== */
.w-mod-anchorTag .richtext a {
  display: inline-block;
  padding: 0 14px;
}
.w-mod-anchorTag .richtext a:first-child { padding-left: 0; }
.w-mod-anchorTag .richtext a:last-child { padding-right: 0; }
@media (max-width: 1023px) {
  .w-mod-anchorTag .richtext { line-height: 2; }
}

/* `simp-custom-faqHeading` — 14 pages.

   DS-09 — the rule that used to live here, `padding-block: 20px`, was lifted from
   live's `.simp-custom-faqHeading { padding: 20px 0 }`. That declaration is DEAD in
   live: at (0,1,0) it loses to the base widget rule

       .homepage    .content_wrapper .no-gutter .widget-wrapper,
       .inner-page  .content_wrapper .no-gutter .widget-wrapper { padding: 30px 0 }   (0,4,0)

   and all 14 live instances measure 30/30, 88px tall. Copied into a stylesheet where
   nothing out-specified it, the dead rule became live and rendered these 14 headings
   at 67.5 — 20px shorter than the six identical sibling headings on the same page.

   Removed. The captured `--pt`/`--pb` of 30/30 now flow through `pad()` exactly as
   they do for every other heading widget. Rendered behaviour is the source of truth,
   not the presence of a declaration in the legacy stylesheet. */

/* ==========================================================================
   `simp-custom-twocard-blogs` — the therapy-category card rows (4 pages, 28 rows).
   Live, all within `.text-content-wrapper`:
       h2                    { margin-top:15px }
       h3                    { margin-top:10px; padding:0 10px 5px; border-bottom:0;
                               text-align:center !important }
       > div                 { text-align:center !important; padding-bottom:0 }
       > div > span          { font-size:18px; font-weight:700 }
       p                     { text-align:center }
       .txt-wdgt-btn-wrapper { margin-top:10px; display:flex; justify-content:center }
   Desktop only — live keeps a separate `.mobile` block for this widget, and the
   blog-3imgtext lesson (I-36) was that applying these unguarded costs more at 390
   than it gains at 1440.
   ========================================================================== */
@media (min-width: 768px) {
  .w-mod-twocard-blogs .richtext h2 { margin-top: 15px; }
  .w-mod-twocard-blogs .richtext h3 {
    margin-top: 10px;
    padding: 0 10px 5px;
    border-bottom: 0;
    text-align: center;
    /* Two lines reserved — therapy names are one or two lines and the row stepped by
       19.8px wherever a pair differed. 19.8 x 2 plus this rule's own 5px below. */
    min-height: calc((19.8px * 2) + 5px);
  }
  .w-mod-twocard-blogs .richtext > div {
    text-align: center;
    padding-bottom: 0;
  }
  .w-mod-twocard-blogs .richtext > div > span {
    font-size: 18px;
    font-weight: 700;
  }
  .w-mod-twocard-blogs .richtext p { text-align: center; }
  .w-mod-twocard-blogs .richtext .txt-wdgt-btn-wrapper {
    margin-top: 10px;
    display: flex;
    justify-content: center;
  }
}

/* `simp-custom-wellnesspack` — the programme INCLUSIONS / CHOICE OF MASSAGE
   columns (16 widgets across 16 programme pages). Each column is a heading
   labelling a bulleted list, and the list has a hard left edge at the bullets, so
   the centred heading `.w-col h3` gives every other column variant left it
   floating above nothing — the misalignment reported on introductory-package.
   Live left-aligns the heading here too, and only here. */
.w-mod-wellnesspack .w-col h2,
.w-mod-wellnesspack .w-col h3 { text-align: left; }

/* Live ships a decorative underline helper used on section headings:
       .border-center:after { content:''; display:block; border:1px solid #6c8f7d;
                              width:100%; text-align:center; margin:20px auto }
   20 + 2 + 20 = the 42px those heading widgets were short. Four pages use it and the
   homepage emits none, so this needs no scoping — the same situation as `.text-align`. */
.border-center::after {
  content: "";
  display: block;
  border: 1px solid #6C8F7D;
  width: 100%;
  margin: 20px auto;
}

/* Live fixes the enquiry widget's height outright:
       .simp-custom-bottom-form { padding-bottom:0 !important; height:160px !important }
   The replica rendered 142, so this row was 18px short on all 17 scored form pages
   (210 pages carry the widget). Desktop only — the mobile form is a different shape. */
@media (min-width: 768px) {
  .section--warm:has(.enquiry-form) { padding-bottom: 0; }
  .enquiry-form { min-height: 130px; }
}

/* ==========================================================================
   PER-WIDGET WIDTH — live sets it by widget id, not by class.

   Three `clearfix widget-wrapper wt1tb` wrappers on rooms-suites/lakeview-king-room
   measure 1224, 1008 and 1080 at 1440 — identical classes, different widths. The
   width comes from custom-plugins.css rules keyed on the wrapper's data-widget-id:

       .content_wrapper .no-gutter .widget-wrapper[data-widget-id="2121152"] { width:70% }
       .content_wrapper .no-gutter .widget-wrapper[data-widget-id="2121154"] { width:75% }

   The replica rendered all of them at the 85% default, so the paragraph under the
   room video wrapped at 1224 against live's 1008 and every line broke in a
   different place. Mined from the captured live stylesheet into
   src/_data/widgetWidth.json; desktop only, because live scopes the mobile
   variants under `.mobile` and none of these ids carry one.
   ========================================================================== */
@media (min-width: 1024px) {
  .for-inner section[data-widget-id="2120268"] > .container { width: 67%; }
  .for-inner section[data-widget-id="2121127"] > .container,
  .for-inner section[data-widget-id="2121146"] > .container,
  .for-inner section[data-widget-id="2121152"] > .container { width: 70%; }
  .for-inner section[data-widget-id="2121134"] > .container,
  .for-inner section[data-widget-id="2121148"] > .container,
  .for-inner section[data-widget-id="2121154"] > .container { width: 75%; }
  .for-inner section[data-widget-id="2117158"] > .container,
  .for-inner section[data-widget-id="2117176"] > .container,
  .for-inner section[data-widget-id="2117281"] > .container,
  .for-inner section[data-widget-id="2117352"] > .container,
  .for-inner section[data-widget-id="2117691"] > .container,
  .for-inner section[data-widget-id="2122480"] > .container,
  .for-inner section[data-widget-id="2122481"] > .container,
  .for-inner section[data-widget-id="2122483"] > .container { width: 76%; }
}

/* ==========================================================================
   MOBILE RHYTHM FOR IMAGE-BEARING ROWS AND SECTION HEADINGS (<1024).

   Image widgets captured 0/0 mobile padding from live's /m/ template — 96% of
   the standalone image rows, 86% of the card rows, 84% of the subheaders — while
   plain text widgets captured real values (15/15 on most). The result on a phone
   is that anything with a picture in it loses its vertical rhythm entirely.

   Two visible consequences on the programme pages, both reported:

   1. Stacked photos alternated 15px / 0 / 15px / 0. Each `.w-images--c2` row
      supplies a 15px grid gap between ITS own two images, which becomes a row gap
      once the grid stacks; between two such widgets there was nothing at all, so
      four photos read as two glued pairs. The gap between rows is now the same
      15px the grid already uses inside one, so the strip is evenly spaced.

   2. A section heading sat flush against the photo above it. It gets 30px of air
      above — the desktop value, and more than the 15px below it, because the
      heading belongs to the content it introduces, not to the images it follows.

   Margins, not padding: the DS-03 mobile rhythm rule owns padding through --ptm
   and out-specifies anything reasonable here. `media-side` is deliberately
   excluded from all of this — its 0/0 is a measured component default (186 of 186
   instances), not a capture artefact.
   ========================================================================== */
@media (max-width: 1023px) {
  .section:has(> .container > .content-col > .w-images)
  + .section:has(> .container > .content-col > .w-images) { margin-top: var(--sp-3); }

  .section--subheader { margin-top: var(--sp-5); }
}

/* `simp-custom-borderbox` — a boxed note, used for the diet footnote on
   detox-program and the terms block on the two special-offer pages.

   Live draws the rule on `.text-content-wrapper`, i.e. INSIDE the column's 15px
   gutter, not on the widget wrapper: measured identically on all three pages at
   both 1440 and 390 —

       border  1px solid rgb(14, 39, 63)   padding 20px   no radius, no fill
       box     1194 wide at x=123 inside a 1224 wrapper at x=108

   Our DOM collapses `.col-*` and `.text-content-wrapper` into `.content-col`, so
   the equivalent node is the `.richtext` inside it — same width, same inset.
   The modifier was already emitted; nothing had ever styled it, so all three
   boxes rendered as bare text. */
.w-mod-borderbox .richtext {
  border: 1px solid rgb(14, 39, 63);
  padding: var(--sp-4);
}

/* The FAQ section title is centred, like every other section heading.

   `.w-mod-faqHeading` is the "FAQs" title that introduces a page's FAQ group, on
   13 pages. It was the ONLY left-aligned section heading on any of them — on
   detox-program its seven siblings (page title, rates, Wellness Journeys Shared,
   GET IN TOUCH, PRESS COVERAGE, CONNECT WITH US, STAY INFORMED) all centre, at
   1440 and at 390 alike. Live centres this one too; the replica simply never
   styled the modifier.

   Centred at every width rather than desktop only, because a desktop-only rule
   would leave it the single left-aligned section heading on the phone — trading
   one inconsistency for another.

   The 14th `faqHeading` widget, ABOUT THE PROPERTY on the FAQ page, is a category
   heading whose six siblings carry no modifier; its stray label is stripped from
   the data (tools/faq-heading-mod.mjs) so this rule cannot reach it and break the
   one-line alignment those headings were given. */
.w-mod-faqHeading .richtext :is(h1, h2, h3) { text-align: center; }

/* ==========================================================================
   THREE-COLUMN CARD ROW — the plain `wt3imgtop-3tbbot` in the page container,
   on discover-your-dosha and rooms-suites. A real card, and the replica drew
   none of it: the images ran edge to edge with the copy loose underneath.

   Measured on live, identical on both pages at 1440 and 1280:
       row        1224 @1440 / 1088 @1280 — the default container
       column     348 wide, 30px inset from the row on both sides, 60px gaps
       card       border 1px solid rgba(50,68,84,.39), fill rgb(245,246,247)
       image      flush to the card edge, flush to the title band below it
       body pad   15px 15px 45px
       title      21px/700 centred, pulled out to the card edges by a -15px
                  side margin, 15px below it, then a 1px rgb(187,189,191) rule

   Scoped to `> .container`, which is the whole discriminator: c3 covers THREE
   different components and the other two are both `fullWidth`, so they render
   `.bleed` and never match. Those two are genuinely different designs on live —
   `blog-3imgtext` (27 article pages) and the row on
   health-guide/weight-management-with-ayurveda both use a white fill, a per-card
   10px/30px margin and a small left-aligned 13px title with no rule. Keying on
   the container rather than naming pages keeps all three apart structurally.

   One deliberate divergence: live justifies the card body. In a 316px column that
   stretches word spacing badly — the same defect that took `.w-col` off `justify`
   — so the body stays left-aligned here too. */
.section--media-top--c3 > .container .grid { gap: 60px; }

@media (min-width: 1024px) {
  .section--media-top--c3 > .container { padding-inline: 30px; }
}

.section--media-top--c3 > .container .media-top__col {
  border: 1px solid rgba(50, 68, 84, .39);
  background: rgb(245, 246, 247);
}
.section--media-top--c3 > .container .media-top__media { margin-bottom: 0; }
.section--media-top--c3 > .container .media-top__col .richtext {
  padding: var(--sp-3) var(--sp-3) 45px;
}
.section--media-top--c3 > .container .media-top__col .richtext :is(h2, h3, h4) {
  margin: 0 calc(-1 * var(--sp-3)) var(--sp-3);
  padding: 0 var(--sp-3) var(--sp-3);
  border-bottom: 1px solid rgb(187, 189, 191);
  font-size: 21px;
  line-height: 1.1;
  font-weight: var(--fw-bold);
  text-align: center;
}

/* `simp-custom-tableFullWidth-243` — the 17-row programme comparison matrix on
   wellness-program-inclusions. Same white-text header as the other five tables
   (see the header band in primitives.css) but live fills it navy rather than
   sage, which reads as a deliberate distinction for the full-width matrix rather
   than a slip: it is a different component from the therapy lists. */
.w-mod-tableFullWidth-243 table tr:first-child :is(td, th):has(span[style*="#ffffff" i]) {
  background: rgb(14, 39, 63);
}

/* THE MATRIX'S COLUMN WIDTHS.

   Every one of the cells is authored `width: 5%`, which totals 85 and leaves the rest
   to `table-layout: auto` — so the browser sized the columns by their CONTENT instead,
   and the fifteen programme columns came out anywhere between 82px and 122px while the
   row-label column took 234.

   `table-layout: fixed` takes the widths from the colgroup that tools/table-colgroup.mjs
   injects, which is what makes the fifteen programme columns identical. The inline
   widths have to be neutralised or they compete with it.

   THE TABLE KEEPS ITS NATURAL WIDTH AND SCROLLS. Seventeen columns do not fit 1250px
   and stay readable: at 70px a column, "Depression and Stress Management" breaks
   mid-word, and an earlier attempt to rescue that by rotating the header labels made
   the whole header unreadable. `.table-responsive` is built for exactly this and is
   what the page already used. So: uniform columns, yes; one screen, no — not legibly,
   and legibility wins.

   Type is 13px here against the site's 14: the body cells run to 140 characters in a
   110px column, and 13/1.4 is what keeps the tallest rows from doubling in height
   without shrinking the text to the point of squinting. */
@media (min-width: 1024px) {
  .w-mod-tableFullWidth-243 table { table-layout: fixed; }
  .w-mod-tableFullWidth-243 table :is(th, td) { width: auto !important; }
  .w-mod-tableFullWidth-243 table :is(th, td) {
    font-size: 13px;
    line-height: 1.4;
    padding: 8px;
    vertical-align: middle;
    overflow-wrap: break-word;
  }
  /* The header row must never break a word: the columns are sized so it does not have
     to (see tools/table-colgroup.mjs), and a broken programme name is the defect this
     whole block exists to fix. Body cells keep `break-word` — their copy runs to 140
     characters and has to wrap somewhere. */
  .w-mod-tableFullWidth-243 table tr:first-child :is(th, td) {
    vertical-align: middle;
    overflow-wrap: normal;
  }
}

/* The card image is a link (see the media-top branch in render.njk). It must fill
   its ratio box exactly as the bare <img> did, or the anchor's inline layout adds
   a descender gap under every linked card image. */
.media-top__link { display: block; height: 100%; }

/* CARD CTA on a media-top row — REMOVED, deliberately.

   A rule here used to pin the Read More buttons of every `.section--media-top` card
   to the card foot, to line them up across a row the way the slider cards do. It was
   wrong twice over:

     * `.w-mod-healthprogram` (16 cards) already pins its CTAs and reserves 60 or 74px
       of foot for them, measured against live. The rule here matched at the same
       specificity and later in the file, so it replaced that reserve with 40px and the
       copy ran into the button.
     * `.w-mod-twocard-blogs` (50 cards) deliberately keeps its button IN FLOW —
       `margin-top: 10px; display: flex` — which is what live does. Pinning it took the
       button out of the flow with no foot reserved at all.

   Measured after that rule: 67 of the 80 buttoned media-top cards had a line of copy
   crossing the button box, by 9 to 20px. The row alignment it bought was not worth
   that, and the components that want pinning already do it for themselves. */

/* ============================================================================
   Standalone image widget (live `wt1img`) — one photograph, on its own, in the
   article measure. See the `image` branch in widgets/render.njk.

   `aspect-ratio` rather than a height: the widget's job is to be as tall as its
   photograph is at the measure it sits in, and the ratio is authored per image.
   Reserving the box also stops a lazy image shifting the article as it arrives,
   which matters most on the pages carrying seven and nine of them.
   ========================================================================== */
.w-image {
  aspect-ratio: var(--img-ratio, 16 / 9);
  overflow: hidden;
}
.w-image img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* ==========================================================================
   `simp-custom-width-95` — the thank-you page's confirmation band.

   Live:
       .simp-custom-width-95 { width: 95% }
   measured at 1440 as width 1368, margin 0 36px, padding 100px 0. One widget on
   one page, but it was rendering full-bleed at 1440 with `.bleed`'s 80px inset —
   the band ran edge to edge where live insets it, and came out 13px short.

   The width goes on the SECTION, not on the `.bleed` inside it, because the
   section is what carries the background image. Narrowing only the inner wrapper
   left the photograph spanning the full 1440 with just the text inset — live
   paints it across 1368 and no further.

   `border-radius: 50px` comes with it. Every other `bgImageContainer` on live is
   square and full-width (checked on annalepam, food-menu and our-story); this one
   widget is the only rounded one, so the radius travels with the modifier that
   makes it narrow. No `overflow: hidden` needed — a background honours the radius
   on its own element.

   Horizontal padding goes because live's is `100px 0`; the block padding still
   comes from the widget's captured `--pt`/`--pb` (100/100) like every section.
   ========================================================================== */
.w-mod-width-95 {
  width: 95%;
  max-width: none;
  margin-inline: auto;
  border-radius: 50px;
}
/* Live's own padding here is `100px 0` — no horizontal inset — and that is right
   while the band is 1368 wide and the text is centred well inside it. Below 1024
   it is not: the band narrows to the viewport and the copy ran into its rounded
   edges. The desktop zero is kept and the standard gutter comes back underneath. */
@media (min-width: 1024px) {
  .w-mod-width-95 > .bleed { padding-inline: 0; }
}

/* …and NO scrim on this one. `.section--bgimage::after` lays `rgba(2,21,36,.6)`
   over every banner, which is correct and is live's own value — home.css records
   a pass that removed it globally and left the band at mean luminance 132.5
   against live's 70.2. But live's ::after on THIS widget is transparent: its
   other bgImageContainers carry the overlay (checked on annalepam and food-menu),
   this one does not. Suppressed here only, so the general rule stands. */
.w-mod-width-95::after { background: none; }
/* The inset and the radius are the DESIGN, so they hold at every width — an
   earlier cut dropped both below 1024 and the band went square and full-bleed,
   which is the one thing the client asked to see rounded. The radius steps down
   with the band: 50px on a 370px-wide band would swallow its corners. */
@media (max-width: 767px) {
  .w-mod-width-95 { border-radius: 24px; }
}

/* ==========================================================================
   LATEST BLOGS rows on thank-you-page — a narrow image column.

   Live's media-side puts the image in a Bootstrap column, and the column class
   is not always the same one. Measured at 1440:

       col-md-6   image 438 in a 468 column   — every other instance on the site
       col-md-2   image 174 in a 204 column   — these three rows
       col-md-4   yo1-events, which carries its own `LimgRtext-hover` modifier

   Ours renders a 1fr 1fr split, so these three came out with a 582px image and a
   642px row against live's 174 and 234 — nearly three times too tall.

   Keyed on `data-widget-id` because NOTHING in the data distinguishes them. Their
   `rawClass` is byte-identical to the article template's 50/50 rows
   (`imageDescriptionSide imageDescriptionSide-leftimage two-col`), `mods` is empty
   on both, and the extractor's `rendered` field is a 15-character placeholder, so
   the column class was never captured. A square source image looked like the
   signal — these three are 965/965, 3333/3333, 3837/3837 — but wellness-programs
   carries a square one at col-md-6, so that is a coincidence, not a rule.

   This is the mechanism the codebase already uses for exactly this situation:
   live keys a handful of per-widget rules off `data-widget-id` in its own
   stylesheet, and mirroring the attribute reproduces them with live's own selector
   rather than inventing a class (see home.css). If these rows are ever re-captured
   with the column class intact, this becomes a data-driven variant instead.

   204/1224 is col-md-2's 2 of 12 columns; the image is that column less
   Bootstrap's 30px gutter, which our `.content-col` already supplies.
   ========================================================================== */
:is([data-widget-id="2145579"], [data-widget-id="2145580"], [data-widget-id="2145581"]) > .media-side {
  grid-template-columns: 204px 1fr;
}
@media (max-width: 1023px) {
  :is([data-widget-id="2145579"], [data-widget-id="2145580"], [data-widget-id="2145581"]) > .media-side {
    grid-template-columns: 1fr;
  }
  /* …and they need separating once stacked. `.section--media-side` is deliberately
     flush at mobile — a measured decision, since all 186 instances that carry
     mobile values are 0/0 (see home.css) — and for a normal image-beside-text row
     that reads fine. These three are a LIST: stacked flush, one row's READ MORE
     butts straight into the next row's photograph. Given their own padding rather
     than changing the shared default, so the other 195 instances keep the measured
     behaviour. 30px is the desktop value these same widgets carry. */
  :is([data-widget-id="2145579"], [data-widget-id="2145580"], [data-widget-id="2145581"]) {
    padding-block: 30px;
  }
}

/* ==========================================================================
   PRESS COVERAGE band — one band, two sections, balanced.

   The heading and the logo row are separate widgets that happen to share a
   background, so the band's spacing was the sum of four unrelated values:

       top     15   the heading section's padding-top
       middle  69   the h2's 10px margin, plus both sections' inner padding
       bottom  44   the logo section's padding-bottom

   Top a third of the bottom, and a middle wider than either. Now 30 / 30 / 30:
   the heading's margin is closed so the gap is owned by the two paddings alone,
   and each of the three is one value you can read off this block.

   A DEPARTURE from live, on the client's instruction: live measures 15 / 74 / 49,
   so its middle is even wider than ours was. Desktop only, because below 1024 the
   painted-band floor already brings both sections to 20 and the band reads even
   there; it also keeps this clear of the DS-03 rhythm rule rather than fighting it
   on specificity.
   ========================================================================== */
@media (min-width: 1024px) {
  /* Second pass, same instruction taken further: the client wants the gap between
     the heading and the logos tighter still, with what comes off the middle given
     back to the top and bottom of the band. 10px moves outward at each end —
     heading 30/15 -> 40/5 and logos 15/30 -> 5/40 — so the band's overall height
     is unchanged while the middle closes from 59 to about 39. */
  .section--band:has(+ .section--band.section--tight) {
    padding-top: 40px;
    padding-bottom: 5px;
  }
  .section--band:has(+ .section--band.section--tight) .richtext :is(h2, h3) {
    margin-bottom: 0;
  }
  .section--band.section--tight { padding-top: 5px; padding-bottom: 40px; }
}

/* ==========================================================================
   yo1-events — live's `simp-custom-LimgRtext-hover`, the 40 event rows on that
   one page (no other page carries the mod).

   These rows are NOT the ordinary image-beside-text widget they were rendered
   as. Live keeps the image out of the layout entirely and pops it in over the
   right-hand edge only while the row is hovered, so the page reads as a list of
   dated entries with the artwork as a peek. Taken verbatim from live's own rule:

     .widget-wrapper.simp-custom-LimgRtext-hover {
       border-top: 1px solid rgb(14,39,63,0.13); padding: 2.5% 0 0;
       position: relative; margin-top: 2.5%; display: flex; align-items: center }
     …            .right-box { display: none; position: absolute; right: 0; padding: 0 }
     …:hover      .right-box { display: block }
     …            .left-box  { padding: 0 7% 0 0 }

   It applies at every width — measured at 390 as well as 1440, all 40 image
   columns are `display: none` on live — so there is no mobile exception here.
   `:focus-within` is added to live's `:hover`: the row's copy holds real links,
   and a keyboard reader should be able to surface the same image a pointer can.
   ========================================================================== */
.w-mod-LimgRtext-hover .media-side {
  position: relative;
  display: flex;
  align-items: center;
  border-top: 1px solid rgba(14, 39, 63, .13);
  margin-top: 2.5%;
  padding-top: 2.5%;
}
.w-mod-LimgRtext-hover .media-side__media {
  display: none;
  position: absolute;
  right: 0;
  /* Live's right-box is `col-md-4 col-sm-6` — a third of the 1224 row, so 408px,
     against the left-box's `col-md-8`. Without the width the absolute box took
     the row's own measure and the revealed image came out 720 wide. */
  width: 33.3333%;
  padding: 0;
}
.w-mod-LimgRtext-hover .media-side:hover .media-side__media,
.w-mod-LimgRtext-hover .media-side:focus-within .media-side__media { display: block; }
/* `col-md-8` — the copy keeps two thirds of the row even though the image is out
   of flow, so the measure does not run the full 1224 the way it did when the
   media column was simply hidden. */
.w-mod-LimgRtext-hover .media-side__body {
  width: 66.6667%;
  padding: 0 7% 0 0;
}

/* The event copy is a two-column table — date, then title and blurb. Live gives
   it `table-layout: auto` with no width attributes, and it settles at 219 / 511
   of a 730 measure: exactly 30/70. Auto layout here follows the authored content,
   which in the replica sized the date column to its text (74px) and let the blurb
   take the rest, so the dates and titles did not line up down the page. The
   measured proportion is set explicitly instead. Cells carry no padding and no
   border on live, against the 8px the bare `table table-bordered` default gives
   them. */
.w-mod-LimgRtext-hover .table { margin: 0; border: 0; }
/* Carries the same `:not()` guard as the base rule in primitives.css — that one
   is (0,4,0) and a plainer selector here lost to it, leaving the measured 8px
   padding in place and every heading inset 8px from live's. */
.w-mod-LimgRtext-hover .table.table-bordered:not(.table-striped):not(.table-condensed) :is(td, th) {
  padding: 0;
  border: 0;
  vertical-align: top;
}
.w-mod-LimgRtext-hover .table td:first-child { width: 30%; }

/* Live sets `padding: 0 0 17px` on these h3s — that bottom padding IS the space
   between an event title and its blurb, and without it the two ran together.
   Not a sitewide value: live's h3 padding is 17px on this page and our-story,
   15px on mind-body-soul and 0 on introductory-package, so it is per-widget
   custom CSS and is scoped here the same way. */
.w-mod-LimgRtext-hover h3 { padding-bottom: 17px; }

/* --------------------------------------------------------------------------
   PHONES — live serves this widget from a different template below 768, with
   none of the hover machinery: no right-box at all (so the image is simply
   absent, not peeked), the copy takes the full width, and each event reads as a
   bordered card with the date stacked above the title. Measured on live at 390:

     wrapper  block, full width
     table    350 wide inside a 20px inset, border 1px #DDD  (kept, not stripped)
     td       display:flex  <- this is what stacks the two cells
     td:1     padding 30px 8px 0 0      td:2  padding 8px 8px 8px 0
     h3       padding-bottom 5px, not the desktop 17px

   Ours had been carrying the desktop geometry down: the copy stayed in its
   66.67% column with 7% of right padding, so at 390 the blurb wrapped inside
   about 220px and the date column crushed against the title.
   ------------------------------------------------------------------------ */
@media (max-width: 767px) {
  .w-mod-LimgRtext-hover .media-side {
    display: block;
    border-top: 0;
    margin-top: 0;
    padding-top: 0;
  }
  .w-mod-LimgRtext-hover .media-side__body {
    width: 100%;
    padding: 0;
  }
  /* the card outline is live's plain `.table-bordered` border, which the desktop
     rule above strips because the desktop rows are separated by a top rule */
  .w-mod-LimgRtext-hover .table { border: 1px solid #DDD; }
  /* column, or the h3 and the anonymous text run beside each other as two flex
     items and the title collapses to about a third of the cell */
  .w-mod-LimgRtext-hover .table.table-bordered:not(.table-striped):not(.table-condensed) :is(td, th) {
    display: flex;
    flex-direction: column;
    /* Live sets `8px 8px 8px 0` here, so its own copy starts hard against the
       card's left outline. The vertical values are live's; only the horizontal
       ones are ours, giving the text a 15px gutter inside the border on both
       sides. The card itself stays on the site's 20px phone gutter. */
    padding: 8px 15px;
  }
  /* carries the same :not() guard as the rule above it — a plainer selector lost
     to it and the date cell kept the generic 8px top */
  .w-mod-LimgRtext-hover .table.table-bordered:not(.table-striped):not(.table-condensed) td:first-child {
    width: auto;
    padding: 30px 15px 0;
  }
  .w-mod-LimgRtext-hover h3 { padding-bottom: 5px; }
}

/* ==========================================================================
   WEDDING PAGE — create-your-perfect-wedding-day-at-yo1.

   This page is skinned by live's own page-level CSS, keyed on five
   `simp-custom-*` classes the capture preserved as widget mods. None of it was
   implemented, so every widget fell back to the generic treatment and the page
   read as "completely different" on the QC sheet: section titles at the default
   25px/left instead of 32px/centred, and the two feature rows as ordinary
   image-beside-text instead of live's overlapping white card with a gold frame
   behind the photograph.

   Values measured off live at 1440 and 390. Live steps its type at 1440/1200 as
   well; reproduced here as one desktop step and one below-1024 step, which is
   where our own breakpoint already sits.
   ========================================================================== */

/* ---- the full-width text sections (intro, GALLERY, YOUR JOURNEY, and the
   PERFECT SETTING heading) ------------------------------------------------
   Live: section padding 2.4% 12.65% 2.2% — a 1076 measure at 1440 — with the
   heading 32px/60px 600 CENTRED and the copy 18px/36px centred. */
/* Live's block padding on these text sections is 2.4% / 2.2% of the viewport —
   34.5 top and 31.7 bottom at 1440. Only the side inset was set here, so each of
   the three (THE PERFECT SETTING, GALLERY, YOUR JOURNEY STARTS HERE) came out
   ~34px shorter than live's 270 / 162 / 270. */
.w-mod-wedding-page-text,
.w-mod-limg-rtext-heading {
  padding-inline: 12.65%;
  padding-top: 2.4%;
  padding-bottom: 2.2%;
}
/* The section padding IS the measure here, so the inner wrappers must not add
   their own: `.bleed` carries 0 80px and `.content-col` another 0 15px, which
   together pulled the 1076 measure in to 886. */
.w-mod-wedding-page-text > .bleed,
.w-mod-limg-rtext-heading > .bleed,
.w-mod-wedding-page-text .content-col,
.w-mod-limg-rtext-heading .content-col {
  max-width: none;
  width: 100%;
  padding-inline: 0;
}
.w-mod-wedding-page-text .richtext :is(h1, h2),
.w-mod-limg-rtext-heading .richtext :is(h1, h2) {
  font-size: 32px;
  line-height: 60px;
  letter-spacing: 0;
  text-align: center;
}
.w-mod-wedding-page-text .richtext p,
.w-mod-limg-rtext-heading .richtext p {
  font-size: 18px;
  line-height: 36px;
  letter-spacing: 0;
  text-align: center;
  margin-top: 0;
}
/* Both wedding text mods carry the same space under the heading — 3.3% of the
   1076 measure, ~36px at 1440. It was applied to limg-rtext-heading only, which
   left GALLERY at 126 against live's 162 and YOUR JOURNEY at 234 against 270:
   the same 36px missing from each. */
.w-mod-limg-rtext-heading .richtext :is(h1, h2),
.w-mod-wedding-page-text .richtext :is(h1, h2) { padding-bottom: 3.3%; }

/* ---- the two feature rows ----------------------------------------------
   Live at 1440: row padding 30px 8.23% 6%; photograph 44.4% of the measure and
   a fixed 474px tall, cover-fitted, with a 4px #f2dfaf frame inset 15/12 behind
   it; copy column 54.6%, and its wrapper lifted OUT of flow as a white card
   97.5% wide, overlapping the photograph by 29px, padded 70/62, on a soft gold
   shadow. Heading 26px/50px, copy 18px/36px. */
.w-mod-wedding-page-limg-rtext,
.w-mod-wedding-page-rimg-ltext {
  padding-inline: 8.23%;
  padding-top: 30px;
  padding-bottom: 6%;
}
.w-mod-wedding-page-limg-rtext .media-side,
.w-mod-wedding-page-rimg-ltext .media-side {
  position: relative;
  align-items: center;
}
/* `.media-side` is a GRID, and its tracks already measure live's 44.4 / 54.6
   split (656.8px + 534.1px at 1440). Setting a width on the items themselves
   sized them INSIDE their own track — 237 in a 534 column — so they only need
   `position: relative` here, for the frame below. */
.w-mod-wedding-page-limg-rtext .media-side__media,
.w-mod-wedding-page-rimg-ltext .media-side__media {
  position: relative;
  /* The grid track is already live's 534.125, but the shared media column carries
     a 15px gutter each side and live's `left-box` carries none — so the
     photograph came out 504 against live's 534. The image fills its track here. */
  padding-inline: 0;
}
.w-mod-wedding-page-limg-rtext .media-side__media img,
.w-mod-wedding-page-rimg-ltext .media-side__media img {
  height: 474px;
  width: 100%;
  object-fit: cover;
  display: block;
}
/* The gold frame sits BEHIND the photograph's edge, not around the column. */
.w-mod-wedding-page-limg-rtext .media-side__media::after,
.w-mod-wedding-page-rimg-ltext .media-side__media::after {
  content: "";
  position: absolute;
  top: 15px;
  left: 12px;
  width: calc(100% - 24px);
  height: calc(100% - 30px);
  border: 4px solid #F2DFAF;
  pointer-events: none;
}
.w-mod-wedding-page-limg-rtext .media-side__body,
.w-mod-wedding-page-rimg-ltext .media-side__body { position: relative; }
.w-mod-wedding-page-limg-rtext .media-side__body .richtext,
.w-mod-wedding-page-rimg-ltext .media-side__body .richtext {
  position: absolute;
  top: 50%;
  translate: 0 -50%;
  width: 97.5%;
  padding: 70px 62px;
  background: var(--c-surface);
  box-shadow: 4px 8px 12px rgba(219, 199, 154, .26);
}
.w-mod-wedding-page-limg-rtext .media-side__body .richtext { left: -29px; }
.w-mod-wedding-page-rimg-ltext .media-side__body .richtext { right: -29px; }
.w-mod-wedding-page-limg-rtext .richtext h2,
.w-mod-wedding-page-rimg-ltext .richtext h2 {
  font-size: 26px;
  line-height: 50px;
  letter-spacing: 0;
  text-align: left;
}
.w-mod-wedding-page-limg-rtext .richtext p,
.w-mod-wedding-page-rimg-ltext .richtext p {
  font-size: 18px;
  line-height: 36px;
  letter-spacing: 0;
  text-align: left;
}

/* Below 1024 live keeps the two columns SIDE BY SIDE at 50/50 — it does not
   stack them — and drops the card back into the flow, so nothing overlaps.
   Measured at 390: padding 30px 8.23% 6%, columns 163 each, heading 19px/24px,
   copy 14px/26px, and the text sections' copy at 14px/22px. */
@media (max-width: 1023px) {
  /* STACKED on phones, not side by side. Live keeps its two columns at 390 and
     fits the copy into 381px; ours cannot — at 163px wide the headings break
     mid-word ("OPPORTUNITI / ES") and the copy becomes a ragged ladder. The QC
     sheet asks for the same thing in row 74: "in the mobile the video and text
     are coming side by side, it should be one below one". Deliberate departure
     from live, on that instruction. */
  .w-mod-wedding-page-limg-rtext .media-side,
  .w-mod-wedding-page-rimg-ltext .media-side { grid-template-columns: 1fr; }
  /* The photograph fills its column rather than collapsing to its own ratio:
     at 390 it was rendering 163x92 beside a 455px text card, so the row read as
     a sliver of image next to a tall block of copy. Live keeps both columns the
     same height (163x381 each). The column stretches and the image covers it. */
  .w-mod-wedding-page-limg-rtext .media-side,
  .w-mod-wedding-page-rimg-ltext .media-side { align-items: stretch; }
  .w-mod-wedding-page-limg-rtext .media-side__media,
  .w-mod-wedding-page-rimg-ltext .media-side__media { height: 100%; }
  .w-mod-wedding-page-limg-rtext .media-side__media img,
  .w-mod-wedding-page-rimg-ltext .media-side__media img {
    /* Stacked, the photograph takes the full width at live's desktop ratio
       rather than stretching to whatever the copy needs. */
    height: auto;
    aspect-ratio: 534 / 474;
    object-fit: cover;
  }
  .w-mod-wedding-page-limg-rtext .media-side__body .richtext,
  .w-mod-wedding-page-rimg-ltext .media-side__body .richtext {
    position: static;
    translate: none;
    width: 100%;
    padding: 4px 15px 13px 17px;
    box-shadow: none;
  }
  .w-mod-wedding-page-limg-rtext .richtext h2,
  .w-mod-wedding-page-rimg-ltext .richtext h2 { font-size: 19px; line-height: 24px; }
  .w-mod-wedding-page-limg-rtext .richtext p,
  .w-mod-wedding-page-rimg-ltext .richtext p { font-size: 14px; line-height: 26px; }
  .w-mod-wedding-page-text,
  .w-mod-limg-rtext-heading { padding-inline: 8.65%; }
  .w-mod-wedding-page-text .richtext p,
  .w-mod-limg-rtext-heading .richtext p { font-size: 14px; line-height: 22px; }
  .w-mod-wedding-page-text .richtext :is(h1, h2),
  .w-mod-limg-rtext-heading .richtext :is(h1, h2) { font-size: 24px; line-height: 34px; }
}

/* ==========================================================================
   SITEMAP — live centres this page's h1; ours was the only page of five checked
   that rendered it left (press, food-menu, our-story and health-guide all match
   already, most of them via an authored inline style). This page's heading has
   no inline style, so live's centring comes from CSS. `.headH1` occurs exactly
   once in the whole build — on this page — so it is a safe hook.
   ========================================================================== */
.richtext h1:has(> .headH1),
.richtext h1 > .headH1 { text-align: center; }

/* ==========================================================================
   FOOD MENU — live runs the menu table the full width of the viewport (1440 of
   1440 measured); ours stopped at 1250 because `.bleed` contributes 80px each
   side and `.content-col` another 15. The mod is on that one widget on that one
   page, so the inset is removed only there.

   Mobile is deliberately NOT made full-bleed: live's own menu overflows its
   viewport at 390 (table 518 in a 390 page, 128px of horizontal scroll), and
   reproducing that would be copying a bug the client has also reported.
   ========================================================================== */
@media (min-width: 1024px) {
  .w-mod-spaceremove > .bleed,
  .w-mod-spaceremove .content-col { padding-inline: 0; }
}

/* ==========================================================================
   BANNER HEADING ALIGNMENT — the page-title band (`section--bgimage`).

   Live centres this heading on every page checked (day-pass, ayurveda, the
   wedding page and therapies all measure `text-align: center`, x202, w1037 at
   1440). Most of ours already centre, but only because the authored content
   carries an inline `text-align: center`; where it does not — day-pass, the QC
   sheet's "header is not center align" — the heading fell left. The alignment is
   the component's, not the content's, so it is set here for all 211 banners.

   NOT changed: live's banner heading also sits in a 1037 measure against our
   1250, and paints at 37.8px against our 28px. Those are a size/measure change
   across every page of the site and are listed for the client rather than done
   unasked.
   ========================================================================== */
.section--bgimage .richtext :is(h1, h2) { text-align: center; }

/* ==========================================================================
   MEDIA-SIDE COLUMN SPLIT — live runs this widget at 55/45 on the therapy and
   experience inner pages and at 50/50 on our-story, and nothing in the captured
   data distinguishes the two (both carry `mods: []`). The split is measured off
   live per widget by tools/probes/measure-media-side.mjs and stamped onto the
   widget by tools/media-side-split.mjs — see those files for why it cannot be
   derived. Rows live records at 50/50 keep the default and are not touched.

   Measured on live at 1440 for the wide variant: row 1224, columns 673/551, the
   media column padded `0 40px 0 15px` so the image lands at 618 — the same 618
   as the video above it, which is the QC sheet's "the image width is not
   matching with the above image or video". Ours was 612/612 with the image at
   582.

   Gated to the two-column band; below 1024 the row stacks and the split is moot.
   ========================================================================== */
@media (min-width: 1024px) {
  .media-side--wide { grid-template-columns: 55% 45%; }
  .media-side--wide.media-side--right { grid-template-columns: 45% 55%; }
  .media-side--wide .media-side__media { padding-right: 40px; }
  .media-side--wide.media-side--right .media-side__media {
    padding-right: var(--gutter);
    padding-left: 40px;
  }
}


/* ---- Offer cards: space between the photograph and its title -------------
   Live pads these headings off the image:

     .widget-wrapper.simp-custom-newoffer h3 { padding-top: 25px }

   Ours had 0, so the title sat flush against the photo. Keyed off the same
   widget modifier live uses, which is why it reaches exactly the same headings
   and no others — measured on live: 4 of the 8 h3 on special-offers (the four
   inside newoffer wrappers) and all 7 on blogs/health-guide, with every other
   heading on both pages at 0. Checked six other card pages: live and ours both
   report 0 there, so nothing else needs it. */
.w-mod-newoffer .richtext h3 { padding-top: 25px; }

/* ===== widgets/gallery.css ===== */
/* ============================================================================
   Gallery category filter (live class `header-pills`, /gallery.html only)

   Every number below is read off reference/pages/gallery/desktop.png at 1440 and
   cross-checked against probe.desktop/laptop/xl.json. The band paints from the
   widget's own data (bg rgb(245,245,245), --pt:150px, --pb:25px, height 260) —
   this file only lays out what sits inside it.

   Measured, and what each value is pinned by:

     item font        Poppins 16px/400, letter-spacing .2px
                      Pinned by laying all 14 labels out in the page and comparing
                      rendered ink to live's, label by label. A first pass measured
                      them on a canvas instead and read 16.5px — canvas fillText
                      ignores CSS letter-spacing, so it was 0.2px per character
                      short and paid for it with a bigger font.
     item colour      #294C4B on every pill, active and not. Sampled per label —
                      the active one is NOT a darker colour, it only gains a rule.
     side padding     20px. The active rule runs x=104..222 while its ink runs
                      125..202, so the box is the ink plus ~20px a side.
     column gap       10px. Not 12: ink-to-ink is 52px and the padding accounts
                      for 40 of it, but the remaining 12 includes each label's side
                      bearings. Row one is the arbiter — live's rule starts at
                      x=104 in a 1340 container, so the nine boxes span exactly
                      1232px, and only a 10px gap puts our advance widths there.
     line box         line-height 22px + 5px padding-bottom + 1px rule = 28px.
     bar padding      10px top, 5px bottom.
     row gap          14px.

   Those last three are one solved system, not three guesses. With ink rows at
   y=165 and y=207, the rule at y=187, and the widget 260 tall on 150/25 padding,
   the four equations have exactly this solution — and it predicts the first box
   starts at x=104, which is where live's rule starts.

   Wrapping is NOT authored. Nine pills on row one and five on row two is what
   falls out of 1232px of pills in a 1340px container; at 1280 live's own
   container drops to 1229 and the ninth pill wraps. See .container--pills.
   ========================================================================== */

/* The bar owns its own header clearance, and is the only widget on the site that
   does. `.outer-page` reserves 145px of flow for the fixed header and promo strip
   because live reserves the same 145 on whatever comes first — on 206 of the 207
   pages measured, live's first widget sits at y=145 and adds its own 100px on top,
   which is exactly what our reservation reproduces.

   The gallery is the exception: live's `header-pills` starts at y=0 and its 150px
   top padding IS that reservation, leaving 5px of real space under the strip. Kept
   in flow the two stack and the page runs 145px long, so the section is pulled back
   under the chrome and its declared padding does the work, as live's does.

   Only >=1024. Below that the header is sticky and in flow, `.outer-page` reserves
   nothing, and there is nothing to pull back from. */
@media (min-width: 1024px) {
  .outer-page > .gallery-filters:first-child {
    margin-top: calc(-1 * (var(--header-h) + var(--promo-h)));
  }
}

/* Live leaves 30px between this widget and the photo grid: `header-pills` ends at
   y=260 and `img-description-padding` starts at 290, with the grid's own 30px top
   padding on top of that. The gap is measured, but not attributed — live reports
   margin-bottom 0 here and the only other page carrying the grid widget captured
   its widgets out of order, so there is nothing to check it against. It is put on
   the bar rather than on the grid because the bar is a one-page widget and the
   grid is not: if the 30 really belongs to the grid, this is the safer place to
   have been wrong. */
.gallery-filters { margin-bottom: 30px; }
/* The captured `--pt:150px` exists to clear the header that overlaps this widget
   when it is the page's first element. With the title banner above it that job
   is done, and 150px reads as a hole between the two — so a normal gap applies
   whenever something precedes it. */
.gallery-filters:not(:first-child) { padding-top: 40px; }
@media (max-width: 767px) { .gallery-filters:not(:first-child) { padding-top: 24px; } }

/* The category label sat 80px below the filter bar and 30px above its own
   photographs — 30 of the bar's margin, 30 of the grid section's padding and 20
   of the label's own margin, stacked. Evened up so the heading reads as
   belonging to the grid beneath it rather than floating between the two. */
.section:has(.gallery-header) { padding-top: 0; }
.richtext > .gallery-header { margin-top: 0; }

.gallery-filters__bar {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  column-gap: 10px;
  row-gap: 14px;
  padding-block: 10px 5px;
}

.gallery-filters__pill {
  /* A real <button>: it changes what the page shows, it does not navigate.
     Live ships an <a href="">, which is a link to nowhere — see LSI notes. */
  appearance: none;
  margin: 0;
  padding: 0 20px 5px;
  border: 0;
  border-bottom: 1px solid transparent;
  background: none;
  font-family: var(--ff-body);
  font-size: var(--fs-md);
  font-weight: var(--fw-regular);
  line-height: 22px;
  letter-spacing: .2px;
  color: var(--c-brand);
  cursor: pointer;
  transition: border-color .2s ease;
}

.gallery-filters__pill.is-active { border-bottom-color: var(--c-brand); }

/* Hover reads as "this is what you would be selecting", so it borrows the active
   rule at half strength rather than inventing a second colour. */
@media (hover: hover) {
  .gallery-filters__pill:hover { border-bottom-color: var(--c-brand-a70); }
}

.gallery-filters__pill:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 2px;
}

/* The dropdown toggle and the carousel chrome exist only below 1024. They are
   built unconditionally by gallery.js, so the desktop hides them here rather than
   the script measuring the viewport. */
.gallery-filters__toggle,
.gallery-cycle,
.gallery-caption { display: none; }


/* ---- Narrow screens: live's /m/ gallery ----------------------------------
   Live serves a separate mobile template at /m/gallery.html and presents this
   page differently there: the fourteen pills collapse into a dropdown, and the
   grid becomes ONE 16:9 photograph at a time with its title and description
   underneath. That template is 1,496px tall against the desktop one's 9,737.

   Values below are /m/'s own, from its stylesheet
   (…/A52545AA6BA83D89/m/css/simp.min.css):

     .gallery-drop              { width:100%; position:relative; z-index:999 }
     .gallery-drop .btn-group   { border:1px solid #999 }        hover #4b5761
     #dropdown-image-menu       { padding:5px 0 5px 10px }
     .fa-custom-gallery         { font-size:18px }
     .drop-menu-header-block    { color:#4b5761; font-weight:400;
                                  padding:10px 10px 10px 25px; background:#fff;
                                  border-top:1px solid #e6e6e6 }
       a:hover …                { background:#f2f2f2 }
       … .text-bold             { background:#e6e6e6 }   (the selected one)
     .ratio-16-9                { padding-bottom:56.25% }
     .cycle-prev / .cycle-next  { position:absolute; width:15%; opacity:.6;
                                  color:#fff; font-size:80px;
                                  text-shadow:0 1px 2px rgba(0,0,0,.6) }
       ::before                 { '\2039' and '\203a' }
     .cycle-content             { margin-top:20px; padding:0 10px 20px }

   NOTE: this is a deliberate exception to the project rule that /m/ is never a
   target (HANDOFF.md §3) — made on the client's instruction for this page. The
   scroll-snap strip it builds on was already chosen to match /m/'s presentation
   and height; what follows completes it. */
@media (max-width: 1023px) {
  /* The pill list becomes the dropdown's menu: closed by default, laid out as
     rows, and floated over the page so it does not push the photograph down.
     Anchored to the field — the control and menu together — not to the container,
     which carries the 20px mobile gutter and would make the menu overhang the
     control by that much on each side. */
  .gallery-filters__field { position: relative; }

  .gallery-filters__toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    width: 100%;
    padding: 10px 12px 10px 10px;
    border: 1px solid #999;
    background: var(--c-surface);
    font-family: var(--ff-body);
    font-size: var(--fs-md);
    font-weight: var(--fw-regular);
    letter-spacing: var(--ls-default);
    line-height: normal;
    color: var(--c-ink);
    text-align: left;
    cursor: pointer;
  }
  .gallery-filters__toggle:hover { border-color: var(--c-ink); }
  .gallery-filters__toggle:focus-visible {
    outline: 2px solid var(--c-focus);
    outline-offset: 2px;
  }

  /* The caret is drawn for the same reason the lightbox chevron is: it is two
     strokes, and it then cannot disagree with the icon font. */
  .gallery-filters__caret {
    width: 11px;
    height: 11px;
    border: solid var(--c-ink);
    border-width: 0 0 2px 2px;
    transform: rotate(-45deg);
    margin-top: -5px;
    flex: none;
    transition: transform .2s ease;
  }
  .gallery-filters__toggle[aria-expanded="true"] .gallery-filters__caret {
    transform: rotate(135deg);
    margin-top: 4px;
  }

  .gallery-filters__bar {
    display: none;
    position: absolute;
    left: 0;
    right: 0;
    z-index: 999;
    padding: 0;
    border: 1px solid #999;
    border-top: 0;
    background: var(--c-surface);
    max-height: 60vh;
    overflow-y: auto;
  }
  .gallery-filters__bar.is-open { display: block; }

  .gallery-filters__pill {
    display: block;
    width: 100%;
    padding: 10px 10px 10px 25px;
    border: 0;
    border-top: 1px solid #E6E6E6;
    font-size: var(--fs-md);
    line-height: normal;
    color: var(--c-ink);
    text-align: left;
    transition: background-color .2s ease-in-out;
  }
  .gallery-filters__pill:first-child { border-top: 0; }
  .gallery-filters__pill:hover { background: #F2F2F2; }
  .gallery-filters__pill.is-active { background: #E6E6E6; }

  /* The section is a plain band once the pills are a dropdown: its 150px is
     desktop header clearance, and below 1024 the header is in flow and clears
     itself, so keeping it leaves a 150px hole above the dropdown.

     30px above, matching the 30px below. That gap below is not this section's —
     it is the photo widget's own `--pt:30px` — so the control ends up evenly
     spaced between the promo strip and the first photograph.

     This is a deliberate departure from /m/, at the client's request. /m/ sits
     the control flush to the strip: measured on the capture, the navy band ends
     at y=183 and the dropdown's top border is the very next row, y=184. Flush is
     what was built first and it read as cramped.

     Set through `--ptm`/`--pbm` rather than by declaring padding. The mobile
     rhythm rule is `.section:not(…):not(…)…{ padding-top: var(--ptm, var(--pt,
     …)) }` — six :not()s of specificity, which a plain `.section.gallery-filters`
     cannot outrank — and those two custom properties are the documented way in.
     Overriding the padding directly would have meant !important or a selector
     written to win an argument; this is the mechanism doing its job. */
  .gallery-filters { background: none; margin-bottom: 0; --ptm: 30px; --pbm: 0px; }

  /* /m/ shows no "All Photos" heading — the photograph's own title, under the
     image, is the only heading on that template. Hidden rather than dropped so
     the filter label still reaches a screen reader. (It used to be this page's
     only h1, which is why removing it was not an option; the page now has a
     real h1 in its banner and this is an h2.) Same declarations as the
     .visually-hidden utility in primitives.css. */
  .gallery-header {
    position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0;
    overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
  }

  /* ---- the carousel ---- */
  .gallery-stage { position: relative; }

  /* The strip is one long horizontal scroller (45,500px of photographs in a
     350px window), and it was using the platform's own scrollbar — a bar sitting
     under the photograph that reads as slider chrome. Live's phone gallery is a
     slick carousel with no bar at all: the chevrons either side are the
     affordance. Hidden, not disabled — swiping and the prev/next buttons still
     scroll it, and the scrollbar is only chrome. */
  .w-images--grid {
    scrollbar-width: none;                 /* Firefox */
    -ms-overflow-style: none;              /* legacy Edge */
  }
  .w-images--grid::-webkit-scrollbar { display: none; }

  .gallery-cycle {
    display: block;
    position: absolute;
    top: 0;
    bottom: 0;
    width: 15%;
    z-index: 800;
    padding: 0;
    border: 0;
    background: none;
    opacity: .6;
    cursor: pointer;
  }
  .gallery-cycle:hover { opacity: .9; }
  .gallery-cycle--prev { left: 0; }
  .gallery-cycle--next { right: 0; }
  .gallery-cycle:focus-visible { outline: 2px solid #FFF; outline-offset: -4px; }

  /* /m/ draws these as ‹ and › at 80px in a white face. Drawn instead, for the
     same reason as the lightbox chevron — the glyphs differ per platform font and
     these have to sit on a photograph. */
  .gallery-cycle span {
    position: absolute;
    top: 50%;
    width: 18px;
    height: 18px;
    border: solid #FFF;
    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, .6));
  }
  .gallery-cycle--prev span {
    left: 8px;
    margin-top: -13px;
    border-width: 0 0 3px 3px;
    transform: rotate(45deg);
  }
  .gallery-cycle--next span {
    right: 8px;
    margin-top: -13px;
    border-width: 3px 3px 0 0;
    transform: rotate(45deg);
  }

  .gallery-caption {
    display: block;
    margin-top: 20px;
    padding: 0 10px 20px;
  }
  .gallery-caption__title {
    margin: 0;
    font-size: var(--fs-xl);
    font-weight: var(--fw-light);
    line-height: 1.3;
    letter-spacing: var(--ls-default);
    color: var(--c-ink);
  }
  .gallery-caption__desc {
    margin: 10px 0 0;
    font-size: var(--fs-sm);
    font-weight: var(--fw-light);
    line-height: var(--lh-body);
    letter-spacing: var(--ls-default);
    color: var(--c-ink-muted);
  }

  /* The hover wash and its chain glyph are a pointer affordance; on a touch
     device they would only ever show as a flash after a tap. */
  .shot__icon { display: none; }
}

/* The grid hides filtered-out photographs with the `hidden` attribute, and its
   items carry a `display` of their own which would beat [hidden]'s UA rule — so
   the grid states it explicitly rather than relying on the cascade. */
.w-images > [hidden] { display: none !important; }

/* Live leaves 60px under the last row of photographs, not 30: the section's own
   30px bottom padding PLUS 30px the grid carries below itself. Measured on the
   Fire Yoga category, where the whole widget is 328 tall on live against 298
   here, and the gap under the last photograph is 60 against our 30 — the top of
   the widget already matched at 50 above the heading, so only the foot was
   short. The row gap is 30 on both sides, so this is an inset below the grid and
   not a gap that would also push the rows apart. Grid-only, which is the gallery
   and nothing else. */
.w-images--grid { padding-bottom: var(--sp-5); }


/* ---- Lightbox ------------------------------------------------------------
   Live opens a popup on a thumbnail: the photograph, its title, an optional line
   of description, and arrows through the set. Built on <dialog>, so the top
   layer, the backdrop, Escape and the focus trap come from the platform. See
   src/scripts/lightbox.js. */

/* A thumbnail is a link now. It must not look like one — live's grid shows bare
   photographs — but it still has to answer the keyboard. */
.shot { text-decoration: none; color: inherit; }
.shot:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 3px; }

/* Thumbnail hover, read off live's own stylesheet (simp.min.css) rather than
   eyeballed — the effect never appears in a screenshot because the captures are
   taken with no pointer over the grid:

       figure                     { position:relative; overflow:hidden }
       figure img                 { transition:all .2s ease-in }
       figure:hover img           { transform:scale(1.1) }
       figure .overlay            { position:absolute; inset:0; opacity:0;
                                    transition:all .2s ease-in-out }
       figure:hover .overlay      { opacity:1 }
       .inner-overlay a           { display:table-cell; vertical-align:middle;
                                    text-align:center }
       …no-gutter a.all-photos    { background:#294c4b; opacity:.6 }
       .inner-overlay i           { color:#fff; padding:7px 7px 7px 9px;
                                    border-radius:30px }

   So: the photo zooms to 1.1 inside a clipped frame while a brand-teal wash fades
   in over it carrying a white chain glyph. The `opacity:.6` sits on the element
   that holds BOTH the wash and the icon, so the glyph is 60% white over 60% teal —
   reproduced here rather than "corrected" to a solid white icon.

   Live's markup also carries an `.overlay-background` with a white inset panel.
   It is dead: its `opacity:0` has no matching `:hover` rule anywhere in the sheet,
   so it never paints. Not reproduced.

   Keyboard gets the same treatment as the pointer — the hover state is the only
   affordance saying a thumbnail opens, and a keyboard user needs it too. */
.w-images--grid > .shot {
  overflow: hidden;
  /* the slot is sized before its photograph arrives; a tint reads as "loading"
     where stark white read as "broken" */
  background: var(--c-surface-warm);
}
.w-images--grid > .shot img { transition: transform .2s ease-in; }
.w-images--grid > .shot:hover img,
.w-images--grid > .shot:focus-visible img { transform: scale(1.1); }

.shot__icon {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  background: var(--c-brand);
  color: var(--c-on-dark);
  opacity: 0;
  transition: opacity .2s ease-in-out;
}
.w-images--grid > .shot:hover .shot__icon,
.w-images--grid > .shot:focus-visible .shot__icon { opacity: .6; }

/* The zoom is motion over the whole thumbnail; the wash is not. Honour the
   viewer's setting for the former and keep the affordance. */
@media (prefers-reduced-motion: reduce) {
  .w-images--grid > .shot:hover img,
  .w-images--grid > .shot:focus-visible img { transform: none; }
}

.lightbox {
  padding: 0;
  border: 0;
  background: none;
  max-width: 100vw;
  max-height: 100vh;
  overflow: visible;
}
.lightbox::backdrop { background: var(--c-overlay-60); }

.lightbox__panel {
  position: relative;
  width: min(92vw, 820px);
  background: var(--c-surface);
  color: var(--c-ink);
}

/* Header values are live's, from the same stylesheet as the arrows:
     .ekko-lightbox .modal-title       { color:#4b5761 }
     .ekko-lightbox .modal-title span  { font-size:12px }   <- the description
     .ekko-lightbox .modal-header      { border-color:#a5abb0 }
   Live nests the description in a <span> inside the title, which is why its size
   is expressed that way; we give it its own element and the same 12px. */
.lightbox__head {
  padding: 20px 52px 16px 20px;
  border-bottom: 1px solid #A5ABB0;
}

.lightbox__title {
  margin: 0;
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  line-height: 1.3;
  letter-spacing: var(--ls-default);
  color: var(--c-ink);
}
.lightbox__desc {
  margin: 6px 0 0;
  font-size: var(--fs-2xs);
  font-weight: var(--fw-bold);
  line-height: var(--lh-body);
  letter-spacing: var(--ls-default);
  color: var(--c-ink);
}

.lightbox__stage { position: relative; line-height: 0; }
.lightbox__img {
  display: block;
  width: 100%;
  height: auto;
  /* The lightbox href IS the thumbnail src — live serves one w_900 asset for both
     — so the panel is sized to that rather than to the viewport, and a tall photo
     is capped instead of overflowing the screen. */
  max-height: calc(100vh - 200px);
  object-fit: contain;
  background: var(--c-surface-grey);
}

.lightbox__nav,
.lightbox__close {
  position: absolute;
  display: grid;
  place-items: center;
  border: 0;
  background: none;
  color: var(--c-surface);
  cursor: pointer;
  padding: 0;
}
/* Arrows sit ON the photograph, so they have to survive a bright one. Live solves
   that with a plate, not a shadow — and it is a plate the stylesheet only points
   at, as `background:url(../css/images/prev-image.png)`. Reading the PNG itself
   settles what it actually is (41x41, 15 distinct colours):

       plate    41 x 41, square, no radius, rgba(255,255,255,.6)   (fill 255,255,255,153)
       chevron  #2C2D2D, 3px stroke, 14 wide x 28 tall, centred    (fill 44,45,45,255)

   So it is a pale plate carrying a DARK chevron. An earlier pass had this exactly
   inverted — a white chevron over the photograph with a hard black shadow, taken
   from the glyph fallback in the same sheet:

       .ekko-lightbox-nav-overlay a { font-size:30px; color:#fff;
                                      text-shadow:2px 2px 4px #000 }

   That rule is real but it is not what live paints: `a.gallery-prev-icon` overrides
   it with the PNG and `opacity:1`. The plate is also simply the better answer — a
   fixed light ground beats a shadow that has to fight whatever is behind it.

   Drawn in CSS rather than shipping the two PNGs: it is a filled square and a
   chevron, it stays sharp at any density, and it costs no request.

   Flush to the edges, as live is — its nav zones start at the container edge and
   pin the plate with `background-position: left/right center`. The 41px plate is
   padded out to a 44px tap target. */
.lightbox__nav {
  top: 50%;
  transform: translateY(-50%);
  width: 41px;
  height: 41px;
  background: rgba(255, 255, 255, .6);
  transition: background-color .2s ease;
}
.lightbox__nav:hover { background: rgba(255, 255, 255, .82); }
.lightbox__nav--prev { left: 0; }
.lightbox__nav--next { right: 0; }

/* The chevron is drawn, not set in the icon font. Live's is 14 wide x 28 tall —
   a 1:2 ratio — and Font Awesome's angle-left is 1:0.62, so no size of it is the
   right shape: fitted against live's box the best any weight managed was 16x26 at
   40px, and it read as a different mark. Two borders on a rotated square give the
   ratio exactly. An L of arm `s` rotated 45 degrees measures s/sqrt2 across and
   s*sqrt2 down — always 1:2 — so s = 20 lands 14.1 x 28.3 with live's 3px stroke. */
.lightbox__chev {
  width: 20px;
  height: 20px;
  border: solid #2C2D2D;
  transform: rotate(45deg);
}
.lightbox__nav--prev .lightbox__chev { border-width: 0 0 3px 3px; margin-left: 5px; }
.lightbox__nav--next .lightbox__chev { border-width: 3px 3px 0 0; margin-right: 5px; }

/* Live: `.ekko-lightbox button.close { font-size:42px; opacity:.8; color:#a5abb0 }`
   with opacity 1 on hover and focus. It sits on the white header, not on the
   photograph, so it needs no shadow. */
.lightbox__close {
  top: 10px;
  right: 12px;
  width: 32px;
  height: 32px;
  font-size: 26px;
  color: #A5ABB0;
  opacity: .8;
  transition: opacity .2s ease;
}
.lightbox__close:hover,
.lightbox__close:focus-visible { opacity: 1; }

/* Both controls now sit on a light ground — the plate and the white header — so the
   site's own focus colour reads on them and there is no need for the white ring an
   earlier pass used to get a chevron seen against open photograph. The nav's ring
   goes inside, because the plate is flush to the image edge and an outward offset
   would be clipped. */
.lightbox__nav:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: -3px;
}
.lightbox__close:focus-visible {
  outline: 2px solid var(--c-focus);
  outline-offset: 2px;
}

@media (max-width: 600px) {
  .lightbox__head { padding: 16px 46px 12px 16px; }
  .lightbox__title { font-size: var(--fs-md); }
}

/* ===== widgets/webstory.css ===== */
/* ============================================================================
   WEB STORY — live `simp-custom-reels-slider`, the two /webstory/ inner pages.

   A full-screen story player, not a card row. Every value below is measured off
   live at 1440x900; the branch comment in widgets/render.njk lists them together.

   The site chrome goes because live's does: on this template live sets both its
   header and its footer to `display: none`, so the story owns the viewport.
   Scoped with `:has()` to a page that actually carries the component, so no other
   page can lose its navigation.
   ========================================================================== */
body:has(.webstory) .site-header,
body:has(.webstory) .site-footer,
body:has(.webstory) .promo-bar { display: none; }
body:has(.webstory) .outer-page { padding-top: 0; }

.webstory {
  position: relative;
  min-height: 100vh;
  display: grid;
  place-items: center;
  padding-block: 0;
  overflow: hidden;
  background: var(--c-near-black);
}

/* The current slide, blurred, filling the viewport behind the card. The image
   rides in from script as `--bg` so the backdrop follows the slide. */
.webstory__bg {
  position: absolute;
  inset: -60px;                  /* the blur would otherwise feather the edges */
  background-image: var(--bg, none);
  background-size: cover;
  background-position: 50% 50%;
  filter: blur(50px) brightness(.6);
  z-index: 0;
}

.webstory__stage {
  position: relative;
  z-index: 1;
  width: 430px;
  height: 816px;
  max-width: 100vw;
}

/* ---- progress ---------------------------------------------------------- */
.webstory__progress {
  position: absolute;
  top: 10px;
  left: 12px;
  right: 12px;
  z-index: 3;
  display: flex;
  gap: 3px;                      /* live pitch 57 = 54 tick + 3 */
  margin: 0;
  padding: 0;
  list-style: none;
}
.webstory__tick {
  flex: 1 1 0;
  height: 2px;
  background: rgba(255, 255, 255, .4);
  border-radius: 2px;
}
.webstory__tick.is-active { background: var(--c-on-dark); }

/* ---- slides ------------------------------------------------------------ */
.webstory__track {
  position: relative;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
  border-radius: 20px;
  overflow: hidden;
}
.webstory__slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity .35s ease;
}
.webstory__slide.is-active { opacity: 1; }
.webstory__slide img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* ---- caption ----------------------------------------------------------- */
.webstory__caption {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  min-height: 243px;
  padding: 34px 26px 26px;
  background: rgba(0, 0, 0, .7);
  border-radius: 100px 100px 20px 20px;
  color: var(--c-on-dark);
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* The caption sits on a dark ground, so its authored type inverts. The rule under
   the heading is live's own separator between title and body. */
.webstory__caption :is(h1, h2, h3, h4, p, strong, span) { color: var(--c-on-dark); }
.webstory__caption :is(h1, h2, h3, h4) {
  position: relative;
  margin: 0 0 14px;
  padding-bottom: 12px;
  font-size: var(--fs-xl);
  line-height: 1.2;
}
.webstory__caption :is(h1, h2, h3, h4)::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  width: 140px;
  height: 3px;
  background: var(--c-green);
}
.webstory__caption p { margin: 0; font-size: var(--fs-sm); line-height: var(--lh-body); }

/* ---- arrows ------------------------------------------------------------ */
.webstory__nav {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  z-index: 3;
  width: 48px;
  height: 48px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: 50%;
  background: var(--c-surface);
  cursor: pointer;
  transition: opacity .2s ease;
}
.webstory__nav--prev { left: -80px; }    /* live: card x 505, arrow x 425 */
.webstory__nav--next { right: -80px; }   /* live: card right 935, arrow x 967 */
.webstory__nav:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 3px; }

/* Drawn, like the lightbox and gallery chevrons, so the mark cannot disagree with
   whatever the icon font happens to resolve. */
.webstory__chev {
  width: 11px;
  height: 11px;
  border: solid var(--c-ink);
  border-width: 0 0 2px 2px;
}
.webstory__nav--prev .webstory__chev { transform: rotate(45deg); margin-left: 4px; }
.webstory__nav--next .webstory__chev { transform: rotate(-135deg); margin-right: 4px; }

/* Below the card's own width the story fills the screen, and the arrows come
   inside it — there is no longer a margin for them to sit in. */
@media (max-width: 620px) {
  .webstory__stage { width: 100vw; height: 100vh; }
  .webstory__track { border-radius: 0; }
  .webstory__nav--prev { left: 10px; }
  .webstory__nav--next { right: 10px; }
  .webstory__caption { border-radius: 60px 60px 0 0; }
}

@media (prefers-reduced-motion: reduce) {
  .webstory__slide { transition: none; }
  .story-card__link:hover::after,
  .story-card__link:focus-visible::after { animation: none; }
}

/* ============================================================================
   WEB STORY LISTING — live `summary_widget_wrapper`, one widget on /webstory.
   Measured at 1440: wrapper padding 30px 0, cards 240x427 centred with a 35px
   gap, image cover-fitted, title 21px white overlaid low in the card.
   ========================================================================== */
.story-cards {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 35px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.story-card { flex: 0 0 240px; }
.story-card__link {
  position: relative;
  display: block;
  width: 240px;
  height: 427px;
  overflow: hidden;
  /* Live rounds these cards (10px on `.summary-widget`, which is this same
     240x427 box, with overflow hidden). The replica left them square. */
  border-radius: 10px;
  text-decoration: none;
}
.story-card__link img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform .3s ease;
}
/* Live's hover is a diagonal shine crossing the card, not a zoom — its image
   measures `transform: none` and `opacity: 1` both hovered and at rest, and the
   effect comes from `.summary-content-wrap::before`: a 120px column the full
   height of the card, skewed -20deg, filled with a white 0 -> .5 -> 0 gradient,
   travelling from left -72px to left 312px while its opacity rises and falls
   (measured .37 at 120ms, 0 at rest and at the end — a ramp a transition cannot
   make, so it is an animation). The replica scaled the photograph instead. */
.story-card__link::after {
  content: "";
  position: absolute;
  top: 0;
  bottom: 0;
  left: -72px;
  width: 120px;
  z-index: 2;
  background: linear-gradient(90deg,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, .5) 50%,
    rgba(255, 255, 255, 0) 100%);
  transform: skewX(-20deg);
  opacity: 0;
  pointer-events: none;
}
@media (hover: hover) {
  .story-card__link:hover::after,
  .story-card__link:focus-visible::after { animation: story-shine .6s ease; }
}
@keyframes story-shine {
  0%   { left: -72px; opacity: 0; }
  50%  { opacity: .5; }
  100% { left: 312px; opacity: 0; }
}
.story-card__link:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 3px; }

/* The title sits ON the photograph, so it carries its own scrim rather than
   relying on the image being dark enough. */
.story-card__body {
  position: absolute;
  inset: auto 0 0 0;
  padding: 40px 10px 18px;
  /* Live: `to top, rgba(0,0,0,.9) -> transparent`, over the bottom third. */
  background: linear-gradient(to top, rgba(0, 0, 0, .9), rgba(0, 0, 0, 0));
  color: var(--c-on-dark);
  text-align: center;
}
.story-card__title {
  display: block;
  font-size: var(--fs-xl);
  line-height: 1.2;
  font-weight: var(--fw-regular);
  color: var(--c-on-dark);
}
.story-card__more {
  display: block;
  margin-top: 10px;
  font-size: var(--fs-sm);
  line-height: 20px;
  color: var(--c-on-dark);
  text-decoration: underline;
}

/* ---- story: logo, share button, share popup ----------------------------
   Live: `.reel-logo` 50x50 disc with a 1px white border, background-image the
   site logo, at card x+20 y+30, linking home. `.share-ele` 24x16 at 25px in from
   the card's right and 38px down. The popup is 300x97, white, radius 12, padding
   20px 30px, centred on the viewport, heading 18px/700 #32383E, then a row of
   copy-link / WhatsApp / Pinterest / Facebook / X / Tumblr.
   ------------------------------------------------------------------------ */
.webstory__logo {
  position: absolute;
  top: 30px;
  left: 20px;
  z-index: 4;
  width: 50px;
  height: 50px;
  border: 1px solid var(--c-on-dark);
  border-radius: 50%;
  background-image: url("https://assets.simplotel.com/simplotel/image/upload/q_90,fl_progressive,w_200,f_auto,c_limit,b_rgb:eeeeee/yo1-longevity-health-resorts-catskills-(latest-cloned-to-be-made-live)/yo1-wellness-resort-_-spa-logo_zauky2");
  background-size: 72%;
  background-position: 50% 50%;
  background-repeat: no-repeat;
  background-color: var(--c-surface);
}
.webstory__logo:focus-visible { outline: 2px solid var(--c-on-dark); outline-offset: 3px; }

.webstory__share {
  position: absolute;
  top: 30px;
  /* The close button takes the outer corner — the position a reader reaches for to
     leave — so share sits one slot inboard: 22px + its own 34px + an 8px gap. */
  right: 64px;
  z-index: 4;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  border: 0;
  background: none;
  color: var(--c-on-dark);
  font-size: 20px;
  cursor: pointer;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, .5));
}
.webstory__share:focus-visible { outline: 2px solid var(--c-on-dark); outline-offset: 2px; }

/* CLOSE. Same white-on-photo treatment as the share control — a drop-shadow rather
   than a chip, which is how every other control on the story stays legible over an
   arbitrary image. Slightly larger than the paper plane because it is the one
   control a reader hunts for. */
.webstory__close {
  position: absolute;
  top: 30px;
  right: 22px;
  z-index: 4;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  color: var(--c-on-dark);
  font-size: 22px;
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  filter: drop-shadow(0 1px 3px rgba(0, 0, 0, .5));
}
.webstory__close:hover { color: var(--c-sage); }
.webstory__close:focus-visible { outline: 2px solid var(--c-on-dark); outline-offset: 2px; }

.webstory__sharebox {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 6;
  width: 300px;
  padding: 20px 30px;
  border-radius: 12px;
  background: var(--c-surface);
  text-align: center;
}
.webstory__sharetitle {
  margin: 0 0 14px;
  font-size: var(--fs-lg);
  font-weight: var(--fw-bold);
  line-height: 1.2;
  color: #32383E;
}
.webstory__shareclose {
  position: absolute;
  top: 8px;
  right: 10px;
  width: 26px;
  height: 22px;
  border: 0;
  background: none;
  color: #32383E;
  font-size: 16px;
  cursor: pointer;
}
.webstory__sharelist {
  display: flex;
  justify-content: space-between;
  gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.webstory__sharelink {
  display: grid;
  place-items: center;
  width: 32px;
  height: 23px;
  border: 0;
  background: none;
  color: var(--c-brand);
  font-size: 20px;
  cursor: pointer;
  text-decoration: none;
}
.webstory__sharelink:hover { color: var(--c-sage); }
.webstory__sharelink:focus-visible { outline: 2px solid var(--c-focus); outline-offset: 2px; }

/* ---- listing card badge ------------------------------------------------
   Live draws this on `.imageblock::after`, and it is ON AT REST — 35x35,
   rgba(0,0,0,.5), a full circle, 10px in from the top and right, z-index 1, with
   the mark as a centred background-image at 60% of the disc. (The `.link_overlay`
   span, which does fade in on hover, has no size and paints nothing — reading its
   `opacity: 0 -> 1` as the badge is what put ours behind a hover in the first
   place, and its `fa fa-link` class is not the mark live actually shows.)

   The mark is three stacked panels, not a chain link: one square face with two
   more offset behind its top-right corner. Drawn here as a stroked SVG rather
   than an icon-font glyph, for the same reason the gallery and story chevrons are
   drawn — the font is not ours to depend on, and `fa-link` demonstrably resolves
   to something else on live. */
.story-card__badge {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 1;
  width: 35px;
  height: 35px;
  border-radius: 50%;
  background-color: rgba(0, 0, 0, .5);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512' fill='none' stroke='%23fff' stroke-width='30' stroke-linejoin='round'%3E%3Crect x='15' y='203' width='361' height='226' rx='15'/%3E%3Cpath d='M75 158h347a15 15 0 0 1 15 15v196'/%3E%3Cpath d='M135 98h347a15 15 0 0 1 15 15v196'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: 50% 50%;
  background-size: 60%;
}

/* ===== index.css (non-import) ===== */
/* Vendored intl-tel-input (MIT). Bundled here rather than fetched from jsDelivr:
   it removes an origin and a render-blocking request, and puts the country
   widget's styles ahead of our own form rules so ours keep winning. */
