/* Blissful Dance & Choreography
   Foundation stylesheet: tokens, base, intro overlay, header/nav.
   Sections are added per the build order in CLAUDE_HANDOVER.md.
   Every value here is lifted from the approved Claude Design
   (design_handoff_blissful_homepage). Do not invent new values. */

:root {
  /* Colour (approved design tokens) */
  --navy: #123A49;        /* primary text on cream, About bg, primary buttons */
  --navy2: #0C2A36;       /* Contact section bg */
  --butter: #F3DE8E;      /* accent: intro line, About accents, Contact primary button */
  --butterpale: #FAEFAE;  /* soft yellow, used sparingly */
  /* --butter as it actually RENDERS on the light sections, i.e. after the grain
     overlay (opacity .22, multiply) darkens it. A butter wave pouring into a
     grained section must use THIS, or the flat wave and the grained ground read as
     two different yellows (Chenélle 2026-07-20). Measured off the composited
     canvas, not guessed. */
  --butter-grained: #ECD78A;
  --gold: #B98F3B;        /* dividers, timeline line and dots, strokes, fills */

  /* The design's gold reads 2.76:1 on cream, so it fails WCAG AA (4.5:1) as
     text: the eyebrows and tags are its smallest, widest-tracked type, which is
     exactly where that hurts. This is the same hue taken 25% darker to 4.60:1,
     used ONLY where gold is type. Every decorative gold line, dot and stroke
     stays on --gold above, exactly as signed off. Chenélle chose this split on
     2026-07-16 over darkening the accent everywhere. */
  --gold-text: #8B6B2C;   /* eyebrows, tags, text hover */

  /* Where butter is allowed to be type, and where it is not. This is the rule
     the whole palette now turns on, so it is written down once, here:

       --butter on --cream  1.24:1  invisible
       --butter on --navy   9.06:1  sings

     So butter is the accent on the DARK sections ("heart" on Meet Thalia) and a
     GROUND on the light ones (the ribbon, the hero's styles strip). It is never
     type on cream. A darkened butter was tried on 2026-07-16 and Chenélle
     rejected it as mustard, correctly: it was the only yellow that survived
     cream, and surviving is not the same as looking good.

     On the light sections the accent is --gold / --gold-text, which is her own
     split from earlier the same day, and "Joy" is --navy, matching the logo
     exactly (the logo carries no yellow at all: it is #123A49). */

  /* Labels on a butter ground: --muted is only 3.69:1 there. Same hue and
     saturation, darker: 4.73:1 on butter, 5.89:1 on cream. */
  --muted-onbutter: #576166;

  --cream: #FBF6EA;       /* page background, light sections, text on navy */
  --ink: #123A49;         /* alias of navy, matches design markup */
  --muted: #667277;       /* body text on cream; design's #6E7B80 was 4.05:1, this is 4.59:1 */
  --mutedd: #AABEC5;      /* body text on navy */
  --hairline: rgba(18, 58, 73, .1);
  --hairline-strong: rgba(18, 58, 73, .16);

  /* Type */
  --serif: 'Cormorant Garamond', Garamond, Georgia, serif;
  --sans: 'Jost', system-ui, sans-serif;

  /* Layout */
  --gutter: 44px;
  --maxw: 1360px;         /* content */
  --maxw-header: 1440px;  /* header is wider than content, per the design */
  /* The design quotes 68px (calc(100vh - 68px) on its hero) and never rendered
     it: the Book a Trial pill made the real header 74.5px. The pill has since
     gone, and the logo has been enlarged to 44px, so the logo is now the tallest
     thing in the bar: 17 + 44 + 17 = 78. That is also what the 44px menu button
     gives below the nav breakpoint, so one value covers both and there is no
     override.

     **78, not 79, since 2026-07-17.** The old sum ended `+ 1px hairline`, and
     that hairline was .site-header's border-bottom, which Chenélle asked off.
     Measured after removing it: the bar is 78.0. This is not decorative rounding.
     .hero's min-height is calc(100vh - this), so an over-tall value pulls the
     hero and its styles strip up off the fold: at 79 the strip ended at 909 in a
     910 viewport, 1px short.

     **Nothing overwrites this at runtime.** A previous note here said script.js
     measures the header and that this was only a no-JS fallback. It does not:
     there is no setProperty, no offsetHeight, no reference to --header-h in any
     script on the site (checked 2026-07-17). This value is the live one, it is
     used for the anchor offset and the hero's height, and it has to be true.
     If the bar's padding or logo size ever changes, re-measure and edit it here. */
  --header-h: 78px;
  --hero-styles-h: 68px;  /* the styles list at the foot of the hero */
  --section-y: clamp(78px, 10vw, 150px);
  --section-y-lg: clamp(96px, 12vw, 180px);

  /* Motion */
  --ease: cubic-bezier(.2, .7, .2, 1);
  --t-hover: .25s;        /* link colour, per the design */
  --t-fill: .3s;          /* button fills, per the design */
  --dancer-cycle: 12.5s;  /* full loop of the five hero poses: 2.5s each */

  /* Thalia browses in dark mode. The design is a light theme, so pin it and
     make sure forced dark cannot repaint it. Binding client constraint. */
  color-scheme: light;
}

/* Breakpoints (no CSS var support in media queries, so they live here as a note):
   --bp-nav:    860px  nav collapses to the menu button
   --bp-tablet: 1024px stack two-column sections (from session 3 on)
   --bp-mobile: 640px  tighter gutters */

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

html, body { margin: 0; padding: 0; }

html {
  scroll-behavior: smooth;
  scroll-padding-top: var(--header-h); /* anchors clear the sticky header */
  /* Hide the scrollbar site-wide (Chenélle 2026-07-20: "you can see the scroll
     bar everywhere ... remove it"). Scrolling still works; only the visible track
     goes. Firefox/old-Edge use these two, WebKit uses the ::-webkit-scrollbar
     rule below. */
  scrollbar-width: none;
  -ms-overflow-style: none;
}
html::-webkit-scrollbar,
body::-webkit-scrollbar { width: 0; height: 0; display: none; }
body { scrollbar-width: none; }

body {
  background: var(--cream);
  color: var(--ink);
  font-family: var(--sans);
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

img { max-width: 100%; }

a { color: inherit; text-decoration: none; }
a.link { transition: color var(--t-hover); }
a.link:hover { color: var(--gold-text); }

::selection { background: var(--butter); color: var(--navy); }

/* Visible keyboard focus. The design does not specify one, so this is the
   accessible default rather than a design change. --gold-text, not --gold: a
   focus ring is a UI component, so WCAG 1.4.11 wants 3:1 against the cream and
   the lighter accent only manages 2.76:1. */
:focus-visible { outline: 2px solid var(--gold-text); outline-offset: 3px; }

/* No --header-h override below the nav breakpoint any more: the 44px menu button
   and the 44px logo are the same height, so the bar is 79px at every width. */

@media (max-width: 640px) {
  :root { --gutter: 24px; }
}

/* ---------------------------------------------------------------- layout */

.shell {
  max-width: var(--maxw);
  margin: 0 auto;
  padding-left: var(--gutter);
  padding-right: var(--gutter);
}

.skip-link {
  position: fixed;
  top: 10px;
  left: -9999px;
  z-index: 300;
}
.skip-link:focus {
  left: 10px;
  background: var(--cream);
  color: var(--ink);
  border: 1px solid var(--ink);
  border-radius: 100px;
  padding: 10px 20px;
  font-size: 12px;
  letter-spacing: .14em;
  text-transform: uppercase;
}

/* ---------------------------------------------------------------- buttons */

.btn {
  display: inline-block;
  border-radius: 100px;      /* full pill, per the design */
  font-family: var(--sans);
  text-transform: uppercase;
  cursor: pointer;
}

/* Header CTA: 1px navy outline, fills navy on hover. */
.btn--outline {
  padding: 11px 22px;
  border: 1px solid var(--ink);
  background: transparent;
  color: var(--ink);
  font-size: 12px;
  letter-spacing: .14em;
  transition: background var(--t-fill), color var(--t-fill);
}
.btn--outline:hover { background: var(--navy); color: var(--cream); }

/* Primary pill: navy fill, cream text, goes gold on hover.
   These are the design's hero values, which session 3 needs first. The design
   varies the pill slightly per section: Fees is 16px 34px / .12em, and Contact
   is a different pill again (butter fill, navy2 text, 17px 38px / .12em / 600).
   Add those in their own sessions rather than guess one shared size now. */
.btn--primary {
  padding: 16px 30px;
  border: 0;
  background: var(--navy);
  color: var(--cream);
  font-size: 13px;
  letter-spacing: .1em;
  transition: background var(--t-fill), color var(--t-fill);
}
.btn--primary:hover { background: var(--gold); color: var(--navy2); }

/* Fees pill: the design draws it slightly wider and wider-tracked than the
   hero's. A modifier, so .btn--primary keeps the hero's values it was set to. */
.btn--fees { padding: 16px 34px; letter-spacing: .12em; }

/* Contact pill: butter on navy, the inverse of the hero's. Its own variant
   rather than a .btn--primary modifier, because every value differs: it is a
   different button, not the primary one adjusted. Hover goes to cream, per the
   design. (It was once the only butter fill on the page. Since 2026-07-16 the
   ribbon, the hero's styles strip and the Fees pill are butter too.) */
.btn--butter {
  padding: 17px 38px;
  border: 0;
  background: var(--butter);
  color: var(--navy2);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .12em;
  transition: background var(--t-fill), color var(--t-fill);
}
.btn--butter:hover { background: var(--cream); }

/* Chenélle 2026-07-17: "make the button glow like the whatsapp button usually
   does but make it glow with the butter yellow colour it is." A modifier, not a
   change to .btn--butter, because .btn--butter is also my-story.html's "Contact
   Thalia" pill, which is an internal anchor and has no business pulsing.

   Two layers, and the split is load-bearing. The resting halo is on the button.
   The pulse ring is a ::after. The reduced-motion block forces
   animation-iteration-count:1 and duration:.01ms on everything, which settles an
   animation on its final keyframe, so had the halo been part of the animation it
   would snap to the ring's transparent end frame and the glow would vanish for
   visitors who opted out of motion, not out of glowing. .hero__dancer documents
   this same trap. Split like this, reduced motion loses the pulse and keeps the
   glow, which is the honest reading of the preference. */
.btn--glow {
  position: relative;
  box-shadow: 0 0 26px 2px rgba(243, 222, 142, .3);
  transition: background var(--t-fill), color var(--t-fill), box-shadow var(--t-fill);
}
.btn--glow:hover { box-shadow: 0 0 34px 6px rgba(243, 222, 142, .45); }

.btn--glow::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;   /* follows the pill, whatever the pill's radius is */
  pointer-events: none;     /* decorative: it must never eat the tap */
  box-shadow: 0 0 0 0 rgba(243, 222, 142, .5);
  animation: butterPulse 2.8s var(--ease) infinite;
}

/* Spreads and fades, like WhatsApp's own. Butter, per her ask, not the green. */
@keyframes butterPulse {
  0%   { box-shadow: 0 0 0 0 rgba(243, 222, 142, .5); }
  70%  { box-shadow: 0 0 0 16px rgba(243, 222, 142, 0); }
  100% { box-shadow: 0 0 0 0 rgba(243, 222, 142, 0); }
}

/* Ghost pill: the outline header CTA inverted onto navy, for the second action
   on my-story.html where two pills sit side by side. Its padding, size, weight
   and tracking are .btn--butter's, not .btn--outline's, so the pair matches in
   height and only the fill separates primary from secondary.

   The border is cream at 45%, which composites to 3.96:1 on navy2. A pill's
   outline is what tells you where the button is, so WCAG 1.4.11 wants 3:1 and a
   softer line would miss it. */
.btn--ghost {
  padding: 17px 38px;
  border: 1px solid rgba(251, 246, 234, .45);
  background: transparent;
  color: var(--cream);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: .12em;
  transition: background var(--t-fill), color var(--t-fill), border-color var(--t-fill);
}
.btn--ghost:hover { background: var(--cream); color: var(--navy2); border-color: var(--cream); }

/* ---------------------------------------------------------------- shared motion */

@keyframes markIn { 0% { opacity: 0; transform: translateY(16px) scale(.92); } 100% { opacity: 1; transform: none; } }
@keyframes fadeUp { 0% { opacity: 0; transform: translateY(16px); } 100% { opacity: 1; transform: none; } }
@keyframes lineGrow { 0% { width: 0; } 100% { width: 210px; } }
/* Five hero poses on one loop, strictly one at a time. Each pose is back to
   opacity 0 at 20%, which is the exact instant the next one's delay expires and
   it starts from 0. The poses therefore never overlap: no two figures are ever
   on screen together, even mid-fade.

   Do not widen the fade past 20%: an earlier version ran the fade-out to 25% so
   it crossed the next pose's fade-in, and two half-opacity dancers ghosted
   through each other. Opacity only: the poses are stills and must not move.

   The percentages carry the whole no-overlap guarantee, so they stay fixed and
   the pace is set by --dancer-cycle alone. At 12.5s each pose gets 2.5s: 0.63s
   in, 1.25s held, 0.63s out. The fades are proportionally wider than the 30s
   version's were, so a quicker loop did not have to mean a snappier fade. */
@keyframes dancerCycle {
  0%        { opacity: 0; }
  5%,  15%  { opacity: 1; }
  20%, 100% { opacity: 0; }
}

/* Reveal on scroll. JS adds .is-visible once the element enters view.
   --d staggers a group: the design cascades 60–320ms down a column. */
.reveal {
  opacity: 0;
  transform: translateY(24px);
  transition:
    opacity .9s var(--ease) var(--d, 0s),
    transform .9s var(--ease) var(--d, 0s);
}
.reveal.is-visible { opacity: 1; transform: none; }

/* ------------------------------------------------- intro overlay (dormant) */

/* PARKED, NOT DEAD. Nothing on the site carries these classes: the intro came
   out on 2026-07-16 so the home page opens on the hero, and Chenélle's intro
   video lands 2026-07-17. Kept because the thinking is the valuable part, not
   the markup. Whatever the video looks like, keep these three:

     1. The fade-out is a CSS animation, never a JS timer, so the overlay always
        clears itself. No JS, a 404 on the asset, or a script error can trap a
        visitor behind it. This is the rule that matters most.
     2. It is decorative, not a modal: it never traps focus.
     3. The once-per-session gate is read before first paint, or a repeat
        visitor gets a flash of the overlay. It used to live inline in <head>
        and came out with the intro; see index.html.

   Reusable as-is for a video: .intro, .intro__skip, .intro.is-dismissed,
   .intro-off .intro, introOut, and the reduced-motion rule near the foot of
   this file. The mark/lockup/line and their markIn, fadeUp and lineGrow
   keyframes are specific to the old logo reveal, and a video almost certainly
   replaces them. A video also wants object-fit: cover on .intro, and a fade
   tied to the clip's length rather than a fixed 2.6s. */
@keyframes introOut {
  from { opacity: 1; visibility: visible; }
  to   { opacity: 0; visibility: hidden; }
}

.intro {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: var(--navy);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 32px;
  animation: introOut 1s 2.6s forwards ease;
}

.intro__mark   { height: 124px; width: auto; opacity: 0; animation: markIn 1.3s .15s forwards ease-out; }
.intro__lockup { height: 42px;  width: auto; opacity: 0; animation: fadeUp 1.1s .7s forwards ease-out; }
.intro__line   { width: 0; height: 1px; background: rgba(243, 222, 142, .6); animation: lineGrow 1.5s .95s forwards ease-out; }

.intro__skip {
  position: absolute;
  bottom: 36px;
  right: 40px;
  background: none;
  border: 0;
  color: rgba(251, 246, 234, .55);
  font-family: var(--sans);
  font-size: 11.5px;
  letter-spacing: .2em;
  text-transform: uppercase;
  cursor: pointer;
  transition: color var(--t-hover);
}
.intro__skip:hover { color: var(--cream); }
.intro__skip:focus-visible { outline: 2px solid var(--butter); outline-offset: 4px; }

