/* ==========================================================================
   HAVA WORKSHOPS — Design System
   A creative space for women to slow down, tune into their intuition and
   create from within. Airy, organic, digestible.
   ========================================================================== */

/* ---- Fonts --------------------------------------------------------------
   Both brand fonts are now REAL, served live via the client's Adobe Fonts
   (Typekit) kit — see the <link rel="stylesheet"
   href="https://use.typekit.net/zsk2klo.css"> in build.py's head(). That
   one kit publishes two families:
     - "hardcover-vf"  — Hardcover Var, a variable font spanning weight 200
       (Extralight, used everywhere here) to 900. Used for every heading.
     - "basic-sans"    — Basic Sans. Adobe's kit dashboard lists this as a
       single static face (Bold), not a variable font, so it is ONLY ever
       requested at font-weight: 700 in this stylesheet, via --font-accent
       below — never mixed into regular-weight body copy. If it were put in
       the same stack as body text, the single Bold face would get matched
       to every weight request (that's how @font-face matching works when
       only one weight is declared for a family), making paragraphs render
       bold by accident.

   IMPORTANT: Adobe Fonts kits are domain-locked in the Adobe dashboard —
   if headings/accents render in the Fraunces/Sora fallback instead of the
   real fonts, the current domain (localhost, a Netlify preview, the live
   domain, etc.) almost certainly just needs adding to this kit's allowed
   domains at fonts.adobe.com.

   NOTE: this :root block is generated by build_style.py from
   design_tokens.py — don't hand-edit the values below, edit that file (or
   the "Design" tab of the content-editor Artifact) and regenerate.
   -------------------------------------------------------------------- */

