/* ---- tokens.css ---- */
/* ============================================================================
   TOKENS — Katie Smith Studio. The brand, and the only per-client file.

   Colour comes from the client (KSS.ca HEX codes.docx) and her logo:
     cream      #FEF0D9  "peachy cream"   the page ground
     lavender   #9F70C4  "lavender"       the enquiry band, the focus ring
     heather    #555489  "dark heather"   headings, buttons, links
     periwinkle #9A98CB  the logo's staff lines, used for the staff here too
   The rest are tints and shades made from those, for text and surfaces.

   Contrast, measured 2026-09-25 (WCAG relative luminance):
     on cream: ink 11.75:1, heather 6.19:1, lavender 3.34:1 (large text only)
     white on lavender 3.76:1, so the lavender band carries only large text.

   Type and spacing use clamp() so they scale between a small phone (~360px)
   and the container without breakpoints:  clamp(MIN, <rem> + <vw>, MAX)
   ========================================================================= */

:root {
  /* ---- brand (the client's) ------------------------------------------- */
  --cream: #FEF0D9;
  --lavender: #9F70C4;
  --heather: #555489;
  --periwinkle: #9A98CB;

  /* ---- made from the brand -------------------------------------------- */
  --ink: #2E2D4A;           /* body text: heather taken near-black */
  --heather-deep: #3F3E68;  /* button hover, pressed */
  --paper: #FFF8EC;         /* raised surfaces: alternate sections, fields */
  --sand: #EBD5B3;          /* hairlines and borders on cream */
  --white: #FFFFFF;

  /* ---- type ----------------------------------------------------------- */
  /* One family, as the logo pairs its script with one plain sans. Declared
     and self-hosted in base/fonts.css. */
  --font: "Figtree", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;

  --fs-body: 1.0625rem;                                        /* 17px */
  --fs-lead: clamp(1.125rem, 1.05rem + 0.35vw, 1.3125rem);     /* 18 → 21 */
  --fs-small: 0.9375rem;                                       /* 15px */
  --fs-h1: clamp(2.25rem, 1.55rem + 3.1vw, 4.25rem);           /* 36 → 68 */
  --fs-h2: clamp(1.75rem, 1.45rem + 1.35vw, 2.625rem);         /* 28 → 42 */
  --fs-h3: clamp(1.25rem, 1.17rem + 0.35vw, 1.5rem);           /* 20 → 24 */

  --lh-body: 1.65;
  --lh-heading: 1.15;
  /* A staff title is drawn once per line box, so its line-height is part of
     the staff's geometry. components/staff.css depends on this value. */
  --lh-staff: 1.25;

  /* ---- spacing -------------------------------------------------------- */
  --section-y: clamp(3.5rem, 2.4rem + 4.8vw, 6.5rem);          /* 56 → 104 */
  --gap: clamp(1.25rem, 0.9rem + 1.5vw, 2.25rem);              /* 20 → 36 */
  --measure: 62ch;

  --container: 1160px;
  --gutter: clamp(16px, 3vw, 28px);

  /* ---- shape ---------------------------------------------------------- */
  /* Radii differ by role on purpose: photos barely rounded, fields a little,
     buttons fully round so they read as the one thing to press. */
  --radius-photo: 4px;
  --radius-field: 8px;
  --radius-pill: 999px;

  /* The side rail (wide screens) and the top bar that replaces it (narrow),
     components/header.css. The bar shrinks once the page scrolls. */
  --rail-w: clamp(232px, 17vw, 272px);
  --bar-h: 76px;
  --bar-h-stuck: 68px;
  --logo-w: clamp(150px, 13vw, 200px);

  --transition: 0.2s ease;

  /* ---- the one structural breakpoint ---------------------------------
     The side rail becoming a top bar with a drawer. Media queries cannot
     read custom properties, so the stylesheets and 20-nav.js repeat
     991px / 992px literally. */
  --bp-nav: 991px;

  /* ---- motion ----------------------------------------------------------
     The Immersive Awwwards style's motion tokens (website-styles/
     immersive-awwwards/DESIGN.md → Motion → Tokens), values unchanged; only
     the motion was taken from that style, not its colours, type or layout.
     --transition above stays for the site's own colour and fill changes. */
  --dur-hover: 0.15s;                                   /* link, button, nav hover */
  --ease-hover: cubic-bezier(0.4, 0, 0.2, 1);
  --dur-reveal: 0.8s;                                   /* text and blocks arriving */
  --ease-reveal: cubic-bezier(0.19, 1, 0.22, 1);
  --dur-panel: 0.7s;                                    /* menu panel, intro lift, page cover */
  --ease-panel: cubic-bezier(0.625, 0.05, 0, 1);
  --dur-intro: 2.5s;                                    /* first-visit intro, in all */
  --dur-crossfade: 0.2s;                                /* every state change under reduced motion */
  --delay-stagger: 70ms;                                /* between words, lines, menu links */
  --delay-page: 0.3s;                                   /* the new page's start behind the cover */
  --dist-rise: 36px;                                    /* block reveal travel */
  --dist-line: 110%;                                    /* hero words rise from below their line */
}

/* ---- base/fonts.css ---- */
/* ============================================================================
   FONTS — self-hosted. Every typeface this site uses is served from its own
   origin, out of assets/fonts/, which build.js copies into dist/.

   No font is loaded from a third-party host: no <link> to a font service, no
   @import, and no font host in the CSP (_headers keeps font-src at 'self').
   The hostnames are deliberately not spelled out in this comment: two checks
   grep src/ for them, and a comment naming them would fail both.
   A remote font is one more origin that can be slow, blocked or gone, and it
   is the one request on every page that tells a third party who visited.

   One @font-face per family, weight and style the CSS actually uses:
     font-family   the exact name tokens.css puts in --font
     src           the woff2 under /assets/fonts/, with format("woff2")
     font-weight   one weight per rule, as the source declared it
     font-style    normal or italic
     font-display  swap
     unicode-range copy it when the source CSS gives one; a subset the page
                   never uses is then never downloaded

   Where the files come from, on a conversion: the original site's own font
   files (the scrape downloads them), or for a Google font the css2 URL fetched
   with a desktop Chrome User-Agent — other User-Agents are served other
   formats — saving every woff2 it lists. Keep the families, weights, styles and
   unicode-range exactly as the source had them.

   Until a face is declared here, the family in --font falls back to the system
   stack after it, which is what the bare template renders with.

   The example is described in words on purpose: verify.js reads every
   url() in the bundle, comments included, and fails the build on one that
   does not resolve.
   ========================================================================= */

/* Figtree, variable (weights 300–900), latin subset. Fontsource on jsDelivr,
   @fontsource-variable/figtree, file figtree-latin-wght-normal.woff2,
   downloaded 2026-09-25 (20,156 bytes). SIL Open Font License. */
@font-face {
  font-family: 'Figtree';
  src: url('/assets/fonts/figtree.woff2') format('woff2-variations'),
       url('/assets/fonts/figtree.woff2') format('woff2');
  font-weight: 300 900;
  font-style: normal;
  font-display: swap;
}

/* ---- base/reset.css ---- */
/* ---- reset & document defaults ------------------------------------------ */

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

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