/* Skipped by the button: the design's faster 650ms fade. */
.intro.is-dismissed {
  animation: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity .65s ease, visibility 0s .65s;
}

/* Already seen this session, or ?embed=1. Set by the inline script in <head>
   before first paint, so there is no flash of the overlay. */
.intro-off .intro { display: none; }

/* ---------------------------------------------------------------- header */

/* Butter, per Chenélle 2026-07-16. This is butter doing the job it is actually
   good at: a ground, not an ink. The navy logo reads 9.06:1 on it and the nav
   links 11.27:1, both far clearer than they were on the old translucent cream.

   Opaque, unlike the .82 cream it replaces. The bar is sticky, so a translucent
   butter would blend with whatever scrolled under it, and the sections below
   alternate navy and cream: the ribbon would have shifted colour down the page.
   backdrop-filter goes with it, since there is nothing left to see through. */
/* No border-bottom. It carried `1px solid rgba(18, 58, 73, .14)` to separate the
   bar from the section under it; Chenélle asked for it off on 2026-07-17: "the
   solid line of the ribbon i do not like". The butter-to-cream edge is its own
   separation and does not need drawing. Its twin on .hero__styles went with it,
   since the two read as one line and she named both. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--butter);
}

.site-header__inner {
  max-width: var(--maxw-header);
  margin: 0 auto;
  padding: 17px var(--gutter);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}

.site-header__logo { display: flex; align-items: center; }
/* The design drew this at 30px, where it read as a footnote next to the nav.
   At 44px it is the tallest thing in the bar and now sets --header-h. */
.site-header__logo img { height: 44px; width: auto; display: block; }

.site-nav {
  display: flex;
  align-items: center;
  gap: 34px;
}

.site-nav__link {
  font-size: 12.5px;
  letter-spacing: .16em;
  text-transform: uppercase;
  opacity: .82;
  transition: opacity var(--t-hover), color var(--t-hover);
}
.site-nav__link:hover { opacity: 1; }

/* Menu button. Hidden on desktop, shown at the nav breakpoint.
   Three navy hairlines, matching the design's linework. */
.nav-toggle {
  display: none;
  position: relative;
  width: 44px;               /* 44px touch target */
  height: 44px;
  margin-right: -11px;       /* keep the bars optically on the gutter */
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  color: var(--ink);
}

.nav-toggle__bars,
.nav-toggle__bars::before,
.nav-toggle__bars::after {
  position: absolute;
  left: 11px;
  width: 22px;
  height: 1.5px;
  background: currentColor;
  transition: transform var(--t-fill) var(--ease), background .15s linear;
}
.nav-toggle__bars { top: 21.25px; }
.nav-toggle__bars::before { content: ''; left: 0; top: -6px; }
.nav-toggle__bars::after  { content: ''; left: 0; top: 6px; }

/* Hide the middle bar via background, not opacity: opacity on the parent would
   fade ::before and ::after with it and take the whole icon out. */
.site-header.is-open .nav-toggle__bars { background: transparent; }
.site-header.is-open .nav-toggle__bars::before { transform: translateY(6px) rotate(45deg); }
.site-header.is-open .nav-toggle__bars::after  { transform: translateY(-6px) rotate(-45deg); }

@media (max-width: 859px) {
  .nav-toggle { display: block; }

  .site-nav {
    position: absolute;
    top: 100%;
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: 4px var(--gutter) 30px;
    background: var(--cream);
    border-bottom: 1px solid var(--hairline);
    max-height: calc(100vh - var(--header-h));
    overflow-y: auto;

    visibility: hidden;
    opacity: 0;
    transform: translateY(-6px);
    transition: opacity .28s var(--ease), transform .28s var(--ease), visibility 0s .28s;
  }

  .site-header.is-open .site-nav {
    visibility: visible;
    opacity: 1;
    transform: none;
    transition: opacity .28s var(--ease), transform .28s var(--ease);
  }

  .site-nav__link {
    padding: 19px 0;
    opacity: 1;
    border-bottom: 1px solid var(--hairline);
  }
}

/* ---------------------------------------------------------------- hero */

/* Design: full height under the header, 2-column grid 1.08fr/1fr, centred.
   The copy sits second in the DOM but first in the grid's right column, so the
   headline leads the reading order while the illustration stays on the left. */
.hero {
  position: relative;
  min-height: calc(100vh - var(--header-h));
  display: grid;
  /* Equal halves, per Chenélle: "symmetrical, with the weight equally
     distributed on the left and right sides". Was 1.08fr 1fr, which quietly
     handed the illustration the wider side. */
  grid-template-columns: 1fr 1fr;
  align-items: center;

  /* **Navy since 2026-07-17.** Chenélle asked for every section's theme to flip:
     "switch each section to the opposite theme. so the hero will then be dark
     theme". The hero was cream, held between the butter ribbon and the butter
     styles strip; it is now the dark panel between them, which butter also
     survives (9.06:1).

     **The three lines below are what flip the copy, and they are the whole trick.**
     --ink, --muted and --gold-text are role tokens: headings, body, accent. They
     are never used as a background or a fill anywhere in this file (checked: 47
     uses, all colour/border), so re-pointing them here re-colours every child of
     this section at once, without touching a single child rule. Read the palette
     note at the top of the file first: it is what dictates these three targets.

     Ratios on navy, from that note and re-measured: cream 11.27, mutedd 6.30,
     butter 9.06. The outgoing values would have been navy-on-navy 1.00, muted
     2.45 and gold-text 2.45: invisible, and two of them failing AA outright.

     --gold is deliberately NOT re-pointed. It is decoration only (.hero__rule),
     it reads 4.08 on navy, and the palette note is explicit that every
     decorative gold line and dot stays --gold exactly as signed off. */
  background: var(--navy);
  color: var(--cream);
  --ink: var(--cream);      /* headings, .hero__link's underline */
  --muted: var(--mutedd);   /* body: .hero__sub */
  --gold-text: var(--butter); /* accent: .hero__eyebrow, link hover */

  /* The row stretches (the default), so it fills the hero's content box top to
     bottom. Chenélle, 2026-07-17: "make the image larger, that the bottom of the
     image align with the line at the bottom just above the styles."

     This USED to be `align-content: center`, which sized the row to the copy
     column so the dancer matched the text's height. That invariant is
     deliberately retired: she wants the figure taller than the copy and landing
     on the strip, and those two cannot both be true.

     **Removing it costs the copy nothing.** .hero__copy is a flex column with
     justify-content:center, so it centres its own content inside whatever box it
     gets. A centred row and a stretched row share the same midpoint, so the
     headline does not move by a single pixel. Measured before and after: text top
     246.1 both ways at 1600x910. The only thing align-content was still doing was
     capping the art.

     The bottom edge is not a tuned number either. padding-bottom below reserves
     exactly --hero-styles-h for the strip, so the content box ends where the
     butter begins, and a stretched row ends there with it. */

  /* Reserve the foot for the styles list, which is taken out of flow below.
     This is also the line the dancer now lands on: see .hero__dancer. */
  padding-bottom: var(--hero-styles-h);
}

/* Paper grain. A single 240px tile of SVG turbulence, repeated, rather than one
   filter stretched over the whole hero: the filter is rasterised once at tile
   size instead of full-bleed. stitchTiles makes the seams invisible. Multiply
   keeps it as tone on the cream rather than a grey film over it. */
.hero::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.75' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23g)'/%3E%3C/svg%3E");

  /* **screen, not multiply, and the blend mode had to flip with the ground.**
     Multiply keeps the darker of the two, which is how this read as tone on the
     cream hero. On navy there is almost nothing darker than the ground, so
     multiply would have quietly deleted the grain the moment the section went
     dark, and Chenélle asked for MORE grain on 2026-07-16, not less. screen is
     multiply's mirror: it keeps the lighter, so the turbulence now reads as fine
     light speckle on the navy instead of mud on mud.
     .styles below keeps multiply, because .styles is still a light ground. */
  opacity: .12;   /* screen on navy carries much further than multiply did on
                     cream, so .26 blew out to a grey film. Matched by eye
                     against the old hero at 1600x910. */
  mix-blend-mode: screen;
}

/* The hero carried two gold curves here, added for Chenélle's "a line or two
   that's curvy". She asked for them off on 2026-07-17: the hero is grain on
   cream now, and the only gold left in it is `.hero__rule` and the eyebrow.
   The curves on `.styles` were not part of that instruction and stay.

   If they ever come back: they were one stretched viewBox rather than a tile,
   so each read as a single continuous sweep, and they were pinned clear of the
   copy band (roughly y204 to y652 in a 0 0 1440 820 box) so no hairline
   wandered through the headline. */

/* Both columns sit above the grain. */
.hero__art  { grid-column: 1; grid-row: 1; position: relative; z-index: 1; }
.hero__copy { grid-column: 2; grid-row: 1; position: relative; z-index: 1; }

/* --- illustration --- */

/* Absolutely positioned so the dancer can bleed past its column without
   stretching the grid row. The approved asset is used as supplied. */
.hero__art {
  position: relative;
  align-self: stretch;
  overflow: hidden;
}

/* **The butter glow, and it is the only reason the dancer is visible at all.**

   Chenélle chose this on 2026-07-17 when the hero went dark, over re-shooting
   her. Measured, the problem is not marginal: 77.9% of the figure's opaque
   pixels sit within 2:1 of navy, and her average colour reads 1.27:1 against it
   (8.86:1 on the old cream). Simulated in the browser before building this, she
   vanished completely at full opacity: present, loaded, hit-testable, invisible.

   **The glow does not lighten her, it lightens the ground behind her**, which is
   the whole mechanism. Her navy dress stays navy and becomes a dark silhouette
   against a lit field, instead of navy on navy. .thalia__bg::before is the same
   device and the note there says so outright: her dress "would otherwise
   dissolve into the background and leave her face and arms floating".

   It is stronger and larger than Thalia's .16, and it has to be: hers ghosts a
   half-opacity figure, this one carries a 539x763 photograph at full opacity.
   The gradient fades to transparent NAVY, not transparent butter, so it never
   greys the section as it falls off. Sized and placed off the figure, not the
   column: 78% wide centred at 46% down, which is where her torso actually sits.

   If the hero ever goes back to a light ground, delete this. On cream it would
   be the butter wash that was removed on 2026-07-16 for looking mustard. */
.hero__art::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 46%;
  transform: translate(-50%, -50%);
  width: 78%;
  aspect-ratio: 1;
  border-radius: 50%;
  pointer-events: none;
  background: radial-gradient(closest-side,
              rgba(243, 222, 142, .30),
              rgba(243, 222, 142, .16) 45%,
              rgba(18, 58, 73, 0) 72%);
}

/* One grid cell, the dancer in it: centred without absolute positioning. The
   track is pinned to 100% rather than left auto, or the dancer's height:100%
   would have nothing definite to resolve against and it would render at
   intrinsic size.

   The box fills the grid row, and since 2026-07-17 that row fills the hero's
   whole content box rather than matching the copy (see .hero above). So this
   runs from under the header down to the top of the butter strip, and the
   photograph fills it. Still a derived height, not a tuned one.

   It carries no inset. An earlier build inset it by the copy's 72px so the
   figure matched the text exactly, and that made it too slight: the figure ended
   up adrift in its half, reading as 193px / 72px outer margins with a 236px
   canyon down the middle. The largest space was in the centre, which is what
   "not symmetrical" was pointing at. */
.hero__art-inner {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template: 100% / 100%;
  place-items: center;
}

/* One photograph, static, since 2026-07-17. No opacity:0 and no animation here:
   the cross-fade was the only thing that ever made a pose visible, so a single
   image on the old rule would have rendered nothing at all. */
.hero__dancer {
  grid-area: 1 / 1;
  height: 100%;          /* = header to butter strip, see .hero__art-inner */
  width: auto;
  max-width: 100%;       /* no bleed: the figure sits within its column */
  object-fit: contain;

  /* **This is what lands the crop on the butter strip, and it is not cosmetic.**

     The photograph is a cutout with a hard horizontal crop at her waist, and the
     ink runs to the very bottom of the canvas: measured alpha bbox is
     (45, 45, 1392, 2000) on a 1414x2000 file, so the bottom margin is 0px. The
     bottom of the image box IS the crop line. Land the box on the strip and the
     cut disappears into the butter instead of floating in cream.

     Wide viewports do not need this: height:100% + width:auto derives the width
     from the aspect ratio, so contain has nothing to letterbox and the box is
     already flush. It earns its place at the narrow end of the desktop range,
     where the column gets narrower than her natural width, max-width clamps the
     box, and contain shrinks the painting inside a box that is still full
     height. Left centred, the crop would drift back up off the strip.

     There is no fixed width to quote for where that starts, because both sides of
     the comparison are viewport-driven: the box height is the viewport height
     less the header and the strip, and the column is half the width. At 900 tall
     it begins around 1065. Measured at 1025x900, where it does bite: the box is
     512.5x753 and the painted figure only 724.9 tall, so centred it would sit
     14px clear of the butter. With `bottom` the crop lands on the line at 1025,
     1200, 1440 and 1600 alike (painted bottom minus strip top: 0.00 at each). */
  object-position: bottom;
}

/* --- hero poses (dormant) ---

   The five-pose cross-fade, kept for the intro video. PARKED, NOT DEAD: the
   same call as the intro overlay above. Chenélle, 2026-07-17: "the current
   images of the hero will be used in the intro which we will do later."

   Nothing matches these selectors while the hero holds one image. To bring the
   poses back: restore the five <img class="hero__dancer"> in .hero__art-inner,
   and move `opacity: 0` and the `animation` line back onto .hero__dancer above.

   Everything here was measured and should not be re-derived. --dancer-cycle is
   12.5s = 2.5s per pose, inside Chenélle's "2-3 seconds". The 0/5/15/20% stops
   in @keyframes dancerCycle are what make each pose reach zero exactly as the
   next begins: verified at 10ms resolution across the loop, 0 overlapping
   samples, 60ms of blank total (~12ms per handover, under one frame at 60fps).
   Retime with --dancer-cycle only; the percentages stay put or ghosting returns. */
.hero__dancer--cycling {
  opacity: 0;
  animation: dancerCycle var(--dancer-cycle) linear infinite;
}
.hero__dancer--cycling:nth-child(1) { animation-delay: calc(var(--dancer-cycle) * 0 / 5); }
.hero__dancer--cycling:nth-child(2) { animation-delay: calc(var(--dancer-cycle) * 1 / 5); }
.hero__dancer--cycling:nth-child(3) { animation-delay: calc(var(--dancer-cycle) * 2 / 5); }
.hero__dancer--cycling:nth-child(4) { animation-delay: calc(var(--dancer-cycle) * 3 / 5); }
.hero__dancer--cycling:nth-child(5) { animation-delay: calc(var(--dancer-cycle) * 4 / 5); }

/* --- copy --- */

.hero__copy {
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 72px clamp(40px, 5vw, 96px) 72px clamp(24px, 3vw, 64px);
}

.hero__eyebrow {
  margin: 0 0 30px;
  font-size: 12.5px;
  letter-spacing: .32em;
  text-transform: uppercase;
  color: var(--gold-text);
  --d: 0s;
}

/* 400, not 700: per Chenélle on 2026-07-17 the headline is no longer bold as a
   whole. "The" and "of" sit at 400 and only "Joy" and "Dance" go to 700, so the
   weight jump is what shapes the line. 400 rather than 500 because Cormorant
   Garamond's 500 is close enough to its 700 that the contrast would go mushy at
   display size. */