:root {
  /* ---- Brand palette (sampled from Hava_Brand Sheet.jpg) ---- */
  --aubergine: #29121d;
  --aubergine-ink: #3d1a2a;
  --blush: #f4d2ec;
  --nude: #edd9cc;
  --olive: #4a4203;
  --terracotta: #bf5d47;
  --terracotta-deep: #a3492f;
  --lime: #d6ed70;
  --cream: #fdf7f1;
  --white: #ffffff;
  --ink: #2b1620;

  /* ---- Subtle background gradients ----
     Each pair is the same brand colour shifted ±3.5% lightness, derived
     automatically from the base colour above (see build_style.py) — barely
     perceptible on its own, but it keeps flat sections from feeling inert. */
  --grad-cream: linear-gradient(180deg,#ffffff 0%, var(--cream) 45%, #fbeee1 100%);
  --grad-blush: linear-gradient(180deg,#f8e0f2 0%, var(--blush) 50%, #f0c4e6 100%);
  --grad-nude: linear-gradient(180deg,#f2e3d9 0%, var(--nude) 50%, #e8cfbf 100%);
  --grad-aubergine: linear-gradient(180deg,#351726 0%, var(--aubergine) 50%, #1d0d14 100%);
  --grad-olive: linear-gradient(180deg,#5b5104 0%, var(--olive) 50%, #393302 100%);

  /* ---- Type ----
     --font-sans is the body-safe stack (regular weight, no Basic Sans —
     see the note above on why). --font-accent is Basic Sans Bold for
     buttons, nav, labels and eyebrows; always paired with font-weight: 700. */
  --font-display: 'hardcover-vf', 'Fraunces', 'Iowan Old Style', Georgia, serif;
  --font-sans: 'Sora', -apple-system, 'Segoe UI', sans-serif;
  --font-accent: 'basic-sans', 'Sora', -apple-system, 'Segoe UI', sans-serif;

  --step--1: clamp(0.84rem, 0.76rem + 0.1vw, 0.85rem);
  --step-0: clamp(1rem, 0.89rem + 0.15vw, 1rem);
  --step-1: clamp(1.08rem, 1.02rem + 0.3vw, 1.2rem);
  --step-2: clamp(1.3rem, 1.18rem + 0.6vw, 1.55rem);
  --step-3: clamp(1.65rem, 1.4rem + 1.2vw, 2.15rem);
  --step-4: clamp(1.8rem, 1.6rem + 1.9vw, 2.85rem);
  --step-5: clamp(2.15rem, 1.85rem + 2.6vw, 3.5rem);

  /* ---- Space (fluid) ----
     The numbers in design_tokens.py are the DESKTOP values; each one is
     emitted as a clamp() that shrinks towards a phone-sized floor as the
     viewport narrows (see SPACE_MOBILE_RATIO in build_style.py). Without
     this, a 6rem section pad meant for a 1440px screen ate a third of a
     375px one, which is what made the phone layout read as a squeezed
     desktop rather than a mobile design. Editing a value in the design
     tokens still means 'how airy is this at desktop' — the phone end
     follows automatically. */
  --space-xs: 0.5rem;
  --space-sm: clamp(0.81rem, 0.7691rem + 0.1818vw, 0.9rem);
  --space-md: clamp(1.2rem, 1.0636rem + 0.6061vw, 1.5rem);
  --space-lg: clamp(1.55rem, 1.1182rem + 1.9192vw, 2.5rem);
  --space-xl: clamp(2rem, 1.0909rem + 4.0404vw, 4rem);
  --space-2xl: clamp(2.76rem, 1.2873rem + 6.5455vw, 6rem);

  /* Corner radius scales too — 1.75rem on a full-width phone card reads
     as a lozenge rather than a soft edge. */
  --radius-soft: clamp(1.225rem, 0.9864rem + 1.0606vw, 1.75rem);
  --radius-pill: 999px;

  --container: 72rem;
  --container-narrow: 44rem;
}

/* ---- Reset ---- */
*, *::before, *::after { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  margin: 0;
  background: var(--grad-cream);
  background-attachment: fixed;
  color: var(--ink);
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: var(--step-0);
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
}
img { max-width: 100%; display: block; }
a { color: inherit; }
h1, h2, h3, h4 { font-family: var(--font-display); font-weight: 200; margin: 0 0 var(--space-sm); line-height: 1.15; letter-spacing: 0.01em; text-wrap: balance; }
h1 { font-size: var(--step-5); }
h2 { font-size: var(--step-4); }
h3 { font-size: var(--step-2); }
h4 { font-size: var(--step-1); font-family: var(--font-accent); font-weight: 700; }
p { margin: 0 0 var(--space-sm); max-width: 62ch; }
ul { padding-left: 1.2em; }

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

/* ---- Skip link (a11y) ---- */
.skip-link {
  position: absolute; left: -999px; top: 0; z-index: 200;
  background: var(--aubergine); color: var(--white);
  padding: 0.75em 1.25em; border-radius: 0 0 0.5em 0; font-family: var(--font-accent); font-weight: 700;
}
.skip-link:focus { left: 0; }

/* ---- Focus states (a11y — visible on every interactive element) ---- */
a:focus-visible, button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible, summary:focus-visible {
  outline: 3px solid var(--terracotta-deep);
  outline-offset: 3px;
  border-radius: 4px;
}

.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0; padding: 0; margin: -1px;
}

/* ---- Layout helpers ---- */
.container { max-width: var(--container); margin-inline: auto; padding-inline: var(--space-md); }
.container-narrow { max-width: var(--container-narrow); margin-inline: auto; padding-inline: var(--space-md); }
.section {
  min-height: 100vh; /* fallback for browsers without svh support */
  min-height: 100svh; /* accounts for mobile browser chrome, unlike 100vh */
  display: flex;
  align-items: center;
  padding-block: var(--space-2xl);
  position: relative;
  overflow: hidden;
}
.section > .container, .section > .container-narrow { width: 100%; }
.section--tight { padding-block: var(--space-xl); }

/* On a phone, a full-viewport minimum per section is the single biggest
   "this is a desktop layout in a narrow window" tell: a three-line quote
   gets a whole screen to itself, and the page ends up TALLER on a 375px
   handset than on a 1440px monitor. Phones get sections sized by their own
   content instead, with the fluid padding above providing the rhythm.
   Heroes keep their full-height treatment — a full-bleed opening screen is
   native to mobile, not borrowed from desktop. */
@media (max-width: 860px) {
  .section { min-height: 0; align-items: stretch; }
}
.eyebrow {
  font-family: var(--font-accent); font-weight: 700; text-transform: uppercase;
  letter-spacing: 0.14em; font-size: var(--step--1); color: var(--terracotta-deep);
  display: inline-flex; align-items: center; gap: 0.6em; margin-bottom: var(--space-sm);
}
.eyebrow::before { content: ''; width: 1.6em; height: 1px; background: currentColor; display: inline-block; }

/* ---- Organic paper texture (subtle grain, applied to <body> via ::before) ---- */
.grain {
  position: fixed; inset: 0; pointer-events: none; z-index: 999; opacity: 0.05; mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ---- Background tone utilities ---- */
.bg-aubergine { background: var(--grad-aubergine); color: var(--blush); }
.bg-aubergine h1, .bg-aubergine h2, .bg-aubergine h3 { color: var(--white); }
.bg-aubergine .eyebrow { color: var(--lime); }
.bg-blush { background: var(--grad-blush); color: var(--aubergine); }
.bg-nude { background: var(--grad-nude); color: var(--aubergine); }
.bg-cream { background: var(--grad-cream); color: var(--ink); }
.bg-olive { background: var(--grad-olive); color: var(--nude); }
.bg-olive h1, .bg-olive h2, .bg-olive h3 { color: var(--white); }

/* ---- Petal / flower motif (recreates the brand mark's dot-cluster as a decorative SVG) ---- */
.petal-motif { position: absolute; opacity: 0.14; pointer-events: none; }
.petal-motif--corner-tr { top: -4rem; right: -4rem; width: clamp(180px, 22vw, 320px); }
.petal-motif--corner-bl { bottom: -4rem; left: -4rem; width: clamp(180px, 22vw, 320px); }
/* The centred watermark reads as texture rather than a second logo: much
   larger than the corner motifs (it's meant to sit behind the copy and
   bleed past the section edges) and correspondingly fainter, so the
   heading keeps its contrast. */
.petal-motif--center {
  top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: clamp(315px, 54vw, 825px);
  opacity: 0.07;
}
/* A slow breath on the centred watermark — 18 seconds for a full cycle, a 3%
   scale change and a small drift in opacity. Deliberately below the threshold
   where you'd catch it moving: you notice the section feels alive, not that
   something is animating. It's a decorative watermark behind the copy, so
   nothing depends on it and it never competes with the heading.
   translate stays in the transform so the centring isn't lost mid-animation. */
@keyframes petal-breathe {
  0%, 100% { transform: translate(-50%, -50%) scale(1)    rotate(-3deg); opacity: 0.05; }
  50%      { transform: translate(-50%, -50%) scale(1.06) rotate(3deg);  opacity: 0.11; }
}
.petal-motif--center { animation: petal-breathe 24s ease-in-out infinite; }
/* The global reduced-motion block near the top of this file cuts every
   animation to 0.01ms, which would freeze this mid-cycle at whichever frame
   it happened to be on; pin it back to its resting state instead. */
@media (prefers-reduced-motion: reduce) {
  .petal-motif--center { animation: none !important; transform: translate(-50%, -50%); opacity: 0.07; }
}

/* ---- Buttons ---- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 0.5em;
  font-family: var(--font-accent); font-weight: 700; font-size: var(--step--1);
  text-transform: uppercase; letter-spacing: 0.08em;
  /* Hairline stroke. On a 2x/3x screen this is a true half-pixel line and
     looks crisp; on a 1x display the browser can't draw half a device pixel,
     so it antialiases to a lighter 1px — the stroke stays visible but reads
     fainter there than the colour value alone suggests. */
  padding: 1em 1.9em; border-radius: var(--radius-pill); border: 0.5px solid transparent;
  text-decoration: none; cursor: pointer; transition: transform 0.2s ease, background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.btn:hover { transform: translateY(-2px); }
.btn-primary { background: var(--aubergine); color: var(--white); }
.btn-primary:hover { background: var(--aubergine-ink); }
.btn-on-dark { background: var(--blush); color: var(--aubergine); }
.btn-on-dark:hover { background: var(--white); }
.btn-outline { background: transparent; border-color: currentColor; color: var(--aubergine); }
.btn-outline:hover { background: var(--aubergine); color: var(--white); border-color: var(--aubergine); }
.btn-outline.on-dark { color: var(--white); }
.btn-outline.on-dark:hover { background: var(--white); color: var(--aubergine); }
.btn-outline-lime { background: transparent; border-color: var(--lime); color: var(--lime); }
.btn-outline-lime:hover { background: var(--lime); color: var(--aubergine); border-color: var(--lime); }

/* ---- Header / Nav ----
   The header has no fill of its own while you're at the top of the page —
   it's fixed in place and floats directly over each page's full-viewport
   hero, so the hero's own colour/imagery shows through around the logo
   and links. Every hero except Home's is a light pastel tone, so the
   default (unmodified) .site-header uses dark aubergine ink for
   legibility there; Home's hero is the one dark (aubergine) hero, so it
   gets the --on-dark modifier, swapping in light blush/white/lime text
   with a soft shadow to hold up against the hero's slightly-varying
   petal-motif background.

   Once the visitor scrolls past the hero, `main.js` adds `.is-scrolled`
   (see the scroll listener there): the header gains a solid aubergine
   backing and switches to the same light text as --on-dark, since past
   that point it's floating over arbitrary page content whose colour we
   can't predict — a solid, guaranteed-legible bar is the safe default
   for "whatever happens to be scrolling underneath". */
.site-header {
  position: fixed; inset-inline: 0; top: 0; z-index: 100;
  background: transparent;
  transition: background 0.25s ease;
}
.site-header.is-scrolled {
  background: var(--aubergine);
}
/* The header floats over each hero rather than taking up layout space, so
   anything that needs to sit clear of it has to know how tall it is. It's the
   brand mark (the tallest thing in the bar) plus the wrap's padding, top and
   bottom — derived rather than a magic number, so it follows --space-sm as
   that shrinks on phones. Used by the home hero's photo frame. */
:root { --header-h: calc(2.75rem + 2 * var(--space-sm)); }
.nav-wrap { display: flex; align-items: center; justify-content: space-between; padding-block: var(--space-sm); }
.brand { display: flex; align-items: center; gap: 0.6em; text-decoration: none; }
/* Sized to roughly match the height of the two-line wordmark beside it, as
   the brand sheet's lockup does. The mark's interlocking arcs need the
   room — below ~40px they close up and it reads as a blob. */
.brand-mark { width: 2.75rem; height: 2.75rem; color: var(--aubergine); flex-shrink: 0; }
.brand-word { font-family: var(--font-display); font-size: var(--step-1); color: var(--aubergine); line-height: 1; }
.brand-word strong { display: block; font-family: var(--font-accent); font-weight: 700; font-size: 0.5em; text-transform: uppercase; letter-spacing: 0.16em; margin-top: 0.2em; color: var(--terracotta-deep); }

/* No drop shadow on nav text — the solid header backgrounds (aubergine on
   dark/scrolled, cream/blush otherwise) give plenty of contrast on their
   own, so this used to add a shadow for legibility; now explicitly off. */
.site-header--on-dark .brand, .site-header--on-dark .nav-list a.nav-pill, .site-header--on-dark .nav-toggle,
.site-header.is-scrolled .brand, .site-header.is-scrolled .nav-list a.nav-pill, .site-header.is-scrolled .nav-toggle {
  text-shadow: none;
}
.site-header--on-dark .brand-mark, .site-header.is-scrolled .brand-mark { color: var(--blush); }
.site-header--on-dark .brand-word, .site-header.is-scrolled .brand-word { color: var(--white); }
.site-header--on-dark .brand-word strong, .site-header.is-scrolled .brand-word strong { color: var(--lime); }

/* ---- Nav links ----
   Regular links are plain uppercase text with a soft underline indicator
   on hover/active — no pill background or border. Book is the one
   exception: a green-outline pill, pinned to the far right —
   .nav-cta-start (on its <li>) carries margin-left: auto, which pushes it
   away from the regular links, leaving a clear gap between the two groups
   exactly like a two-cluster layout drawn by hand, without needing to
   split the list in markup. */
.nav-list { display: flex; align-items: center; gap: 1.6rem; list-style: none; margin: 0; padding: 0; }
.nav-list li.nav-cta-start { margin-left: auto; }
.nav-list a.nav-pill {
  text-decoration: none; font-family: var(--font-accent); font-weight: 700; font-size: var(--step--1);
  text-transform: uppercase; letter-spacing: 0.14em; /* matches .eyebrow's tracking */
  color: var(--aubergine); padding: 0.4em 0.1em;
  border: none; border-bottom: 1px solid transparent;
  display: inline-flex; align-items: center;
  transition: border-color 0.2s ease, color 0.2s ease;
}
.nav-list a.nav-pill:hover { border-color: var(--aubergine); }
.nav-list a.nav-pill[aria-current="page"] { border-color: var(--aubergine); }
.site-header--on-dark .nav-list a.nav-pill, .site-header.is-scrolled .nav-list a.nav-pill { color: var(--blush); }
.site-header--on-dark .nav-list a.nav-pill:hover, .site-header.is-scrolled .nav-list a.nav-pill:hover { border-color: var(--blush); }
.site-header--on-dark .nav-list a.nav-pill[aria-current="page"], .site-header.is-scrolled .nav-list a.nav-pill[aria-current="page"] { border-color: var(--blush); }

.nav-list a.nav-pill--book {
  font-family: var(--font-sans); text-transform: none; letter-spacing: normal;
  background: transparent; border: 0.5px solid var(--lime); border-radius: var(--radius-pill);
  color: var(--aubergine); padding: 0.7em 1.5em; font-weight: 700;
  transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease;
}
.nav-list a.nav-pill--book:hover, .nav-list a.nav-pill--book[aria-current="page"] {
  background: var(--lime); color: var(--aubergine); border-color: var(--lime); text-shadow: none;
}
.nav-list a.nav-pill--book:hover { transform: translateY(-1px); }
/* Lime only reads as a stroke/fill on the dark aubergine header (on-dark,
   scrolled, and the mobile dropdown, which is always dark) — there it clears
   ~9.6:1. Floating over the *light* hero backgrounds (blush/nude/cream) on a
   non-scrolled desktop header, lime sits near 1:1 contrast, well under the
   3:1 WCAG non-text minimum, so that specific state swaps in terracotta-deep
   (~4.3:1+ against every light hero colour) with white label text on the
   filled state to stay readable on it. Scoped to desktop widths only — the
   mobile dropdown panel is always dark and keeps the lime treatment above. */
@media (min-width: 861px) {
  .site-header:not(.site-header--on-dark):not(.is-scrolled) .nav-list a.nav-pill--book {
    border-color: var(--terracotta-deep);
  }
  .site-header:not(.site-header--on-dark):not(.is-scrolled) .nav-list a.nav-pill--book:hover,
  .site-header:not(.site-header--on-dark):not(.is-scrolled) .nav-list a.nav-pill--book[aria-current="page"] {
    background: var(--terracotta-deep); color: var(--white); border-color: var(--terracotta-deep);
  }
}
.site-header--on-dark .nav-list a.nav-pill--book, .site-header.is-scrolled .nav-list a.nav-pill--book { color: var(--lime); }
.site-header--on-dark .nav-list a.nav-pill--book:hover, .site-header--on-dark .nav-list a.nav-pill--book[aria-current="page"],
.site-header.is-scrolled .nav-list a.nav-pill--book:hover, .site-header.is-scrolled .nav-list a.nav-pill--book[aria-current="page"] {
  color: var(--aubergine); border-color: var(--lime); text-shadow: none;
}

.nav-toggle {
  display: none; background: none; border: none; cursor: pointer; padding: 0.5em;
  color: var(--aubergine);
}
.site-header--on-dark .nav-toggle, .site-header.is-scrolled .nav-toggle { color: var(--blush); }
.nav-toggle svg { width: 1.6rem; height: 1.6rem; }

@media (max-width: 860px) {
  /* The <nav> is still a flex item here even though its list is an absolutely
     positioned dropdown with no width, so .nav-wrap's space-between was
     distributing gaps across three items and parking the toggle in the middle
     of the bar — 113px short of the right edge at 390px. margin-left: auto
     overrides that and pins it right. The negative right margin pulls the
     button's padding back so the ICON lines up with the container gutter (and
     so with the brand mark opposite), while the 44px box keeps the tap target
     at the recommended size. */
  .nav-toggle {
    display: inline-flex; align-items: center; justify-content: center;
    margin-left: auto;
    /* Cancel exactly the space the 44px tap box adds around the 1.6rem icon,
       so the icon's edge — not the button's — lines up with the gutter. */
    margin-right: calc((44px - 1.6rem) / -2);
    padding: 0; min-width: 44px; min-height: 44px;
  }
  /* ---- Full-screen mobile menu ----
     Opened, the nav covers the whole viewport in blush rather than dropping a
     dark panel below the header. Two consequences worth spelling out:

     z-index 90 puts it UNDER the header (100), so the logo and the toggle stay
     on top of it and the toggle can still be pressed to close it. That in turn
     means both have to be recoloured for a light background while it is open
     (see the html.nav-open rules) — on the home page they are otherwise set
     light for the dark hero and would vanish against blush.

     And with the whole screen covered there is no page left to tap on to
     dismiss it, so main.js closes it on Escape as well as on a link. */
  .nav-list {
    position: fixed; inset: 0; z-index: 90;
    flex-direction: column; align-items: stretch; justify-content: center;
    background: var(--grad-blush);
    /* Clears the floating header at the top; generous at the bottom so the
       last item never sits against the edge on a short screen. */
    padding: calc(var(--space-2xl) + 3.5rem) var(--space-lg) var(--space-2xl);
    gap: var(--space-xs);
    overflow-y: auto;
    display: none;
  }
  .nav-list.is-open { display: flex; }

  /* A full-screen overlay that lets the page scroll behind it is the classic
     mobile-menu bug: you close the menu and find yourself somewhere else. */
  html.nav-open, html.nav-open body { overflow: hidden; }

  /* The header floats over the open menu, so it must not carry its scrolled
     aubergine fill, and its contents must read on blush whichever variant the
     page would otherwise be using. */
  html.nav-open .site-header { background: transparent; }
  /* The menu is a DESCENDANT of the header, not a sibling, so its z-index
     competes inside the header's own stacking context — a positioned child
     paints over its static siblings whatever the numbers say. Lifting the
     brand and the toggle above it is what keeps the logo visible and, more
     importantly, keeps the toggle pressable: on a phone there is no Escape
     key, so if it were buried the menu could only be closed by choosing a
     link. */
  html.nav-open .site-header .brand,
  html.nav-open .site-header .nav-toggle { position: relative; z-index: 95; }
  html.nav-open .site-header .brand-mark { color: var(--terracotta-deep); }
  html.nav-open .site-header .brand-word { color: var(--aubergine); }
  html.nav-open .site-header .brand-word strong { color: var(--terracotta-deep); }
  html.nav-open .site-header .nav-toggle { color: var(--aubergine); }

  .nav-list li.nav-cta-start {
    margin-left: 0; margin-top: var(--space-sm); padding-top: var(--space-md);
    border-top: 1px solid rgba(41, 18, 29, 0.16);
  }

  /* Full-width, comfortably tall rows, and larger type than the desktop bar —
     a whole screen invites a proper size rather than a shrunken copy of the
     header strip. */
  .nav-list a.nav-pill {
    justify-content: center; width: 100%; min-height: 52px;
    padding: 0.6em 0.5em; font-size: var(--step-1);
    color: var(--aubergine); text-shadow: none;
  }
  .site-header--on-dark .nav-list a.nav-pill,
  .site-header.is-scrolled .nav-list a.nav-pill { color: var(--aubergine); }
  /* The current page keeps an underline, but a short centred one rather than
     the element's own border. The link is a full-width row for the tap target,
     so a border-bottom ran the whole width and read as a divider — sitting, as
     it happened, directly above the real divider above Book, which made both
     ambiguous. A fixed short rule under the label is unmistakably a marker. */
  .nav-list a.nav-pill:hover,
  .nav-list a.nav-pill[aria-current="page"],
  .site-header--on-dark .nav-list a.nav-pill[aria-current="page"],
  .site-header.is-scrolled .nav-list a.nav-pill[aria-current="page"] { border-color: transparent; }
  .nav-list a.nav-pill { position: relative; }
  .nav-list a.nav-pill[aria-current="page"]::after {
    content: ''; position: absolute; left: 50%; transform: translateX(-50%);
    bottom: 0.45em; width: 3.25rem; height: 1px; background: currentColor;
  }
  /* The Book pill is a filled button, not a text link — an underline inside it
     would read as an underlined button label. */
  .nav-list a.nav-pill--book[aria-current="page"]::after { content: none; }

  /* Book is the one action on this screen, so here it is a filled aubergine
     pill rather than an outline. Lime, which carries it on the dark header,
     sits at roughly 1:1 against blush and would be illegible; aubergine with
     white text clears 17:1 and reads unmistakably as the thing to press. */
  .nav-list a.nav-pill--book,
  .site-header--on-dark .nav-list a.nav-pill--book,
  .site-header.is-scrolled .nav-list a.nav-pill--book {
    min-height: 52px; margin-top: var(--space-xs);
    background: var(--aubergine); border-color: var(--aubergine); color: var(--white);
  }
  .nav-list a.nav-pill--book:hover,
  .nav-list a.nav-pill--book[aria-current="page"],
  .site-header--on-dark .nav-list a.nav-pill--book[aria-current="page"],
  .site-header.is-scrolled .nav-list a.nav-pill--book[aria-current="page"] {
    background: var(--aubergine-ink); border-color: var(--aubergine-ink); color: var(--white);
  }
}

/* ---- Footer ---- */
.site-footer { background: var(--grad-aubergine); color: var(--blush); padding-block: var(--space-xl) var(--space-lg); }
.footer-grid { display: grid; grid-template-columns: 1.3fr 1fr 1fr; gap: var(--space-lg); }
.site-footer h4 { color: var(--white); margin-bottom: var(--space-sm); }
.site-footer a { text-decoration: none; color: var(--blush); }
.site-footer a:hover { color: var(--lime); }
.footer-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.6em; }
.footer-bottom {
  margin-top: var(--space-lg); padding-top: var(--space-md); border-top: 1px solid rgba(244,210,236,0.18);
  display: flex; flex-wrap: wrap; gap: var(--space-sm); justify-content: space-between; font-size: var(--step--1); color: rgba(244,210,236,0.75);
}
@media (max-width: 720px) { .footer-grid { grid-template-columns: 1fr; } }

/* ---- Cards ----
   Cards always reset text to --ink regardless of the section they sit in,
   so a light card dropped onto a dark section (e.g. the booking form on
   the aubergine section) never inherits low-contrast light-on-light text. */
.card {
  background: var(--white); color: var(--ink); border-radius: var(--radius-soft); padding: var(--space-lg);
  border: 1px solid rgba(83, 36, 57, 0.14);
}
.card h1, .card h2, .card h3 { color: var(--aubergine); }

/* ---- Hero ---- */
.hero {
  min-height: 100vh; /* fallback for browsers without svh support */
  min-height: 100svh; /* accounts for mobile browser chrome, unlike 100vh */
  display: flex;
  align-items: center;
  padding-block: var(--space-2xl);
  position: relative;
  overflow: hidden;
}

/* ---- Hero photograph (home only) ----
   The photo is a real <img> inside the hero rather than a CSS background-image
   for two reasons: the single-file preview build inlines images by their
   src attribute (a background url() would silently go missing there), and an
   <img> gives us width/height for the aspect box and fetchpriority for the LCP.

   isolation: isolate creates a stacking context on the hero, which is what
   lets .hero-bg sit at z-index: -1 — above the section's own aubergine fill
   (that fill is the fallback if the photo is slow or fails) but below all the
   hero content, without needing a z-index on every text element.

   The scrim is var(--aubergine) at an opacity, not a hardcoded rgba(), so it
   follows the brand colour if it's ever changed in the design editor. The
   opacity is set high enough that the white heading, the white body copy and
   the lime eyebrow all clear WCAG AA against the *brightest* part of the
   photograph, not just its average — see the measured figures in the notes. */
/* Content is pushed in by the width of the frame on top of the container's own
   gutter, so nothing sits outside the photo. Without this the full-width phone
   CTAs and the eyebrow's leading rule — which align to the container gutter,
   narrower than the frame — hang over the frame's edge onto the flat aubergine.
   Absolutely positioned children measure from the padding box, so this moves
   the content without moving .hero-bg. */
.hero--photo { isolation: isolate; padding-inline: var(--hero-frame, var(--space-lg)); }
/* Inset, not full-bleed: the photo sits in a frame of the section's own
   aubergine, so the hero reads as a mounted image rather than a background.
   The inset uses a space token, so it scales down on phones with everything
   else — a 40px frame around a 375px screen would be a border, not a margin.
   The corner radius is the same --radius-soft every other image on the site
   uses, and .hero's overflow: hidden keeps the photo inside it. */
.hero-bg {
  position: absolute; inset: var(--hero-frame, var(--space-lg)); z-index: -1;
  /* The top edge clears the floating header as well as the frame, so the nav
     and wordmark sit on flat aubergine instead of being cut in half by the
     photo's edge. That asymmetry is deliberate — it's what makes the hero
     read as a mounted image under a header rather than a cropped background. */
  top: calc(var(--hero-frame, var(--space-lg)) + var(--header-h));
  border-radius: var(--radius-soft); overflow: hidden;
}
/* The photo keeps its own colour — deliberately, by the client's choice. A
   desaturate-then-tint duotone makes the hero read more purely aubergine, but
   it costs the warmth of the wood and the paints, so the veil sits over the
   full-colour photograph and the result leans warm. If it ever needs to read
   more aubergine again, `filter: grayscale(1)` here is the whole change. */
.hero-bg img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 45%; display: block; }
.hero-bg::after {
  content: ""; position: absolute; inset: 0;
  background: var(--aubergine);
  opacity: var(--hero-scrim-opacity, 0.80);
}

.hero-grid { display: grid; grid-template-columns: 1.1fr 0.9fr; gap: var(--space-xl); align-items: center; }
/* Image ratios live here rather than as inline styles on each block, so the
   phone breakpoint below can retune them all in one place. */
.hero-media { border-radius: var(--radius-soft); overflow: hidden; aspect-ratio: 4/5; position: relative; }
.hero-media--square { aspect-ratio: 1/1; }
.hero-media--wide { aspect-ratio: 16/10; }
/* Real photos fill the slot's shape rather than dictating it, so a landscape
   photo can sit in the square desktop slot and the 3/2 phone one without the
   layout shifting around it. */
.hero-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
figure.hero-media { margin: 0; }
/* Credit line over the image. The gradient is what makes it legible: these
   are photographs, so the corner behind the text could be any tone — here
   it's a bright magazine page — and a flat colour would either fail on
   light photos or sit as a hard band on dark ones. */
.hero-media figcaption {
  position: absolute; inset-inline: 0; bottom: 0; z-index: 1;
  padding: var(--space-lg) var(--space-md) var(--space-sm);
  font-family: var(--font-accent); font-weight: 700;
  font-size: var(--step--1); letter-spacing: 0.06em;
  color: var(--white);
  background: linear-gradient(180deg, rgba(41,18,29,0) 0%, rgba(41,18,29,0.58) 28%, rgba(41,18,29,0.95) 100%);
}
@media (max-width: 900px) { .hero-grid { grid-template-columns: 1fr; } }
/* A portrait or square image is a good half-column shape; full-bleed on a
   phone it becomes a 375px-tall wall you have to scroll past to reach the
   next sentence. Narrow screens get a landscape band instead. */
@media (max-width: 600px) {
  .hero-media, .hero-media--square, .hero-media--wide { aspect-ratio: 3/2; }
  /* ...except where the photo is of a person. A 3/2 crop of an upright figure
     either takes the top off her head or cuts her off at the mouth, whichever
     way you bias it, so these slots stay upright on phones and accept the
     extra height. Opt in per image — the rule above is still the default. */
  .hero-media--portrait-subject { aspect-ratio: 4/5; }
}

/* ---- Scroll-down cue (bottom of every hero) ---- */
.scroll-cue {
  position: absolute; left: 50%; bottom: var(--space-md);
  color: var(--aubergine); opacity: 0.45;
  animation: scroll-cue-bob 2.4s ease-in-out infinite;
  pointer-events: none;
}
.scroll-cue.on-dark { color: var(--blush); }
.scroll-cue svg { width: 22px; height: 22px; display: block; }
@keyframes scroll-cue-bob {
  0%, 100% { transform: translate(-50%, 0); }
  50% { transform: translate(-50%, 8px); }
}
@media (prefers-reduced-motion: reduce) { .scroll-cue { animation: none; } }

/* ---- Placeholder imagery block (organic gradient + texture, swap with real photos) ---- */
.ph-img {
  width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
  color: rgba(255,255,255,0.75); font-family: var(--font-sans); font-size: var(--step--1); text-align: center; padding: var(--space-md);
  position: relative; overflow: hidden;
}
.ph-img::after {
  content: ''; position: absolute; inset: 0; opacity: 0.5; mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
.ph-img span { position: relative; z-index: 1; max-width: 20ch; }
.ph-1 { background: linear-gradient(155deg, var(--terracotta), var(--aubergine)); }
.ph-2 { background: linear-gradient(155deg, var(--blush), var(--terracotta)); color: var(--aubergine); }
.ph-3 { background: linear-gradient(155deg, var(--nude), var(--olive)); color: var(--aubergine); }
.ph-4 { background: linear-gradient(155deg, var(--olive), var(--aubergine)); }
.ph-5 { background: linear-gradient(155deg, var(--aubergine), var(--terracotta-deep)); }
.ph-6 { background: linear-gradient(155deg, var(--lime), var(--nude)); color: var(--olive); }

/* Home invitation cards: a unique gradient per card (reusing the ph-*
   gradients + grain texture above) instead of a plain white card. Text
   colour switches per card so it stays legible on both the light and
   dark ends of these gradients. */
/* The photo is the tile's background — it fills the whole card, with the
   copy sitting on top of it. A dark scrim between the two keeps the text
   readable over any photograph, however light or busy it turns out to be;
   without it, legibility would depend on whatever image happens to be
   dropped in. */
.invite-card {
  border-radius: var(--radius-soft); padding: var(--space-md);
  position: relative; overflow: hidden;
  /* Text block centred on the y axis; left alignment and the padding inset on
     the x axis are unchanged. */
  display: grid; align-content: center; gap: 0;
  min-height: clamp(12rem, 36vh, 22rem);
  color: var(--white);
}
.invite-card__bg { position: absolute; inset: 0; z-index: 0; }
.invite-card__bg img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* Strongest through the middle band, where the copy now sits, easing off at
   both edges so the image still reads as an image rather than a flat dark
   panel. This tracked the text: it used to be heaviest at the top, which is
   where the copy used to be. */
.invite-card::before {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg,
    rgba(41,18,29,0.52) 0%,
    rgba(41,18,29,0.80) 28%,
    rgba(41,18,29,0.80) 72%,
    rgba(41,18,29,0.52) 100%);
}
/* A ch-based cap rather than a hard-coded break, so it survives copy edits:
   12ch is under the 13.4ch that "Come as you are" needs on one line, and over
   the ~11.2ch of the longest word-pair in "Give yourself permission" — so all
   four titles land on exactly two lines, which text-wrap: balance then splits
   evenly. */
.invite-card h3 { max-width: 12ch; }
.invite-card__sub { font-size: var(--step--1); }
/* One condensed line or two from the session's full Book-page description.
   Capped in ch so it wraps to a readable measure rather than the full width
   of the card, and given a little air under the technique label above it. */
.invite-card__desc { font-size: var(--step--1); max-width: 34ch; margin-top: var(--space-xs); }
.invite-card .eyebrow { color: var(--lime); }
.invite-card h3, .invite-card p { color: var(--white); }
.invite-card > *:not(.invite-card__bg) { position: relative; z-index: 2; }
/* The last line's bottom margin counts as part of the grid's content height,
   so with align-content: center it pushes the visible text ~15px above the
   true centre. Drop it so the block optically centres. */
.invite-card > *:last-child { margin-bottom: 0; }


/* ---- Grid utilities ---- */
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-lg); }
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-lg); }
.grid-4 { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-md); }
@media (max-width: 900px) { .grid-3 { grid-template-columns: 1fr 1fr; } .grid-4 { grid-template-columns: 1fr 1fr; } }
@media (max-width: 600px) { .grid-2, .grid-3, .grid-4 { grid-template-columns: 1fr; } }