body {
  margin: 0;
  font-family: var(--font);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: var(--lh-body);
  color: var(--ink);
  background: var(--cream);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

img {
  max-width: 100%;
  height: auto;
  display: block;
  border: 0;
}

a {
  color: var(--heather);
  text-decoration: underline;
  text-decoration-thickness: 1.5px;
  text-underline-offset: 0.18em;
  text-decoration-color: var(--periwinkle);
  transition: color var(--transition), text-decoration-color var(--transition);
}
a:hover { color: var(--heather-deep); text-decoration-color: currentColor; }

p { margin: 0 0 1rem; }
p:last-child { margin-bottom: 0; }

ul, ol { margin: 0 0 1rem; padding-left: 1.25rem; }

/* Keyboard focus: lavender, offset so it reads on cream and on lavender. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: 3px solid var(--lavender);
  outline-offset: 3px;
}

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

.skip-link {
  position: absolute;
  top: -60px;
  left: 12px;
  z-index: 2000;
  padding: 10px 18px;
  background: var(--heather);
  color: var(--cream);
  font-weight: 600;
  transition: top var(--transition);
}
.skip-link:focus { top: 12px; color: var(--cream); }

/* ---- base/typography.css ---- */
/* ---- typography ----------------------------------------------------------
   One family (Figtree). Hierarchy comes from size and weight, never from
   capitals or a label above the heading. All sizes are fluid via clamp().
   -------------------------------------------------------------------------- */

h1, h2, h3, h4 {
  margin: 0 0 0.6em;
  color: var(--heather);
  font-weight: 750;
  line-height: var(--lh-heading);
  letter-spacing: -0.015em;
  text-wrap: balance;
}
h1 { font-size: var(--fs-h1); font-weight: 800; letter-spacing: -0.025em; }
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); font-weight: 700; letter-spacing: -0.005em; }

p, li { text-wrap: pretty; }

.lead {
  font-size: var(--fs-lead);
  line-height: 1.55;
  max-width: var(--measure);
}

.prose { max-width: var(--measure); }
.prose p + p { margin-top: 0.9em; }
.prose h2 { margin-top: 1.6em; }
.prose h2:first-child { margin-top: 0; }

/* A section's opening heading and the sentence that explains it. */
.section-head { max-width: var(--measure); margin-bottom: clamp(1.75rem, 1.3rem + 2vw, 2.75rem); }
.section-head p { font-size: var(--fs-lead); line-height: 1.55; }

/* Facts as term / value pairs: "Level — Beginner to advanced". */
.facts { margin: 0; display: grid; gap: 0.85rem; }
.facts div { display: grid; gap: 0.1rem; }
.facts dt { font-size: var(--fs-small); font-weight: 650; color: var(--heather); }
.facts dd { margin: 0; }

.text-link { font-weight: 650; }

/* ---- base/layout.css ---- */
/* ---- layout primitives ---------------------------------------------------
   Grids reflow with auto-fit + minmax, so there are no media queries here.
   -------------------------------------------------------------------------- */

.container {
  width: 100%;
  max-width: calc(var(--container) + 2 * var(--gutter));
  margin: 0 auto;
  padding: 0 var(--gutter);
}

.section { padding: var(--section-y) 0; position: relative; }
.section--paper { background: var(--paper); }

/* Text beside a picture. Stacks by itself when the pair no longer fits. */
.split {
  display: grid;
  gap: clamp(2rem, 1rem + 4vw, 4.5rem);
  align-items: center;
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
}
.split--top { align-items: start; }

.photo {
  margin: 0;
}
.photo img {
  width: 100%;
  border-radius: var(--radius-photo);
  object-fit: cover;
}
.photo figcaption {
  margin-top: 0.6rem;
  font-size: var(--fs-small);
  color: var(--heather);
}

/* ---- components/button.css ---- */
/* ---- buttons --------------------------------------------------------------
   Fully round, heather, one size. The pill is the only fully rounded shape on
   the site, so a button never looks like anything else.
   -------------------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 48px;
  padding: 0.7em 1.5em;
  border: 2px solid var(--heather);
  border-radius: var(--radius-pill);
  background: var(--heather);
  color: var(--cream);
  font: inherit;
  font-size: 1rem;
  font-weight: 700;
  line-height: 1.2;
  text-decoration: none;
  cursor: pointer;
}
.btn:hover { background: var(--heather-deep); border-color: var(--heather-deep); color: var(--cream); }

/* Second choice beside a solid button. */
.btn--quiet { background: transparent; color: var(--heather); }
.btn--quiet:hover { background: var(--paper); color: var(--heather-deep); }

/* On the lavender band: cream on heather would disappear into the band's
   own colour family, so the button turns cream with heather text. */
.btn--cream { background: var(--cream); border-color: var(--cream); color: var(--heather); }
.btn--cream:hover { background: var(--paper); border-color: var(--paper); color: var(--heather-deep); }

.btn[disabled] { cursor: not-allowed; opacity: 0.55; }
.btn[disabled]:hover { background: var(--heather); border-color: var(--heather); }

.actions { display: flex; flex-wrap: wrap; gap: 0.75rem; margin-top: 1.75rem; }

/* ---- components/header.css ---- */
/* ---- header & navigation -------------------------------------------------
   One header, two layouts, split at 991/992px — the only structural
   breakpoint (--bp-nav in tokens.css documents it; media queries cannot
   read custom properties, so they repeat it):

     992px and up   a rail down the left edge, fixed while the page scrolls
                    past it: the logo, the links, and the button at the foot.
                    The site is one long page, and the rail is its contents.
     991px and down the usual bar across the top, the links in a drawer.

   Each link hangs from a note head, and on the rail the heads sit on one
   line, like notes on a staff. The filled head is where you are: the page
   (build.js marks it) or, on the home page, the section in view (30-spy.js).
   -------------------------------------------------------------------------- */

.site-header {
  z-index: 1000;
  background: var(--cream);
}

.header-inner {
  display: flex;
  gap: var(--gap);
}

.brand { display: block; flex: none; }
.brand img { width: var(--logo-w); height: auto; }

.nav-toggle {
  display: none;
  width: 48px; height: 48px;
  margin-left: auto;
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--heather);
  cursor: pointer;
}
.nav-toggle svg { margin: 0 auto; }

/* What the note heads are filled with when open: the ground they sit on. */
.main-nav { --nav-ground: var(--cream); }

.nav-list {
  margin: 0;
  padding: 0;
  list-style: none;
}
.nav-list > li { position: relative; }
.nav-list > li > a {
  position: relative;
  display: flex;
  align-items: center;
  min-height: 44px;
  padding-left: 32px;
  color: var(--ink);
  font-weight: 600;
  text-decoration: none;
  transition: color var(--dur-hover) var(--ease-hover);
}
/* The note head: an open oval, tilted as a printed one is. Its centre is
   9px from the left, which is where the rail's line runs. */
.nav-list > li > a::before {
  content: "";
  position: absolute;
  left: 1px;
  top: 50%;
  width: 16px;
  height: 12px;
  border: 2px solid var(--periwinkle);
  border-radius: 50%;
  background: var(--nav-ground);
  transform: translateY(-50%) rotate(-22deg);
  transition: background-color var(--dur-hover) var(--ease-hover), border-color var(--dur-hover) var(--ease-hover);
}
.nav-list > li > a:hover { color: var(--heather); }
.nav-list > li > a:hover::before { background: var(--periwinkle); }
.nav-list > li.is-active > a { color: var(--heather); }
.nav-list > li.is-active > a::before { background: var(--heather); border-color: var(--heather); }

/* The links' place in the stagger (rail and drawer both). */
.nav-list > li:nth-child(2) { --i: 1; }
.nav-list > li:nth-child(3) { --i: 2; }
.nav-list > li:nth-child(4) { --i: 3; }
.nav-list > li:nth-child(5) { --i: 4; }
.nav-list > li:nth-child(6) { --i: 5; }
.nav-list > li:nth-child(7) { --i: 6; }
.nav-list > li:nth-child(8) { --i: 7; }

.header-cta { flex: none; }

/* The backdrop is a SIBLING of .site-header. The sticky header's z-index
   makes it a stacking context, so the drawer inside it (1200) paints at the
   header's 1000 as far as anything outside is concerned; a backdrop above
   1000 would cover the drawer it belongs to. 950 keeps it below. */