.hero__title {
  margin: 0;
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(54px, 6.8vw, 102px);
  line-height: .96;
  letter-spacing: -.02em;
  color: var(--ink);
  --d: .08s;
}
/* "Joy" is the same colour as the words around it, and the accent is carried
   entirely by type: bold italic against roman. That is the whole reason the
   italic 700 is loaded for this one word. Without it the browser slants and
   smears the 400, a didone's hairlines do not survive a faux bold, and "Joy"
   would then be doing nothing at all to stand out. Do not drop that weight.

   It was --navy to match the logo exactly, per Chenélle on 2026-07-16. Since the
   hero went dark on 2026-07-17 it inherits --ink, which .hero re-points to
   --cream, so it is cream along with the rest of the headline. **The principle
   survives the flip and the hex does not**: the point was always that "Joy" is
   the same colour as its neighbours and earns its emphasis from weight, not
   hue. Matching the logo was how that read on cream; the logo is navy and lives
   on the butter ribbon, which has not moved. */
.hero__title em {
  font-style: italic;
  font-weight: 700;
  /* inherits --ink from .hero__title; stated so the intent is not mistaken for
     an oversight and "helpfully" given a colour later */
  color: inherit;
}

/* "Dance": bold but upright. <b> defaults to bold-as-the-browser-sees-it, so the
   weight is stated to pin it to the 700 that is actually loaded. */
.hero__title b {
  font-weight: 700;
  font-style: normal;
  color: inherit;
}

.hero__rule {
  width: 64px;
  height: 2px;
  margin: 32px 0 0;
  background: var(--gold);
  --d: .12s;
}

/* One line, per Chenélle.

   `nowrap` cannot wrap out of trouble: it can only push text out of the column,
   which is the failure mode that is hardest to spot, because `body` has
   `overflow-x: hidden` and will swallow the evidence. The `clamp()` is the guard
   against that: the copy column tracks the viewport at roughly .40vw, and 1.55vw
   is that relationship expressed as type, floored at 16px and capped at the
   design's 18.5px.

   The numbers that originally forced it are gone. The subhead was "Where joy and
   excellence come together through dance." (466px at 18.5px, 56px past a 411px
   column at 1025) until 2026-07-17, when Chenélle cut "through dance". Measured
   after that cut and after the Jost swap: 285px in a 515px column at 1030, 230px
   of slack, one line.

   So the clamp no longer binds, and is kept as a guard rather than a fix. If the
   subhead copy ever grows again, this is what stops it silently overflowing, and
   the measurement above is the thing to redo. */
.hero__sub {
  margin: 24px 0 0;
  font-size: clamp(16px, 1.55vw, 18.5px);
  line-height: 1.62;
  color: var(--muted);
  white-space: nowrap;
  --d: .16s;
}

/* Where the studio is, above the fold. Chenélle 2026-07-17: "put somewhere
   Pretoria East, South Africa, so that users can see that it is in Pretoria,
   South Africa." The page has always said Pretoria in its title, meta and
   schema, and named the suburbs down in Good to know; none of that is visible
   without scrolling, and a first-time visitor deciding whether a dance school is
   near them should not have to hunt.

   **This reuses the design's small-label language rather than inventing a
   style**: uppercase sans 12px at .14em with a 5px dot, which is what the Meet
   Thalia credential row and .hero__styles already are. See the note on
   .hero__styles, which makes the same point. It reads as a label, not as a
   second subheading, and it cannot be mistaken for the eyebrow above the
   headline: that is butter at .32em, this is muted at .14em.

   --muted, not a literal, so it flips with the section. .hero re-points --muted
   to --mutedd, which is 6.30:1 on the navy ground (plain --muted would be 2.45
   and fail). Put the hero back on cream and this follows it without an edit.

   --d sits between the subheading's .16s and the CTAs' .24s, so the reveal
   cascade still runs straight down the column. */
.hero__place {
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 20px 0 0;
  font-size: 12px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--muted);
  --d: .2s;
}

/* The dot marker, matching .hero__styles li::before. --gold, per the palette
   note: every decorative gold dot stays --gold, and it reads 4.08:1 on navy,
   which clears the 3:1 that non-text needs. (.hero__styles' dots are navy
   instead only because they sit on butter, where gold goes muddy at 2.22.) */
.hero__place::before {
  content: '';
  flex: none;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--gold);
}

.hero__ctas {
  display: flex;
  align-items: center;
  gap: 28px;
  margin-top: 46px;
  flex-wrap: wrap;
  --d: .24s;
}

/* **The primary pill inverts on the dark hero.** .btn--primary is a navy fill
   with cream text, which was the design's hero pill on cream and is now navy on
   navy: 1.00:1, a pill-shaped hole. Butter on navy is 9.06:1.

   Overridden here rather than by putting .btn--butter on the markup, because
   .btn--butter is a different SIZE (17px 38px / 600 / .12em against this one's
   16px 30px / .1em), and the note on it says that is deliberate: "it is a
   different button, not the primary one adjusted". Swapping the class would have
   silently resized the hero's CTA. This takes the Contact pill's colours and
   keeps the hero pill's proportions.

   Hover follows .btn--butter's too: butter goes to cream, since the design's
   gold hover (4.08 on navy) would be a muddy step down from a 9.06 butter. */
.hero .btn--primary {
  background: var(--butter);
  color: var(--navy2);
}
.hero .btn--primary:hover {
  background: var(--cream);
  color: var(--navy2);
}

.hero__link {
  position: relative;
  font-size: 14px;
  letter-spacing: .06em;
  border-bottom: 1px solid var(--ink);
  padding-bottom: 3px;
  transition: color var(--t-hover), border-color var(--t-hover);
}
.hero__link:hover { color: var(--gold-text); border-color: var(--gold); }

/* The link's text is only 22px tall, which is a mean target on a phone. Grow the
   hit area to 44px without moving the text or its underline. */
.hero__link::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  height: 44px;
}

/* --- stacked (tablet and below) ---
   The design is desktop-only and hands the rebuild responsibility for stacking
   the hero. Copy leads, illustration follows in a band beneath it. min-height
   is dropped so the section takes its natural height instead of squashing. */
@media (max-width: 1024px) {
  .hero {
    min-height: 0;
    grid-template-columns: 1fr;
    /* Stacked, the styles list is back in flow as the last row, so there is
       nothing to reserve room for. */
    padding-bottom: 0;

    }

  .hero__copy {
    grid-column: 1;
    grid-row: 1;
    padding: clamp(56px, 9vw, 88px) var(--gutter) 0;
  }

  .hero__art {
    grid-column: 1;
    grid-row: 2;
    align-self: auto;
    height: clamp(300px, 44vh, 440px);
    margin-top: clamp(40px, 6vw, 64px);
  }

  /* The 72px inset exists to match the copy's padding in the two-column layout.
     Stacked, the art has its own explicit height and the inset would just eat
     144px of the figure. */
  .hero__art-inner { inset: 0; }

  .hero__dancer { height: 100%; max-width: 100%; }
}

/* The one-line subheading is a desktop and tablet promise only. No phone is wide
   enough for the string at a readable size, and nowrap there would not shrink it,
   it would push it off the screen sideways. Let it wrap and stay legible. */
@media (max-width: 640px) {
  .hero__sub { font-size: 17px; white-space: normal; }
  .hero__ctas { gap: 22px; }
}

/* ---------------------------------------------------------------- hero styles list */

/* The dance styles, pinned to the foot of the hero so they land inside the first
   screen. Taken out of flow, with .hero reserving --hero-styles-h of padding for
   them, so they cannot push the copy or the illustration around.

   Reuses the design's own small-label language (the Meet Thalia credential row:
   uppercase sans 12px, .14em, dot marker) rather than inventing a chip or pill.
   space-between spreads them across the full width rather than bunching them in
   the middle. */
.hero__styles {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;

  box-sizing: border-box;
  height: var(--hero-styles-h);

  /* Full-bleed, so the butter reaches both edges. It used to be a centred
     1360px box, which was invisible while it was transparent but would now paint
     a butter band floating in a cream hero. The padding keeps the labels on the
     same 1360px measure as every other section: the gutter until the viewport
     passes --maxw, then whatever centres it. */
  margin: 0;
  padding: 0 max(var(--gutter), (100% - var(--maxw)) / 2);

  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px 32px;

  list-style: none;
  font-size: 12px;
  letter-spacing: .14em;
  text-transform: uppercase;

  /* Butter, per Chenélle: the hero is now a cream panel held between two butter
     bands, this one and the ribbon. --muted-onbutter, not --muted, which is only
     3.69:1 here.

     No border-top, matching the ribbon. It was the same
     `1px solid rgba(18, 58, 73, .14)` hairline and Chenélle named both at once
     on 2026-07-17: "it is also visible on the top of the styles". See
     .site-header. The butter band is its own edge against the cream. */
  background: var(--butter);
  color: var(--muted-onbutter);
}

.hero__styles li {
  display: flex;
  align-items: center;
  gap: 9px;
}
/* Navy, not --gold: the dots sit on butter now, where --gold is 2.22:1 and just
   goes muddy. Navy is 9.06:1 on it. */
.hero__styles li::before {
  content: '';
  flex: none;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--navy);
}

/* Back into flow the moment the hero stops being a full-height two-column
   screen, and 1024 is where that happens. This used to say 900, which left the
   band from 901 to 1024 with the list still absolutely pinned to bottom: 0 while
   the stacked rule above had already dropped the padding that reserved room for
   it. The row landed on top of the illustration: a measured 68px over the
   dancer's feet at 950px wide. The two numbers are the same decision and have to
   stay the same number. Wrapping as a block also lets the labels breathe once
   there is no longer width to spread six of them along one line. */
@media (max-width: 1024px) {
  .hero__styles {
    position: static;
    height: auto;
    padding: 22px var(--gutter);
    flex-wrap: wrap;
    justify-content: flex-start;
    gap: 12px 26px;
  }
}

/* ---------------------------------------------------------------- meet thalia */

/* The design's navy band. Padding is the design's larger section rhythm
   (clamp(96px,12vw,180px)), matching the hero.

   No overflow:hidden here, and that reverses a session-4 note calling it
   load-bearing for the dancer bleed. Measured in session 5: it is not.
   .thalia__bg is inset:0 with its own overflow:hidden, so it already clips the
   dancer to the section box. Removing this rule and re-checking, the figure
   still cuts cleanly at the top edge, and body{overflow-x:hidden} catches
   horizontal bleed site-wide.

   It had to go. The wave below hangs 1px past this section's bottom on purpose,
   and a clip here cuts off precisely the 1px that hides the seam. Putting
   overflow:hidden back reinstates a navy hairline across the Classes edge. */
.thalia {
  position: relative;
  background: var(--navy);
  color: var(--cream);
  padding: var(--section-y-lg) 0;
}

/* --- the dancer --- */

/* The design draws this as an ambient layer: thalia-dancer at left:40%,
   height:114%, top:-6%, **opacity:.5**, centred behind the copy over a butter
   glow, reading as a ghosted texture rather than a photograph.

   **It is not that any more.** Chenélle moved her to the left and took the
   transparency off on 2026-07-16, so she is the section's image and the copy
   sits beside her rather than on top of her. The layer is still
   position:absolute rather than a grid cell, because she bleeds past the
   section's top and bottom edges (height:114%, top:-6%) and a grid cell would
   have to stretch the row to hold her.

   **The half-opacity was doing contrast work, and moving her left is what pays
   for removing it.** Ghosting her was what kept the body text legible where she
   crossed it. She no longer crosses it: .thalia__copy is pinned to column 2 and
   the figure is clear of it. Verified at every breakpoint. If you ever move her
   back under the copy, the opacity has to come back with her. */
/* Constrained to the grid's own 1320px box rather than the full section, and
   that is what makes the section symmetrical. Left at inset:0 the dancer
   measured from the viewport edge while the copy measured from the centred
   1320 box, so she bled off the screen while the copy kept a margin: two
   different rulers on one row. Sharing the box means her left edge and the
   copy's right edge are both one --gutter in from the same line. */
.thalia__bg {
  position: absolute;
  inset: 0;
  max-width: 1320px;
  margin: 0 auto;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

/* The butter glow, moved left with her and kept. At half opacity it stopped the
   cutout reading as a hard silhouette; at full opacity it does something more
   useful, because her dress is navy on a navy section and would otherwise
   dissolve into the background and leave her face and arms floating. The
   gradient fades to transparent navy, not transparent butter, so it never greys
   the background. */
.thalia__bg::before {
  content: '';
  position: absolute;
  left: 80%;   /* follows the figure: she moved right on 2026-07-17 */
  top: 48%;
  transform: translate(-50%, -50%);
  width: min(680px, 52vw);
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(243, 222, 142, .16), rgba(18, 58, 73, 0) 70%);
}

/* The whole figure, inside the box, with air above and below her.

   The design's top:-6% / height:114% deliberately overflowed her past both
   edges, which is the right call for a texture and the wrong one for a
   photograph: .thalia__bg clips, so the 6% it hung above the section took her
   raised hand off with it. Chenélle asked for the hand back on 2026-07-17.

   height:80% + top:10% since 2026-07-17. Chenélle: "make the image on the thalia
   page a bit smaller, it is too large." She was at 92%/4%, which stood her a
   full 791px tall against a 500px copy column and let her run the section.

   **The two numbers move together and are not independent.** 4% + 92% = 96%, so
   the old pair left 4% of air above her and 4% below: she was centred, and that
   is the composition, not an accident. Shrink the height alone and the leftover
   all collects at the bottom, hanging her off the section's top edge. 10% + 80%
   = 90% keeps the air even at 10% a side, so she gets smaller in place.

   The old note here warned that anything over ~96% starts cutting her raised
   hand, which sits within a few pixels of this image's top edge, and that the
   top margin keeps the hand out of the header's way. Both still hold, and this
   change moves away from that ceiling on both counts: shorter, and lower. The
   warning is for anyone growing her again. **If you raise either number, look at
   her hand.** */
.thalia__bg img {
  position: absolute;
  right: var(--gutter);  /* the copy's own margin, mirrored. RIGHT since
                            2026-07-17: Chenélle moved the figure to the right
                            and the copy to the left. If this ever goes back to
                            `left`, object-position below flips with it, and so
                            do .thalia__bg::before and .thalia__grid's columns. */
  top: 10%;
  height: 80%;
  width: auto;
  /* **A guard, not a live constraint, and it is doing contrast work rather than
     layout tidying.**

     She is sized off the section's height, so she grows as the section gets
     taller, and nothing about the copy's position stops her. At the old 92% she
     genuinely collided: she ended at x=667 against a copy starting at 734 at
     1440, but by 1025 she reached 566 against a copy starting at 519, putting
     her hand behind body text at full opacity. That is exactly what the design's
     opacity:.5 was preventing, and removing it is only safe while she and the
     copy do not meet. This cap was what kept them apart.

     **At 80% it no longer bites anywhere on desktop, and that is worth saying
     plainly rather than leaving the old note to imply it is still load-bearing.**
     Her natural width is now well inside it at every width measured: 395.8 vs a
     567.6 cap at 1440, 388.8 vs 516 at 1200, 361.4 vs 440.75 at 1025. The gap to
     the copy runs 120px to 241px across that range, so contain never has to
     shrink her.

     It stays anyway. It costs nothing while slack and it is the only thing
     standing between a future height increase and her hand landing on the words
     again. Grow the height and this is what catches you. */
  max-width: 43%;
  opacity: 1;
  object-fit: contain;
  object-position: top right;
}

/* --- grid --- */

/* Design: max-width 1320 (narrower than the page's 1360), columns .92fr/1.02fr,
   so the copy column is the wider of the two. z-index lifts it over the dancer.

   The two tracks are SWAPPED against the design (1.02fr first, then .92fr)
   because the copy moved to the left on 2026-07-17. The design's intent was
   never "the left column is wider": it was "the copy column is wider than the
   figure's". Keeping 1.02fr on the copy wherever it sits is what honours it. */
.thalia__grid {
  position: relative;
  z-index: 2;
  max-width: 1320px;
  margin: 0 auto;
  padding: 0 var(--gutter);
  display: grid;
  /* minmax(0,…) per the design: a bare fr has an auto minimum, so a long
     unbreakable word in the copy could otherwise widen the column. */
  grid-template-columns: minmax(0, 1.02fr) minmax(0, .92fr);
  align-items: center;
  gap: clamp(48px, 7vw, 140px);
}

/* --- portrait --- */