/* Photo + text sections (Welcome, About story, Experience mid-page) get
   more breathing room between the image and the copy than a plain grid-2. */
.grid-2:has(.hero-media) { gap: var(--space-xl); }

/* Home "Four creative invitations": a text+visual grid-2 like Welcome,
   but the visual side is a 2x2 card grid instead of a photo. Below 900px
   the two sides stack so the nested card grid keeps a comfortable width. */
/* Cards now carry a photo each, so they take the larger share of the row and
   sit further apart. The section still has to hold one viewport, so the
   growth comes out of the text column rather than adding height. */
.invitations-grid { grid-template-columns: 0.8fr 1.2fr; }
.invitations-cards { gap: var(--space-md); }
/* Taller cards + a full-viewport section is a tight fit on shorter laptop
   screens, so this one section trades some of its own padding to keep
   everything on one screen rather than spilling. */
@media (min-width: 901px) {
  .section--invitations { padding-block: var(--space-xl); }
}
@media (max-width: 900px) { .invitations-grid { grid-template-columns: 1fr; } }

/* ---- Steps / process list (Experience page) ----
   A 3-up grid rather than one long stacked column, so all six steps sit
   in two short rows and the section can fit a single viewport. */
.step-list { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-lg) var(--space-md); counter-reset: step; }
@media (max-width: 900px) { .step-list { grid-template-columns: 1fr 1fr; } }
@media (max-width: 600px) { .step-list { grid-template-columns: 1fr; } }
/* align-items: baseline lines the number up with the first line of text
   next to it (the "— STEP ONE" eyebrow), rather than floating centered
   across the whole eyebrow+title block. */