.nav-backdrop {
  position: fixed;
  inset: 0;
  z-index: 950;
  background: rgba(46, 45, 74, 0.45);
  opacity: 0;
  visibility: hidden;
  /* Closed, it takes no clicks while it fades: the page is usable at once. */
  pointer-events: none;
  transition: opacity var(--dur-panel) var(--ease-panel), visibility 0s linear var(--dur-panel);
}

/* ---- the rail ----------------------------------------------------------- */
@media (min-width: 992px) {
  /* Everything else — main, the lavender band, the footer — moves over. */
  body { padding-left: var(--rail-w); }

  .site-header {
    position: fixed;
    inset: 0 auto 0 0;
    width: var(--rail-w);
    border-right: 1px solid var(--sand);
    overflow-y: auto;
  }
  /* .header-inner is also a .container; the rail is its own width. */
  .header-inner {
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    max-width: none;
    min-height: 100%;
    padding: clamp(1.5rem, 4vh, 2.5rem) clamp(1.25rem, 1.8vw, 1.75rem);
  }
  .brand { align-self: stretch; }
  .brand img { width: 100%; max-width: 190px; }

  .main-nav { margin: clamp(1.75rem, 7vh, 4rem) 0 2rem; }
  .nav-list { position: relative; }
  /* The line, from the first note head's centre to the last one's. */
  .nav-list::before {
    content: "";
    position: absolute;
    left: 8px;
    top: 22px;
    bottom: 22px;
    width: 2px;
    background: var(--periwinkle);
  }

  /* The rail's full width, on one line: "Inquire about lessons" is 156px at
     16px, and the rail's inner width is 191px at its narrowest (992px wide),
     so the side padding is slimmer than a button's usual 1.5em. */
  .header-cta {
    align-self: stretch;
    margin-top: auto;
    padding-left: 0.75em;
    padding-right: 0.75em;
    white-space: nowrap;
  }
}

/* The rail's links ink in: opacity and a short rise from the left, one
   --delay-stagger after another. An animation with both fills, so it always
   ends and the links are never left hidden. It waits for the intro's lift
   while the curtain plays (html.intro-played, which stays for the whole load:
   a wait that changed while the animation runs would make the links jump), and
   for the cover to open when the page was reached from another page
   (html.arrived). A skipped intro (html.intro-skip) drops the wait, and
   15-intro.js restarts the animation. */
@keyframes nav-in {
  from { opacity: 0; transform: translateX(-14px); }
  to   { opacity: 1; transform: none; }
}
@media (min-width: 992px) and (prefers-reduced-motion: no-preference) {
  .js .nav-list > li > a {
    --nav-wait: 0s;
    animation: nav-in var(--dur-reveal) var(--ease-reveal) both;
    animation-delay: calc(var(--nav-wait) + var(--i, 0) * var(--delay-stagger));
  }
  .js.intro-played .nav-list > li > a { --nav-wait: calc(var(--dur-intro) * 0.72); }
  .js.arrived .nav-list > li > a { --nav-wait: calc(var(--delay-page) + var(--dur-panel)); }
  .js.intro-played.intro-skip .nav-list > li > a { --nav-wait: 0s; }
}

/* ---- the bar and its drawer --------------------------------------------- */
@media (max-width: 991px) {
  .site-header {
    position: sticky;
    top: 0;
    border-bottom: 1px solid transparent;
    transition: border-color 0.25s ease;
  }
  .site-header.is-stuck { border-bottom-color: var(--sand); }

  .header-inner {
    align-items: center;
    min-height: var(--bar-h);
    transition: min-height 0.25s ease;
  }
  .site-header.is-stuck .header-inner { min-height: var(--bar-h-stuck); }

  .nav-toggle { display: inline-flex; align-items: center; justify-content: center; order: 3; }
  /* Above the open drawer (1200), so the same button closes it. */
  .nav-toggle { position: relative; z-index: 1300; }
  .header-cta { display: none; }

  .main-nav {
    --nav-ground: var(--paper);
    position: fixed;
    inset: 0 0 0 auto;
    width: min(320px, 86vw);
    z-index: 1200;
    padding: 88px 28px 40px;
    background: var(--paper);
    /* Closed: clipped away from the bottom up, so it unclips from the top
       when it opens. */
    clip-path: inset(0 0 100% 0);
    overflow-y: auto;
    /* Hidden once it has been clipped away, so its links leave the tab order,
       and a link followed from it (focus stays on it, off-screen) lets focus
       go. The delay is the clip: hidden at once, the drawer would vanish
       rather than close. */
    visibility: hidden;
    transition: clip-path var(--dur-panel) var(--ease-panel), visibility 0s linear var(--dur-panel);
  }
  /* The shadow belongs to the OPEN drawer only; on the closed one it showed
     as a dark band down the right edge of the page. A clip-path cuts a
     shadow off with the box, so the open clip reaches 60px past the left
     edge, where the shadow falls. */
  .main-nav.is-open {
    clip-path: inset(0 0 0 -60px);
    box-shadow: -8px 0 40px rgba(46, 45, 74, 0.25);
    visibility: visible;
    transition: clip-path var(--dur-panel) var(--ease-panel), visibility 0s;
  }

  /* The links ink in once the panel is part open: opacity and a short rise,
     --delay-stagger apart from 0.2s. Closing reverses at once, no stagger. */
  .js .main-nav .nav-list > li > a {
    opacity: 0;
    transform: translateY(14px);
    transition: color var(--dur-hover) var(--ease-hover), opacity var(--dur-crossfade) linear, transform var(--dur-crossfade) linear;
  }
  .js .main-nav.is-open .nav-list > li > a {
    opacity: 1;
    transform: none;
    transition:
      color var(--dur-hover) var(--ease-hover),
      opacity var(--dur-reveal) var(--ease-reveal) calc(0.2s + var(--i, 0) * var(--delay-stagger)),
      transform var(--dur-reveal) var(--ease-reveal) calc(0.2s + var(--i, 0) * var(--delay-stagger));
  }

  .nav-list > li { border-bottom: 1px solid var(--sand); }
  .nav-list > li > a { min-height: 52px; font-size: 1.125rem; }

  .nav-backdrop.is-open {
    opacity: 1;
    visibility: visible;
    pointer-events: auto;
    transition: opacity var(--dur-panel) var(--ease-panel), visibility 0s;
  }
}

/* Reduced motion: the panel crossfades and nothing travels. The global rule
   in utilities.css cuts every duration to 0.01ms with !important, so the
   crossfade has to say !important too, on a selector that outranks `*`. */
@media (max-width: 991px) and (prefers-reduced-motion: reduce) {
  .site-header .main-nav {
    clip-path: none;
    opacity: 0;
    transition: opacity var(--dur-crossfade) linear, visibility var(--dur-crossfade) linear;
    transition-duration: var(--dur-crossfade) !important;
  }
  .site-header .main-nav.is-open { clip-path: none; opacity: 1; }
  .js .site-header .main-nav .nav-list > li > a,
  .js .site-header .main-nav.is-open .nav-list > li > a {
    opacity: 1;
    transform: none;
    transition: color var(--dur-crossfade) linear;
    transition-duration: var(--dur-crossfade) !important;
  }
  .nav-backdrop.nav-backdrop { transition-duration: var(--dur-crossfade) !important; }
}