.thalia__portrait {
  position: relative;
  z-index: 3;   /* over the dancer layer */
  margin: 0;
  max-width: 440px;
}

/* The design's offset hairline frame: a butter box pushed down-left behind the
   photo. Both are positioned, so paint order alone puts the frame behind, no
   z-index needed. This is the section's single image accent, per the brief. */
.thalia__portrait::before {
  content: '';
  position: absolute;
  inset: 20px -20px -20px 20px;
  border: 1px solid rgba(243, 222, 142, .5);
}

.thalia__portrait img {
  position: relative;
  display: block;
  width: 100%;
  /* height:auto is load-bearing, not tidying: the height attribute on the <img>
     is a presentational hint that otherwise beats aspect-ratio and renders the
     photo at its full 1600px, stretching the crop. */
  height: auto;
  aspect-ratio: 4 / 5;
  /* The source is 2:3, so the design's 4:5 crop takes ~109px off the vertical.
     Centred, which is the design's own default: it lands the crop on her face. */
  object-fit: cover;
}

/* "Fades in gently", per the brief: opacity only, no rise, and slower than the
   copy so the photo settles while the text is still arriving. */
.thalia__portrait.reveal {
  transform: none;
  transition: opacity 1.4s var(--ease) .1s;
}

/* --- copy --- */

/* Column 1, per Chenélle 2026-07-17: the figure moves right and the copy left.
   It sat in column 2 from 2026-07-16, when the arrangement was the mirror of
   this one.

   **This one line is load-bearing and an explicit grid-column must stay here,
   whichever number it holds.** It was once missing while the comment claimed it
   existed, and the copy auto-placed into column 1 straight on top of a
   photograph: a 536px overlap, body text over a full-opacity figure. Do not
   remove it, and if the sides ever swap again, change the number rather than
   trusting auto-placement to follow.

   **Right-aligned, and the column stays 1**, per Chenélle 2026-07-17: "keep the
   text on the left but right align it". Those are two different axes and it is
   worth not confusing them. grid-column puts the *block* on the page's left,
   which is unchanged; text-align turns the block's *lines* to face the figure
   across the gap, so the copy's clean edge and the dancer's edge answer each
   other instead of both sides being ragged.

   text-align alone will not do it: .thalia__lead is capped at its measure, so
   the block would keep its position and only its lines would move. The margin on
   .thalia__lead is what carries the block itself, and it flips with this. */
.thalia__copy {
  grid-column: 1;
  position: relative;
  z-index: 3;   /* over the dancer layer */
  text-align: right;
}

.thalia__eyebrow {
  margin: 0 0 22px;
  font-size: 12.5px;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--butter);
  --d: 0s;
}

.thalia__title {
  margin: 0;
  font-family: var(--serif);
  font-weight: 500;
  font-size: clamp(36px, 4.6vw, 64px);
  line-height: 1.02;
  letter-spacing: -.01em;
  color: var(--cream);
  --d: .08s;
}
/* "heart" in butter, per Chenélle. This is the one place the palette note at the
   top of this file allows butter to be type: Meet Thalia is a navy section, so
   butter reads 9.06:1 here. The same colour on the cream hero was 1.24:1, which
   is why "Joy" is navy and this is not. */
.thalia__title em { font-style: italic; color: var(--butter); }

/* The measure is 58ch, not the design's 52ch, and the two properties below are a
   pair: changing one without the other puts the orphan back.

   Chenélle asked on 2026-07-17 for the second paragraph to stop breaking onto a
   stub line. It needs 578px to set in two lines and the column offers 595, but
   52ch capped it at 562: it was missing two lines by sixteen pixels and dropping
   "their own goals." onto a line of its own, 121px wide.

   58ch alone is not the fix. It sets paragraph two in two lines but simply moves
   the problem up: paragraph one then ends on a 234px tail where it used to end
   on 361px. text-wrap:balance is what settles both, evening paragraph one to
   559/488/513/505/503 and leaving paragraph two at 573/579.

   Balance earns its place here rather than being a flourish: the copy is
   right-aligned, so every line start is ragged and a stub line reads as a
   mistake rather than as a paragraph ending. Below ~1300px the column is too
   narrow for two lines and paragraph two returns to three, which is ordinary
   reflow; balance keeps them even when it does. Unsupported browsers fall back
   to normal wrapping and still get two lines, because 58ch does that part. */
.thalia__lead {
  margin: 26px 0 0 auto;   /* auto left: hold the measure, flush right */
  font-size: 18px;
  line-height: 1.68;
  color: var(--mutedd);
  max-width: 58ch;
  text-wrap: balance;
  --d: .16s;
}
.thalia__lead + .thalia__lead { --d: .24s; }

/* Appears last, per the brief. The reveal lives here on the wrapper rather than
   on the link, so it cannot collide with the link's hover transition. */
.thalia__actions {
  margin: 40px 0 0;
  --d: .32s;
}

/* Understated secondary link, not a button, per the brief. */
.thalia__link {
  position: relative;
  display: inline-block;
  font-size: 14px;
  letter-spacing: .06em;
  color: var(--cream);
  border-bottom: 1px solid rgba(243, 222, 142, .5);
  padding-bottom: 3px;
  transition: color var(--t-hover), border-color var(--t-hover);
}
.thalia__link:hover { color: var(--butter); border-color: var(--butter); }

/* Same 44px hit area the hero link uses: the text alone is a mean phone target. */
.thalia__link::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  height: 44px;
}

/* --- section waves --- */

/* Every section edge on the page curves. Chenélle, 2026-07-17: "between every
   section i want you to put the curved line and not a straight line, as between
   the hero and about thalia page."

   One geometry, shared. It was already duplicated across two rules before the
   third and fourth edges were asked for; four copies of the same nine
   declarations is how they drift apart, so they are one selector list. Only the
   fill changes per edge, below.

   Each wave is absolute at its section's edge, so it overlays that section's own
   padding rather than adding height beneath it: the flat ground under the copy
   shortens, the section does not grow. No layout moves when one is added.

   preserveAspectRatio="none" is what lets a fixed 92px height stretch across any
   viewport width: the curve is meant to distort, not scale.

   bottom:-1px is the design's value and it is load-bearing, not a rounding
   nicety. Flush at bottom:0, the two grounds meet on exactly one line and the
   browser blends it: a faint hairline right across the seam, plainly visible at
   dpr 2. Hanging 1px into the section below lets the wave's own fill cover the
   join, so there is no edge left to blend. This is also why .thalia can no
   longer clip: see the note there.

   z-index 4 is for Meet Thalia, whose dancer layer (z-index 0) bleeds to this
   edge and would otherwise show through the wave. It is harmless on the other
   three: every wave sits inside its section's bottom padding, well clear of the
   copy, and none of them take a click. */
.thalia__wave,
.styles__wave,
.fees__wave,
.gtk__wave {
  position: absolute;
  left: 0;
  bottom: -1px;
  z-index: 4;
  width: 100%;
  height: 92px;
  display: block;
  pointer-events: none;
}

/* THE RULE FOR EVERY FILL BELOW: a wave is always filled with the ground of the
   section on the OTHER side of the edge, never a colour of its own. It is the
   next section reaching up into this one. Filled from tokens, not literals, so a
   wave cannot drift from the section it has to meet.

   This is the thing that breaks silently. If any section's ground changes, its
   neighbours' fills change with it, or the wave lands as a stripe of the wrong
   colour. The grounds, top to bottom: hero cream, its styles strip butter,
   Meet Thalia navy, Classes butter, Fees navy, Good to know cream, Contact
   navy2. */
.thalia__wave path { fill: var(--butter-grained); }   /* Classes below is butter, as grained */
.styles__wave path { fill: var(--navy); }     /* Fees below */
.fees__wave   path { fill: var(--cream); }    /* Good to know below */

/* The hero → Meet Thalia wave, added on Chenélle's ask. Mirrors the rule above:
   same box, flipped to the top edge, and filled with the colour ARRIVING at the
   edge (the hero's butter styles strip) rather than the one leaving.

   z-index 1, not the 4 its sibling uses. The bottom wave can sit over
   everything because nothing else is down there; this one has the copy (z 3) and
   the grid (z 2) below it, and at 4 it would paint over the top of the words. It
   still needs to clear .thalia__bg at z 0, whose dancer bleeds to top:-6%, so
   the wave masks her top edge into the butter. */
.thalia__wave--top {
  top: -1px;
  bottom: auto;
  z-index: 1;
}
.thalia__wave--top path { fill: var(--butter-grained); }

/* --- stacked (tablet and below) ---
   The design is desktop-only and hands stacking to the rebuild. The portrait is
   first in the DOM, so it stacks first with no reordering, per the brief. */
@media (max-width: 1024px) {
  .thalia__grid {
    grid-template-columns: 1fr;
    gap: clamp(44px, 6vw, 64px);
    justify-items: start;
  }

  /* The two-column pin has to be released with the two columns. This grid has a
     single track here, so grid-column:2 put the copy in an IMPLICIT second one:
     measured at 390px, the copy sat at 68-366 instead of the gutter's 44-346,
     hanging 20px past the right margin with a dead 0px track and a 44px gap
     shoving it there. Silent, because body{overflow-x:hidden} swallowed it. */
  .thalia__copy { grid-column: 1; }

  /* **The transparency stays here, and only here.** Chenélle took it off on
     2026-07-17, but that was the two-column layout, where the copy sits beside
     the dancer and never touches her. Stacked, there is one column: the copy
     runs the full width and lands straight on top of her. At opacity 1 the body
     text is unreadable across her lit face and arms, so she drops back to
     texture, which is what the design had her doing all along.

     translateX is back with her, and it is load-bearing again: the desktop rule
     drops it because she is measured from the left gutter there, but left:50%
     without it puts her *left edge* on the centre line rather than centring her,
     and hangs her off the right of the screen. */
  .thalia__bg img {
    left: 50%;
    /* right:auto is not tidying. The desktop rule pins her with `right` since
       2026-07-17, and left+right+width:auto is over-constrained: this only
       centres because the spec then drops `right` for direction:ltr. Saying auto
       states the intent instead of banking on that fallback. */
    right: auto;
    transform: translateX(-50%);
    opacity: .22;
  }
  .thalia__bg::before { width: min(520px, 82vw); left: 50%; }
}

@media (max-width: 640px) {
  .thalia__portrait { max-width: 100%; }
  .thalia__lead { font-size: 17px; }

  /* The frame's -20px right/bottom offset would push past the gutter and add a
     scrollbar at this width. Tighten it rather than drop it. */
  .thalia__portrait::before { inset: 14px -14px -14px 14px; }
}

/* ---------------------------------------------------------------- classes */

/* The design's centred timeline. Cream, so it meets the wave above cleanly, and
   the design's standard section rhythm (--section-y), not the taller --section-y-lg
   the hero and Meet Thalia use. */
/* Butter, not cream, since 2026-07-17. Chenélle: "not a fan of the mustard gold
   yellow of this section", asking for a brand blue or butter instead. Neither
   works as an ACCENT on cream, and the numbers are worth keeping because they
   close the question rather than reopen it every time:

     butter on cream        1.24:1   invisible, as type or as a line
     Deep Teal on cream    11.26:1   legible, but 1.00:1 against the body navy

   That second number is the trap. A blue accent cannot separate from navy body
   text: to read on cream it has to be dark, and dark blue IS the navy. Gold got
   away with a similar 2.45:1 against navy only because hue did the separating,
   and a blue has no hue left to spend. There is no tint between the two that
   passes 4.5:1 on cream and still reads as an accent; the window is closed.

   So butter becomes the GROUND and navy becomes the ink, which is exactly the
   rule the tokens block already states: butter is a ground on light sections,
   never type on cream. navy on butter is 9.06:1, and body copy uses
   --muted-onbutter (4.73:1), which is why that token exists.

   The section has to stay light: the page alternates hero light, Meet Thalia
   navy, Classes, Fees navy. Going dark here to let butter be the accent would
   put three dark sections in a row. */
.styles {
  position: relative;
  background: var(--butter);
  padding: var(--section-y) 0;

  /* The class name's size, hoisted to a variable because two rules need it and
     they must not drift: the name itself, and the dot that has to sit centred on
     the name's first line at every width this clamp passes through. */
  --styles-name-fs: clamp(24px, 2.9vw, 40px);
  --styles-name-lh: 1.06;
}

/* Chenélle: "way too bright and way too plain". Same paper grain as the hero,
   same tile, so the two light sections share one surface rather than each
   inventing a texture. Multiply, so it is tone on the cream and not a grey film.
   It darkens the ground very slightly, which moves every contrast pair in this
   section the safe way. */
.styles::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.75' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23g)'/%3E%3C/svg%3E");
  opacity: .22;
  mix-blend-mode: multiply;
}

/* The two long curves that swept this section's left and right flanks are gone,
   on Chenélle's instruction (2026-07-17: "remove these lines on the left and
   right side"). They were a `.styles::after` layer carrying an inline SVG of two
   hairline paths, added earlier as "the movement she asked for" and recoloured
   gold → navy when the ground went butter.

   Deleted rather than commented out or set to opacity 0: a dead decorative layer
   is one more thing to recolour on the next theme change, and this one had
   already been recoloured once. It is in the history if it is wanted back.

   The spine is NOT this. That is .styles__spine, the timeline's own centre line,
   and it stays. */

/* The timeline rides above both layers. */
.styles__inner { position: relative; z-index: 1; }

/* 1060px, per the design: narrower than the page's --maxw so the two sides stay
   close enough to the spine to read as one object. */
.styles__inner {
  max-width: 1060px;
  margin: 0 auto;
  padding: 0 var(--gutter);
}

.styles__head {
  text-align: center;
  margin-bottom: clamp(54px, 6vw, 84px);
}

/* Navy, where every other eyebrow on the site is the section's accent. On a
   butter ground the accent IS the ground, so there is no third colour for this
   to be: navy is the only ink butter carries (9.06:1). It reads as an eyebrow on
   size and tracking rather than on colour. */
.styles__eyebrow {
  margin: 0 0 20px;
  font-size: 12.5px;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--navy);
  --d: 0s;
}

.styles__title {
  margin: 0;
  font-family: var(--serif);
  font-weight: 600;
  font-size: clamp(34px, 4.4vw, 58px);
  line-height: 1.02;
  letter-spacing: -.01em;
  color: var(--ink);
  --d: .06s;
}
.styles__title em { font-style: italic; font-weight: 500; }

/* --- timeline --- */

.styles__timeline { position: relative; }

/* Fades to transparent at both ends rather than butting into the first and last
   entries, per the design. The stops are the design's own (11% / 89%). */
.styles__spine {
  position: absolute;
  left: 50%;
  top: 6px;
  bottom: 6px;
  width: 2px;
  transform: translateX(-50%);
  /* Navy at .5, not solid: the gold this replaces was only 2.76:1 on cream, so
     it drew a delicate line. Solid navy on butter is 9.06:1 and would rule the
     section in half. Half-strength puts the line back at the weight it was
     designed to be, while the dots stay solid and keep their punctuation. */
  background: linear-gradient(
    rgba(18, 58, 73, 0),
    rgba(18, 58, 73, .5) 11%,
    rgba(18, 58, 73, .5) 89%,
    rgba(18, 58, 73, 0)
  );
}

/* Three tracks: side, spine gutter, side. The design ships an empty <div> to
   push each entry to one side; grid-column does the same with no filler markup. */
/* align-items:start, not center: an entry grows downward when it opens, and
   centred rows would slide their name and dot down the spine as the panel
   expands. The dot is pinned to the name's line instead, below. */
.styles__item {
  display: grid;
  grid-template-columns: 1fr 44px 1fr;
  align-items: start;
  margin-bottom: clamp(30px, 3.4vw, 44px);
  color: var(--ink);
}
.styles__item:last-of-type { margin-bottom: 0; }

/* The reveal cascade. :nth-of-type counts only the rows, because the spine is a
   <span> and they are <div>s. 80ms a step: the same rhythm the hero and Meet
   Thalia use. The fifth lands at 320ms, the top of the design's documented
   60–320ms range. Five rows, six classes: the ages-6+ row carries two, and they
   share its delay because they arrive together. */