.step { display: grid; grid-template-columns: auto 1fr; gap: var(--space-sm); align-items: baseline; }
.step-num {
  counter-increment: step; font-family: var(--font-display); font-size: var(--step-3); color: var(--terracotta-deep);
  width: 2.2ch; line-height: 1;
}
.step-num::before { content: counter(step, decimal-leading-zero); }
.step p { font-size: var(--step--1); }

/* ---- FAQ accordion ----
   A wide, simple, line-separated list (no card boxes): a rule above the
   first item and below every item, question left / chevron right. */
.accordion { border-top: 1px solid rgba(43,22,32,0.14); }
.accordion-item { border-bottom: 1px solid rgba(43,22,32,0.14); }
.accordion-item summary {
  cursor: pointer; list-style: none; padding: var(--space-md) 0;
  font-family: var(--font-accent); font-weight: 700; font-size: var(--step-0);
  display: flex; align-items: center; justify-content: space-between; gap: var(--space-md);
}
.accordion-item summary::-webkit-details-marker { display: none; }
.accordion-item summary .icon { flex-shrink: 0; width: 1.4rem; height: 1.4rem; transition: transform 0.25s ease; color: var(--terracotta-deep); }
.accordion-item[open] summary .icon { transform: rotate(180deg); }
.accordion-body { padding: 0 0 var(--space-md); color: var(--ink); }
.accordion-body p { max-width: 68ch; }
.accordion-item summary:focus-visible { outline-offset: -3px; }