/* ---- components/staff.css ---- */
/* ---- the staff -----------------------------------------------------------
   The one bold idea on this site, taken from the client's own logo: in it,
   the word "studio" sits on a five-line music staff with a heavy bar line at
   each end, its baseline on the bottom line and its tall letters reaching the
   top one. A page title here is set the same way, and so are the titles of
   the one-page home's Lessons, About Katie and Studios sections (h2s there:
   chapter.css).

   The staff is drawn per LINE BOX, as background tiles one line-height tall,
   so a title that wraps gets one staff per line, like the systems of a score.
   That makes the title's line-height part of the geometry: every position
   below is in em of the title and assumes line-height var(--lh-staff).

     --staff-base   baseline, from the top of the line box
     --staff-cap    Figtree's cap height (the top line sits there)
     --staff-w      line thickness, never under 1px
     --staff-bar    bar-line width (the logo's bars are ~0.18 of the staff)
   The first two were measured in Chrome on 2026-09-25 from Figtree itself, at
   100px: ascent 95, descent 25, cap height 70.08. In a 1.25 line box the
   half-leading is 0.025em, so the baseline sits at 0.025 + 0.95 = 0.975em.
   Change the font and they must be measured again.
   -------------------------------------------------------------------------- */

.staff-title h1,
.staff-title h2,
h1.staff-title {
  --staff-base: 0.975em;
  --staff-cap: 0.7em;
  --staff-w: max(1px, 0.03em);
  --staff-bar: 0.13em;
  --staff-gap: calc(var(--staff-cap) / 4);
  --l5: calc(var(--staff-base) - var(--staff-w) / 2);
  --l4: calc(var(--l5) - var(--staff-gap));
  --l3: calc(var(--l4) - var(--staff-gap));
  --l2: calc(var(--l3) - var(--staff-gap));
  --l1: calc(var(--l2) - var(--staff-gap));

  margin: 0;
  padding-inline: calc(var(--staff-bar) + 0.3em);
  line-height: var(--lh-staff);
  background-image:
    /* bar line, left */
    linear-gradient(to bottom, transparent var(--l1), var(--periwinkle) var(--l1) calc(var(--l5) + var(--staff-w)), transparent calc(var(--l5) + var(--staff-w))),
    /* bar line, right */
    linear-gradient(to bottom, transparent var(--l1), var(--periwinkle) var(--l1) calc(var(--l5) + var(--staff-w)), transparent calc(var(--l5) + var(--staff-w))),
    /* the five lines */
    linear-gradient(to bottom,
      transparent var(--l1),
      var(--periwinkle) var(--l1) calc(var(--l1) + var(--staff-w)),
      transparent calc(var(--l1) + var(--staff-w)) var(--l2),
      var(--periwinkle) var(--l2) calc(var(--l2) + var(--staff-w)),
      transparent calc(var(--l2) + var(--staff-w)) var(--l3),
      var(--periwinkle) var(--l3) calc(var(--l3) + var(--staff-w)),
      transparent calc(var(--l3) + var(--staff-w)) var(--l4),
      var(--periwinkle) var(--l4) calc(var(--l4) + var(--staff-w)),
      transparent calc(var(--l4) + var(--staff-w)) var(--l5),
      var(--periwinkle) var(--l5) calc(var(--l5) + var(--staff-w)),
      transparent calc(var(--l5) + var(--staff-w)));
  background-size:
    var(--staff-bar) calc(var(--lh-staff) * 1em),
    var(--staff-bar) calc(var(--lh-staff) * 1em),
    100% calc(var(--lh-staff) * 1em);
  background-position: left top, right top, left top;
  background-repeat: repeat-y;
  text-wrap: balance;
}

/* ---- inner-page banner (build.js bannerHtml) ---------------------------- */
.page-banner {
  padding: clamp(2.5rem, 1.6rem + 4vw, 5rem) 0 clamp(2rem, 1.4rem + 2.5vw, 3.5rem);
}
.page-intro {
  margin-top: clamp(1.25rem, 1rem + 1vw, 2rem);
  font-size: var(--fs-lead);
  line-height: 1.55;
  max-width: var(--measure);
}

/* ---- components/chapter.css ---- */
/* ---- the home page's sections (src/pages/index.html) --------------------
   Since 2026-09-27 the site is one page. Lessons, About Katie and Studios
   were pages of their own; each is now a section that opens the way those
   pages did, with its title on the staff (staff.css), and closes with a
   button to the contact page.

   The button stands at a double bar line, which is how a score marks the
   end of a section: a short run of staff, then two thin bar lines.
   -------------------------------------------------------------------------- */

.chapter { padding: var(--section-y) 0; }
.chapter--paper { background: var(--paper); }

.chapter-head { margin-bottom: clamp(2.25rem, 1.6rem + 3vw, 4rem); }
/* The size and weight the pages' own h1 had, so a section title reads as
   one: it is an h2 now only because the page has one h1, the hero's. */
.chapter-head h2 {
  font-size: var(--fs-h1);
  font-weight: 800;
  letter-spacing: -0.025em;
}

/* One level down, the headings keep the size they had as their page's h2s. */
.chapter h3 { font-size: var(--fs-h2); }

/* A part of a section: what was a band of its own page. */
.chapter-block { margin-top: clamp(3.5rem, 2.4rem + 4.8vw, 6.5rem); }

.coda {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 1rem 1.25rem;
  margin: clamp(3rem, 2rem + 4vw, 5rem) 0 0;
}
/* Five 2px lines 9px apart (38px), then two 2px bar lines 4px apart at the
   right-hand end. Decoration only; nothing reads it. */
.coda::before {
  content: "";
  flex: none;
  width: 72px;
  height: 38px;
  background:
    linear-gradient(var(--periwinkle), var(--periwinkle)) right top / 2px 100% no-repeat,
    linear-gradient(var(--periwinkle), var(--periwinkle)) calc(100% - 6px) top / 2px 100% no-repeat,
    repeating-linear-gradient(to bottom, var(--periwinkle) 0 2px, transparent 2px 9px);
}

/* ---- where an in-page link lands ----------------------------------------
   Under the narrow layout's sticky bar, a target must clear the bar; a
   section lands with its top edge just beneath it, anything else with a
   little room. Beside the rail nothing covers the top of the window.
   -------------------------------------------------------------------------- */
[id] { scroll-margin-top: calc(var(--bar-h-stuck) + 1rem); }
.hero[id], .chapter[id], .section[id] { scroll-margin-top: var(--bar-h-stuck); }
/* The hero is the top of the page: the full bar and its 1px border above
   it, so /#top scrolls all the way up (the bar is not yet stuck there). */
.hero[id] { scroll-margin-top: calc(var(--bar-h) + 1px); }

@media (min-width: 992px) {
  [id] { scroll-margin-top: 1.5rem; }
  .hero[id], .chapter[id], .section[id] { scroll-margin-top: 0; }
}

/* ---- components/home.css ---- */
/* ---- home ---------------------------------------------------------------- */

.hero { padding: clamp(2.25rem, 1.5rem + 3.5vw, 4.5rem) 0 var(--section-y); }
.hero h1 { font-size: clamp(2.5rem, 1.6rem + 4vw, 5rem); }

.hero-body {
  display: grid;
  gap: clamp(2rem, 1rem + 4vw, 4.5rem);
  align-items: start;
  margin-top: clamp(1.75rem, 1.2rem + 2.5vw, 3.25rem);
  grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
}
.hero-text .lead { margin-bottom: 1rem; }
.hero-text { padding-top: clamp(0rem, -1rem + 3vw, 2rem); }
.hero-photo img { aspect-ratio: 4 / 5; max-height: 620px; }

/* The Lessons section's voice and piano, separated the way two measures
   are: by a bar line. */
.two-lessons {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
  gap: 0;
}
.lesson-summary { padding: 0.25rem clamp(1rem, 0.5rem + 2.5vw, 2.75rem) 0.5rem; }
.lesson-summary:first-child { padding-left: 0; }
.lesson-summary + .lesson-summary { border-left: 3px solid var(--periwinkle); }
.lesson-summary h3 { font-size: var(--fs-h2); margin-bottom: 1.1rem; }
@media (max-width: 719px) {
  .lesson-summary { padding-left: 0; padding-right: 0; }
  .lesson-summary + .lesson-summary {
    border-left: 0;
    border-top: 3px solid var(--periwinkle);
    margin-top: 1.75rem;
    padding-top: 1.75rem;
  }
}
.portrait img { aspect-ratio: 1; max-width: 520px; }