.styles__item:nth-of-type(1) { --d: 0s; }
.styles__item:nth-of-type(2) { --d: .08s; }
.styles__item:nth-of-type(3) { --d: .16s; }
.styles__item:nth-of-type(4) { --d: .24s; }
.styles__item:nth-of-type(5) { --d: .32s; }

/* Sides are explicit, not :nth-of-type. They used to be "odd rows left, even
   rows right", which stopped describing anything the moment one row had a class
   on both sides. Explicit also means inserting a row (Adult Classes did) cannot
   silently flip every row beneath it. */
.styles__side--left {
  grid-column: 1;
  text-align: right;
  padding-right: 34px;
}
.styles__side--right {
  grid-column: 3;
  text-align: left;
  padding-left: 34px;
}

/* --- the trigger --- */

/* The whole header is the button, so text-align is inherited rather than set:
   it follows whichever side the row is on. width:100% makes the entire text
   column the target, not just the words. */
.styles__q { margin: 0; }

.styles__trigger {
  display: block;
  width: 100%;
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
  text-align: inherit;
  color: inherit;
  font-family: inherit;
}

/* No colour change on hover, deliberately, and this is the one real cost of the
   butter ground. The name used to go navy → gold. Butter carries a single ink,
   so the only shift available is navy → navy2, which is imperceptible in a
   hover: you never see the two states side by side.

   The hover is therefore the name's 1.075 scale (Chenélle's own ask, and it was
   never a colour in the first place) plus the dot filling on the spine. Both are
   below. Do not reinstate a colour here without a second ink to move to. */

/* Chenélle: hovering AcroTots, Acrobatics and the rest should make the name
   larger. transform, not font-size: a font-size change reflows the row, nudges
   what sits under it and shifts the dot on the spine, so the whole timeline
   would twitch on hover. A transform paints bigger and reflows nothing.

   inline-block, not inline: a transform does nothing on an inline box, so the
   scale below would silently stop working if this were left inline.

   The origin is the spine side, matching each row's text-align, so the name
   grows outward from the spine instead of sliding across it. */
.styles__name {
  display: inline-block;
  margin: 0;
  font-family: var(--serif);
  font-weight: 600;
  font-size: var(--styles-name-fs);
  line-height: var(--styles-name-lh);
  color: inherit;   /* follows the trigger's hover to gold */
  transform-origin: right center;
  transition: transform .28s var(--ease);
}
.styles__side--right .styles__name { transform-origin: left center; }

.styles__trigger:hover .styles__name,
.styles__trigger:focus-visible .styles__name { transform: scale(1.075); }

/* The same drawn +/× as the FAQ's, and deliberately the same: one accordion
   language on the page, so a visitor learns the affordance once. Chenélle's
   reference showed a chevron, but that came attached to the card layout she
   then dropped, and a second icon language would be the page's only one.

   Second use of this treatment, so it is written out rather than factored. If a
   third appears, factor it with .faq__icon. */
.styles__icon {
  position: relative;
  display: inline-block;
  width: 15px;
  height: 15px;
  margin-left: 14px;
  vertical-align: middle;
  transition: transform .35s var(--ease);
}
.styles__icon::before,
.styles__icon::after {
  content: '';
  position: absolute;
  inset: 50% 0 auto;
  height: 1.5px;
  background: var(--navy);
  transform: translateY(-50%);
}
.styles__icon::after { transform: translateY(-50%) rotate(90deg); }

.styles__entry[data-open="1"] .styles__icon { transform: rotate(45deg); }

/* block, not inline-block: it sits inside the trigger now, under the name, and
   an inline-block would ride up onto the name's line whenever there was room.

   --muted-onbutter, not navy: the tag used to separate from the name by hue
   (gold against navy). With one ink left it separates by weight instead, or it
   would read as loudly as the class name under it. 4.73:1 on butter, so it
   still passes as type. Plain --muted is only 3.69:1 here and must not be used. */
.styles__tag {
  display: block;
  margin-top: 11px;
  font-size: 11.5px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--muted-onbutter);
}

/* --- the panel --- */

/* The same bargain with script.js as the FAQ's, and it must stay the same:
   open in plain CSS, collapsed only under `js`, which the inline <head> script
   sets before first paint. No flash, and a visitor whose JS fails reads every
   description instead of finding six dead buttons. */
.styles__panel { overflow: hidden; }

.js .styles__panel {
  max-height: 0;
  visibility: hidden;
  /* visibility is delayed to the end of the collapse, so the description does
     not vanish before it has finished closing. It is also what takes the
     panel's "Book a trial class" link out of the tab order: overflow:hidden
     clips it visually but leaves it tabbable. */
  transition: max-height .5s ease, visibility 0s linear .5s;
}

.js .styles__entry[data-open="1"] .styles__panel {
  visibility: visible;
  transition: max-height .5s ease, visibility 0s linear 0s;
}

/* Padding, not margin: a margin on the answer would collapse out of the panel
   and the closed height would not be zero. */
.styles__answer { padding: 16px 0 4px; }

/* Left-aligned even on the right-hand rows, where everything else is flush
   right. Ranged-left is what a paragraph is legible as; a four-line description
   set ragged-left is not. The block still hugs the spine via margin-left:auto
   below, so the row still reads as belonging to its side. */
/* --muted-onbutter, not --muted: the usual body grey is 3.69:1 on this ground
   and fails AA. This is the same hue taken darker, 4.73:1. It is the token's
   whole reason for existing. */
.styles__answer p {
  margin: 0;
  max-width: 44ch;
  text-align: left;
  font-size: 15.5px;
  line-height: 1.65;
  color: var(--muted-onbutter);
}
.styles__answer p + p { margin-top: 12px; }

.styles__side--left .styles__answer p { margin-left: auto; }

/* The Adult Classes placeholder. Deliberately ugly: it is a note to us sitting on
   a client page, and it should look like one so it cannot be mistaken for copy or
   quietly survive to launch. Not a token colour, because it is not part of the
   palette and is not meant to be. Delete this rule with the placeholder. */
.styles__tbc {
  max-width: none !important;
  padding: 12px 14px;
  border: 1px dashed #B3261E;
  border-radius: 3px;
  color: #B3261E !important;
  font-size: 13.5px !important;
}

/* The CTA each row carried before it became a button. */
.styles__cta { margin-top: 18px !important; }

/* The link keeps a real hover where the class name could not: it moves its rule
   rather than its ink, from a half-strength navy to solid. That needs no second
   colour, so butter does not cost it anything. */
.styles__cta a {
  display: inline-block;
  font-size: 13.5px;
  letter-spacing: .06em;
  color: var(--navy);
  border-bottom: 1px solid rgba(18, 58, 73, .45);
  padding-bottom: 3px;
  transition: border-color var(--t-hover);
}
.styles__cta a:hover { border-color: var(--navy); }

/* --- the dot --- */

/* The cream ring is not decoration: it masks the spine behind the dot so the
   line appears to pass under it.

   The dot is pinned to the name's first line, not centred on the row: the row
   is align-items:start, so an entry that opens grows downward and its dot would
   otherwise walk down the spine as the panel expanded. Computed from the name's
   own size rather than hand-tuned, so it holds across the clamp. */
.styles__dot {
  grid-column: 2;
  grid-row: 1;
  justify-self: center;
  margin-top: calc((var(--styles-name-fs) * var(--styles-name-lh) - 15px) / 2);
  width: 15px;
  height: 15px;
  border-radius: 50%;
  /* Both of these are the section's ground, not "cream". The ring is what masks
     the spine behind the dot, so it has to be whatever the section is painted:
     left as cream it would draw a pale halo on the butter. */
  background: var(--butter);
  border: 2px solid var(--navy);
  box-shadow: 0 0 0 5px var(--butter);
  /* Scales in with the row, the "icons animate subtly" note from the brief kept
     restrained. The parent .reveal owns its own transform; a child's transform
     composes with it rather than fighting it. */
  transform: scale(.55);
  transition:
    transform .7s var(--ease) var(--d, 0s),
    background var(--t-hover);
}
.styles__item.is-visible .styles__dot { transform: scale(1); }

/* The design fills the last dot: the spine arrives rather than trailing off. */
.styles__item:last-of-type .styles__dot { background: var(--navy); }

/* Hovering a text column fills the row's dot, and an open entry keeps it filled
   so the spine shows where you are. Both carry real weight: the name no longer
   changes colour on hover (see .styles__trigger), so the dot and the name's
   scale are the whole hover.

   :has() on the ROW, not `.styles__side:hover + .styles__dot`, which is what
   these were while every row had one side. The ages-6+ row has a side on either
   side of its dot, and a sibling combinator only ever reaches forwards: it would
   answer to Contemporary & Lyrical and stay dead under Acrobatics. Asking the row
   whether anything inside it is hovered or open is the only phrasing that treats
   one shared node as belonging to both classes, which is the entire point of the
   merge.

   Scoped to .styles__side rather than the row: only the text columns do anything,
   so only the text columns may look like they do. Hovering the empty half of a
   one-sided row must not light its dot. */
.styles__item:has(.styles__side:hover) .styles__dot,
.styles__item:has(.styles__entry[data-open="1"]) .styles__dot { background: var(--navy); }

/* --- single column ---
   The design's README hands the rebuild one instruction for this section:
   "collapse to a single left-aligned list". Measured, not chosen by taste: the
   two sides hold to about 780px, below which "Contemporary & Lyrical" wraps
   against a 40ch measure squeezed under 250px and the spine stops reading as a
   centre line. The spine moves to the left edge and every entry hangs off it. */
@media (max-width: 780px) {
  .styles__spine {
    left: 6.5px;
    transform: none;
  }

  .styles__item {
    grid-template-columns: 15px minmax(0, 1fr);
    column-gap: 22px;
  }

  .styles__dot {
    grid-column: 1;
    justify-self: start;
    /* The desktop rule already centres it on the name's first line and the sum
       still holds here, so it is not restated. */
  }

  /* Both sides collapse into the one track. */
  .styles__side--left,
  .styles__side--right {
    grid-column: 2;
    text-align: left;
    padding-left: 0;
    padding-right: 0;
  }

  /* Everything is left-aligned in the single track, so every name grows from the
     left. Without this the odd rows keep their right origin and their names
     would swell leftwards, off the edge of the column. */
  .styles__side--left .styles__name,
  .styles__side--right .styles__name { transform-origin: left center; }

  /* Same: one track, so the description no longer hugs a spine on its right. */
  .styles__side--left .styles__answer p { margin-left: 0; }

  .styles__item { margin-bottom: 34px; }

  /* The ages-6+ row has two classes and one dot. Side by side that reads as a
     pair at a glance; stacked in a single track it does not, so without this gap
     Acrobatics butts against Contemporary & Lyrical and reads as part of it.
     Same 34px as the row rhythm, so the pair still sits closer to its own dot
     than the next row does.

     .styles__dot + .styles__side, because the DOM is side / dot / side: the
     right-hand class is the one that follows the dot. On desktop it must not
     have this, which is why it lives in here. */
  .styles__dot + .styles__side { margin-top: 34px; }
}

/* ---------------------------------------------------------------- fees */

/* Cream on cream, so the boundary is a hairline. The design puts a wave only at
   a colour change (Meet Thalia → Classes, FAQ → Contact). Do not add one here. */
/* Navy, per Chenélle's alternating scheme (2026-07-16): hero light, Meet Thalia
   dark, Classes light, Fees dark, Good to know light, FAQ light, Contact dark.
   This is the only section whose ground actually had to change; the rest already
   alternated.

   Everything below it in this block is the consequence, not decoration. Flipping
   a section's ground means re-homing every colour that sat on the old one, and
   Fees had eight: eyebrow, title, row rule, label, leader dots, price, notes and
   the pill. The pill was the dangerous one, see .btn--fees in the markup.

   The hairline is gone with the cream. It separated cream from cream; navy
   against cream needs no help. */
/* position: relative is what .fees__wave hangs off. Without it the wave would
   find the nearest positioned ancestor instead and land somewhere else entirely. */
.fees {
  position: relative;
  background: var(--navy);
  color: var(--cream);
  padding: var(--section-y) 0;
}

/* 820px, per the design: the narrowest column on the page. The rows are a
   two-ended pattern and a wide column strands the price away from its label. */
.fees__inner {
  max-width: 820px;
  margin: 0 auto;
  padding: 0 var(--gutter);
  text-align: center;
}

/* No .fees__eyebrow: `SIMPLE, HONEST FEES` came out on Chenélle's instruction
   (2026-07-17) and the title now leads, so it takes the cascade's first beat.
   The rows keep their own delays: 0s → .1s is a longer gap than the .06s → .1s
   it replaces, but 40ms inside a 900ms reveal is not a visible difference, and
   re-numbering five rows to buy it would be churn. */
.fees__title {
  margin: 0 0 46px;
  font-family: var(--serif);
  font-weight: 600;
  font-size: clamp(36px, 4.6vw, 60px);
  line-height: 1.02;
  letter-spacing: -.01em;
  color: var(--cream);
  --d: 0s;
}

/* The heading is centred; the rows are not. */
.fees__list { text-align: left; }

.fees__row {
  display: flex;
  align-items: baseline;
  gap: 16px;
  padding: 22px 0;
  /* --hairline-strong is navy ink on a light ground and vanishes here. Same
     idea inverted: cream at the same alpha. */
  border-bottom: 1px solid rgba(251, 246, 234, .18);
}

/* The design's own cascade for this section, 40ms a step after the title. */
.fees__row:nth-child(1) { --d: .1s; }
.fees__row:nth-child(2) { --d: .14s; }
.fees__row:nth-child(3) { --d: .18s; }
.fees__row:nth-child(4) { --d: .22s; }
.fees__row:nth-child(5) { --d: .26s; }

.fees__label {
  font-family: var(--serif);
  font-size: clamp(20px, 2.3vw, 26px);
  color: var(--cream);
}

/* Not a token: the design draws this leader at .4 alpha, stronger than either
   hairline, because a dotted line reads lighter than a solid one at the same
   value. Lifted straight from the design markup. The -5px lifts it off the
   baseline the label and price share, to sit mid-x-height between them. */
.fees__leader {
  flex: 1;
  border-bottom: 1px dotted rgba(251, 246, 234, .42);
  transform: translateY(-5px);
}

.fees__price {
  font-size: 17px;
  color: var(--cream);
  white-space: nowrap;
}

/* Fine print, behind a disclosure (2026-07-17, Chenélle). The accordion's third
   caller: script.js owns the behaviour, this only dresses it. */
.fees__notes {
  margin: 28px 0 40px;
  text-align: left;
  --d: .3s;
}

/* Same inverted hairline as .fees__row above: navy ink vanishes on this ground. */
.fees__note { border-top: 1px solid rgba(251, 246, 234, .18); }

.fees__note-q { margin: 0; }

/* A label, not a question, so it is set as one: the section's fine print asking
   to be opened. The FAQ's trigger is a serif question because it *is* one. What
   the two share is the icon and the behaviour, which is what makes it read as
   the same control. */
.fees__note-trigger {
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 20px;
  padding: 16px 0;
  min-height: 44px;
  background: none;
  border: 0;
  cursor: pointer;
  font-family: var(--sans);
  font-size: 12.5px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--butter);   /* 9.06:1 on navy; --gold-text is a cream-ground colour */
  transition: color var(--t-hover);
}
.fees__note-trigger:hover { color: var(--cream); }

.fees__note-icon {
  position: relative;
  flex: none;
  width: 18px;
  height: 18px;
  transition: transform .35s var(--ease);
}
/* currentColor, so the + is whatever the label is and the two hover together.
   The FAQ's icon is a fixed --gold because it sits on cream. */
.fees__note-icon::before,
.fees__note-icon::after {
  content: '';
  position: absolute;
  inset: 50% 0 auto;
  height: 1.5px;
  background: currentColor;
  transform: translateY(-50%);
}
.fees__note-icon::after { transform: translateY(-50%) rotate(90deg); }