/* ---- Session / Book cards ----
   Four across, one per session, so the whole series is comparable at a glance
   without scrolling. Each card carries the same .ph-N gradient as that
   session's invitation tile on the home page, so a session is the same colour
   and the same artwork in both places. */
/* Wider than the site container: four cards at the standard 72rem leaves each
   one a narrow column, so this section is allowed to run out to 84rem (still
   inside a 1440 viewport with room either side) and the gap is tightened a
   step. Both go to the cards. Below 1440 the width: 100% keeps it inside the
   viewport as normal. */
.section--sessions > .container { max-width: 88rem; width: 100%; }
.session-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-lg); align-items: stretch; }
/* Flex column rather than grid with a positional `grid-template-rows`.
   The old template was `auto auto auto auto 1fr auto`, which put the flexible
   row on the description *by counting position*. Adding the session time as a
   new element shifted every row along by one, so the 1fr landed on the
   technique line instead — which then absorbed all the slack and pushed the
   description halfway down cards 2, 3 and 4 while card 1, whose copy filled
   the space anyway, looked fine. A column with `margin-top: auto` on the
   price row expresses the actual intent — content packed from the top, price
   pinned to the bottom — and cannot be broken by inserting another line. */
.session-card {
  display: flex; flex-direction: column; gap: var(--space-xs);
  color: var(--ink); border-radius: var(--radius-soft); padding: var(--space-md);
  position: relative; overflow: hidden;
}
/* Cards are stretched to the tallest sibling, so nothing should squeeze the
   photo — but flex items shrink by default along the main axis, and here that
   axis is vertical. */
.session-card .hero-media { flex-shrink: 0; }
/* The gradient is veiled back to a tint rather than shown at full strength.
   At full strength these cards fail on body copy — the readings are white on
   ph-1's terracotta end 4.31:1, aubergine on ph-2's terracotta end 4.09:1, and
   aubergine on ph-3's olive end 1.74:1, all against a 4.5:1 requirement, and
   no single text colour fixes a gradient that runs light-to-dark underneath a
   paragraph. A cream veil keeps each card's hue and direction while putting
   every card back on a light surface, where the existing dark text clears
   12:1. The home page's tiles don't need this: their photos sit under a heavy
   dark scrim with white text. */
.session-card::before {
  content: ''; position: absolute; inset: 0;
  background: var(--cream); opacity: 0.82;
}
.session-card > * { position: relative; }
.session-card h3 { color: var(--aubergine); margin-bottom: 0; }
/* A quarter-width column is a short measure, so the description drops a step
   to keep the four cards inside one viewport. */
.session-card p { margin-bottom: 0; max-width: none; }
.session-card .session-desc { font-size: var(--step--1); }
.session-card .hero-media--wide { aspect-ratio: 16/9; margin-bottom: var(--space-xs); }
/* align-items: end, not center: only one card carries an offer, so its price
   block is two lines taller than the others. Centring the button inside that
   taller row lifts it out of line with the other three; bottom-aligning keeps
   all four buttons on the same line across the row. */