/* What families say: the warm-up photo the full width, the quotes under it. */
.families-photo { margin-top: 1.75rem; }
.families-photo img { aspect-ratio: 1600 / 918; }

/* Quotes: large, unboxed, a heather rule to the left. */
.quotes {
  display: grid;
  gap: clamp(1.75rem, 1.2rem + 2vw, 2.75rem);
  grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
  margin: 1.75rem 0 0;
  padding: 0;
  list-style: none;
}
.quote blockquote { margin: 0; }
.quote blockquote p { font-size: var(--fs-lead); line-height: 1.55; }
.quote-by { margin-top: 0.75rem; font-weight: 700; color: var(--heather); }

.not-found { min-height: 50vh; }
.not-found .lead { margin-top: clamp(1.25rem, 1rem + 1vw, 2rem); }

/* ---- components/lessons.css ---- */
/* ---- the Lessons section ------------------------------------------------- */

.lesson-approach { margin-top: 1.5rem; }

/* How lessons work: the lesson length. */
.lesson-terms { margin-bottom: 1.5rem; }

/* ---- components/about.css ---- */
/* ---- the About Katie section --------------------------------------------- */

/* The portrait, and the button to Katie's music site under it, stay in view
   beside the bio: under the narrow layout's bar, or at the top of the window
   beside the rail, where nothing covers them.

   .about-portrait-inner sticks, not the figure, because a sticky box travels
   only within its parent. The figure is stretched to the height of the row:
   beside the bio that is the bio's height, so the picture and button ride
   along with the text; once the split stacks (no media query decides when,
   see .split), the row is their own height and they scroll away like any
   picture. With the figure itself sticky, the stacked picture rode down over
   the bio (measured 2026-09-28 at 390, 600 and 992px wide). */
.about-portrait { align-self: stretch; }
.about-portrait-inner {
  position: sticky;
  top: calc(var(--bar-h-stuck) + 1.5rem);
  max-width: 520px;
}
@media (min-width: 992px) { .about-portrait-inner { top: 1.5rem; } }
.about-portrait img { aspect-ratio: 1; }
/* Centred under the picture (the operator, 2026-10-02); on a phone too
   narrow for its one line, its two lines are centred as well. */
.about-music {
  display: flex;
  width: fit-content;
  max-width: 100%;
  margin: 1rem auto 0;
  text-align: center;
}

.about-columns {
  display: grid;
  gap: clamp(2.5rem, 1.5rem + 4vw, 5rem);
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
}

.awards, .memberships { margin: 0; padding: 0; list-style: none; }
.award-group + .award-group { margin-top: 1.75rem; }
/* h4s now, styled as the h3s they were (typography.css's h3). */
.award-group h4 { font-size: var(--fs-h3); font-weight: 700; letter-spacing: -0.005em; margin-bottom: 0.6rem; }
.award-group ul { margin: 0; padding: 0; list-style: none; }
.award-group li {
  display: flex;
  gap: 1rem;
  padding: 0.55rem 0;
  border-bottom: 1px solid var(--sand);
}
.award-year {
  flex: none;
  min-width: 3.2em;
  font-weight: 750;
  color: var(--heather);
  font-variant-numeric: tabular-nums;
}

.memberships li { padding: 0.8rem 0; border-bottom: 1px solid var(--sand); }
.memberships strong { color: var(--heather); }
.memberships span { display: block; font-size: var(--fs-small); }
.award-what { color: var(--ink); }

/* ---- components/studios.css ---- */
/* ---- the Studios section ------------------------------------------------- */

.studio-head {
  display: grid;
  gap: var(--gap);
  align-items: start;
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
  margin-bottom: clamp(1.75rem, 1.3rem + 2vw, 2.75rem);
}
.studio-address {
  font-style: normal;
  font-size: var(--fs-lead);
  line-height: 1.5;
  margin-bottom: 0.75rem;
}

/* ---- the studios' slideshows (40-slideshow.js) ---------------------------
   One photo at a time. Every slide is the track's full width and one height
   (the stage), and its photo is contained in it, never cropped: the
   landscape building fills the width, the portrait rooms stand in the middle
   on a pale lilac ground. The track scrolls sideways and snaps to each
   photo, one photo per swipe, and moves on by itself every 5 seconds, round
   in a circle (40-slideshow.js).
   -------------------------------------------------------------------------- */
/* 309px tall at 390px wide, 576px at 1280px, 600px from 1360px; never more
   than 72% of the window's height, so the stage and its buttons fit on a
   short laptop screen. */
.slideshow { --slide-h: min(clamp(280px, 12rem + 30vw, 600px), 72vh); }

.slideshow-track {
  display: flex;
  gap: var(--gap);
  margin: 0;
  padding: 0 0 0.75rem;
  list-style: none;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
}
/* With the buttons working, the scrollbar is noise. Without JavaScript it is
   the only sign the row scrolls, so it stays. */
.slideshow.is-ready .slideshow-track { scrollbar-width: none; }
.slideshow.is-ready .slideshow-track::-webkit-scrollbar { display: none; }
.slideshow-track:focus-visible { outline: 3px solid var(--heather); outline-offset: 4px; }

.slide {
  flex: 0 0 100%;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}
/* The stage: --paper with 30% --periwinkle (#E1DBE2 where color-mix is
   missing). */
.slideshow .slide img {
  width: 100%;
  height: var(--slide-h);
  object-fit: contain;
  background: #E1DBE2;
  background: color-mix(in srgb, var(--periwinkle) 30%, var(--paper));
}

.slideshow-controls {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.75rem;
  margin-top: 0.75rem;
}
.slideshow-count {
  min-width: 3.5em;
  margin: 0;
  text-align: center;
  color: var(--heather);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}
.slideshow-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  padding: 0;
  border: 2px solid var(--heather);
  border-radius: var(--radius-pill);
  background: transparent;
  color: var(--heather);
  cursor: pointer;
}
.slideshow-btn[hidden] { display: none; }
.slideshow-btn:hover { background: var(--heather); color: var(--cream); }

/* The timer (operator, 2026-10-12): a hairline between the caption and the
   buttons, filled from the left in the time before the next photo.
   40-slideshow.js sets .is-running whenever it starts that wait and
   --slide-delay to its length; anything that holds the slideshow takes the
   class off, and the fill drops back to empty. Decorative (aria-hidden): the
   pause button is the control. */
.slideshow-timer {
  height: 2px;
  margin-top: 0.25rem;
  border-radius: 1px;
  overflow: hidden;
  background: rgba(85, 84, 137, 0.15);   /* --heather at 15% */
}
.slideshow-timer[hidden] { display: none; }
.slideshow-timer span {
  display: block;
  height: 100%;
  background: var(--heather);
  transform: scaleX(0);
  transform-origin: left center;
}
.slideshow-timer.is-running span {
  transform: scaleX(1);
  transition: transform var(--slide-delay, 5s) linear;
}

/* Pause apart from the arrows; bars while playing, a triangle while held. */
.slideshow-pause { margin-right: auto; }
.slideshow-pause .icon-play,
.slideshow-pause.is-held .icon-pause { display: none; }
.slideshow-pause.is-held .icon-play { display: block; }

/* On paper a sideways scroller would print only what is in view: lay every
   photo out in rows instead, and drop the buttons, the timer and the two copies that
   make the circle (40-slideshow.js). */