.fees__note[data-open="1"] .fees__note-icon { transform: rotate(45deg); }

/* Open by default. Only `html.js` collapses it, and that is set inline in <head>
   before first paint, so this never flashes. These are the actual costs a parent
   pays: a visitor whose JS fails reads them rather than finding a dead button.
   Do not give this panel a closed state outside `.js`. */
.fees__note-panel { overflow: hidden; }

.js .fees__note-panel {
  max-height: 0;
  visibility: hidden;
  /* visibility is held back until the collapse finishes, so the text does not
     vanish mid-close, and it is what takes the panel out of the tab order:
     overflow:hidden only clips. */
  transition: max-height .5s ease, visibility 0s linear .5s;
}

.js .fees__note[data-open="1"] .fees__note-panel {
  visibility: visible;
  transition: max-height .5s ease, visibility 0s linear 0s;
}

/* Padding, not margin: a margin on the body would collapse out of the panel and
   the closed height would not be zero. */
/* Two columns, so the fine print fills the 820 column rather than hugging the
   left under the full-width rows above it (that void was the asymmetry Chenélle
   flagged, 2026-07-20). columns balances the fill for any paragraph count, where
   a 2-up grid would leave an empty cell on an odd one; break-inside keeps a
   paragraph whole. The 62ch cap is gone: each column is now the measure. */
.fees__note-body {
  padding: 2px 0 24px;
  columns: 2;
  column-gap: clamp(32px, 5vw, 60px);
}

.fees__note-body p {
  font-size: 14px;
  line-height: 1.6;
  /* --muted is the body colour for cream grounds; --mutedd is its navy twin. */
  color: var(--mutedd);
  margin: 0 0 14px;
  break-inside: avoid;
}
.fees__note-body p:last-child { margin-bottom: 0; }

.fees__cta { --d: .34s; }

/* The pill, re-homed rather than reclassed. .btn--primary is navy on cream: on
   this section it became a navy pill on a navy band, i.e. nothing but a floating
   cream label. Butter on navy is the same inversion Contact already uses.

   Scoped here rather than swapping the markup to .btn--butter, because
   .btn--butter is declared after .btn--fees and would therefore have won the
   padding cascade, quietly replacing this pill's own 16px 34px / .12em geometry
   with Contact's 17px 38px. The class swap looks like the tidier change and is
   the one that loses the design. */
.fees .btn--primary { background: var(--butter); color: var(--navy2); }
.fees .btn--primary:hover { background: var(--cream); color: var(--navy2); }

/* The row is label / leader / price on one line, and the price must never wrap
   away from its own label. Measured at 375px, not guessed: "Private lessons" at
   its 20px floor plus "R300 per session" at 17px leaves the leader 28.2px,
   which reads as a smudge rather than a line. Stepping the type down here takes
   it to 65.1px. The design has no mobile spec for this section, so these are
   the smallest values that keep the leader legible. */
@media (max-width: 560px) {
  .fees__row { gap: 12px; padding: 18px 0; }
  .fees__label { font-size: 18px; }
  .fees__price { font-size: 15px; }
  /* One column on phones: two would be ~26ch each, too narrow to read. Matches
     the story gallery, which collapses at the same width. */
  .fees__note-body { columns: 1; }
}

/* ---------------------------------------------------------------- section stubs */

/* TEMPORARY. Each stub is replaced by its real section in the session named on
   it, so anchor links are testable now. None of this survives session 8. */
/* ---------------------------------------------------------- good to know (#faq)

   One section, three areas: the studio location, what to bring, and the FAQ.
   Chenélle briefed this composition on 2026-07-16. It replaces the approved
   design's FAQ layout (a sticky left header beside a right-hand accordion), so
   the values below are the design's own, recombined:

   - 1240px inner and the clamp(40px,6vw,86px) gap are the design's FAQ figures.
   - The eyebrow, title, question, answer and hairline values are the design's
     FAQ values unchanged.
   - The subsection heading is the one size with no design precedent. It sits
     between the design's FAQ question (max 26px) and its class name (max 40px),
     which is what makes it outrank the questions below it without competing
     with the section title above.

   Cream-to-cream, so the top edge is a hairline, never a wave. The FAQ → Contact
   wave is session 8's, with the section it resolves into. */

.gtk {
  position: relative;
  background: var(--cream);
  padding: var(--section-y) 0;
  border-top: 1px solid var(--hairline);

  /* The wave to Contact is 92px and overlays this padding rather than adding
     height beneath it, so the padding has to outrun the wave or the last FAQ
     row sits under it. --section-y bottoms out at 78px, 14px short, hence the
     floor. Above a 1040px viewport --section-y wins and this does nothing. Meet
     Thalia never needed it: --section-y-lg starts at 96px and clears on its own. */
  padding-bottom: max(var(--section-y), 104px);
}

/* The section's one decorative treatment.

   It is in normal flow, in the band between the two columns and the FAQ, and
   that is the whole point: Chenélle's rule (2026-07-17) is that it must never
   cross text at any width. Absolutely positioning it behind the content is what
   broke that rule, and reshaping the curve cannot fix it, because the section
   reflows from two columns to one and any path that threads the gaps at 1440
   runs through a paragraph at 768. A flow element in a gap cannot overlap text
   at any width, so the guarantee holds without being re-checked per breakpoint.

   Its own margins are the gap, so .gtk__faq adds none. Full-bleed because it is
   a child of .gtk rather than .gtk__inner. */
.gtk__motif {
  display: block;
  width: 100%;
  height: clamp(64px, 7vw, 96px);
  margin: clamp(52px, 5.5vw, 78px) 0;
  pointer-events: none;
  fill: none;
  stroke: var(--gold);
  stroke-width: 1.5;
  opacity: .16;
}

.gtk__inner {
  position: relative;
  z-index: 1;
  max-width: 1240px;
  margin: 0 auto;
  padding: 0 var(--gutter);
}

/* Centred, matching Classes and Fees directly above it. The design's FAQ head is
   left-aligned because it was a sticky column; it is not one here. */
.gtk__head { text-align: center; }

.gtk__eyebrow {
  --d: 0s;
  font-size: 12.5px;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--gold-text);
  margin: 0 0 22px;
}

.gtk__title {
  --d: .06s;
  font-family: var(--serif);
  font-weight: 600;
  font-size: clamp(32px, 4vw, 54px);
  line-height: 1.04;
  letter-spacing: -.01em;
  margin: 0;
  color: var(--ink);
}

.gtk__sub {
  --d: .12s;
  font-size: 16.5px;
  line-height: 1.6;
  color: var(--muted);
  margin: 26px auto 0;
  max-width: 58ch;
}

.gtk__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: clamp(40px, 6vw, 86px);
  margin-top: clamp(56px, 6vw, 84px);
}

/* The two columns arrive as a pair, one just behind the other. The FAQ below
   fades in as one block (a single .reveal on .faq), per the brief. */
.gtk__col:nth-child(1) { --d: 0s; }
.gtk__col:nth-child(2) { --d: .08s; }

/* 700, where every other serif heading on the page is 600, and the exception is
   deliberate. These three ("Find the Studio", "What to Bring", "Frequently Asked
   Questions") are the section's signposts, and they are the smallest headings
   asked to do that job: the section title carries 54px, so size alone gives it
   presence, while these have 30px and must still outrank the 26px FAQ questions
   directly beneath them.

   At 600 they did. That was under Bodoni Moda. Cormorant Garamond is a much
   lighter face at the same weight, so the serif swap quietly thinned them until
   "Frequently Asked Questions" stopped reading as a heading at all (Chenélle,
   2026-07-17). Nothing was broken and no rule had changed: the same 600 simply
   means less in this typeface. 700 restores the rank the 600 used to carry.

   All three move together. They are peers, and boldening only the FAQ one would
   break the composition to fix a symptom. */
.gtk__subhead {
  font-family: var(--serif);
  font-weight: 700;
  font-size: clamp(22px, 2.4vw, 30px);
  line-height: 1.15;
  letter-spacing: -.01em;
  margin: 0;
  color: var(--ink);
}

/* The FAQ's signpost, bigger than its two siblings. Chenélle asked for this on
   2026-07-17 so a visitor scrolling past knows what they have arrived at.

   The three subheads are peers in the content, so this splits them on purpose
   and the reason is width, not rank. "Find the Studio" and "What to Bring" head
   533px columns and each sits directly above its own container, a bordered card
   and an icon list, which give them context and hold them down. This one heads
   the full 1152px and has nothing under it but hairline rules, so at 30px it was
   a small heading floating over a wide, quiet block. 38px is proportionate to
   the width it has to carry; 38px over a half-width column would be top-heavy,
   which is why the other two stay at 30.

   38px is a ceiling, not a preference. The section title is 54px and has to stay
   clearly above this: measured at 42px the two start competing and the FAQ reads
   as its own section, which is the one thing the brief for this section ruled
   out. Do not raise it further without re-checking against the title. */
.gtk__subhead--faq {
  font-size: clamp(26px, 3vw, 38px);
}

.gtk__lead {
  font-size: 16.5px;
  line-height: 1.6;
  color: var(--muted);
  margin: 14px 0 0;
  max-width: 52ch;
}

/* ------------------------------------------------ good to know: the location */

.gtk__card {
  margin-top: 22px;
  padding: clamp(24px, 2.6vw, 34px);
  border: 1px solid var(--hairline-strong);
  border-radius: 14px;
  background: var(--cream);
  box-shadow: 0 18px 40px -28px rgba(18, 58, 73, .5);
}

.venues { list-style: none; margin: 0; padding: 0; }

.venue + .venue {
  margin-top: 26px;
  padding-top: 26px;
  border-top: 1px solid var(--hairline);
}

.venue__name {
  font-family: var(--serif);
  font-weight: 500;
  font-size: 21px;
  line-height: 1.2;
  margin: 0;
  color: var(--ink);
}

.venue__addr {
  font-size: 16px;
  line-height: 1.6;
  color: var(--muted);
  margin: 8px 0 0;
  max-width: 34ch;
}

.venue__link { margin-top: 16px; }

/* Was .gtk__tbc, a dashed [TBC] placeholder. It is real copy now, so it loses
   the dashed box that marked it as unfinished and sets as a quiet note. */
.gtk__note {
  font-size: 13.5px;
  line-height: 1.55;
  color: var(--muted);
  margin: 24px 0 0;
}

/* ------------------------------------------- good to know: what to bring */

.bring {
  list-style: none;
  margin: 26px 0 0;
  padding: 0;
}

/* An open list, not a card each: five cards beside the location card would read
   as a grid of unrelated blocks, which is the thing this section exists to
   avoid. The hairline between items is the only division they need. */
.bring__item {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  gap: 18px;
  align-items: start;
}

.bring__item + .bring__item {
  margin-top: 22px;
  padding-top: 22px;
  border-top: 1px solid var(--hairline);
}

.bring__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  margin-top: 1px;
}

.bring__icon svg {
  width: 100%;
  height: 100%;
  fill: none;
  stroke: var(--gold);
  stroke-width: 1.25;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.bring__name {
  font-family: var(--serif);
  font-weight: 500;
  font-size: 19px;
  line-height: 1.25;
  margin: 0;
  color: var(--ink);
}

.bring__text p {
  font-size: 15.5px;
  line-height: 1.6;
  color: var(--muted);
  margin: 6px 0 0;
}

/* ------------------------------------------------------ good to know: the FAQ */

/* No margin-top: the motif above is a flow element and its own margins are the
   gap between the columns and the FAQ. Adding one here would double it. */
.gtk__faq { margin-top: 0; }

.faq { margin-top: 30px; }

.faq__item { border-top: 1px solid var(--hairline-strong); }
.faq__item:last-child { border-bottom: 1px solid var(--hairline-strong); }

/* The question is a heading so the accordion can be navigated by heading, and a
   button so it can be operated by keyboard. The heading carries no style of its
   own: the button below is the visible thing. */
.faq__q { margin: 0; font: inherit; }

.faq__trigger {
  width: 100%;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 20px;
  padding: 24px 0;
  min-height: 44px;
  background: none;
  border: 0;
  cursor: pointer;
  text-align: left;
  font-family: var(--serif);
  font-weight: 500;
  font-size: clamp(19px, 2vw, 26px);
  line-height: 1.3;
  color: var(--ink);
  transition: color var(--t-hover);
}
.faq__trigger:hover { color: var(--gold-text); }

/* Drawn rather than typed: the design's "+" is a text glyph, which sits off
   centre in the sans and rotates around its own baseline box rather than the
   crossing point. Two bars rotate cleanly to an ×. This is why the icon survived
   the sans change: it never depended on the font's metrics. */
.faq__icon {
  position: relative;
  flex: none;
  width: 18px;
  height: 18px;
  transition: transform .35s var(--ease);
}
.faq__icon::before,
.faq__icon::after {
  content: '';
  position: absolute;
  inset: 50% 0 auto;
  height: 1.5px;
  background: var(--gold);
  transform: translateY(-50%);
}
.faq__icon::after { transform: translateY(-50%) rotate(90deg); }

.faq__item[data-open="1"] .faq__icon { transform: rotate(45deg); }

/* Open by default. Only the `js` class collapses them, and it is set inline in
   <head> before first paint, so this never flashes. A visitor whose JS fails
   reads all seven answers instead of finding seven buttons that do nothing. */
.faq__panel { overflow: hidden; }

.js .faq__panel {
  max-height: 0;
  visibility: hidden;
  /* visibility is delayed to the end of the collapse, so the answer does not
     vanish before it has finished closing. It is what takes the panel's links
     out of the tab order: overflow:hidden only clips them visually, so without
     this a keyboard visitor tabs into an invisible WhatsApp link. */
  transition: max-height .5s ease, visibility 0s linear .5s;
}

.js .faq__item[data-open="1"] .faq__panel {
  visibility: visible;
  transition: max-height .5s ease, visibility 0s linear 0s;
}

/* Padding, not margin: a margin on the answer would collapse out of the panel
   and the closed height would not be zero. */
.faq__answer { padding: 0 0 26px; }

.faq__answer p {
  font-size: 16.5px;
  line-height: 1.65;
  color: var(--muted);
  margin: 0;
  max-width: 60ch;
}

.faq__rules {
  margin: 0;
  padding: 0 0 0 20px;
  max-width: 68ch;
}
.faq__rules li {
  font-size: 16.5px;
  line-height: 1.6;
  color: var(--muted);
}
.faq__rules li + li { margin-top: 8px; }
.faq__rules::marker { color: var(--gold-text); }

.faq__link {
  color: var(--ink);
  border-bottom: 1px solid var(--gold);
  transition: color var(--t-hover);
}
.faq__link:hover { color: var(--gold-text); }

/* Stack at the tablet breakpoint: location first, then what to bring, then the
   FAQ, which is the DOM order already. */
@media (max-width: 1024px) {
  .gtk__grid {
    grid-template-columns: minmax(0, 1fr);
    gap: clamp(48px, 7vw, 68px);
  }
  .gtk__card { max-width: 640px; }
  .bring { max-width: 640px; }
}

@media (max-width: 640px) {
  .gtk__card { padding: 22px 18px; }
  .bring__item { gap: 14px; }

  /* The design's outline pill measures 39.5px, which is what the header ships
     and is fine for a mouse. These two are the section's only real touch
     targets, so on a phone they go full width and take the 44px minimum. */
  .venue__link {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
  }
}

/* --- wave to Contact --- */

/* Geometry is shared: see "section waves" up by .thalia__wave. Only the fill
   lives here, and it is Contact's navy2, per the rule stated there.

   This wave used to run at z-index:auto and paint under .gtk__inner (z-index:1).
   It now takes the shared z-index:4 and paints over it instead. That is
   deliberate and it is safe: the wave is anchored to the section's bottom edge,
   so an opening FAQ panel grows the section and carries the wave down with it.
   The section's bottom padding (--section-y, 78px at its floor against ~50px of
   actual fill) is what keeps the curve off the words. If the padding here is
   ever cut, check this edge. */
.gtk__wave path { fill: var(--navy2); }

/* ------------------------------------------------------------ screen reader only */

/* Both venue buttons read "Get Directions". This is what keeps their accessible
   names distinct without printing the venue twice on screen. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------- contact */

/* The approved design's closing band, and the page's second and last dark
   section. No bottom padding: the footer closes it, and the two share the navy
   so the join between them is invisible. */
/* The bottom padding is load-bearing, not rhythm. .contact__inner has no bottom
   padding or border, so the panel's bottom margin collapses straight through it
   and out of this section: with padding-bottom:0 that margin became an 80px gap
   between the navy Contact and the navy footer, and the body's cream showed
   through it as a stripe across the foot of the page. Spacing below the panel
   has to be padding here, never margin down there. */
.contact {
  background: var(--navy2);
  color: var(--cream);
  padding: clamp(40px, 5vw, 70px) 0 clamp(56px, 6vw, 80px);
}

.contact__inner {
  max-width: 900px;
  margin: 0 auto;
  padding: 0 var(--gutter);
  text-align: center;
}

.contact__eyebrow {
  margin: 0 0 22px;
  font-size: 12.5px;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--butter);
  --d: 0s;
}