.session-card .price-row { display: flex; align-items: end; justify-content: space-between; gap: var(--space-sm); margin-top: var(--space-xs); padding-top: var(--space-sm); border-top: 1px solid rgba(83,36,57,0.18); }
/* Old price, new price, offer label — one stacked block beside the button.
   The old price is struck through with <s> and carries a visually-hidden
   "Was "/"now " for screen readers, which otherwise get two bare numbers with
   no indication which one applies. It stays fully legible rather than being
   faded to a hint: it's still information a customer is reading. */
.session-card .price-block { display: grid; gap: 0.1em; margin: 0; }
.session-card .price-was { font-family: var(--font-display); font-size: var(--step-0); color: var(--ink); opacity: 0.75; line-height: 1.1; }
.session-card .price { font-family: var(--font-display); font-size: var(--step-2); line-height: 1.1; }
.session-card .price-unit { font-size: 0.5em; }
.session-card .price-note {
  font-family: var(--font-accent); font-weight: 700; font-size: var(--step--1);
  text-transform: uppercase; letter-spacing: 0.1em;
}
/* A deeper terracotta for the small accent text on these cards only.
   The standard --terracotta-deep reaches just 3.65-4.34:1 against the veiled
   card tints (they sit darker than the cream the rest of the site's light
   sections use), and both of these are small bold text needing 4.5:1. This is
   the same hue taken down about a quarter, which clears it on all four cards.
   It is deliberately NOT a new palette token: everywhere else on the site the
   standard terracotta-deep still applies, so the design editor's colours are
   untouched. */
.session-card .eyebrow,
.session-card .price-note { color: #7a3723; }
/* "Hava No.1 · 26 September" is the longest of the four and was wrapping.
   Tighter tracking than the site's 0.14em eyebrows buys back ~13px of it —
   the rest comes from the wider container above. From 1400px up there's
   enough room to forbid the wrap outright; below that a four-column card is
   simply narrower than the line, so it's allowed to wrap rather than
   overflow. */
.session-card .eyebrow { letter-spacing: 0.1em; }
@media (min-width: 1400px) { .session-card .eyebrow { white-space: nowrap; } }
/* The session time sits on its own line rather than inside the eyebrow.
   Appending it there pushed that nowrap line to 390px inside a 310px card,
   which overflowed and clipped the photo and the Book button on every card.
   As a separate line it also reads better: the label stays a label, and the
   practical detail is set quietly beneath it in body type rather than as a
   second run of uppercase tracking. 75% ink measures 6.3–6.8:1 on all four
   card tints, so it stays comfortably readable despite being understated. */
.session-when {
  font-family: var(--font-sans);
  font-size: var(--step--1);
  color: var(--ink); opacity: 0.75;
  margin: calc(var(--space-sm) * -0.6) 0 0;
}
/* Between roughly 1150 and 1300px the four-column grid squeezes each card to
   ~258px, and Hava No.1's price block — struck-through £35, £30, and the
   "Introductory offer" note — is wide enough that the price and the Book
   button no longer fit on one line. Left unwrapped that pushed the button a
   few pixels past the card edge, where the card's overflow clipped it. The
   phone breakpoint already allowed this row to wrap; letting it wrap at every
   width costs nothing when there is room and prevents the clip when there
   isn't. */
.session-card .price-row {
  flex-wrap: wrap; row-gap: var(--space-sm);
  /* Pins the price row to the bottom of the card so all four line up, whatever
     the length of the description above. Declared here rather than with the
     flex-column rule above because the fuller .price-row rule in between sets
     its own margin-top and would otherwise win. */
  margin-top: auto;
}
/* The card's Book button is the same control as the header's Book pill, so it
   is set the same way: sans rather than the accent face, sentence case rather
   than uppercase, normal tracking, and the same 0.7em/1.5em pill padding at
   the same --step--1 size. Only the colour differs, because these sit on a
   light card rather than the dark header. */
.session-card .price-row .btn {
  font-family: var(--font-sans); font-weight: 700; font-size: var(--step--1);
  text-transform: none; letter-spacing: normal;
  padding: 0.7em 1.5em;
}
/* Four across is a tighter section than the site's default rhythm; the extra
   vertical padding is what pushes it past a laptop viewport. */
.section--sessions { padding-block: var(--space-xl); }
/* Four across holds down to ~900px — a 1024 laptop still fits all four inside
   one viewport, at 227px a card. Below that the description gets too narrow to
   read comfortably, so two, then one on a phone. */
@media (max-width: 900px) { .session-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 620px) { .session-grid { grid-template-columns: 1fr; } }

/* ---- Legal pages (privacy, booking terms) ----
   Long-form prose in a narrow measure. Section headings step down from the
   site's display sizes — a policy is a document to read, not a landing page,
   so h2 here sits closer to the body than it does elsewhere. */
.hero--short { min-height: 0; padding-block: calc(var(--space-2xl) + 3.25rem) var(--space-xl); }
.hero--short .scroll-cue { display: none; }
.legal-updated { font-family: var(--font-accent); font-size: var(--step--1); opacity: 0.75; margin-top: var(--space-md); }
.legal-body { display: grid; gap: var(--space-lg); }
.legal-section h2 { font-size: var(--step-2); margin-bottom: var(--space-xs); }
.legal-section p, .legal-section li { max-width: 68ch; }
.legal-section ul { display: grid; gap: 0.4em; margin: 0 0 var(--space-sm); }
.legal-section a { color: inherit; }
/* Unfilled placeholders. Deliberately loud: these are the bits only the
   client can supply, and shipping a policy with one still in it would be
   worse than shipping no policy at all. */
mark.todo {
  background: var(--lime); color: var(--aubergine);
  font-family: var(--font-accent); font-weight: 700; font-size: 0.85em;
  text-transform: uppercase; letter-spacing: 0.06em;
  padding: 0.15em 0.5em; border-radius: 0.3em;
}
.footer-legal { display: flex; gap: var(--space-md); }
.footer-legal a { color: rgba(244,210,236,0.85); }

/* ---- Forms ---- */
.form-grid { display: grid; gap: var(--space-md); }

/* Booking form: a lot of fields in one section, so it gets tighter
   vertical rhythm than other sections to come closer to fitting a
   single viewport (see build notes for the exact numbers). */
#booking-form { padding-block: var(--space-md); }
/* Intro copy left, form right, with the intro centred against the form's
   height. The plain .grid-2 rule doesn't stack until 600px, which would leave
   the form in a ~270px column on a tablet — stack at 900px instead. When it
   stacks, centring would leave an odd gap, so the intro goes back to the top. */
.booking-grid { align-items: center; }
@media (max-width: 900px) { .booking-grid { grid-template-columns: 1fr; gap: var(--space-md); align-items: start; } }
#booking-form form { gap: var(--space-sm); padding: var(--space-md); }
#booking-form textarea { min-height: 4rem; }
#booking-form .checkbox-field { gap: 0.5em; }
/* min-width: 0 / width: 100% keep controls inside their column: a <select>
   sizes itself to its longest <option> by default, which on the Book form
   ("HAVA NO.1 — Come as you are (26 September)") ran wider than a phone
   screen and got clipped by the section's overflow. */
/* align-content: start keeps the label/hint/control rows at their natural
   heights. Without it, two fields side by side in a .grid-2 are stretched to
   equal height and the extra space is shared between their rows — so a field
   with a hint and a field without one end up with their inputs on different
   baselines, which reads as a mistake. */
.field { display: grid; gap: 0.4em; min-width: 0; align-content: start; }
.field input, .field textarea, .field select { width: 100%; max-width: 100%; min-width: 0; }
.field label { font-family: var(--font-accent); font-weight: 700; font-size: var(--step--1); }
.field .hint { font-size: var(--step--1); color: rgba(43,22,32,0.65); font-weight: 400; }
/* Underline-only fields — no boxed/filled inputs, just a bottom rule
   that darkens on focus. */
.field input, .field textarea, .field select {
  font: inherit; padding: 0.6em 0.1em; border-radius: 0; background: transparent; color: var(--ink);
  border: none; border-bottom: 1px solid rgba(43,22,32,0.3);
}
.field input:focus, .field textarea:focus, .field select:focus { outline: none; border-bottom-color: var(--aubergine); }
.field textarea { resize: vertical; min-height: 8rem; }
/* Browser autofill (saved name/email) forces its own opaque background
   and dark text via -webkit-autofill — override both so a filled field
   still looks like our underline-only style instead of a highlighted box. */
.field input:-webkit-autofill,
.field input:-webkit-autofill:hover,
.field input:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0px 1000px transparent inset;
  -webkit-text-fill-color: var(--ink);
  caret-color: var(--ink);
  transition: background-color 9999s ease-in-out 0s;
}
/* Book's form sits on the dark aubergine section, so its fields, labels
   and notes get light-on-dark colours instead of the ink-on-light default. */