@media print {
  .slideshow-track { flex-wrap: wrap; overflow: visible; }
  .slideshow-controls, .slideshow-timer, .slide--copy { display: none; }
}

/* ---- components/form.css ---- */
/* ---- the enquiry form ----------------------------------------------------
   Styles the template's form markup (src/partials/form.html) as well as the
   prototype's stand-in, so the draft site's real form looks the same.
   -------------------------------------------------------------------------- */

.contact-layout {
  display: grid;
  gap: clamp(2.5rem, 1.5rem + 4vw, 5rem);
  align-items: start;
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 900px) { .contact-layout { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); } }

.contact-aside h2 { font-size: var(--fs-h3); margin-top: 1.5rem; }
.contact-aside h2:first-child { margin-top: 0; }

#contact-form,
.enquiry-form { max-width: 680px; }

.form-row {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr));
  gap: 0 18px;
}

.field { margin-bottom: 1.25rem; }
.field label,
.choice legend {
  display: block;
  margin-bottom: 0.4rem;
  color: var(--heather);
  font-weight: 650;
}
.req { color: var(--lavender); }

.field input,
.field textarea,
.field select {
  width: 100%;
  padding: 12px 14px;
  border: 1.5px solid var(--sand);
  border-radius: var(--radius-field);
  background: var(--white);
  font-family: inherit;
  /* 16px minimum: anything smaller makes iOS Safari zoom on focus. */
  font-size: 1rem;
  color: var(--ink);
  transition: border-color var(--transition);
}
.field input:focus,
.field textarea:focus,
.field select:focus { border-color: var(--heather); }
.field textarea { min-height: 150px; resize: vertical; }

/* Katie's either/or questions: each a fieldset of two large tap targets. */
.choice {
  margin: 0 0 1.25rem;
  padding: 0;
  border: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem;
}
.choice legend { width: 100%; padding: 0; }
.choice label {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  min-height: 48px;
  padding: 0.55rem 1.1rem 0.55rem 0.9rem;
  border: 1.5px solid var(--sand);
  border-radius: var(--radius-field);
  background: var(--white);
  cursor: pointer;
}
.choice input { width: 1.15rem; height: 1.15rem; margin: 0; accent-color: var(--heather); }
.choice label:has(input:checked) { border-color: var(--heather); background: var(--cream); }

.form-actions { margin-top: 0.5rem; }

.form-status {
  margin-top: 16px;
  padding: 12px 16px;
  border-radius: var(--radius-field);
  display: none;
}
.form-status.is-ok { display: block; background: #e5f2e8; color: #1c5b33; }
.form-status.is-err { display: block; background: #fbe9e7; color: #9a2418; }

/* The honeypot's wrapper. A CLASS, never a style attribute: under a strict
   `style-src 'self'` an inline style is dropped and the field it hid becomes
   a visible "Company" box. Off-canvas rather than display:none, because some
   bots skip what is not displayed, and the point is that they fill it. */
.form-hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Turnstile renders its own iframe here; reserve its 300x65 so the button
   does not jump when it loads. (Not on the prototype, which has no widget.) */
.cf-turnstile { min-height: 65px; margin: 4px 0; }

/* The form column. */
.enquiry-panel { min-width: 0; }

/* ---- components/enquire.css ---- */
/* ---- the lavender enquiry band (build.js enquireHtml) -------------------
   The one full-width use of lavender. White on lavender measures 3.76:1, so
   only large text sits on it: the heading, never body copy.
   -------------------------------------------------------------------------- */

.enquire-band { background: var(--lavender); color: var(--white); }
.enquire-inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem 2.5rem;
  padding-top: clamp(2.5rem, 1.8rem + 3vw, 4rem);
  padding-bottom: clamp(2.5rem, 1.8rem + 3vw, 4rem);
}
.enquire-band h2 {
  margin: 0;
  color: var(--white);
  font-size: clamp(1.625rem, 1.3rem + 1.4vw, 2.5rem);
  font-weight: 750;
}

/* "here", the link to the form, drawn as the band's cream button so it reads
   as the thing to press while staying part of the sentence. */
.enquire-here {
  display: inline-block;
  margin-left: 0.1em;
  padding: 0.02em 0.55em 0.1em;
  border-radius: var(--radius-pill);
  background: var(--cream);
  color: var(--heather);
  text-decoration: none;
  white-space: nowrap;
  transition: background-color var(--transition), color var(--transition);
}
.enquire-here:hover { background: var(--paper); color: var(--heather-deep); }
.enquire-here:focus-visible { outline: 3px solid var(--white); outline-offset: 3px; }

/* ---- components/footer.css ---- */
/* ---- footer --------------------------------------------------------------- */

.site-footer {
  background: var(--paper);
  border-top: 1px solid var(--sand);
  padding: clamp(2.75rem, 2rem + 3vw, 4.5rem) 0 1.5rem;
  font-size: var(--fs-small);
}
.footer-inner {
  display: grid;
  gap: 2rem var(--gap);
  grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
}
.footer-brand img { width: 190px; height: auto; margin-bottom: 0.75rem; }
.footer-heading { font-size: 1rem; margin-bottom: 0.75rem; }
.footer-list { margin: 0; padding: 0; list-style: none; }
.footer-list li + li { margin-top: 0.75rem; }
.footer-list a { display: inline-flex; align-items: center; min-height: 28px; }

.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.5rem 1.5rem;
  margin-top: 2.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--sand);
}
.footer-bottom p { margin: 0; }
.footer-where, .footer-contact { min-width: 0; }
.footer-where strong { color: var(--heather); }

/* ---- components/social.css ---- */
/* ---- social logos ---------------------------------------------------------
   The studio's Facebook and Instagram, as the networks' own logos in their
   own colours (site.json `social`, drawn by socialHtml() in build.js). Three
   copies: under the rail's button (992px and up), at the foot of the phone
   menu (991px and down), and in the footer's Get in touch column (always).
   The rail and drawer copies are display:none at each other's widths, so a
   screen reader meets one of them, not both.

   Each logo is 26px in a 44px round target. Where the row lines up with text
   on its left (drawer, footer), it moves 9px left, so the logo's left edge,
   not its target's, meets the text's.
   -------------------------------------------------------------------------- */

.social {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  margin: 0;
  padding: 0;
  list-style: none;
}
.social-link {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
}
.social-link img { width: 26px; height: 26px; }

.social--footer { margin: 1rem 0 0 -9px; }

@media (min-width: 992px) {
  .social--drawer { display: none; }
  /* Centred under the button, which spans the rail. */
  .social--rail {
    align-self: stretch;
    justify-content: center;
    margin-top: 0.75rem;
  }
}

@media (max-width: 991px) {
  .social--rail { display: none; }
  .social--drawer { margin: 1.5rem 0 0 -9px; }

  /* It inks in with the menu's links (header.css), as the step after the
     fifth: 0.2s + 5 x --delay-stagger. Closing, it goes at once with them. */
  .js .main-nav .social--drawer {
    opacity: 0;
    transform: translateY(14px);
    transition: opacity var(--dur-crossfade) linear, transform var(--dur-crossfade) linear;
  }
  .js .main-nav.is-open .social--drawer {
    opacity: 1;
    transform: none;
    transition:
      opacity var(--dur-reveal) var(--ease-reveal) calc(0.2s + 5 * var(--delay-stagger)),
      transform var(--dur-reveal) var(--ease-reveal) calc(0.2s + 5 * var(--delay-stagger));
  }
}

/* Reduced motion: shown with the menu, no rise and no delay. The global rule
   in utilities.css shortens durations but leaves delays, so the delay above
   would still hold it back. */
@media (max-width: 991px) and (prefers-reduced-motion: reduce) {
  .js .site-header .main-nav .social--drawer {
    opacity: 1;
    transform: none;
    transition: none;
  }
}