/* The 82px cap is the design's and stays: this is the page's closing line and its
   largest type. The floor came down from 40px to 36px on 2026-07-17, when
   Chenélle asked for the title on one line.

   Measured, not chosen. "Come dance with us." renders 7.79x its font size in
   Cormorant, so at the old 40px floor it needed 311.7px against the 312px a
   360px phone leaves after gutters: 0.3px of slack, which any rendering
   difference would eat, and the line would wrap to a one-word orphan. 36px needs
   280px and clears the same phone by 31px. It is also not an odd number here:
   every other section title on the site floors between 32px and 36px, so this
   still has the largest cap on the page and now shares their floor. */
.contact__title {
  margin: 0;
  font-family: var(--serif);
  font-weight: 500;
  font-size: clamp(36px, 5.6vw, 82px);
  line-height: 1;
  letter-spacing: -.015em;
  color: var(--cream);
  --d: .06s;
}
.contact__title em { font-style: italic; }

/* Chenélle asked for one sentence per line on 2026-07-17, so the break is an
   explicit <br> in the markup and text-wrap:balance came back off. The two fight:
   balance re-breaks whatever the <br> already decided, so the line would come out
   split in a place nobody chose. An author break beats an automatic one.

   No max-width either, for the same reason: at 46ch the first sentence wrapped
   before it reached the <br> and the line came out 3 lines, not 2. The measure is
   now the sentence itself. .contact__inner caps it at 812px, which is 68ch here,
   comfortably longer than either sentence needs. */
.contact__lead {
  margin: 28px auto 0;
  font-size: 19px;
  line-height: 1.6;
  color: var(--mutedd);
  --d: .12s;
}

/* Below this the first sentence no longer fits a line of its own, so the authored
   break stops helping: it would force 2 lines then 1 rather than an even rag.
   Off it goes and the sentence wraps.

   Measured, not chosen: "Ask a question, check class availability, or just say
   hello." renders 447.4px at 19px, and the 24px gutters run that out at a 496px
   viewport. The query sits at 520 rather than 496 so the fallback starts a little
   before the cliff instead of exactly on it.

   text-wrap:balance is safe here and only here. With the <br> gone there is no
   authored break for it to fight, and this line is centred, so the box it
   declines to shrink costs nothing. Do not copy it onto a left-aligned line: see
   the note where .contact__note used to be. */
@media (max-width: 520px) {
  .contact__lead br { display: none; }
  .contact__lead { text-wrap: balance; }
}

.contact__actions { margin: 40px 0 0; --d: .16s; }

/* .contact__note is gone: Chenélle removed the sentence it set on 2026-07-17.
   Its rules went with it rather than being left to rot.

   Worth keeping from it, because the next person to fight a rag here will hit
   the same thing: it was a 30ch measure and deliberately NOT text-wrap:balance.
   Balance fixes a rag by shortening the lines but it does not shrink the box,
   and that box sized the panel's right grid track, so balancing it left 116px of
   empty track in a left-aligned column and pulled the panel 57px off the centred
   head. It fixed the sentence and re-broke the symmetry. A measure fixed both,
   because greedy wrapping always fills its box to within a few px. Balance is
   safe on .contact__lead only because that line is centred, so its box costs
   nothing. */

/* --- the panel: photograph left, ask right --- */

/* Chenélle 2026-07-16. The portrait used to sit above the eyebrow at its
   intrinsic 880x1318 with no rule of its own at all, which is why it came out
   "way too large": nothing was sizing it. It now has a size, a frame and a side.

   .contact__inner is centred, and the eyebrow and title stay that way. The panel
   underneath is not: two columns want their own alignment. */
.contact__panel {
  /* Top only. A bottom margin here collapses out of the section: see .contact. */
  margin: clamp(44px, 5vw, 68px) 0 0;
  display: grid;
  /* Both tracks size to their content, and the pair is then centred as one unit.
     This was `auto minmax(0, 1fr)`, which stretched the right track to fill the
     812px box while the note inside it stayed capped at its 42ch measure: 149px
     of the track was empty, so the panel's visible mass sat 75px left of the
     centred head above it. Measured at 1440: head centre 712, panel content
     centre 638. That gap is what read as "very off". Content-sized tracks plus
     justify-content put both centres on the same axis. */
  grid-template-columns: auto auto;
  justify-content: center;
  align-items: center;
  gap: clamp(30px, 5vw, 60px);
  text-align: left;
}

/* "make it small, with the frame that it used to have on the Meet Thalia page" */
.contact__portrait {
  position: relative;
  margin: 0;
  width: clamp(170px, 21vw, 250px);
  --d: .12s;
}

/* Meet Thalia's offset gold frame. That portrait sat on the left and pushed its
   frame down-right; this one is on the left too, so it does the same and leans
   away from the copy rather than into it. Gold as a line, so it stays on the
   signed-off --gold rather than --gold-text, which is only ever type. */
.contact__portrait::before {
  content: '';
  position: absolute;
  inset: 20px -20px -20px 20px;   /* top right bottom left: down and to the right */
  border: 1px solid rgba(185, 143, 59, .45);
  /* 15px against the photo's 14px, and the extra pixel is not a rounding error.
     This frame sits 20px outside the photo, so it is the outer of two concentric
     corners: at an equal radius the outer curve reads tighter than the inner one
     where they cross. One px of slack is what makes them look like the same
     corner. See the radius note on .contact__portrait img. */
  border-radius: 15px;
}

.contact__portrait img {
  position: relative;   /* over the frame's offset square */
  display: block;
  width: 100%;
  /* Load-bearing, as on every other portrait here: the height attribute is a
     presentational hint and beats aspect-ratio without it. */
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  /* Chenélle asked for soft corners here, 2026-07-17. 14px is not picked by eye:
     it is .gtk__card's radius, the nearest thing on the site to a panel of this
     weight (the pills are fully round). Borrowing it keeps this on the radii the
     site already has rather than adding a third.

     Meet Thalia's portrait uses the same offset-frame device and is still
     square. Chenélle was told; it stays square until she says otherwise. */
  border-radius: 14px;
}

/* Fades rather than rises, matching the other portraits. */
.contact__portrait.reveal {
  transform: none;
  transition: opacity 1.4s var(--ease) .1s;
}

/* .contact__note and its overrides are gone with the sentence they set, removed
   on Chenélle's 2026-07-17 ask. The pill is the whole of the ask column now, so
   it needs no top margin against a paragraph that no longer exists. */
.contact__ask .contact__actions { margin-top: 0; }

/* .contact__details and .contact__link are gone with the number and the email,
   removed on Chenélle's 2026-07-16 ask, and their rules went with them rather
   than being left to rot: the list, its drawn separator dot, the 44px hit area
   and the 460px stacking rule. The WhatsApp pill is now the only way to reach
   Thalia from this section, which is the point.

   The pill is a real button, so it already clears the 44px target the old 14px
   links needed that hit area for.

   Note the email address blissfuldancing.sa@gmail.com now appears nowhere on
   the site. That was deliberate on her part, but it is a real contact route
   being retired, so it is logged in CONTENT_SOURCE_OF_TRUTH.md rather than just
   deleted here. */

/* --- stacked --- */

/* The panel is a picture beside a paragraph. Below this it is a picture above a
   paragraph, and the portrait stops being a column and becomes a header. */
@media (max-width: 700px) {
  .contact__panel {
    grid-template-columns: 1fr;
    justify-items: center;
    text-align: center;
    gap: clamp(34px, 7vw, 48px);
  }

  /* The offset frame pushes 20px past the picture on the right. Centred in a
     single column that overhang is what decides whether the section scrolls
     sideways on a phone, so the portrait keeps clear of the gutter. */
  .contact__portrait { width: min(230px, 62vw); }
}

/* ---------------------------------------------------------------- footer */

/* Outside <main>, but on Contact's navy, so it reads as the design's nested
   footer while still being a page-level landmark.

   The top spacing is padding here rather than a margin on the inner: a margin
   would collapse straight out through this box and open a cream gap between
   Contact and the footer. */
.site-footer {
  background: var(--navy2);
  padding: clamp(64px, 8vw, 100px) var(--gutter) 0;
}

.site-footer__inner {
  /* The design's footer sits inside Contact's 900px box, which is itself inset
     by the gutter, so its hairline spans 812px and not the full 900. */
  max-width: calc(900px - var(--gutter) * 2);
  margin: 0 auto;
  padding: 30px 0;
  border-top: 1px solid rgba(251, 246, 234, .14);
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 16px;
}

.site-footer__lockup {
  height: 26px;
  width: auto;
  opacity: .9;
}

/* Was a flex container holding the copyright and the credit as two spans, which
   is what made the bar two items instead of three and put 192px on one side of
   the copyright against 18px on the other. It is now simply a line of text: one
   of three peers the parent distributes. */
.site-footer__legal {
  margin: 0;
  font-size: 12px;
  letter-spacing: .06em;
  color: var(--mutedd);
}

.site-footer__link {
  color: var(--mutedd);
  border-bottom: 1px solid rgba(243, 222, 142, .4);
  padding-bottom: 2px;
  transition: color var(--t-hover), border-color var(--t-hover);
}
.site-footer__link:hover { color: var(--butter); border-color: var(--butter); }

/* Once the three can no longer share a line, space-between stops being the right
   distribution: it works per flex line, so a wrapped bar would push the lockup
   and the copyright to opposite edges of line one and drop the credit alone on
   line two. Stacked, they go left instead and share the lockup's edge, on one
   consistent gap.

   Measured, not chosen: the three need 601.7px of ink plus their 2 x 16px
   minimum gaps, which the 44px gutters run out of at a **721.7px** viewport.

   This query read 740px until 2026-07-17, measured back when the footer was two
   items rather than three (the pair needed 652.6px, running out at 741). Left
   there it fired across 722-740, a band where the three still fit on one line:
   they would have bunched left on a 22px gap instead of distributing, which is
   the exact thing Chenélle asked to have fixed. Re-measure this if the lockup or
   either line of text changes. */
@media (max-width: 720px) {
  .site-footer__inner {
    justify-content: flex-start;
    gap: 22px;
  }
}

/* ================================================================ my-story.html */

/* The Meet Thalia page. There is no approved design for it: the Claude Design
   covers the homepage only, so everything below is built from patterns the
   design already established, not invented alongside them.

   **Rebuilt 2026-07-17.** Chenélle: "way too plain... make this section more
   beautiful... give it the same vibe as the rest of the site. It needs to be
   wow." It was plain for a reason worth naming, because the fix follows from it:
   the page was cream from the breadcrumb to the closing band, one colour change
   over its whole length, while the home page had grown to navy → butter → navy →
   butter → cream → navy2 with a curve on every edge. Calm on its own; next to
   the rest of the site it read unfinished.

   It now takes the home page's rhythm: **navy → cream → butter → cream →
   navy2**, curve on every join, the same wave path and the same fills. The hero
   is navy because that is what the cut-out needs. Her dress is near-black and
   her arms are stage-lit, so on cream she reads as a dark shape stuck to a pale
   page; on navy she reads the way she does on the home page's hero and Meet
   Thalia, which is the arrangement Chenélle asked this page to match.

   IMPORTANT, and it governs every glow below: --muted on flat cream is 4.59:1,
   the tightest passing pair on the site. Composited over --butterpale it drops
   to 4.25:1 and fails AA. So no butter wash sits behind body copy on cream. On
   navy there is no such constraint, which is the other reason the glow can be
   generous up there and has to be disciplined down here. */

/* --- breadcrumb --- */

/* Navy, so it and the hero read as one band rather than leaving a cream strip
   stranded above a navy section. The inner box keeps the page's measure; the
   background is full-bleed. */
.crumb {
  background: var(--navy);
  padding: clamp(22px, 3vw, 34px) var(--gutter) 0;
}

.crumb__inner {
  max-width: var(--maxw);
  margin: 0 auto;
}

.crumb__list {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: 11.5px;
  letter-spacing: .16em;
  text-transform: uppercase;
}

/* The separator is a hairline drawn in CSS, not an arrow typed into the markup,
   for the reason the Contact list's dots are: it is punctuation between links
   and a screen reader has no business reading it out. The <ol>, the
   aria-label and aria-current carry the structure. */
.crumb__item + .crumb__item::before {
  content: '';
  display: inline-block;
  width: 16px;
  height: 1px;
  margin-right: 12px;
  vertical-align: middle;
  background: var(--hairline-strong);
}

.crumb__item { color: var(--mutedd); }        /* 6.30:1 on navy */
.crumb__link { color: var(--cream); opacity: .82; transition: opacity var(--t-hover); }
.crumb__link:hover { opacity: 1; }
.crumb__item + .crumb__item::before { background: rgba(251, 246, 234, .28); }

/* --- hero --- */

/* Navy, and the cut-out stands on the floor of it. padding-bottom is the wave's
   own height and no more: the figure is bottom-aligned, so the section's floor
   is where she stands and the curve is drawn across it. */
.story-hero {
  position: relative;
  overflow: hidden;
  background: var(--navy);
  color: var(--cream);
  padding: clamp(30px, 4vw, 54px) 0 92px;
}

.story-hero__grid {
  position: relative;
  z-index: 1;
  max-width: 1320px;
  margin: 0 auto;
  padding: 0 var(--gutter);
  display: grid;
  /* Figure left, copy right, per Chenélle 2026-07-17. The copy still takes the
     wider column: it holds the page's h1. */
  grid-template-columns: minmax(0, .82fr) minmax(0, 1.08fr);
  align-items: center;
  gap: clamp(32px, 5vw, 90px);
}

/* Bottom-aligned, so she stands on the section's floor rather than floating in
   a centred row, and the eye reads figure-then-words along the ground line.
   The negative margin is what lets her overlap the curve instead of stopping
   politely above it: it is the difference between a photo placed on a page and a
   figure standing in it. It cannot cause a scrollbar, because .story-hero
   clips. */
.story-hero__figure {
  position: relative;
  z-index: 1;
  margin: 0 0 -92px;
  align-self: end;
  justify-self: center;
  max-width: 460px;
}

/* The glow, and it hangs off the figure rather than off the section on purpose.

   It is doing real work, not decoration: her dress is near-black on a navy
   section and dissolves into it without something behind her. But it is also the
   most dangerous thing on this page. --mutedd is 6.3:1 on flat navy; composited
   over the centre of this wash it falls to **3.64:1 and fails AA**. It is the
   cream hero's old trap exactly, mirrored: butter lifts the ground toward light
   copy the same way it lifts it toward dark copy.

   Pinning it to the figure is the fix, not a tidy-up. Centred on the section it
   sat 268px left of her, so her right side dissolved anyway AND the wash drifted
   toward the copy as the viewport changed. Anchored here it tracks her at every
   breakpoint for free.

   **150%, and the number is measured rather than chosen.** The gradient reaches
   zero alpha at 70% of its radius, so what has to clear the copy is that stop,
   not the box. At 165% the worst case was 1040-1120px, where the copy column
   narrows faster than the figure does: 22px of clearance. True zero, so it
   passed, but 22px is inside the noise of a font swap or a measure change and
   would have broken silently. 150% roughly doubles it everywhere and costs
   almost nothing visually, because the outer fifth of that circle was already
   painting nothing. **If you grow this, or push the 70% stop out, re-measure the
   stop against .story-hero__copy's left edge at 1040 and not just at 1440.** */