#booking-form .field label, #booking-form .checkbox-field label { color: var(--blush); }
#booking-form .field .hint, #booking-form .required-note { color: rgba(244,210,236,0.65); }
#booking-form .field input, #booking-form .field textarea, #booking-form .field select {
  color: var(--white); border-bottom-color: rgba(244,210,236,0.35);
}
#booking-form .field input:-webkit-autofill,
#booking-form .field input:-webkit-autofill:hover,
#booking-form .field input:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--white);
  caret-color: var(--white);
}
#booking-form .field input:focus, #booking-form .field textarea:focus, #booking-form .field select:focus { border-bottom-color: var(--lime); }
.checkbox-field { display: flex; align-items: flex-start; gap: 0.6em; }
.checkbox-field input { margin-top: 0.3em; }
.required-note { font-size: var(--step--1); color: rgba(43,22,32,0.6); }
.form-status { padding: var(--space-sm) var(--space-md); border-radius: 0.9em; background: var(--nude); color: var(--aubergine); display: none; }
.form-status.is-visible { display: block; }

/* ---- Gallery ---- */
.gallery-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: var(--space-md); }
.gallery-grid .ph-img { border-radius: var(--radius-soft); aspect-ratio: 1/1; }
.gallery-grid .tall { aspect-ratio: 3/4; }
@media (max-width: 900px) { .gallery-grid { grid-template-columns: 1fr 1fr; } }
/* Phones keep the two-column mosaic rather than collapsing to one tile per
   screen — nine full-width squares turn a gallery into 3,000px of scrolling,
   and the mixed square/tall rhythm is what makes it read as a gallery at all.
   Tighter gaps and padding keep the small tiles from feeling cramped. */
@media (max-width: 560px) {
  .gallery-grid { gap: var(--space-sm); }
  .gallery-grid .ph-img { padding: var(--space-sm); font-size: var(--step--1); }
  /* An odd tile count leaves the last one stranded in a half-width column;
     let it run the full width instead so the mosaic closes cleanly. */
  .gallery-grid > .ph-img:last-child:nth-child(odd) { grid-column: span 2; aspect-ratio: 2/1; }
}

/* ---- Utility text ---- */
.text-center { text-align: center; }
.max-prose { max-width: 62ch; }
.mx-auto { margin-inline: auto; }
.mt-lg { margin-top: var(--space-lg); }
.mt-md { margin-top: var(--space-md); }
.stack { display: grid; gap: var(--space-sm); }
.cluster { display: flex; flex-wrap: wrap; gap: var(--space-sm); align-items: center; }

/* ---- Scroll reveal (progressive enhancement) ----
   Only takes effect once main.js confirms it will handle it — it adds
   .js-reveal to <html>, so with JS disabled (or reduced motion requested)
   everything just renders normally; nothing is ever stuck hidden.
   Content already on screen when the page loads is marked .is-visible
   immediately (synchronously, before first paint) so it appears at once
   with no animation — only content the visitor actually scrolls to gets
   the fade/rise-in, per "content should only reveal as you scroll".

   The animated target is each section's inner .container/.container-narrow
   (its content), never the .hero/.section element itself — that element
   carries the section's background colour/gradient, which should always
   render immediately, not fade in along with the content sitting on it.

   :not(.hero-intro) excludes the home hero's content — it gets its own
   always-plays-on-load animation below instead of the scroll-triggered
   one here, since it's above the fold and this system only reveals
   content once actually scrolled to. */
.js-reveal :is(.hero, .section) > :is(.container, .container-narrow):not(.hero-intro) {
  opacity: 0; transform: translateY(24px);
  transition: opacity 0.7s ease, transform 0.7s ease;
}
.js-reveal :is(.hero, .section).is-visible > :is(.container, .container-narrow):not(.hero-intro) { opacity: 1; transform: translateY(0); }

.js-reveal :is(.grid-2, .grid-3, .grid-4, .step-list, .accordion, .gallery-grid) > * {
  opacity: 0; transform: translateY(16px);
  transition: opacity 0.5s ease, transform 0.5s ease;
  transition-delay: calc(var(--reveal-i, 0) * 80ms);
}
.js-reveal :is(.hero, .section).is-visible :is(.grid-2, .grid-3, .grid-4, .step-list, .accordion, .gallery-grid) > * {
  opacity: 1; transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  .js-reveal :is(.hero, .section) > :is(.container, .container-narrow):not(.hero-intro),
  .js-reveal :is(.grid-2, .grid-3, .grid-4, .step-list, .accordion, .gallery-grid) > * {
    opacity: 1; transform: none; transition: none;
  }
}

/* ---- Home hero intro (always plays on load, independent of scroll) ----
   The hero is above the fold on load, so the scroll-reveal system above
   deliberately leaves it alone (it's the one thing that should NOT wait
   for a scroll). This is a separate, pure-CSS fade + gentle rise that
   just plays once the page loads — no JS involved, so it can never be
   left in a stuck-hidden state even if JS fails. */
.hero-intro {
  animation: hero-intro-in 0.9s ease both;
}
@keyframes hero-intro-in {
  from { opacity: 0; transform: translateY(18px); }
  to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .hero-intro { animation: none; }
}


/* ==========================================================================
   PHONE COMPOSITION
   Everything above is written mobile-out (fluid tokens, stacking grids).
   This block is the part that isn't just "the desktop layout, narrower":
   the handful of places where a phone wants a genuinely different
   composition — full-width actions, copy before imagery, body text that
   doesn't shrink to caption size, and touch targets sized for a thumb.
   ========================================================================== */
@media (max-width: 600px) {
  /* Actions span the column. A 140px pill floating in a 375px screen is a
     desktop button that happens to be on a phone; a full-width control is
     what a thumb expects, and it lands in the same place every time. */
  .btn { width: 100%; max-width: 26rem; padding-block: 0.95em; }
  .text-center .btn, .btn.mx-auto { margin-inline: auto; }
  .cluster { flex-direction: column; align-items: stretch; }
  .cluster .btn { width: 100%; max-width: none; }
  /* ...except where a button is deliberately paired with something else on
     one line, or is a nav pill sized by the header. */
  .price-row .btn, .nav-list .btn { width: auto; }
  .price-row { flex-wrap: wrap; gap: var(--space-sm); }
  .price-row .btn { flex: 1 1 auto; }

  /* Copy leads, image follows — consistently. On desktop these sections
     alternate the photo left/right for rhythm, which when stacked means
     some sections open with a wall of image and others with text. One
     predictable order reads as designed for the phone.
     The child combinators matter: without them this also matches the photo
     nested inside each Book session card, flipping those cards' own
     image-then-copy order. */
  .grid-2:has(> .hero-media) > .hero-media { order: 2; }

  /* Body copy stays body copy. These two were sized down to fit desktop
     columns; at phone width there's no column to fit, just small text. */
  .step p, .invite-card__sub { font-size: var(--step-0); }

  /* Forms: full-width submit, and a comfortable gap between stacked
     fields now that they're no longer side by side. */
  .form-grid { gap: var(--space-md); }
  .form-grid .btn { justify-self: stretch; }
  .field input, .field select, .field textarea { padding-block: 0.7em; }
  .field textarea { min-height: 6.5rem; }
  #booking-form textarea { min-height: 5rem; }

  /* Accordion rows get a taller hit area than the desktop rule gives. */
  .accordion-item summary { padding-block: var(--space-md); min-height: 56px; }

  /* Decorative motifs are sized for a wide canvas; at phone width they
     crowd the copy they're meant to sit behind. */
  .petal-motif--center { width: 260px; opacity: 0.1; }
  .petal-motif--corner-tr, .petal-motif--corner-bl { width: 150px; }
}

/* Heroes stay full-height on a phone, but the fixed header floats over
   them — so their content is centred in the space BELOW the header rather
   than behind it. */
@media (max-width: 860px) {
  .hero { padding-top: calc(var(--space-2xl) + 3.25rem); }
}

/* ---- Live availability on the session cards ----
   Written in by js/main.js from /api/availability. Hidden until there is
   something to say: a session with plenty of room shows nothing at all, so
   the notice only ever appears when it carries information. */
.session-places {
  font-family: var(--font-accent); font-weight: 700; font-size: var(--step--1);
  text-transform: uppercase; letter-spacing: 0.1em;
  color: #7a3723; margin: 0;
}
.session-places.is-full { color: var(--aubergine); }
/* A sold-out card steps back rather than disappearing — people still want to
   see what they missed, and it tells them what the series looks like. */