/* ---- components/todo.css ---- */
/* ---- TODO boxes ------------------------------------------------------------
   A box marks a fact Katie has not given yet, with its CLIENT-QUESTIONS.md
   number in a comment beside it. None is on the site since 2026-10-12, when
   the operator had them all removed before showing Katie the site; the rules
   stay for any new one, which must be answered and removed before go-live.
   -------------------------------------------------------------------------- */

.todo {
  margin: 1.25rem 0;
  padding: 0.9rem 1.1rem;
  border: 2px dashed var(--lavender);
  border-radius: var(--radius-field);
  background: var(--paper);
  color: var(--ink);
  font-size: var(--fs-small);
}
.todo p + p { margin-top: 0.5rem; }
.todo strong { color: var(--heather); }

/* ---- components/intro.css ---- */
/* ---- first-visit intro (STYLE.md → Intro). Owner: the intro builder. ------
   A cream curtain: the logo wipes in left to right above a staff of five
   periwinkle lines that draw across it, so something changes in every 100 ms
   window; the curtain lifts from 1.8 s (72% of --dur-intro) over --dur-panel;
   2.5 s in all. Timed in CSS, so it plays and ends with no JavaScript.
   15-intro.js hides it (html.intro-off) unless this is the first home load of
   the visit from outside the site, and a click or a key skips it.
   ---------------------------------------------------------------------------- */

.intro {
  position: fixed;
  inset: 0;
  z-index: 1400;                 /* above the header 1000, drawer 1200, toggle 1300 */
  display: grid;
  grid-template-rows: 53% 1fr;   /* the logo sits in the first row, the staff starts at its foot */
  justify-items: center;
  padding: 0 var(--gutter);
  background: var(--cream);
  animation: intro-lift var(--dur-intro) var(--ease-panel) both;
}

/* The staff: five lines edge to edge, a little below the middle. */
.intro-lines {
  position: absolute;
  left: 0;
  right: 0;
  top: 53%;
  display: grid;
  gap: clamp(10px, 1.1vw, 16px);
}
.intro-lines i {
  display: block;
  height: max(1px, 2px);
  background: var(--periwinkle);
  transform: scaleX(0);
  transform-origin: 0 50%;
  animation: intro-line 1.3s cubic-bezier(0.45, 0, 0.3, 1) both;
}
.intro-lines i:nth-child(1) { animation-delay: 0.1s; }
.intro-lines i:nth-child(2) { animation-delay: 0.2s; transform-origin: 100% 50%; }
.intro-lines i:nth-child(3) { animation-delay: 0.3s; }
.intro-lines i:nth-child(4) { animation-delay: 0.4s; transform-origin: 100% 50%; }
.intro-lines i:nth-child(5) { animation-delay: 0.5s; }

/* The logo, image only: it wipes in left to right (0.12 s to 1.4 s) while its
   opacity climbs 0.25 to 1 from the first frame to 1.8 s, so the first 0.1 s and
   the last 0.4 s before the lift change too. */
.intro-logo {
  grid-row: 1;
  align-self: end;
  width: min(60vw, 420px);
  height: auto;
  margin-bottom: clamp(18px, 2.2vw, 32px);
  clip-path: inset(0 100% 0 0);
  opacity: 0.25;
  animation:
    intro-wipe 1.28s cubic-bezier(0.4, 0, 0.2, 1) 0.12s both,
    intro-fade 1.8s linear both;
}

@keyframes intro-line { to { transform: scaleX(1); } }
@keyframes intro-wipe { to { clip-path: inset(0 0 0 0); } }
@keyframes intro-fade { to { opacity: 1; } }
/* 72% of 2.5 s = 1.8 s, then --dur-panel (0.7 s) lifting. The easing is set on
   the element, not in the keyframes: Chrome measured a var() inside a keyframe's
   timing function as linear. The held first stretch has no change to ease. */
@keyframes intro-lift {
  0%, 72% { clip-path: inset(0 0 0 0); }
  100%    { clip-path: inset(0 0 100% 0); visibility: hidden; }
}

/* Playing: the curtain takes the clicks that skip it. Arrived from another page of
   the site: a page cover is opening, so no curtain either. */
.intro-running .intro { pointer-events: auto; }
.arrived .intro,
.intro-off .intro,
.intro-skip .intro { display: none; }

@media (prefers-reduced-motion: reduce), print {
  .intro { display: none; }
}

/* ---- components/reveal.css ---- */
/* ---- reveals: hero, text, blocks, media (STYLE.md → Reveals). Owner: the reveal builder. ----

   50-reveal.js names the targets and adds the classes below; markup is
   untouched. Every hidden or dimmed state is under .js AND under
   (prefers-reduced-motion: no-preference): without JavaScript, or with a
   reduced-motion setting, everything is in its final place and ink.

     rv-hero   page titles: words rise from --dist-line inside their line
     rv-ink    headings and statements: words ink in by opacity + a short rise
     rv-block  blocks rise --dist-rise and fade in
     rv-media  photos unclip from the bottom 88%
     rv-load   in the first screen at load: waits for the curtain / page cover
     rv-in     revealed (set once)       rv-now  revealed without a transition

   Words are inline-blocks on the baseline, never overflow:hidden (that moves an
   inline-block's baseline, and the staff titles are geometry, STYLE.md); the
   hero's words are clipped with clip-path instead.

   Screen only: a printed page shows everything, whatever has or has not been
   scrolled to. And a headings floor, --rv-floor: text caught between "not yet"
   and "revealed" (a scroll-scrubbed word waits at it for as long as the visitor
   stops) must stay legible. It rests at 0.7, measured: heather titles on the
   cream 3.23:1 (the 3:1 large text needs), the body-size lead and intro lines
   in ink 4.84:1 (the 4.5:1 they need). The white title on the lavender band
   rests at 0.85 (3.18:1; at full strength it is only 3.76:1). The style's
   anchors fall to 0.08-0.28 on a dark ground; a lower floor here is a
   stronger effect and costs legibility while a word waits mid-scroll (0.55
   measured 2.41:1 on the cream).
   --------------------------------------------------------------------------- */