.story-hero__figure::before {
  content: '';
  position: absolute;
  left: 50%;
  top: 46%;
  transform: translate(-50%, -50%);
  z-index: -1;
  width: 150%;
  aspect-ratio: 1;
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(243, 222, 142, .22), rgba(18, 58, 73, 0) 70%);
  pointer-events: none;
}

.story-hero__figure img {
  position: relative;
  display: block;
  width: 100%;
  /* Load-bearing, not tidying: the height attribute is a presentational hint and
     would otherwise render this at its full 1435px. */
  height: auto;
}

/* Fades rather than rises, and slower than the copy, so she settles while the
   words are still arriving. Matches the home page's portrait. */
.story-hero__figure.reveal {
  transform: none;
  transition: opacity 1.6s var(--ease) .1s;
}

.story-hero__copy { position: relative; z-index: 2; }

.story-hero__eyebrow {
  margin: 0 0 22px;
  font-size: 12.5px;
  letter-spacing: .28em;
  text-transform: uppercase;
  /* --butter, not --gold-text. The darkened gold exists for type on cream; on
     navy it is butter that sings at 9.06:1, and --gold-text would go muddy. */
  color: var(--butter);
  --d: 0s;
}

.story-hero__title {
  margin: 0;
  font-family: var(--serif);
  font-weight: 500;
  font-size: clamp(38px, 5vw, 68px);
  line-height: 1.02;
  letter-spacing: -.01em;
  color: var(--cream);
  --d: .08s;
}
.story-hero__title em { font-style: italic; color: var(--butter); }

.story-hero__lead {
  margin: 28px 0 0;
  font-size: 18px;
  line-height: 1.68;
  color: var(--mutedd);
  max-width: 48ch;
  --d: .16s;
}

/* --- the story: one cream field, centred, symmetric --- */

/* Was five colour-banded sections with a wave on every join. Chenélle
   2026-07-20: "do not break everything into different sections... make the flow
   smooth" and "ensure it is symmetrical all over". So the opening, the
   qualifications and her closing reflection now share one cream ground and one
   centred measure, spaced apart rather than walled off by colour and a curve.
   The top padding clears the hero's cut-out, which overlaps the wave by 92px. */
.story {
  background: var(--cream);
  padding: var(--section-y) 0 var(--section-y);
}

.story__inner {
  max-width: 720px;
  margin: 0 auto;
  padding: 0 var(--gutter);
  text-align: center;
}

/* Each movement is a beat of the same story; the space between them is what
   separates them now that the bands are gone. */
.story__movement + .story__movement { margin-top: clamp(64px, 9vw, 116px); }

.story__heading {
  margin: 0 0 28px;
  font-family: var(--serif);
  font-weight: 600;
  font-size: clamp(28px, 3.2vw, 40px);
  line-height: 1.1;
  letter-spacing: -.01em;
  color: var(--ink);
  --d: 0s;
}

/* The opening line, set larger than the rest: it is the first thing Thalia says
   in her own voice and it carries the switch from the hero's third person. */
.story__opening {
  margin: 0 auto;
  font-size: clamp(20px, 1.9vw, 24px);
  line-height: 1.6;
  color: var(--muted);
  max-width: 34ch;
  --d: .08s;
}

/* Her closing reflection: short first-person paragraphs, centred on a
   comfortable measure. Centred body copy is a readability risk on long text, but
   these are three short paragraphs on a personal page, and centring is what
   makes the column read symmetric, which is the explicit ask. */
.story__p {
  margin: 0 auto;
  font-size: 18px;
  line-height: 1.72;
  color: var(--muted);
  max-width: 46ch;
  --d: .12s;
}
.story__p + .story__p { margin-top: 22px; --d: .18s; }

/* The line the page turns on, lifted a little: serif italic, its own space. */
.story__p--close {
  margin-top: 32px;
  font-family: var(--serif);
  font-style: italic;
  font-size: clamp(20px, 2.1vw, 26px);
  line-height: 1.4;
  color: var(--ink);
  max-width: 32ch;
  --d: .24s;
}

/* --- qualifications: a centred 2x2 grid, not a butter band --- */

/* Four terms and their descriptions, the grid balanced left and right so the
   block stays symmetric. Text inside each cell is left-aligned: centred
   multi-line descriptions read poorly, and a 2x2 grid is symmetric regardless of
   its cells' inner alignment. 2021 is the only year Thalia gives, so there is no
   dated spine to invent the rest of. */
.creds__grid {
  margin: clamp(34px, 4vw, 48px) auto 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(26px, 3.5vw, 44px) clamp(34px, 5vw, 64px);
  text-align: left;
}

.creds__cell {
  padding-top: 22px;
  border-top: 1px solid rgba(18, 58, 73, .16);
}

.creds__term {
  font-family: var(--serif);
  font-weight: 500;
  font-size: clamp(19px, 1.7vw, 23px);
  line-height: 1.3;
  color: var(--ink);
  margin-bottom: 10px;
}

.creds__desc {
  margin: 0;
  font-size: 16px;
  line-height: 1.68;
  /* --muted is 4.59:1 on cream and passes. The butter band that forced
     --muted-onbutter is gone, so plain --muted is correct again. */
  color: var(--muted);
}

/* --- gallery --- */

.story-gallery {
  position: relative;
  background: var(--cream);
  padding: 0 0 calc(var(--section-y) + 92px);   /* room for the wave */
}

/* 960px, not the page's --maxw. Chenélle 2026-07-17: the section was "too large
   ... make them smaller". At full width nine photographs read as a wall; pulled
   in to 960 with two columns they read as a set, each frame smaller and the
   whole section shorter. */
.story-gallery__inner {
  position: relative;
  z-index: 1;
  max-width: 960px;
  margin: 0 auto;
  padding: 0 var(--gutter);
}

.story-gallery__head {
  text-align: center;
  margin-bottom: clamp(38px, 4vw, 62px);
}

.story-gallery__eyebrow {
  margin: 0 0 18px;
  font-size: 12.5px;
  letter-spacing: .28em;
  text-transform: uppercase;
  color: var(--gold-text);   /* gold as type on cream: --gold is 2.76:1 */
  --d: 0s;
}

.story-gallery__title {
  margin: 0;
  font-family: var(--serif);
  font-weight: 600;
  font-size: clamp(30px, 3.6vw, 46px);
  line-height: 1.04;
  letter-spacing: -.01em;
  color: var(--ink);
  --d: .06s;
}
.story-gallery__title em { font-style: italic; font-weight: 500; }

/* A horizontal filmstrip, not a stacked grid. Chenélle 2026-07-17: the section
   was "too large ... either make them smaller or put it in a carousel". A grid of
   nine photographs is five rows tall however it is columned; a filmstrip is one
   row, so the section shrinks from ~1900px to ~560px and reads as a considered
   reel rather than a wall.

   Every frame is set to a shared HEIGHT and lets its width follow its own aspect
   ratio, which is what makes mixed orientations sit together without cropping:
   the seven landscapes come out wide, the two portraits narrow and tall, all on
   one baseline. No object-fit crop, so nothing is cut, which was the whole reason
   the grid could not use one aspect-ratio.

   Native overflow scroll, no JS: it works with trackpad, touch, shift-wheel and
   the keyboard (the strip is focusable and arrow-scrollable), and it cannot break
   the way a scripted carousel can. scroll-snap settles each frame to the left
   edge. The list keeps its semantics, so a screen reader reads nine list items
   in order and never has to operate a widget. */
.story-gallery__grid {
  display: flex;
  gap: clamp(14px, 1.4vw, 20px);
  margin: 0;
  padding: 0 var(--gutter) 6px;   /* gutter padding so the first/last frame isn't flush to the edge */
  list-style: none;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-left: var(--gutter);
  /* A thin scrollbar that matches the page rather than the OS default slab. */
  scrollbar-width: thin;
  scrollbar-color: rgba(18, 58, 73, .3) transparent;
  /* Momentum scroll on iOS. */
  -webkit-overflow-scrolling: touch;
}

/* The strip is full-bleed, so it can run edge to edge while the head above it
   keeps the page measure. It overrides .story-gallery__inner's 960 cap for its
   own width only. */
.story-gallery__inner { max-width: none; padding: 0; }
.story-gallery__head { max-width: 960px; margin-left: auto; margin-right: auto; padding: 0 var(--gutter); }

/* The strip is a focusable scroll region: give keyboard users a visible ring, on
   --gold-text for the 3:1 a focus indicator needs on cream (WCAG 1.4.11). */
.story-gallery__grid:focus-visible {
  outline: 2px solid var(--gold-text);
  outline-offset: 4px;
}

/* A soft fade on the right edge, so the strip reads as "there is more" rather
   than ending on a hard cut. Pure decoration over the cream, above the frames
   but never taking a click. */
.story-gallery__inner { position: relative; }
.story-gallery__inner::after {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  bottom: 6px;   /* clears the scrollbar */
  width: clamp(40px, 6vw, 96px);
  background: linear-gradient(90deg, rgba(251, 246, 234, 0), var(--cream));
  pointer-events: none;
  z-index: 1;
}

.story-gallery__item {
  flex: 0 0 auto;
  scroll-snap-align: start;
  margin: 0;
}

.story-gallery__item img {
  display: block;
  /* Shared height, width follows aspect: this is what lets landscape and
     portrait share one row without a crop. height:auto everywhere else on this
     page; here the height is the fixed dimension and the width is free. */
  height: clamp(300px, 40vw, 460px);
  width: auto;
  border-radius: 2px;
}

/* No per-item --d stagger and no per-item .reveal: a horizontally-scrolled item
   that starts off to the right is not intersecting the viewport, so an observer
   would leave it stuck at opacity 0 until it happened to be scrolled in, and
   .is-visible is added once and never removed. The strip is revealed as one
   block instead (the .reveal is on the <ul>), which is correct and cannot
   half-fire. */

/* Credentials and philosophy are no longer their own sections. Both folded into
   .story above on 2026-07-20 (Chenélle: "do not break everything into different
   sections... make the flow smooth"): the credentials as the centred .creds__grid
   and her philosophy as centred .story__p paragraphs, all on the one cream field.
   Their old butter and cream bands, and the two waves that joined them, are gone
   with them. --muted-onbutter is no longer needed on this page: with no butter
   band, all body copy sits on cream, where plain --muted passes at 4.59:1. */

/* The two waves left on this page curve like the home page's: same path, same
   height, same bottom:-1px, and every note on .thalia__wave applies. The 1px
   overhang is load-bearing, not a rounding nicety, or the two colours meet on one
   line and the browser blends a hairline across the seam. Each is filled with the
   colour of the section BELOW it. */
.story-hero__wave,
.story-gallery__wave {
  position: absolute;
  left: 0;
  bottom: -1px;
  z-index: 2;
  width: 100%;
  height: 92px;
  display: block;
  pointer-events: none;
}

.story-hero__wave path    { fill: var(--cream); }    /* the story, below */
.story-gallery__wave path { fill: var(--navy2); }    /* the closing band, below */

/* --- closing quote --- */

/* The quote and the invitation share navy2 so they read as one closing band,
   and the footer sits on the same colour: three sections, one dark field, no
   seams to hide. */
.story-quote {
  background: var(--navy2);
  color: var(--cream);
  padding: clamp(72px, 9vw, 128px) 0 clamp(56px, 7vw, 92px);
}

.story-quote__inner {
  max-width: 900px;
  margin: 0 auto;
  padding: 0 var(--gutter);
  text-align: center;
}

.story-quote__q { margin: 0; --d: 0s; }

.story-quote__q p {
  margin: 0;
  font-family: var(--serif);
  font-style: italic;
  font-weight: 400;
  font-size: clamp(25px, 3.2vw, 40px);
  line-height: 1.36;
  letter-spacing: -.005em;
  color: var(--cream);
}

.story-quote__by {
  margin-top: 28px;
  font-size: 11.5px;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: var(--butter);
  --d: .12s;
}

/* --- closing invitation --- */

/* No bottom padding, exactly as .contact: the footer closes the page and the
   two share the navy, so the join between them is invisible. */
.page-cta {
  background: var(--navy2);
  color: var(--cream);
  padding: 0 0 0;
}

.page-cta__inner {
  max-width: 900px;
  margin: 0 auto;
  padding: 0 var(--gutter);
  text-align: center;
}

.page-cta__title {
  margin: 0;
  font-family: var(--serif);
  font-weight: 500;
  font-size: clamp(32px, 4vw, 54px);
  line-height: 1.04;
  letter-spacing: -.01em;
  color: var(--cream);
  --d: 0s;
}
.page-cta__title em { font-style: italic; }

.page-cta__lead {
  margin: 24px auto 0;
  font-size: 17.5px;
  line-height: 1.68;
  color: var(--mutedd);
  max-width: 46ch;
  --d: .08s;
}

.page-cta__actions {
  margin: 40px 0 0;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 14px;
  --d: .16s;
}

/* --- stacked --- */

@media (max-width: 1024px) {
  .story-hero__grid {
    grid-template-columns: 1fr;
    gap: clamp(32px, 5vw, 52px);
  }

  /* The figure leads the DOM, so it stacks first with no reordering, and she
     keeps standing on the floor of the section with the curve across her feet.
     The glow needs nothing here: it is anchored to her, so it shrinks with her
     and stays centred without a second rule to keep in sync. */
  .story-hero__figure {
    justify-self: center;
    max-width: min(340px, 72vw);
  }
}

/* The qualifications grid collapses to a single column where two would crush. */
@media (max-width: 640px) {
  .creds__grid {
    grid-template-columns: 1fr;
    gap: clamp(22px, 6vw, 32px);
  }
}

@media (max-width: 640px) {
  .story-hero__lead { font-size: 17px; }
  .page-cta__actions { flex-direction: column; align-items: stretch; }
}

/* ---------------------------------------------------------------- reduced motion */

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
  .reveal { opacity: 1; transform: none; }

  /* The block above forces .reveal visible in CSS, with no JS involved. The
     timeline dot scales in off .is-visible, which only JS adds, so without this
     it would sit at 55% forever for a reduced-motion visitor whose JS failed. */
  .styles__dot { transform: none; }

  /* The hero poses must not cycle. Left alone, the block above would run
     dancerCycle once in .01ms and settle every figure on its 100% keyframe,
     which is opacity 0: the hero would lose its illustration entirely. Hold the
     first pose as a still instead.

     Scoped to --cycling since 2026-07-17, and that is a fix, not tidying. These
     read `.hero__dancer` flat, so with the hero on one static photo they were
     setting opacity:0 on it and clawing it back only via :first-child. It
     happened to be the first child, so it happened to render. Anything added
     ahead of it in .hero__art-inner would have blanked the hero for every
     reduced-motion visitor, with nothing in the hero rules to explain why. The
     dormant poses keep their guard; the live photograph is no longer touched. */
  .hero__dancer--cycling { animation: none !important; opacity: 0; }
  .hero__dancer--cycling:first-child { opacity: 1; }

  /* The WhatsApp pill stops pulsing and keeps its glow. The halo lives on the
     button, not in the keyframes, precisely so this rule costs nothing but the
     motion. See .btn--glow. */
  .btn--glow::after { animation: none; }

  /* The block above zeroes transition-duration but not transition-delay, and a
     closed FAQ panel delays its visibility to the end of the collapse. Without
     this the answer would stay in the tab order for half a second after it has
     already snapped shut. */
  .js .faq__panel { transition-delay: 0s !important; }

  /* Reveal the homepage immediately rather than flashing the overlay. */
  .intro { display: none; }
}