.session-card.is-full .hero-media { opacity: 0.55; }
.session-card.is-full .price-block { opacity: 0.6; }
/* A sold-out CTA is a statement, not a dimmed invitation. It stays fully
   opaque and takes a solid aubergine fill, so it reads as a stamp across the
   card rather than a button someone keeps trying to press. The faded image and
   price above it already carry the "this one has gone" signal; this just has to
   be unambiguous. pointer-events: none plus the removed href (js/main.js) make
   it genuinely inert and out of the tab order, not merely styled that way. */
.btn.is-disabled {
  background: var(--aubergine); border-color: var(--aubergine); color: var(--white);
  opacity: 1; cursor: not-allowed; pointer-events: none; transform: none;
}
/* Errors in the booking form have to be visibly different from the friendly
   confirmation the same element shows on success. */
.form-status.is-error { color: var(--white); font-weight: 700; }
#booking-form .form-status.is-error { color: var(--lime); }

/* ---- Founder portrait + Instagram handle (About hero, and the footer) ----
   The portrait sits under the About hero's subhead as a signature to the
   statement above it, rather than as a feature image — hence the small,
   circular treatment. It is above the fold, so it is NOT lazy-loaded.

   Colour note, measured rather than assumed: terracotta-deep on the blush
   gradient runs 4.77:1 at the top of the section down to 3.87:1 at the
   bottom, so it is not safe for small text this far down the hero. The
   handle itself is therefore aubergine (well past 4.5:1 anywhere on that
   gradient) and only the glyph — a non-text element, needing 3:1 — keeps the
   terracotta accent. */
.founder {
  margin-top: var(--space-lg);
  display: flex; flex-direction: column; align-items: center; gap: 0.85rem;
}
.founder__photo {
  width: clamp(112px, 12vw, 168px);
  /* The <img> carries width/height attributes (720x720) so the browser can
     reserve space before it loads. Those attributes also set the CSS height,
     which makes both dimensions definite and causes aspect-ratio to be
     ignored — the portrait renders 168px wide by 720px tall, a lozenge rather
     than a circle. height:auto hands the vertical dimension back to
     aspect-ratio while keeping the attributes doing their anti-CLS job. */
  height: auto;
  aspect-ratio: 1 / 1;
  border-radius: 50%;
  object-fit: cover;
  box-shadow: 0 12px 32px rgba(41, 18, 29, 0.16);
}
.social-link {
  display: inline-flex; align-items: center; gap: 0.5em;
  text-decoration: none;
  font-family: var(--font-accent); font-weight: 700;
  font-size: var(--step--1); letter-spacing: 0.06em;
}
.social-link svg { width: 1.15em; height: 1.15em; flex-shrink: 0; }
.social-link span { border-bottom: 1px solid transparent; transition: border-color 0.2s ease; }
.social-link:hover span { border-bottom-color: currentColor; }

.founder__handle { color: var(--aubergine); }
.founder__handle svg { color: var(--terracotta-deep); }

/* In the footer it sits on aubergine, where blush clears 12.7:1 and the lime
   hover matches every other footer link. */
.social-link--footer { color: var(--blush); margin-top: 0.9rem; }
.social-link--footer:hover { color: var(--lime); }

/* ---- Feedback form: multiple-choice rows ----
   Three options that fit on one line at any width, as a fieldset so the
   question is announced with its answers rather than as a stray label. The
   native radio is kept (not hidden and redrawn) because it comes with keyboard
   and screen-reader behaviour that a div cannot be talked into. */
.field--choices { border: 0; padding: 0; margin: 0; min-width: 0; }
.field--choices legend {
  padding: 0; margin-bottom: 0.6em;
  font-family: var(--font-accent); font-weight: 700; font-size: var(--step--1);
}
.choice-row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
.choice {
  display: inline-flex; align-items: center; gap: 0.5em;
  /* Two-word answers ("Not really", "About right") were wrapping inside the
     pill and giving each option a different height. They are short enough to
     keep whole; the row wraps between pills instead. */
  white-space: nowrap;
  padding: 0.55em 1.1em;
  border: 0.5px solid rgba(43, 22, 32, 0.35); border-radius: var(--radius-pill);
  cursor: pointer; transition: background 0.2s ease, border-color 0.2s ease;
}
.choice:hover { border-color: var(--aubergine); }
/* :has() lets the whole pill respond to its own radio being chosen. Where it
   is unsupported the radio itself still shows the state, so nothing is lost —
   the pill simply stays plain. */
.choice:has(input:checked) { background: var(--nude); border-color: var(--aubergine); }
.choice input { margin: 0; accent-color: var(--aubergine); }

/* The feedback page's textareas are short by default — two lines is an
   invitation to answer briefly, which is what gets answers at all. */
#feedback-form textarea { min-height: 3.4rem; }
#feedback-form .checkbox-field .hint { display: block; margin-top: 0.25em; }

/* ---- Named permission block (feedback thank-you screen) ----
   Set apart from the thank-you message above it so it reads as a separate,
   optional offer rather than a second half of the form someone has just
   finished. The rule and the top margin are doing the work of saying "this is
   a different thing". */
.perm-block {
  margin-top: var(--space-lg);
  padding-top: var(--space-lg);
  border-top: 1px solid rgba(43, 22, 32, 0.16);
}
.perm-block h3 { margin-bottom: var(--space-xs); }
.perm-block .form-grid { margin-top: var(--space-md); }
.perm-block .btn { justify-self: start; }
@media (max-width: 600px) { .perm-block .btn { justify-self: stretch; } }

/* ---- Private results page ----
   An internal reading view, so it is set for scanning rather than for
   atmosphere: tighter rhythm than the public pages, and quotes set apart from
   the counts so the two kinds of answer don't blur together. */
.results-key { max-width: 26rem; }
.results-note {
  font-size: var(--step--1); color: var(--ink); opacity: 0.7;
  margin-bottom: var(--space-lg);
}
.results-session {
  padding-top: var(--space-lg); margin-top: var(--space-lg);
  border-top: 1px solid rgba(43, 22, 32, 0.16);
}
.results-session:first-of-type { border-top: 0; margin-top: 0; padding-top: 0; }
.results-session h2 { margin-bottom: 0.2em; }
.results-session h3 {
  font-family: var(--font-accent); font-weight: 700; font-size: var(--step--1);
  text-transform: uppercase; letter-spacing: 0.12em; color: var(--terracotta-deep);
  margin: var(--space-md) 0 0.5em;
}
.results-count {
  font-family: var(--font-accent); font-weight: 700; font-size: var(--step--1);
  text-transform: uppercase; letter-spacing: 0.1em; color: var(--ink); opacity: 0.7;
  margin-bottom: var(--space-sm);
}
/* The share-consent tally closes a session block, so it needs air above it —
   without this it butts straight up against the last quote. */
.results-quotes + .results-count { margin-top: var(--space-md); }
/* Question on the left, tally on the right — a two-column list rather than
   stacked pairs, so four questions read as one block at a glance. */
.results-list { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0.5em var(--space-md); margin: 0; }
.results-list dt { font-weight: 400; }
.results-list dd { margin: 0; text-align: right; white-space: nowrap; }
@media (max-width: 560px) {
  .results-list { grid-template-columns: 1fr; gap: 0.15em; }
  .results-list dd { text-align: left; margin-bottom: 0.6em; }
}
/* Participants' own words, set as quotes rather than list items — these are
   the point of the page. */
.results-quotes, .results-perms { list-style: none; padding: 0; margin: 0; display: grid; gap: 0.6em; }
.results-quotes li {
  padding-left: var(--space-sm); border-left: 2px solid rgba(163, 73, 47, 0.5);
  max-width: 60ch;
}
.results-perms li { font-size: var(--step--1); }

/* Honeypot field on the contact form. Moved out of view rather than
   display:none — some form-fillers skip anything genuinely hidden, and the
   point is that a bot DOES fill it in. aria-hidden and tabindex="-1" on the
   markup keep it away from screen readers and the keyboard, so no real person
   ever meets it. */
.honeypot {
  position: absolute; left: -9999px; width: 1px; height: 1px;
  overflow: hidden; opacity: 0; pointer-events: none;
}

/* Booked-attendee list on the results page. The seat number leads so the list
   can be read against a room, and access/emergency notes are flagged rather
   than run into the line — on the morning of a session an access need is the
   one thing that must not be skimmed past. */
.results-seat {
  display: inline-block; min-width: 1.8em; text-align: center;
  font-family: var(--font-accent); font-weight: 700; font-size: 0.85em;
  color: var(--ink); opacity: 0.55;
}
.results-flag {
  font-family: var(--font-accent); font-weight: 700; font-size: 0.8em;
  text-transform: uppercase; letter-spacing: 0.1em;
  color: var(--terracotta-deep);
}