@media screen and (prefers-reduced-motion: no-preference) {
  /* How long a load-time reveal waits: the curtain lifts at 72% of the intro
     (1.8 s); a page reached through the cover waits for it (about 1 s). */
  .js.intro-running { --rv-w: calc(var(--dur-intro) * 0.72); }
  .js.arrived { --rv-w: calc(var(--delay-page) + var(--dur-panel)); }
  .js .rv-load { --rv-wait: var(--rv-w, 0s); }

  .js .rv-w,
  .js .rv-w > span { display: inline-block; vertical-align: baseline; }

  /* ---- hero and page titles ---- */
  .js .rv-hero .rv-w { clip-path: inset(-0.25em -0.3em 0 -0.3em); }
  .js .rv-hero .rv-w > span {
    transform: translateY(var(--dist-line));
    transition: transform var(--dur-reveal) var(--ease-reveal)
      calc(var(--rv-wait, 0s) + var(--i, 0) * var(--delay-stagger));
  }
  .js .rv-hero.rv-in .rv-w > span { transform: none; }

  /* ---- text inks in: opacity and a short rise, word by word ---- */
  .js .rv-ink { --rv-floor: 0.7; }
  .js .enquire-band .rv-ink { --rv-floor: 0.85; }
  .js .rv-ink .rv-w > span {
    opacity: var(--rv-floor);
    transform: translateY(0.5em);
    transition:
      opacity var(--dur-reveal) var(--ease-reveal) calc(var(--rv-wait, 0s) + var(--k, 0) * 4 * var(--delay-stagger)),
      transform var(--dur-reveal) var(--ease-reveal) calc(var(--rv-wait, 0s) + var(--k, 0) * 4 * var(--delay-stagger));
  }
  .js .rv-ink.rv-in .rv-w > span { opacity: 1; transform: none; }

  /* Scrubbed by scroll on the element's own view timeline, each word offset by
     its place in reading order k (0 … 1): done when the element's middle
     reaches the middle of the window. Text in the first screen at load, and
     any text 50-reveal.js has already revealed, uses the transition above. */
  @supports (animation-timeline: view()) {
    .js .rv-ink:not(.rv-load):not(.rv-in) { view-timeline: --rv-ink block; }
    .js .rv-ink:not(.rv-load):not(.rv-in) .rv-w > span {
      transition: none;
      animation: rv-ink linear both;
      animation-timeline: --rv-ink;
      animation-range: cover calc(8% + var(--k, 0) * 32%) cover calc(14% + var(--k, 0) * 32%);
    }
  }

  /* ---- blocks and media ---- */
  .js .rv-block {
    opacity: 0;
    transform: translateY(var(--dist-rise));
    transition:
      opacity var(--dur-reveal) var(--ease-reveal) var(--rv-wait, 0s),
      transform var(--dur-reveal) var(--ease-reveal) var(--rv-wait, 0s);
  }
  .js .rv-block.rv-in { opacity: 1; transform: none; }

  .js .rv-media img {
    clip-path: inset(12% 0 0 0 round var(--radius-photo));
    transition: clip-path var(--dur-reveal) var(--ease-reveal) var(--rv-wait, 0s);
  }
  .js .rv-media.rv-in img { clip-path: inset(0 0 0 0 round var(--radius-photo)); }

  /* If 50-reveal.js ever throws part-way, 50-reveal.js sets html.rv-failed and
     everything it touched is shown as it is on a page without scripting. */
  .js.rv-failed .rv-block { opacity: 1 !important; transform: none !important; transition: none !important; }
  .js.rv-failed .rv-media img { clip-path: none !important; transition: none !important; }
  .js.rv-failed .rv-w { clip-path: none !important; }
  .js.rv-failed .rv-w > span { opacity: 1 !important; transform: none !important; transition: none !important; animation: none !important; }

  /* Something inside took focus: show it now, with no transition. */
  .js .rv-now,
  .js .rv-now img,
  .js .rv-now .rv-w > span { transition: none !important; animation: none !important; }
}

@keyframes rv-ink {
  from { opacity: var(--rv-floor, 0.7); transform: translateY(0.5em); }
  to   { opacity: 1;    transform: none; }
}

/* ---- components/hover.css ---- */
/* ---- hover: links and buttons (STYLE.md -> Hover) ------------------------
   Only where a real pointer can hover: nothing here for touch. The hover
   colours are the ones reset.css and button.css already set; this file adds
   the motion.

   Text links: the underline wipes out to the right and back in from the left,
   --dur-hover each way. The resting underline is the browser's own (reset.css,
   periwinkle). On hover that one is made transparent and a background line
   of the same thickness, in the hover colour, takes its place and wipes. A
   background cannot shift layout. Inline-flex links (the footer's, a phone
   link) sit in a taller box than their text, so --u-y puts the line at the
   text's underline rather than the box's foot.

   Buttons: fill and text swap over --dur-hover.

   Social logos (social.css): the logo rises 2px. They take no underline,
   so the footer's link rules leave them out. Reduced motion: nothing moves.

   Reduced motion: no wipe, the underline turns in place; the colours still
   crossfade (--dur-crossfade), which needs !important to beat the global
   0.01ms rule in utilities.css.
   -------------------------------------------------------------------------- */

@media (hover: hover) and (pointer: fine) {
  .text-link,
  .prose a:not(.btn),
  .contact-aside a:not(.btn),
  .site-footer a:not(.social-link) {
    --u-y: 100%;
    transition: color var(--dur-hover) var(--ease-hover);
  }
  .footer-list a { --u-y: calc(50% + 0.43em); }

  .text-link:hover,
  .prose a:not(.btn):hover,
  .contact-aside a:not(.btn):hover,
  .site-footer a:not(.social-link):hover {
    text-decoration-color: transparent;
    background: linear-gradient(currentColor, currentColor) no-repeat 0 var(--u-y) / 100% 1.5px;
  }

  .btn {
    transition: background-color var(--dur-hover) var(--ease-hover), border-color var(--dur-hover) var(--ease-hover), color var(--dur-hover) var(--ease-hover);
  }
}

@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: no-preference) {
  .text-link:hover,
  .prose a:not(.btn):hover,
  .contact-aside a:not(.btn):hover,
  .site-footer a:not(.social-link):hover {
    animation: underline-wipe calc(var(--dur-hover) * 2) var(--ease-hover) both;
  }

  .social-link img { transition: transform var(--dur-hover) var(--ease-hover); }
  .social-link:hover img { transform: translateY(-2px); }
}

@media (hover: hover) and (pointer: fine) and (prefers-reduced-motion: reduce) {
  .text-link,
  .prose a:not(.btn),
  .contact-aside a:not(.btn),
  .site-footer a:not(.social-link),
  .btn { transition-duration: var(--dur-crossfade) !important; }
}

/* 0-50%: the line shrinks into the right edge; 50-100%: it grows from the left. */
@keyframes underline-wipe {
  0%    { background-size: 100% 1.5px; background-position: 100% var(--u-y); }
  50%   { background-size: 0 1.5px;    background-position: 100% var(--u-y); }
  50.1% { background-size: 0 1.5px;    background-position: 0 var(--u-y); }
  100%  { background-size: 100% 1.5px; background-position: 0 var(--u-y); }
}

/* ---- components/transitions.css ---- */
/* ---- page cover (STYLE.md -> Page transition) -----------------------------
   Cross-document view transitions: a same-origin page change (/, /contact/,
   /privacy/, the 404 and thank-you pages) runs without a router. The old page
   is clipped away upward over --dur-panel, exposing the cream ::view-transition
   layer; the new page unclips from the bottom from --delay-page, in place at
   about 1 s. A browser without support (Firefox) navigates as usual.

   Under reduced motion the transition stays and crossfades over
   --dur-crossfade instead of wiping. The rule that opts in sits outside the
   motion query for that reason.
   -------------------------------------------------------------------------- */

@view-transition { navigation: auto; }

::view-transition { background: var(--cream); }

@media (prefers-reduced-motion: no-preference) {
  ::view-transition-old(root) { animation: vt-out var(--dur-panel) var(--ease-panel) both; }
  ::view-transition-new(root) { animation: vt-in var(--dur-panel) var(--ease-panel) var(--delay-page) both; }
}

@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(root),
  ::view-transition-old(root),
  ::view-transition-new(root) { animation-duration: var(--dur-crossfade); }
}

@keyframes vt-out {
  from { clip-path: inset(0 0 0 0); }
  to   { clip-path: inset(0 0 100% 0); }
}
@keyframes vt-in {
  from { clip-path: inset(100% 0 0 0); }
  to   { clip-path: inset(0 0 0 0); }
}

/* ---- utilities.css ---- */
/* ---- utilities, motion, print ------------------------------------------- */

/* Any phone link anywhere is a tap target (WCAG 2.2 minimum 24x24), including
   one written straight into page content. */
a[href^="tel:"] {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
}

/* 00-utils.js adds .js to <html> as soon as scripts run. Nothing on this site
   hides content behind it; the rule records that the class is known. */
.js body { text-rendering: optimizeLegibility; }

/* In-page anchors (/#studios, a rail link) scroll smoothly; with a reduced-motion
   preference they jump. */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

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

@media print {
  .site-header,
  .nav-backdrop,
  .enquire-band,
  .skip-link { display: none; }

  body { color: #000; background: #fff; padding-left: 0; }
  .section { padding: 1rem 0; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.75em; }
}
