/* Generated by workers/sites-renderer/scripts/build-static.mjs. Do not edit.
 * Sources:
 *   @booka/sites-registry/src/fonts.css
 *   @booka/sites-registry/src/styles/00-base.css
 *   @booka/sites-registry/src/styles/10-sections.css
 *   @booka/sites-registry/src/styles/20-booking.css
 */

/*
 * Font faces.
 *
 * Split out of the variant stylesheet because two surfaces need them and only
 * one needs the rest: published pages get this prepended to the bundled
 * stylesheet at build time, and the builder imports it so the specimens in its
 * font picker are set in the faces they name.
 *
 * Prepended rather than `@import`ed into the served asset: an `@import` at the
 * top of a render-blocking stylesheet is a second serial round trip before the
 * first glyph, on the path every visitor takes.
 */

/*
 * The families, self-hosted.
 *
 * These declarations are what make the theme tokens mean anything. Without
 * them a preload fetches a file the page has no way to use and every site
 * falls through to Georgia and the system sans.
 *
 * Fetched by `scripts/fetch-fonts.mjs`; see that file for why latin and
 * latin-ext are two files and why Instrument Serif is the one face with an
 * italic. The declared weight ranges match what that script requests — a
 * range wider than the file carries is a synthesised bold, and a narrower one
 * is a weight the stylesheet asks for and never gets.
 *
 * `swap` rather than `optional` or `block`: the text is the content on a
 * booking page, so it may never be invisible, and a provider who picked a
 * typeface should get it even on a slow connection rather than sometimes.
 */

@font-face {
	font-family: "Inter";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/inter.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Inter";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/inter-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Geist";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/geist.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Geist";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/geist-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Manrope";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/manrope.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Manrope";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/manrope-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Plus Jakarta Sans";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/jakarta.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Plus Jakarta Sans";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/jakarta-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Fraunces";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/fraunces.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Fraunces";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/fraunces-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Instrument Serif";
	font-style: normal;
	font-weight: 400;
	font-display: swap;
	src: url("/_static/fonts/instrument-serif.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Instrument Serif";
	font-style: normal;
	font-weight: 400;
	font-display: swap;
	src: url("/_static/fonts/instrument-serif-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Instrument Serif";
	font-style: italic;
	font-weight: 400;
	font-display: swap;
	src: url("/_static/fonts/instrument-serif-italic.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Instrument Serif";
	font-style: italic;
	font-weight: 400;
	font-display: swap;
	src: url("/_static/fonts/instrument-serif-italic-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Newsreader";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/newsreader.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Newsreader";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/newsreader-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Lora";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/lora.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Lora";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/lora-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "DM Sans";
	font-style: normal;
	font-weight: 300 700;
	font-display: swap;
	src: url("/_static/fonts/dm-sans.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "DM Sans";
	font-style: normal;
	font-weight: 300 700;
	font-display: swap;
	src: url("/_static/fonts/dm-sans-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Space Grotesk";
	font-style: normal;
	font-weight: 300 700;
	font-display: swap;
	src: url("/_static/fonts/space-grotesk.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Space Grotesk";
	font-style: normal;
	font-weight: 300 700;
	font-display: swap;
	src: url("/_static/fonts/space-grotesk-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Jost";
	font-style: normal;
	font-weight: 200 700;
	font-display: swap;
	src: url("/_static/fonts/jost.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Jost";
	font-style: normal;
	font-weight: 200 700;
	font-display: swap;
	src: url("/_static/fonts/jost-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Bodoni Moda";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/bodoni-moda.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Bodoni Moda";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/bodoni-moda-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Gloock";
	font-style: normal;
	font-weight: 400;
	font-display: swap;
	src: url("/_static/fonts/gloock.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Gloock";
	font-style: normal;
	font-weight: 400;
	font-display: swap;
	src: url("/_static/fonts/gloock-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "EB Garamond";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/eb-garamond.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "EB Garamond";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/eb-garamond-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Hanken Grotesk";
	font-style: normal;
	font-weight: 300 700;
	font-display: swap;
	src: url("/_static/fonts/hanken-grotesk.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Hanken Grotesk";
	font-style: normal;
	font-weight: 300 700;
	font-display: swap;
	src: url("/_static/fonts/hanken-grotesk-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Archivo";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/archivo.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Archivo";
	font-style: normal;
	font-weight: 400 700;
	font-display: swap;
	src: url("/_static/fonts/archivo-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Oswald";
	font-style: normal;
	font-weight: 300 700;
	font-display: swap;
	src: url("/_static/fonts/oswald.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Oswald";
	font-style: normal;
	font-weight: 300 700;
	font-display: swap;
	src: url("/_static/fonts/oswald-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Spline Sans Mono";
	font-style: normal;
	font-weight: 400 500;
	font-display: swap;
	src: url("/_static/fonts/spline-sans-mono.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Spline Sans Mono";
	font-style: normal;
	font-weight: 400 500;
	font-display: swap;
	src: url("/_static/fonts/spline-sans-mono-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

/*
 * Light cuts added after the range files above shipped at immutable URLs.
 * Keeping 300 in its own file makes the cache change explicit and means a
 * regular-weight page does not pay for glyphs it never draws.
 */

@font-face {
	font-family: "Inter";
	font-style: normal;
	font-weight: 300;
	font-display: swap;
	src: url("/_static/fonts/inter-light.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Inter";
	font-style: normal;
	font-weight: 300;
	font-display: swap;
	src: url("/_static/fonts/inter-light-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Geist";
	font-style: normal;
	font-weight: 300;
	font-display: swap;
	src: url("/_static/fonts/geist-light.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Geist";
	font-style: normal;
	font-weight: 300;
	font-display: swap;
	src: url("/_static/fonts/geist-light-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Manrope";
	font-style: normal;
	font-weight: 300;
	font-display: swap;
	src: url("/_static/fonts/manrope-light.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Manrope";
	font-style: normal;
	font-weight: 300;
	font-display: swap;
	src: url("/_static/fonts/manrope-light-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Plus Jakarta Sans";
	font-style: normal;
	font-weight: 300;
	font-display: swap;
	src: url("/_static/fonts/jakarta-light.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Plus Jakarta Sans";
	font-style: normal;
	font-weight: 300;
	font-display: swap;
	src: url("/_static/fonts/jakarta-light-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Fraunces";
	font-style: normal;
	font-weight: 300;
	font-display: swap;
	src: url("/_static/fonts/fraunces-light.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Fraunces";
	font-style: normal;
	font-weight: 300;
	font-display: swap;
	src: url("/_static/fonts/fraunces-light-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Newsreader";
	font-style: normal;
	font-weight: 300;
	font-display: swap;
	src: url("/_static/fonts/newsreader-light.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Newsreader";
	font-style: normal;
	font-weight: 300;
	font-display: swap;
	src: url("/_static/fonts/newsreader-light-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

@font-face {
	font-family: "Archivo";
	font-style: normal;
	font-weight: 300;
	font-display: swap;
	src: url("/_static/fonts/archivo-light.woff2") format("woff2");
	unicode-range:
		U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
		U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
	font-family: "Archivo";
	font-style: normal;
	font-weight: 300;
	font-display: swap;
	src: url("/_static/fonts/archivo-light-ext.woff2") format("woff2");
	unicode-range:
		U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329,
		U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
		U+A720-A7FF;
}

/*
 * Variant stylesheet — base layer.
 *
 * One stylesheet for every site on the platform, authored as ordered partials
 * in this directory and concatenated by `scripts/emit-static.mjs` (after
 * `fonts.css`) into the single `/_static/styles.css` every published page and
 * the builder preview link. Published renderers give the bundle an immutable
 * versioned URL, so it caches once globally rather than per tenant — a site's
 * identity comes entirely from the custom properties injected in its <head>.
 *
 *   00-base.css      reset, layout primitives, buttons, header, footer, motion
 *   10-sections.css  every section variant and the compositions they share
 *   20-booking.css   islands, availability, and the booking flow
 *
 * Rules:
 *   - Only var() values. No raw colors, no raw font families, ever.
 *   - No layout animation. Transform and opacity only.
 *   - Every image reserves its space, because CLS is a conversion tax.
 *
 * The faces the font tokens resolve to live in `../fonts.css`, which is
 * prepended when the static asset is built. They are kept separate because the
 * builder needs the same declarations for its own specimens and must not take
 * the rest of this file with them — not because they are optional.
 */
/* ---------- reset ---------- */

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

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

body,
h1,
h2,
h3,
p,
figure,
blockquote,
ul,
ol,
table,
dd {
	margin: 0;
}

ul[role="list"] {
	list-style: none;
	padding: 0;
}

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

.bs-body {
	min-block-size: 100vh;
	min-block-size: 100dvh;
	display: flex;
	flex-direction: column;
	margin: 0;
	background: var(--color-surface);
	color: var(--color-ink);
	font-family: var(--font-body);
	font-size: var(--text-base);
	line-height: 1.65;
	-webkit-font-smoothing: antialiased;
	text-rendering: optimizeLegibility;
	/*
	 * The parts of the page we do not draw.
	 *
	 * Checkboxes, radios, the caret, scrollbars, and the date and number
	 * spinners all come from the browser, and without a colour scheme they come
	 * light — a white checkbox on a dark site, and a white flash between
	 * navigations. One declaration puts them on the same side as the theme.
	 */
	color-scheme: var(--color-scheme, light);
	accent-color: var(--color-brand);
	caret-color: var(--color-brand);
}

/*
 * Selection, in the site's own colour.
 *
 * A default blue highlight is the cheapest tell that a page was assembled
 * rather than built. `brand-soft` is already derived to carry ink at reading
 * contrast in both modes, which is exactly what a selection has to do.
 */
::selection {
	background: var(--color-brand-soft);
	color: var(--color-brand-strong);
}

/*
 * Headings, in the theme's own voice.
 *
 * Weight, case, tracking and line all come from tokens rather than being
 * assumed here, because the three launch looks disagree on every one of them:
 * Atelier is a regular-weight serif, Studio a regular grotesk, Still a light
 * grotesk in tracked capitals. A heading rule that hardcoded `600` would set
 * Still in a weight its look never chose.
 */
h1,
h2,
h3 {
	font-family: var(--font-heading);
	font-weight: var(--heading-weight, 600);
	line-height: var(--heading-line, 1.15);
	letter-spacing: var(--heading-tracking, -0.015em);
	text-transform: var(--heading-transform, none);
	text-wrap: balance;
}

p {
	text-wrap: pretty;
}

a {
	color: inherit;
}

/*
 * One focus ring for the whole site.
 *
 * `border-radius` used to be set here too, which quietly squared off every pill
 * the moment it took focus — the ring is drawn from the element's own shape, so
 * it has no business changing it. The offset does the separating instead.
 */
:where(a, button, [tabindex]):focus-visible {
	outline: 2px solid var(--color-brand);
	outline-offset: 2px;
}

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

.bs-main {
	flex: 1 0 auto;
}

.bs-container {
	width: min(100% - calc(var(--space-unit) * 8), 68rem);
	margin-inline: auto;
}

.bs-measure {
	max-width: var(--measure);
}

/*
 * A measured section keeps the page's left rail.
 *
 * `bs-container bs-measure` on one element used to inherit the container's
 * auto margins, so every reading-width section floated to the page's center
 * while the hero and the galleries held the container edge — five sections,
 * two competing left edges. The start margin below is the same gutter the
 * container resolves to, so measured sections line up with everything else.
 * Deliberately centered compositions opt back out underneath.
 */
.bs-container.bs-measure {
	margin-inline: max(calc(var(--space-unit) * 4), calc((100% - 68rem) / 2)) auto;
}

.bs-container.bs-measure.bs-contact__center,
.bs-testimonials--feature .bs-container.bs-measure {
	margin-inline: auto;
}

.bs-section {
	padding-block: var(--space-section);
	/* The header is sticky; an anchor jump must land clear of it. */
	scroll-margin-block-start: calc(var(--space-unit) * 18);
}

.bs-section:first-child {
	padding-block-start: calc(var(--space-section) * 0.6);
}

.bs-section__header {
	margin-block-end: calc(var(--space-gap) * 1.5);
}

.bs-section__heading {
	font-size: var(--text-2xl);
	letter-spacing: -0.02em;
}

.bs-section__heading--center {
	text-align: center;
}

.bs-section__intro {
	margin-block-start: calc(var(--space-unit) * 3);
	color: var(--color-ink-muted);
	max-width: 52ch;
	text-wrap: pretty;
}

/*
 * The small label above a heading.
 *
 * It used to draw a brand-coloured rule in front of itself, on the argument
 * that the rule could do the pointing so the words could be quiet. No design
 * has that rule: it is a mark the page makes about its own label, it lands
 * left of a centred banner where there is nothing to point from, and the looks
 * that wear a hairline already spend it on the section headers.
 *
 * Written in parts, so a label can say two things — a name and a trade — with
 * the page supplying the divider between them.
 */
.bs-eyebrow {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: calc(var(--space-unit) * 1.5) calc(var(--space-unit) * 3);
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.75rem);
	letter-spacing: var(--detail-tracking, 0.14em);
	text-transform: uppercase;
	color: var(--color-ink-subtle);
	font-weight: var(--detail-weight, 500);
	margin-block-end: calc(var(--space-unit) * 5);
}

/* The same disc the facts line divides on, for the same reason. */
.bs-eyebrow__part + .bs-eyebrow__part::before {
	content: "";
	display: inline-block;
	inline-size: 3px;
	block-size: 3px;
	border-radius: 50%;
	margin-inline-end: calc(var(--space-unit) * 3);
	vertical-align: middle;
	background: color-mix(in srgb, var(--color-ink) 25%, transparent);
}

/*
 * The quiet look says it in sentence case, and the other two shout it.
 *
 * A structural choice a token cannot make: the detail voice carries the face,
 * the weight and the tracking a look sets every small label in, and all three
 * launch looks share one voice — but only two of them capitalise. Tracked
 * capitals over a serif display line read as a form; the design sets that one
 * as ordinary words, at the tracking of ordinary words.
 */
html[data-chrome="quiet"] .bs-eyebrow {
	text-transform: none;
	letter-spacing: normal;
	font-size: var(--detail-size-data, 0.82rem);
}

.bs-card {
	background: var(--color-surface);
	border: var(--frame-width, 1px) solid var(--frame-color, var(--color-border));
	border-radius: var(--radius-lg);
	padding: var(--space-gap);
}

/* ---------- tone ---------- */

/*
 * A section on the ink instead of the paper.
 *
 * The renderer wraps an inverted section in `.bs-tone[data-tone="inverted"]`,
 * and this is the whole implementation: the surface and ink swap, the muted
 * steps are re-derived from the swapped pair, and the chrome tokens follow —
 * a filled button on a black band is white, an outlined one is outlined in
 * white. Nothing inside the wrapper knows; every rule in this stylesheet reads
 * the same tokens it always did and gets the inverted answer.
 */
.bs-tone[data-tone="inverted"] {
	--color-surface: var(--tone-ink);
	--color-surface-alt: color-mix(in srgb, var(--tone-ink) 90%, var(--tone-surface));
	--color-surface-sunken: color-mix(in srgb, var(--tone-ink) 84%, var(--tone-surface));
	--color-ink: var(--tone-surface);
	--color-ink-muted: color-mix(in srgb, var(--tone-surface) 76%, transparent);
	--color-ink-subtle: color-mix(in srgb, var(--tone-surface) 58%, transparent);
	--color-border: color-mix(in srgb, var(--tone-surface) 22%, transparent);
	--frame-color: var(--tone-surface);
	--btn-bg: var(--tone-surface);
	--btn-ink: var(--tone-ink);
	--btn-border: var(--tone-surface);
	--btn-bg-hover: color-mix(in srgb, var(--tone-surface) 86%, transparent);
	--btn-ink-hover: var(--tone-ink);
	background: var(--color-surface);
	color: var(--color-ink);
}

/*
 * Under the airy chrome the outlined button stays outlined when inverted —
 * the fill is the paper's own colour, which on the ink is the frame.
 */
html[data-chrome="airy"] .bs-tone[data-tone="inverted"] {
	--btn-bg: transparent;
	--btn-ink: var(--tone-surface);
	--btn-bg-hover: var(--tone-surface);
	--btn-ink-hover: var(--tone-ink);
}

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

.bs-btn {
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: calc(var(--space-unit) * 2);
	min-block-size: 2.75rem;
	padding: calc(var(--space-unit) * 2.5) calc(var(--space-unit) * 6);
	border-radius: var(--radius-md);
	font: inherit;
	font-family: var(--font-detail, var(--font-body));
	font-weight: var(--detail-weight, 500);
	font-size: var(--detail-size, 0.75rem);
	letter-spacing: var(--detail-tracking, 0.08em);
	text-transform: uppercase;
	text-decoration: none;
	border: 1px solid transparent;
	cursor: pointer;
	transition: transform 120ms ease-out, background-color 150ms ease-out,
		border-color 150ms ease-out, color 150ms ease-out;
	will-change: transform;
}

/*
 * The primary button is drawn from the chrome tokens, not the brand directly.
 *
 * `--btn-*` resolve per chrome preset: a brand fill under Atelier, an ink fill
 * under Studio, an outline under Still. One rule, three looks — and a section
 * that runs inverted swaps the same tokens, so the button on a black band is
 * white without a second rule knowing about it.
 */
.bs-btn--primary {
	background: var(--btn-bg, var(--color-brand));
	color: var(--btn-ink, var(--color-brand-contrast));
	border-color: var(--btn-border, var(--color-brand));
}

.bs-btn--primary:hover {
	background: var(--btn-bg-hover, var(--color-brand-strong));
	color: var(--btn-ink-hover, var(--color-brand-contrast));
	border-color: var(--btn-bg-hover, var(--color-brand-strong));
}

/*
 * The background is declared, not inherited.
 *
 * Without it a `<button>` keeps the user agent's `buttonface`, which is a light
 * grey no theme controls — on a dark site that shipped a near-white slab with
 * near-white text on it, which is to say an invisible button. Anchors styled as
 * ghosts had no background at all, so the two never even matched.
 *
 * The border is the chrome's frame, not the hairline: under Studio a ghost
 * button is a 2px ink box like every other frame on that look, and under the
 * quiet and airy chromes the frame tokens resolve to the hairline anyway.
 */
.bs-btn--ghost {
	background: transparent;
	border-width: var(--frame-width, 1px);
	border-color: var(--frame-color, var(--color-border));
	color: var(--color-ink);
}

.bs-btn--ghost:hover {
	border-color: var(--color-brand);
	color: var(--color-brand);
	background: var(--color-brand-soft);
}

.bs-btn:active {
	transform: translateY(1px);
}

.bs-btn:disabled {
	cursor: default;
	opacity: 0.6;
	transform: none;
}

/* ---------- header and footer ---------- */

/*
 * The masthead travels with the reader.
 *
 * Sticky, on a translucent wash of the site's own paper, so the way back to
 * "Book" is never a long scroll away. The blur keeps whatever passes beneath it
 * legible as texture rather than noise; where the platform cannot blur, the
 * high-opacity wash still carries it.
 */
.bs-header {
	position: sticky;
	inset-block-start: 0;
	z-index: 10;
	border-block-end: 1px solid var(--color-border);
	background: color-mix(in srgb, var(--color-surface) 86%, transparent);
	-webkit-backdrop-filter: blur(14px);
	backdrop-filter: blur(14px);
}

/*
 * A rail header is two rows tall on a phone, so an anchor jump under it needs
 * more room to land clear of it than a single row does. From `48rem` the rail
 * shares the brand's row and the base `.bs-section` margin is enough.
 */
@media (max-width: 47.99rem) {
	.bs-body:has(.bs-header--rail) .bs-section {
		scroll-margin-block-start: calc(var(--space-unit) * 30);
	}
}

/*
 * The brand row, in the menu style: one grid of three columns — a tool on
 * the left, the brand in the middle, the button on the right — with the
 * brand held to the centre column whether or not the other two have anything
 * in them. `1fr auto 1fr` keeps the two sides equal, so the name sits on the
 * page's own centre line: the whole design of the header is that the site
 * introduces itself first, and a name nudged off centre by a button it does
 * not know about reads as a mistake rather than a layout.
 */
.bs-header__inner {
	align-items: center;
	column-gap: var(--space-gap);
	min-block-size: calc(var(--space-unit) * 17);
	padding-block: calc(var(--space-unit) * 3);
}

.bs-header--menu .bs-header__inner {
	display: grid;
	grid-template-columns: 1fr auto 1fr;
}

/*
 * The brand row, in the rail style: a phone stacks the centred name over the
 * rail; a wide screen puts the two on one line, name left and links right.
 * The same markup either way — the rail is a flex item that wraps onto its
 * own full-width row until there is room beside the name.
 */
.bs-header--rail .bs-header__inner {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	row-gap: 0;
	padding-block-end: 0;
}

.bs-header--rail .bs-header__brand {
	flex: 0 1 auto;
	padding-block-end: calc(var(--space-unit) * 3);
}

@media (min-width: 48rem) {
	.bs-header--rail .bs-header__inner {
		flex-wrap: nowrap;
		justify-content: space-between;
		padding-block-end: calc(var(--space-unit) * 3);
	}

	/*
	 * The block sits at the row's start, but inside it the name and the city
	 * stay centred on each other — the same lockup the phone header draws,
	 * moved to the left rather than re-set as a left-aligned pair.
	 */
	.bs-header--rail .bs-header__brand {
		padding-block-end: 0;
	}
}

.bs-header__menu {
	grid-column: 1;
	justify-self: start;
}

.bs-header__brand {
	grid-column: 2;
	/*
	 * A wrapping flex row rather than a block, so the brand can be a mark, a
	 * name, or both on one line, with the subline forced under them all by
	 * its own `flex-basis`. Alone, any of the three sits where it always did.
	 */
	display: inline-flex;
	flex-wrap: wrap;
	justify-content: center;
	align-items: center;
	gap: 0 calc(var(--space-unit) * 2.5);
	text-align: center;
	text-wrap: balance;
	/*
	 * Set in the heading voice, at a fixed display size rather than a scale
	 * step: the brand is the one heading that has to fit a phone's header at
	 * every scale, so it is not the scale's to enlarge. Weight, case and
	 * tracking are the tokens' — Atelier a regular serif, Studio a regular
	 * grotesk, Still light tracked capitals — and one rule serves all three.
	 */
	font-family: var(--font-heading);
	font-size: 1.7rem;
	font-weight: var(--heading-weight, 600);
	line-height: 1.05;
	letter-spacing: var(--heading-tracking, -0.01em);
	text-transform: var(--heading-transform, none);
	text-decoration: none;
	color: var(--color-ink);
}

/*
 * The words under the name — the city — in the detail voice, quiet. A whole
 * row of its own inside the brand link, so tapping it goes where tapping the
 * name goes.
 */
.bs-header__subline {
	flex-basis: 100%;
	margin-block-start: calc(var(--space-unit) * 1);
	font-family: var(--font-detail, var(--font-body));
	font-size: 0.625rem;
	font-weight: var(--detail-weight, 500);
	line-height: 1.3;
	letter-spacing: 0.015em;
	text-transform: none;
	color: color-mix(in srgb, var(--color-ink-subtle) 90%, var(--color-surface));
}

/*
 * A provider's own mark takes the name's place without changing the header's
 * rhythm. Both axes are bounded: a square icon cannot make the bar enormous,
 * and a long wordmark cannot push the button off a phone.
 */
.bs-header__logo {
	inline-size: auto;
	block-size: auto;
	max-inline-size: min(12rem, 48vw);
	max-block-size: 2.75rem;
	object-fit: contain;
	object-position: center;
}

.bs-header__cta {
	grid-column: 3;
	justify-self: end;
	flex: 0 0 auto;
	margin-inline-start: var(--space-gap);
	min-block-size: 0;
	padding: calc(var(--space-unit) * 2) calc(var(--space-unit) * 4.5);
}

/*
 * Under the rail the button is a wide-screen affordance only.
 *
 * The phone design has no button in its header: "Book" is a link in the rail,
 * and the header is already two rows tall there, so a third thing beside the
 * brand would crowd the name the whole row centres on. On a wide screen the
 * brand row has room at its end, which is where the desktop design keeps its
 * button too. The menu header keeps its button at every width — one row, and
 * the button is the third thing that row is for.
 */
@media (max-width: 47.99rem) {
	.bs-header--rail .bs-header__cta {
		display: none;
	}
}

/* ---------- the rail ---------- */

/*
 * The menu as a row of links: About · Portfolio · Book · Location · Reviews
 * · FAQ. On a phone it is a band of its own under the brand — full-bleed,
 * with a hairline above it and the links centred inside — which the
 * `100vw` margins carve out of the container it sits in. From `48rem` it
 * gives the band up and takes the rest of the brand's row, links to the end.
 */
.bs-nav--rail {
	flex: 1 1 100%;
	inline-size: 100vw;
	margin-inline: calc(50% - 50vw);
	border-block-start: 1px solid var(--color-border);
}

@media (min-width: 48rem) {
	.bs-nav--rail {
		flex: 1 1 auto;
		inline-size: auto;
		min-inline-size: 0;
		margin-inline: 0;
		border-block-start: 0;
	}

	.bs-nav--rail .bs-nav__list {
		justify-content: flex-end;
		margin-inline: 0;
		padding-inline: 0;
		overflow-x: visible;
	}

	.bs-nav--rail .bs-nav__list > li:first-child {
		margin-inline-start: 0;
	}

	.bs-nav--rail .bs-nav__list > li:last-child {
		margin-inline-end: 0;
	}
}

.bs-nav__list {
	display: flex;
	/* The scroll-spy scrolls the rail to the live link by its offset, which is
	   measured from the nearest positioned ancestor: this one. */
	position: relative;
}

/*
 * Centred when it fits, scrolling from the left edge when it does not.
 *
 * `justify-content: center` on an overflowing flex row clips its *start* —
 * the first links vanish off the left and cannot be scrolled to. Auto margins
 * on the end items do what centring cannot: with free space they share it
 * and the row sits in the middle; without any they collapse to nothing and
 * the row starts at the edge and scrolls. The rail already spans the viewport;
 * keep the list inside it and use padding for the links' page gutters. Another
 * negative margin here would widen the list beyond both screen edges.
 */
.bs-nav--rail .bs-nav__list {
	margin-inline: 0;
	padding-inline: calc(var(--space-unit) * 4);
	scroll-padding-inline: calc(var(--space-unit) * 4);
	overflow-x: auto;
	overscroll-behavior-x: contain;
	-webkit-overflow-scrolling: touch;
	scrollbar-width: none;
}

.bs-nav--rail .bs-nav__list::-webkit-scrollbar {
	display: none;
}

.bs-nav--rail .bs-nav__list > li:first-child {
	margin-inline-start: auto;
}

.bs-nav--rail .bs-nav__list > li:last-child {
	margin-inline-end: auto;
}

/*
 * The links, in the detail voice. Muted at rest and ink when live or hovered;
 * the mark under them is drawn separately below.
 */
.bs-nav__link {
	text-decoration: none;
	color: var(--color-ink-muted);
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.72rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: var(--detail-tracking, 0.12em);
	text-transform: uppercase;
	transition: color 150ms ease-out;
}

.bs-nav__link:hover,
.bs-nav__link[aria-current] {
	color: var(--color-ink);
}

.bs-nav--rail .bs-nav__link {
	position: relative;
	display: block;
	white-space: nowrap;
	padding: calc(var(--space-unit) * 3) calc(var(--space-unit) * 3);
}

/*
 * The active mark: a line under the live link, in the highlight colour.
 *
 * `aria-current` in either value — `page` from the renderer for a plain page
 * link, `location` from the scroll-spy for the section in view — because the
 * reader does not care which kind of link told them where they are. Drawn as
 * a pseudo-element scaled from zero rather than as a background that
 * changes size, so it animates on transform alone and can never move the
 * row it sits under. Hover previews the same line in the ink.
 */
.bs-nav--rail .bs-nav__link::after {
	content: "";
	position: absolute;
	inset-inline: calc(var(--space-unit) * 3);
	inset-block-end: 0;
	block-size: 1.5px;
	background: currentColor;
	transform: scaleX(0);
	transform-origin: left center;
	transition: transform 250ms cubic-bezier(0.16, 1, 0.3, 1), background-color
		150ms ease-out;
}

.bs-nav--rail .bs-nav__link:hover::after {
	transform: scaleX(1);
}

.bs-nav--rail .bs-nav__link[aria-current]::after {
	transform: scaleX(1);
	background: var(--color-highlight, var(--color-brand));
}

/*
 * The framed chrome marks the live link with a dot, not a line — the one
 * touch of neon the Studio look spends on the header. Bold, too: the rail is
 * the loudest text on that look's page, and the detail weight token is set
 * for labels beside prices, which this is not. A structural choice no token
 * can express, so it is the chrome attribute's to make.
 */
html[data-chrome="framed"] .bs-nav--rail .bs-nav__link {
	font-weight: 700;
}

html[data-chrome="framed"] .bs-nav--rail .bs-nav__link::after {
	inset-inline: 50% auto;
	inset-block-end: calc(var(--space-unit) * 1.25);
	inline-size: 6px;
	block-size: 6px;
	border-radius: 50%;
	transform: translateX(-50%) scale(0);
	transform-origin: center;
}

html[data-chrome="framed"] .bs-nav--rail .bs-nav__link:hover::after,
html[data-chrome="framed"] .bs-nav--rail .bs-nav__link[aria-current]::after {
	transform: translateX(-50%) scale(1);
}

/* ---------- collapsed menu ---------- */

/*
 * The navigation tucked behind one button, on the left of the brand — the
 * desktop design's header, and the provider's choice for a menu with too
 * many links to wear in a row. A native <details>, so it needs no script to
 * open; the panel hangs off it absolutely, and the sticky header's own
 * z-index carries it above the page.
 */
.bs-header__menu {
	position: relative;
}

/*
 * Bare glyph, no box: the bars are affordance enough, and a border here would
 * put a drawn control opposite the Book button — two boxes competing across
 * the bar. The radius stays for the hover and open fills to take shape in.
 * Pulled out to the container's edge by its own padding, so the bars sit on
 * the page's left margin rather than a hit-target's width in from it.
 */
.bs-header__menu-toggle {
	display: flex;
	align-items: center;
	justify-content: center;
	inline-size: calc(var(--space-unit) * 10);
	block-size: calc(var(--space-unit) * 10);
	margin-inline-start: calc(var(--space-unit) * -2.5);
	border-radius: var(--radius-md);
	color: var(--color-ink);
	cursor: pointer;
	list-style: none;
	transition: background-color 150ms ease-out;
}

.bs-header__menu-toggle::-webkit-details-marker {
	display: none;
}

/*
 * The bars are the state. Open, the outer pair folds into a close mark and
 * the middle bar steps aside; closed, the transition simply runs backwards —
 * which is why these are transitions and not keyframes, and why the panel's
 * own entrance below is the opposite. `fill-box` makes each bar rotate about
 * its own centre instead of the SVG's corner.
 */
.bs-header__menu-toggle path {
	transform-box: fill-box;
	transform-origin: center;
	transition: transform 250ms cubic-bezier(0.16, 1, 0.3, 1), opacity 120ms
		ease-out;
}

.bs-header__menu[open] .bs-header__menu-toggle path:nth-child(1) {
	transform: translateY(5px) rotate(45deg);
}

.bs-header__menu[open] .bs-header__menu-toggle path:nth-child(2) {
	opacity: 0;
}

.bs-header__menu[open] .bs-header__menu-toggle path:nth-child(3) {
	transform: translateY(-5px) rotate(-45deg);
}

.bs-header__menu-toggle:hover,
.bs-header__menu[open] .bs-header__menu-toggle {
	background-color: color-mix(in srgb, var(--color-ink) 6%, transparent);
}

/*
 * Left-aligned under the toggle it hangs off, sized to its own content with a
 * floor wide enough that short labels do not produce a sliver. The viewport
 * clamp keeps a long label from pushing the card past the right edge on a
 * phone. Framed in the chrome's own frame, so under Studio it is a 2px box.
 */
.bs-header__panel {
	position: absolute;
	inset-inline-start: 0;
	inset-block-start: calc(100% + var(--space-unit) * 2);
	min-inline-size: calc(var(--space-unit) * 55);
	max-inline-size: calc(100vw - var(--space-unit) * 8);
	padding: calc(var(--space-unit) * 2);
	background: var(--color-surface);
	border: var(--frame-width, 1px) solid var(--frame-color, var(--color-border));
	border-radius: var(--radius-md);
	box-shadow: 0 12px 32px color-mix(in srgb, var(--color-ink) 12%, transparent);
	transform-origin: top left;
}

/*
 * Arrival, not appearance: the panel settles down and out from the toggle it
 * hangs off, on the same deceleration curve as the rail's underline.
 *
 * The animations are gated on `[open]`, and that gate is load-bearing, not a
 * nicety: a closed <details> keeps its content in the render tree (hidden via
 * the element's own content slot), so an ungated animation runs once while
 * nobody can see it and never again. Matching only while open means the
 * animation-name toggles with the attribute, and toggling is what restarts a
 * CSS animation — every open replays the entrance, and closing stays instant,
 * which is the right order: exits faster than entrances.
 */
.bs-header__menu[open] .bs-header__panel {
	animation: bs-menu-panel 260ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

@keyframes bs-menu-panel {
	from {
		opacity: 0;
		transform: translateY(calc(var(--space-unit) * -1.5)) scale(0.98);
	}
}

/*
 * The links arrive as the list they are — a short cascade behind the panel,
 * capped at five steps so a long menu never turns the tail into a wait.
 * `backwards` holds each row invisible through its own delay. Gated on
 * `[open]` for the same reason the panel is.
 */
.bs-header__menu[open] .bs-header__panel .bs-nav__list > li {
	animation: bs-menu-link 220ms cubic-bezier(0.16, 1, 0.3, 1) backwards;
	animation-delay: calc(40ms + var(--bs-menu-i, 0) * 28ms);
}

.bs-header__panel .bs-nav__list > li:nth-child(1) {
	--bs-menu-i: 0;
}
.bs-header__panel .bs-nav__list > li:nth-child(2) {
	--bs-menu-i: 1;
}
.bs-header__panel .bs-nav__list > li:nth-child(3) {
	--bs-menu-i: 2;
}
.bs-header__panel .bs-nav__list > li:nth-child(4) {
	--bs-menu-i: 3;
}
.bs-header__panel .bs-nav__list > li:nth-child(n + 5) {
	--bs-menu-i: 4;
}

@keyframes bs-menu-link {
	from {
		opacity: 0;
		transform: translateY(calc(var(--space-unit) * -1));
	}
}

.bs-header__panel .bs-nav__list {
	flex-direction: column;
}

/*
 * Row targets, not the rail's underline: a vertical list wants a full row a
 * thumb can hit, and a grow-in line reads wrong stacked. The live mark moves
 * to the inline edge for the same reason, in the same highlight the rail
 * uses, and answers both `aria-current` values as the rail does.
 */
.bs-header__panel .bs-nav__link {
	display: block;
	padding: calc(var(--space-unit) * 2.5) calc(var(--space-unit) * 3);
	border-radius: var(--radius-sm);
}

.bs-header__panel .bs-nav__link:hover {
	background-color: color-mix(in srgb, var(--color-ink) 6%, transparent);
}

.bs-header__panel .bs-nav__link[aria-current] {
	box-shadow: inset 2px 0 0 var(--color-highlight, var(--color-brand));
}

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

/*
 * The footer: the provider's small links, a hairline, then the byline.
 *
 * Centred and quiet — it closes the page rather than adding to it. The links
 * are in the detail voice like the rail's, so the top and bottom of every
 * page speak the same small voice.
 */
.bs-footer {
	border-block-start: 1px solid var(--color-border);
	margin-block-start: calc(var(--space-unit) * 12);
	padding-block: calc(var(--space-unit) * 6.5) calc(var(--space-unit) * 10);
	color: var(--color-ink-subtle);
	font-size: var(--text-sm);
	text-align: center;
}

.bs-footer__inner {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: calc(var(--space-unit) * 5.5);
}

/*
 * One rule, not two. The footer's own top border is the separator that closes
 * the page, with or without links; a second hairline between the links and the
 * byline read as a double rule — and, drawn under a centred list, as one long
 * underline beneath every link at once. The column's gap is all that holds the
 * row and the byline apart.
 */
.bs-footer__links {
	display: flex;
	flex-wrap: wrap;
	justify-content: center;
	gap: calc(var(--space-unit) * 2) calc(var(--space-unit) * 4.5);
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.72rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: var(--detail-tracking, 0.12em);
	text-transform: uppercase;
}

.bs-footer__link {
	color: inherit;
	text-decoration: none;
	transition: color 150ms ease-out;
}

.bs-footer__link:hover,
.bs-footer__link:focus-visible {
	color: var(--color-ink);
}

/*
 * "Powered by Booka" — the one fixed thing in the footer.
 *
 * Its own appearance is inline on the element (see `poweredByBooka` in
 * `branding.ts`), so it cannot come out underlined or broken across two lines
 * when a page renders against a cached copy of this file older than its markup.
 * What is left here is the one thing that is genuinely this file's business:
 * how loud it is against the palette the provider chose.
 */
.bs-footer__powered {
	font-size: var(--text-sm);
	opacity: 0.75;
	transition: opacity 150ms ease;
}

.bs-footer__powered:hover,
.bs-footer__powered:focus-visible {
	opacity: 1;
}

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

@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 {
	.bs-header,
	.bs-footer {
		display: none;
	}
}

/*
 * Variant stylesheet — sections.
 *
 * Every section variant, the compositions they share, and their responsive
 * rules. See `00-base.css` for the rules of the file and how the partials are
 * bundled.
 *
 * Three looks, one file. Nothing here names a look: Atelier, Studio and Still
 * differ only in what the tokens resolve to — `--heading-*` for how a title
 * is set, `--frame-*` for whether a chip or a card wears a hairline or a
 * 2px ink frame, `--btn-*` for what a filled control looks like. The two
 * structural choices a token cannot express (a circular portrait, a centred
 * section header) hang off `html[data-chrome]`.
 */

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

/*
 * The composition every section opens on: the title on the left, one small
 * fact on the right, a rule under both. The rule is drawn in the ink rather
 * than the hairline because it is the section's edge, not a divider inside
 * it — and it stays 1px under every look, since the 2px frame is for things
 * that are boxes.
 *
 * Under the airy chrome the header is centred and loses its rule: a tracked
 * capital title with a line under it reads as a form, and Still is anything
 * but.
 */
.bs-section__header {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: calc(var(--space-unit) * 4);
	padding-block-end: calc(var(--space-unit) * 2.5);
	border-block-end: 1px solid var(--color-ink);
	margin-block-end: calc(var(--space-gap) * 1.15);
}

.bs-section__header .bs-section__heading {
	min-inline-size: 0;
	overflow-wrap: anywhere;
}

.bs-section__count {
	flex: none;
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.75rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: var(--detail-tracking, 0.08em);
	text-transform: uppercase;
	color: var(--color-ink-subtle);
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

/* The intro reads under the rule, not inside the header row. */
.bs-section__header + .bs-section__intro {
	margin-block: calc(var(--space-gap) * -0.5) calc(var(--space-gap) * 1.15);
}

html[data-chrome="airy"] .bs-section__header {
	flex-direction: column;
	align-items: center;
	text-align: center;
	gap: calc(var(--space-unit) * 2);
	padding-block-end: 0;
	border-block-end: 0;
	margin-block-end: calc(var(--space-gap) * 1.5);
}

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

.bs-hero {
	padding-block: calc(var(--space-section) * 1.05);
}

/*
 * Double class to outrank `.bs-section:first-child`: the general rule that
 * tucks a page's first section under the masthead is right for a services
 * list, but the hero is the page's opening statement and keeps its air.
 */
.bs-hero.bs-section:first-child {
	padding-block-start: calc(var(--space-section) * 0.9);
}

/* A photo-led opening meets the masthead without an extra band of space. */
.bs-hero--photo.bs-section {
	padding-block-start: 0;
}

.bs-hero--photo.bs-section:first-child {
	padding-block-start: 0;
}

.bs-hero__heading {
	font-size: var(--text-display);
	line-height: 1.04;
	max-width: 16ch;
	overflow-wrap: anywhere;
}

.bs-hero__body {
	font-size: var(--text-lg);
	line-height: 1.55;
	color: var(--color-ink-muted);
	max-width: 46ch;
	text-wrap: pretty;
	overflow-wrap: anywhere;
}

.bs-hero__foot {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 4);
	margin-block-start: calc(var(--space-unit) * 6);
	min-inline-size: 0;
}

.bs-hero__actions {
	display: flex;
	gap: calc(var(--space-unit) * 3);
	flex-wrap: wrap;
	align-items: center;
	max-inline-size: 100%;
}

.bs-hero__actions .bs-btn {
	max-inline-size: 100%;
	text-align: center;
	overflow-wrap: anywhere;
}

/*
 * One column until there is actually a photo to share the row with.
 *
 * The two-column split used to apply from the grid alone, so a hero with no
 * image reserved an empty half and crushed its own headline into the other —
 * the emptiest possible use of the widest moment on the page. The modifier is
 * stamped by the renderer only when media rendered.
 */
.bs-hero__grid {
	width: min(100% - calc(var(--space-unit) * 8), 80rem);
	display: grid;
	gap: calc(var(--space-gap) * 2);
	align-items: center;
}

/* Grid children must be allowed to shrink around long provider-written copy. */
.bs-hero__copy,
.bs-hero__media {
	min-inline-size: 0;
}

.bs-hero__image {
	/* Constrain the photograph, not its shape: both dimensions stay intrinsic. */
	width: auto;
	height: auto;
	max-width: 100%;
	max-height: 75svh;
	margin-inline: auto;
	border-radius: var(--radius-lg);
}

/*
 * On a phone the photograph opens the page.
 *
 * Above the words, with room to reach both edges and touching the masthead: the
 * picture is the subject and the copy is its caption, centred underneath.
 * The markup keeps the words first — a screen reader hears the headline
 * before the picture either way — and `order` moves the picture up. The
 * negative margin is the container's own gutter, which at these widths is
 * always four units a side.
 */
@media (max-width: 55.99rem) {
	.bs-hero--photo .bs-hero__grid {
		gap: calc(var(--space-gap) * 1.5);
	}

	.bs-hero--photo .bs-hero__media {
		order: -1;
		margin-inline: calc(var(--space-unit) * -4);
	}

	.bs-hero--photo .bs-hero__image {
		border-radius: 0;
	}

	.bs-hero--photo.bs-section:first-child {
		padding-block-start: 0;
	}

	.bs-hero--photo .bs-hero__copy {
		display: flex;
		flex-direction: column;
		align-items: center;
		text-align: center;
	}

	.bs-hero--photo .bs-hero__heading {
		max-width: 20ch;
	}

	.bs-hero--photo .bs-hero__facts,
	.bs-hero--photo .bs-hero__actions {
		justify-content: center;
	}
}

.bs-hero__copy--center {
	text-align: center;
	display: flex;
	flex-direction: column;
	align-items: center;
}

.bs-hero__copy--center .bs-hero__heading {
	max-width: 20ch;
}

.bs-hero__copy--center .bs-hero__body,
.bs-hero__copy--center .bs-section__intro {
	margin-inline: auto;
}

.bs-hero__copy--center .bs-hero__facts,
.bs-hero__copy--center .bs-hero__actions {
	justify-content: center;
}

.bs-hero--centered {
	padding-block: calc(var(--space-section) * 1.35);
}

/*
 * A centered hero with no button is a page title — "Services" over a list —
 * not a landing statement, and holding a title in a landing hero's air pushes
 * the content it introduces below the fold. Engines without `:has` keep the
 * larger padding, which is only ever extra quiet.
 */
.bs-hero--centered:not(:has(.bs-hero__actions)) {
	padding-block: calc(var(--space-section) * 0.7);
}

/*
 * The hero arrives once, as one gesture.
 *
 * Four elements, one motion, small offsets and short delays — an entrance, not
 * a slideshow. `backwards` holds the delayed pieces invisible until their turn.
 * The builder's preview suppresses this so editing does not replay it.
 */
@media (prefers-reduced-motion: no-preference) {
	.bs-hero .bs-eyebrow,
	.bs-hero__heading,
	.bs-hero__body,
	.bs-hero__facts,
	.bs-hero__actions,
	.bs-hero__media {
		animation: bs-rise 700ms cubic-bezier(0.16, 1, 0.3, 1) backwards;
	}

	.bs-hero__heading {
		animation-delay: 70ms;
	}

	.bs-hero__body {
		animation-delay: 150ms;
	}

	.bs-hero__facts {
		animation-delay: 195ms;
	}

	.bs-hero__actions {
		animation-delay: 230ms;
	}

	.bs-hero__media {
		animation-delay: 160ms;
	}
}

@keyframes bs-rise {
	from {
		opacity: 0;
		transform: translateY(14px);
	}
}

/* ---------- hero: the facts line ---------- */

/*
 * Booka's three facts, on the banner's own last line.
 *
 * Inside the hero rather than under it: the designs read headline, sentence,
 * facts, Book, and a line that belongs to the opening should not be a section
 * a provider can drag halfway down the page. It sits in the footer block, so
 * it inherits the banner's alignment — left on a desk, centred under a
 * phone's opening photograph — with no rules of its own for either.
 */
.bs-hero__facts {
	display: flex;
	flex-wrap: wrap;
	align-items: center;
	gap: calc(var(--space-unit) * 2) calc(var(--space-unit) * 3);
	font-size: var(--text-sm);
	/* The colour of the headline above, not a caption grey: these are the three
	   numbers a stranger came for, and all three looks set them in full ink. */
	color: var(--color-ink);
	font-variant-numeric: tabular-nums;
}

/*
 * The separator is a small disc, not a middot.
 *
 * A "·" is a glyph: it carries its own side bearings, sits high against a row
 * aligned on centres, and grows with the type around it. The designs draw a
 * fixed 3px dot evenly spaced from both neighbours, which reads as a divider
 * rather than as punctuation.
 *
 * Drawn on the fact that follows rather than the one before, so a fact that
 * renders nothing — the next open time, until the runtime fills it — takes its
 * own dot away with it.
 */
.bs-hero__fact + .bs-hero__fact::before {
	content: "";
	display: inline-block;
	inline-size: 3px;
	block-size: 3px;
	border-radius: 50%;
	margin-inline-end: calc(var(--space-unit) * 3);
	vertical-align: middle;
	background: color-mix(in srgb, var(--color-ink) 25%, transparent);
}

/*
 * Empty until the island answers — and gone, not merely blank, so the dot in
 * front of it goes too. The loading state keeps its box for the runtime's
 * inline shimmer.
 */
.bs-hero__fact--next:empty:not([data-state="loading"]) {
	display: none;
}

/* ---------- statement ---------- */

/*
 * The provider's own words, at the size a page gives the thing it is actually
 * about. Set in the heading face because that is what makes it read as a
 * statement rather than as a long caption — and held to a short measure so it
 * stays a statement rather than becoming an essay.
 */
.bs-statement__p {
	font-family: var(--font-heading);
	font-weight: var(--heading-weight, 600);
	font-size: var(--text-xl);
	line-height: 1.32;
	letter-spacing: var(--heading-tracking, -0.015em);
	max-width: 26ch;
	text-wrap: pretty;
}

.bs-statement__p + .bs-statement__p {
	margin-block-start: 1em;
}

.bs-statement__note {
	margin-block-start: calc(var(--space-unit) * 6);
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.75rem);
	letter-spacing: var(--detail-tracking, 0.1em);
	text-transform: uppercase;
	color: var(--color-ink-subtle);
}

.bs-statement__action {
	margin-block-start: calc(var(--space-unit) * 6);
}

/*
 * The sign-off: centred, and the largest thing on the page after the banner.
 * The measure widens because a closing line is read in one go rather than as
 * a paragraph, and a 26ch measure would break six words across three lines.
 */
.bs-statement--close {
	text-align: center;
	padding-block: calc(var(--space-section) * 1.25);
}

.bs-statement--close .bs-container {
	display: flex;
	flex-direction: column;
	align-items: center;
}

.bs-statement--close .bs-statement__p {
	font-size: var(--text-3xl);
	line-height: 1.08;
	max-width: 18ch;
	text-wrap: balance;
}

/*
 * The banner: the one place a section takes the page's colours away.
 *
 * Inverted rather than brand-filled — a full-bleed field of a provider's
 * chosen colour is a gamble on that colour, while the ink surface is a
 * guaranteed-good band in every palette. The brand still appears, on the
 * button, which is where it does the most work.
 */
.bs-statement--banner {
	background: var(--color-ink);
	color: var(--color-surface);
	padding-block: calc(var(--space-section) * 1.1);
}

.bs-statement--banner .bs-eyebrow {
	color: color-mix(in srgb, var(--color-surface) 62%, transparent);
}

.bs-statement--banner .bs-statement__note {
	color: color-mix(in srgb, var(--color-surface) 72%, transparent);
}

.bs-statement__banner {
	display: grid;
	gap: var(--space-gap);
	align-items: end;
}

.bs-statement__heading {
	font-family: var(--font-heading);
	font-size: var(--text-3xl);
	line-height: 1.06;
	max-width: 16ch;
	text-wrap: balance;
	margin-block-end: calc(var(--space-unit) * 5);
}

.bs-statement--banner .bs-statement__p {
	font-family: var(--font-body);
	font-weight: 400;
	font-size: var(--text-base);
	line-height: 1.6;
	letter-spacing: 0;
	max-width: 46ch;
	color: color-mix(in srgb, var(--color-surface) 82%, transparent);
}

/*
 * The banner's own button, drawn from the inverted surface rather than the
 * page's. Without this the ghost button's border is the page's hairline
 * against ink, which is invisible, and the primary's contrast is computed for
 * paper it is no longer sitting on.
 */
.bs-statement--banner .bs-btn--primary {
	background: var(--color-surface);
	color: var(--color-ink);
	border-color: var(--color-surface);
}

.bs-statement--banner .bs-btn--primary:hover {
	background: color-mix(in srgb, var(--color-surface) 88%, transparent);
}

/* ---------- moving band ---------- */

.bs-marquee {
	border-block: 1px solid var(--color-border);
	overflow: hidden;
	padding-block: calc(var(--space-unit) * 3.5);
	/* Its own rhythm: a band between two sections, never a section itself. */
	margin-block: 0;
}

/*
 * The loop translates the track by half its width, which is seamless only
 * while half the track is at least as wide as the band. The minimum makes
 * that true for any list on any screen: each run stretches to the band's
 * full width. Short authored lists repeat visually inside the run, giving
 * `space-around` enough beats to maintain a compact, even rhythm.
 */
.bs-marquee__track {
	display: flex;
	inline-size: max-content;
	min-inline-size: 200%;
	animation: bs-marquee 42s linear infinite;
}

.bs-marquee__run {
	display: flex;
	flex: 1 0 auto;
	min-inline-size: 50%;
	/*
	 * `space-around`, not `space-between`: the half-gaps it leaves at each
	 * run's edges meet across the seam to make one ordinary gap, so the join
	 * between the two runs is indistinguishable from any other step.
	 */
	justify-content: space-around;
}

.bs-marquee__item,
.bs-marquee__mark {
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.75rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: var(--detail-tracking, 0.14em);
	text-transform: uppercase;
	white-space: nowrap;
	color: var(--color-ink-subtle);
	padding-inline-end: 1em;
}

.bs-marquee__mark {
	color: var(--color-brand);
}

@keyframes bs-marquee {
	to {
		transform: translateX(-50%);
	}
}

/*
 * The still variant — and, below, what the scrolling one becomes under reduced
 * motion: a centred row of facts rather than a stalled animation.
 */
.bs-marquee--still .bs-marquee__track {
	animation: none;
	inline-size: 100%;
	min-inline-size: 0;
	justify-content: center;
	flex-wrap: wrap;
	gap: 0 calc(var(--space-unit) * 2);
}

@media (prefers-reduced-motion: reduce) {
	.bs-marquee__track {
		animation: none;
		inline-size: 100%;
		min-inline-size: 0;
		justify-content: center;
		flex-wrap: wrap;
	}

	.bs-marquee__repeat {
		display: none;
	}

	/* Keep only the authored cycle when motion is removed. */
	.bs-marquee__duplicate {
		display: none;
	}

	/*
	 * Dissolve the run wrapper so the items are the track's own children
	 * again and can wrap and centre as the still band does.
	 */
	.bs-marquee__run {
		display: contents;
	}
}

/* ---------- services ---------- */

.bs-services__grid {
	display: grid;
	gap: var(--space-gap);
}

.bs-service__name {
	font-size: var(--text-xl);
	min-inline-size: 0;
	overflow-wrap: anywhere;
}

.bs-service__desc {
	margin-block-start: calc(var(--space-unit) * 2);
	color: var(--color-ink-muted);
	font-size: var(--text-sm);
	max-width: 52ch;
	text-wrap: pretty;
}

/*
 * Durations and prices in the detail voice. Tracking is fixed rather than the
 * voice token: 0.22em between the digits of a price reads as a typo where the
 * same tracking on a label reads as a poster.
 */
.bs-service__meta {
	margin-block-start: calc(var(--space-unit) * 3);
	display: flex;
	gap: calc(var(--space-unit) * 4);
	align-items: baseline;
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size-data, 0.85rem);
	letter-spacing: 0.04em;
	text-transform: uppercase;
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

.bs-service__duration {
	color: var(--color-ink-subtle);
}

.bs-service__price {
	color: var(--color-ink);
	font-weight: 600;
}

/*
 * The price list is a quiet catalog, not a menu or a stack of cards.
 *
 * A single hairline appears only between siblings. The outer edges stay open,
 * so type and whitespace establish the section instead of a frame around it.
 */
.bs-services__list {
	display: grid;
	/*
	 * A track that can shrink. The default auto track holds the widest row's
	 * min-content — one long service name against a no-wrap price pushed the
	 * whole list past a phone's edge. Capped at the container, the name wraps
	 * and the price stays on the rail.
	 */
	grid-template-columns: minmax(0, 1fr);
	gap: 0;
}

.bs-service-row {
	padding-block: calc(var(--space-unit) * 6);
}

.bs-service-row + .bs-service-row:not(.bs-service-row--menu) {
	border-block-start: 1px solid var(--color-border);
}

.bs-service-row__head {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	align-items: baseline;
	column-gap: calc(var(--space-unit) * 6);
}

/*
 * Duration and price behave as one compact fact group on the right rail.
 * The phone breakpoint below moves the group intact under the name instead of
 * squeezing either side into an unreadable sliver.
 */
.bs-service-row .bs-service__meta {
	margin-block-start: 0;
	min-inline-size: 0;
	justify-self: end;
	justify-content: flex-end;
}

@media (max-width: 30rem) {
	.bs-service-row:not(.bs-service-row--menu) .bs-service-row__head {
		grid-template-columns: minmax(0, 1fr);
		row-gap: calc(var(--space-unit) * 2);
	}

	.bs-service-row:not(.bs-service-row--menu) .bs-service__meta {
		justify-self: start;
		justify-content: flex-start;
	}
}

/* ---------- services: menu ---------- */

/*
 * The filter chips are JavaScript, so they arrive with it.
 *
 * Without the runtime the menu is simply the whole menu, which is the honest
 * page; a row of buttons that did nothing would be a promise the page could
 * not keep. The `data-bs-runtime` flag is set by the island script the moment
 * it boots.
 */
.bs-services__filters {
	display: none;
	flex-direction: column;
	gap: calc(var(--space-unit) * 2.5);
	margin-block-end: calc(var(--space-unit) * 5);
}

html[data-bs-runtime="ready"] .bs-services__filters {
	display: flex;
}

/*
 * A row of chips scrolls sideways on a phone rather than wrapping into a
 * wall: the viewport-width breakout lets it run to either screen edge. Start
 * padding puts the first chip on the page rail; the generated end spacer lets
 * the last chip return to that rail without clipping overflow early. From
 * tablet widths they wrap.
 */
.bs-chips {
	display: flex;
	gap: calc(var(--space-unit) * 1.5);
	inline-size: 100vw;
	overflow-x: auto;
	margin-inline: calc(50% - 50vw);
	padding-inline-start: calc(var(--space-unit) * 4);
	padding-inline-end: 0;
	padding-block: 2px;
	scroll-padding-inline: calc(var(--space-unit) * 4);
	overscroll-behavior-inline: contain;
	-webkit-overflow-scrolling: touch;
	scrollbar-width: none;
}

.bs-chips::after {
	content: "";
	flex: 0 0 calc(var(--space-unit) * 2.5);
}

.bs-chips::-webkit-scrollbar {
	display: none;
}

/*
 * A chip is a small outlined control that fills when pressed.
 *
 * The frame tokens draw its edge — a hairline under Atelier and Still, a 2px
 * ink line under Studio — and the button tokens draw its pressed state, so a
 * pressed chip is the same colour as a Book button on the same page.
 */
.bs-chip {
	flex: 0 0 auto;
	display: inline-flex;
	align-items: center;
	justify-content: center;
	gap: calc(var(--space-unit) * 1.5);
	min-block-size: 2.5rem;
	padding: calc(var(--space-unit) * 1.75) calc(var(--space-unit) * 3.5);
	border: var(--frame-width, 1px) solid var(--frame-color, var(--color-border));
	border-radius: var(--radius-sm);
	background: transparent;
	color: var(--color-ink);
	font: inherit;
	font-size: var(--text-sm);
	line-height: 1.2;
	white-space: nowrap;
	cursor: pointer;
	transition: background-color 150ms ease-out, border-color 150ms ease-out,
		color 150ms ease-out;
}

.bs-chip:hover {
	border-color: var(--color-ink);
}

.bs-chip:disabled {
	border-color: var(--color-border);
	color: var(--color-ink-subtle);
	opacity: 0.48;
	cursor: not-allowed;
}

.bs-chip:focus-visible {
	outline: none;
	text-decoration: underline;
	text-decoration-thickness: 0.1em;
	text-underline-offset: 0.18em;
}

.bs-chip[aria-pressed="true"] {
	background: var(--btn-bg, var(--color-brand));
	color: var(--btn-ink, var(--color-brand-contrast));
	border-color: var(--btn-border, var(--color-brand));
}

/*
 * Under the airy chrome the filled button is an outline, so a pressed chip
 * would look like an unpressed one with a darker edge. A tonal fill makes the
 * state legible without borrowing a colour the look does not use.
 */
html[data-chrome="airy"] .bs-chip[aria-pressed="true"] {
	background: var(--color-surface-alt);
}

.bs-chip__fee {
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.75rem);
	letter-spacing: 0.04em;
	color: var(--color-ink-subtle);
	font-variant-numeric: tabular-nums;
}

.bs-chip[aria-pressed="true"] .bs-chip__fee {
	color: inherit;
	opacity: 0.8;
}

.bs-services__menu {
	display: grid;
	grid-template-columns: minmax(0, 1fr);
	gap: 0;
	border-block-start: 1px solid var(--color-border);
}

/* The title already supplies the rule when the menu follows it directly. */
html:not([data-chrome="airy"]) .bs-section__header:has(+ .bs-services__menu) {
	margin-block-end: 0;
}

html:not([data-chrome="airy"]) .bs-section__header + .bs-services__menu {
	border-block-start: 0;
}

/*
 * One row: the words on the left, the facts and the button on the right.
 *
 * Two columns at every width — the design keeps the price rail even on a
 * phone, where the button under it is the thing a thumb is looking for. The
 * left column shrinks around a long name; the right is as wide as its
 * widest fact.
 */
.bs-service-row--menu {
	display: grid;
	grid-template-columns: minmax(0, 1fr) auto;
	column-gap: calc(var(--space-unit) * 5);
	align-items: start;
	padding-block: calc(var(--space-unit) * 5);
	border-block-end: 1px solid var(--color-border);
}

/* The runtime hides a filtered-out row with `hidden`; the grid must yield. */
.bs-service-row--menu[hidden] {
	display: none;
}

.bs-service-row__main {
	min-inline-size: 0;
}

.bs-service-row--menu .bs-service__name {
	font-size: var(--text-lg);
}

.bs-service-row__aside {
	display: flex;
	flex-direction: column;
	align-items: flex-end;
	gap: calc(var(--space-unit) * 2.5);
}

.bs-service-row--menu .bs-service__meta {
	gap: calc(var(--space-unit) * 1.5);
	font-size: var(--detail-size, 0.75rem);
	letter-spacing: var(--detail-tracking, 0.08em);
	color: var(--color-ink-subtle);
}

.bs-service-row--menu .bs-service__price {
	color: var(--color-ink-subtle);
	font-weight: var(--detail-weight, 500);
}

.bs-service__sep {
	color: var(--color-ink-subtle);
}

.bs-service__book {
	min-block-size: 2.25rem;
	padding: calc(var(--space-unit) * 1.5) calc(var(--space-unit) * 5);
}

/*
 * Two lines of description, and a word to ask for the rest.
 *
 * The clamp applies only once the runtime is up, because only the runtime can
 * offer the rest: a clamped paragraph with no way to open it is a paragraph
 * cut off. The "more" control ships `hidden` and is revealed by the script
 * for exactly the descriptions that overflow, so a two-line description never
 * grows a button that would show nothing.
 */
html[data-bs-runtime="ready"]
	.bs-service__desc[data-service-desc]:not([data-expanded]) {
	display: -webkit-box;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 2;
	overflow: hidden;
}

.bs-service__more {
	display: none;
	margin-block-start: calc(var(--space-unit) * 1);
	padding: 0;
	border: 0;
	background: none;
	font: inherit;
	font-size: var(--text-sm);
	color: var(--color-brand);
	text-decoration: underline;
	text-underline-offset: 0.2em;
	cursor: pointer;
}

html[data-bs-runtime="ready"] .bs-service__more:not([hidden]) {
	display: inline;
}

.bs-services__none {
	padding-block: calc(var(--space-unit) * 6);
	color: var(--color-ink-muted);
	font-size: var(--text-sm);
}

/* ---------- testimonials ---------- */

.bs-testimonials__grid {
	display: grid;
	gap: var(--space-gap);
	margin-block-start: calc(var(--space-gap) * 1.5);
}

.bs-quote__text {
	font-family: var(--font-heading);
	font-weight: var(--heading-weight, 600);
	font-size: var(--text-lg);
	line-height: 1.4;
	text-wrap: pretty;
}

.bs-quote__text::before {
	content: "\201C";
}

.bs-quote__text::after {
	content: "\201D";
}

/*
 * Grid quotes ride on the alternate paper rather than in outlined cards — a
 * borderless tonal block reads as a voice, an outlined box reads as a form.
 */
.bs-testimonials__grid .bs-quote {
	min-inline-size: 0;
	border-color: transparent;
	background: var(--color-surface-alt);
	padding: calc(var(--space-gap) * 1.25);
}

.bs-quote__cite {
	margin-block-start: calc(var(--space-unit) * 5);
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 1);
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.75rem);
	letter-spacing: 0.06em;
	text-transform: uppercase;
}

.bs-quote__author {
	font-style: normal;
	font-weight: var(--detail-weight, 600);
}

.bs-quote__detail {
	color: var(--color-ink-subtle);
}

/*
 * The featured quote is the site's one grand editorial gesture: an oversized
 * brand-colored mark, the words at display scale in the heading face, the
 * attribution small and far below. The band beneath it is the neutral alternate
 * paper — the brand-soft field it replaces turned muddy the moment a dark theme
 * derived it, and the words never needed the help.
 */
.bs-testimonials--feature {
	background: var(--color-surface-alt);
	padding-block: calc(var(--space-section) * 1.15);
}

.bs-quote--large {
	text-align: center;
	display: flex;
	flex-direction: column;
	align-items: center;
}

.bs-quote--large::before {
	content: "\201C";
	font-family: var(--font-heading);
	font-size: calc(var(--text-display) * 1.35);
	line-height: 0.62;
	block-size: 0.5em;
	color: var(--color-brand);
	display: block;
}

.bs-quote--large .bs-quote__text {
	font-size: var(--text-3xl);
	line-height: 1.22;
	text-align: center;
	text-wrap: balance;
	max-width: 24ch;
}

.bs-quote--large .bs-quote__text::before,
.bs-quote--large .bs-quote__text::after {
	content: none;
}

.bs-quote--large .bs-quote__cite {
	margin-block-start: calc(var(--space-unit) * 7);
	align-items: center;
}

/*
 * Review photos stay part of the voice they belong to. A compact horizontal
 * reel shows every attachment without turning a six-review section into a
 * gallery wall, and remains naturally swipeable on the phones most clients use.
 */
.bs-review-media-wrap {
	min-inline-size: 0;
}

.bs-review-media {
	display: grid;
	grid-auto-flow: column;
	grid-auto-columns: clamp(5.5rem, 24vw, 7rem);
	gap: calc(var(--space-unit) * 1.5);
	inline-size: 100%;
	margin-block-start: calc(var(--space-unit) * 3);
	padding: 0 0 calc(var(--space-unit) * 1);
	list-style: none;
	overflow-x: auto;
	overscroll-behavior-inline: contain;
	scroll-snap-type: inline proximity;
	scrollbar-color: var(--color-brand-muted) transparent;
	scrollbar-width: thin;
}

.bs-review-media--single {
	grid-auto-columns: minmax(0, min(100%, 12rem));
}

.bs-review-media__item {
	min-inline-size: 0;
	aspect-ratio: 1;
	overflow: hidden;
	border-radius: var(--radius-sm);
	background: var(--color-surface-sunken);
	scroll-snap-align: start;
}

/* Studio frames every photograph, review ones included. */
html[data-chrome="framed"] .bs-review-media__item {
	border: var(--frame-width, 2px) solid var(--frame-color, var(--color-ink));
}

.bs-review-media__image {
	display: block;
	inline-size: 100%;
	block-size: 100%;
	object-fit: cover;
}

.bs-review-media__more {
	display: none;
	min-block-size: 2.75rem;
	margin-block-start: calc(var(--space-unit) * 1.5);
	padding: 0;
	border: 0;
	background: transparent;
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.75rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: var(--detail-tracking, 0.08em);
	text-transform: uppercase;
	color: var(--color-ink-muted);
	text-decoration: underline;
	text-underline-offset: 0.24em;
	cursor: pointer;
}

html[data-bs-runtime="ready"]
	[data-collapsible-media][data-media-expanded="false"]
	[data-media-extra] {
	display: none;
}

html[data-bs-runtime="ready"] .bs-review-media__more {
	display: inline-block;
}

.bs-review-media__more:hover {
	color: var(--color-ink);
}

.bs-review-media::-webkit-scrollbar {
	block-size: calc(var(--space-unit) * 1.5);
}

.bs-review-media::-webkit-scrollbar-thumb {
	border-radius: 999px;
	background: var(--color-brand-muted);
}

.bs-quote--media-only .bs-review-media {
	margin-block-start: 0;
}

.bs-quote--large .bs-review-media {
	max-inline-size: 48rem;
	margin-inline: auto;
}

.bs-quote--large .bs-review-media--single {
	justify-content: center;
}

.bs-quote--large.bs-quote--media-only::before {
	display: none;
}

/* ---------- reviews: cards and feed ---------- */

/*
 * One reading column of ruled cards: stars, the words in the heading face,
 * the photos, and who said it. The feed and the card layout share every row
 * — the island runtime appends a page of the same rows under either — and
 * differ only in what sits on the rule above.
 */
.bs-review-feed__body {
	max-width: 52rem;
}

/* Cards use the section's full measure so review photos can sit to the right. */
.bs-reviews--cards .bs-review-feed__body {
	max-width: none;
}

.bs-review-feed__list {
	list-style: none;
	padding: 0;
	margin: 0;
	border-block-start: 1px solid var(--color-border);
}

/* The section heading already supplies this edge; do not draw a second rule. */
.bs-section__header + .bs-review-feed__body .bs-review-feed__list {
	border-block-start: 0;
}

/* The first review owns both sides of its row spacing, like every other row. */
html:not([data-chrome="airy"]) .bs-review-feed .bs-section__header {
	margin-block-end: 0;
}

.bs-review-feed__item {
	padding-block: calc(var(--space-gap) * 1.1);
	border-block-end: 1px solid var(--color-border);
	min-inline-size: 0;
}

.bs-review__stars {
	display: flex;
	align-items: center;
	gap: 0;
	font-size: var(--text-sm);
	letter-spacing: 0.06em;
	line-height: 1;
}

/*
 * The stars are the one place a review is allowed to be loud, so they take
 * the highlight — the same colour as the active nav mark and the next open
 * time, and nothing else on the page.
 */
.bs-review__lit {
	color: var(--color-highlight);
}

.bs-review__unlit {
	color: var(--color-border);
}

.bs-review__score {
	margin-inline-start: calc(var(--space-unit) * 1.5);
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.75rem);
	letter-spacing: 0.02em;
	color: var(--color-ink-subtle);
	font-variant-numeric: tabular-nums;
}

.bs-review__quote {
	margin-block-start: calc(var(--space-unit) * 2.5);
	max-width: 60ch;
	font-family: var(--font-heading);
	font-weight: var(--heading-weight, 600);
	letter-spacing: var(--heading-tracking, -0.015em);
	font-size: var(--text-lg);
	line-height: 1.42;
	text-wrap: pretty;
	overflow-wrap: anywhere;
}

.bs-review__more {
	padding: 0;
	border: 0;
	background: transparent;
	font-family: var(--font-body);
	font-size: var(--text-sm);
	line-height: inherit;
	color: var(--color-ink);
	text-decoration: underline;
	text-underline-offset: 0.22em;
	cursor: pointer;
}

.bs-review__quote[data-review-expanded] .bs-review__more {
	display: block;
	margin-block-start: calc(var(--space-unit) * 1.5);
}

.bs-review__meta {
	display: flex;
	flex-wrap: wrap;
	align-items: baseline;
	gap: calc(var(--space-unit) * 1.5) calc(var(--space-unit) * 2);
	margin-block-start: calc(var(--space-unit) * 2.5);
	font-size: var(--text-sm);
	color: var(--color-ink-muted);
	overflow-wrap: anywhere;
}

.bs-review__meta-item + .bs-review__meta-item::before {
	content: "\00b7";
	margin-inline-end: calc(var(--space-unit) * 2);
	color: var(--color-ink-subtle);
}

@media (min-width: 48rem) {
	.bs-review__card--with-media {
		display: grid;
		grid-template-columns: minmax(0, 1fr) minmax(10rem, 12rem);
		gap: calc(var(--space-gap) * 2);
		align-items: start;
	}

	.bs-review__card--with-media .bs-review-media-wrap {
		inline-size: 100%;
	}

	.bs-review__card--with-media .bs-review-media {
		grid-auto-flow: row;
		grid-auto-columns: auto;
		grid-template-columns: repeat(2, minmax(0, 1fr));
		inline-size: 100%;
		margin-block-start: 0;
		padding-block-end: 0;
		overflow: visible;
	}

	.bs-review__card--with-media .bs-review-media--single {
		grid-template-columns: minmax(0, 8.5rem);
		justify-content: end;
	}
}

.bs-review-feed__pagination {
	display: flex;
	justify-content: center;
	padding-block-start: calc(var(--space-gap) * 1);
}

/* No inert promise when JavaScript is unavailable: six reviews simply remain. */
.bs-review-feed__more {
	display: none;
}

html[data-bs-runtime="ready"] .bs-review-feed__more {
	display: inline-flex;
}

/* "All 128 reviews" runs the width of the column, as the design has it. */
.bs-reviews--cards .bs-review-feed__more {
	inline-size: 100%;
}

.bs-reviews__empty {
	color: var(--color-ink-muted);
	max-width: 46ch;
	text-wrap: pretty;
}

/* ---------- about ---------- */

.bs-about--editorial > .bs-container {
	width: min(100% - calc(var(--space-unit) * 8), 80rem);
}

/*
 * The editorial about: a one-column read on a phone in the order the design
 * gives — the pull quote, then the portrait, then the story — and from a
 * tablet the portrait on the left with everything else beside it. The grid
 * areas do the rearranging over one markup order, so the words a screen
 * reader hears come in the order they are read.
 */
.bs-about__grid {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 6);
	align-items: stretch;
}

.bs-about__quote {
	font-family: var(--font-heading);
	font-style: italic;
	font-weight: var(--heading-weight, 600);
	letter-spacing: var(--heading-tracking, -0.015em);
	text-transform: var(--heading-transform, none);
	font-size: var(--text-xl);
	line-height: 1.28;
	max-width: 30ch;
	text-wrap: pretty;
}

/*
 * The serif takes the quote in italic; the two grotesk looks set it upright.
 * Space Grotesk and Jost have no italic cut worth synthesising, and the
 * design sets them roman.
 */
html[data-chrome="framed"] .bs-about__quote,
html[data-chrome="airy"] .bs-about__quote {
	font-style: normal;
}

.bs-about__quote em {
	color: var(--color-brand);
}

.bs-about__portrait {
	margin: 0;
	/* Inset on a phone, so the portrait reads as a figure rather than a banner. */
	padding-inline: calc(var(--space-unit) * 8);
}

.bs-about__crop {
	aspect-ratio: 4 / 5;
	overflow: hidden;
	border-radius: var(--radius-lg);
	background: var(--color-surface-alt);
}

/* Studio frames its portrait in ink; the other looks leave the crop bare. */
html[data-chrome="framed"] .bs-about__crop {
	border: var(--frame-width, 2px) solid var(--frame-color, var(--color-ink));
}

/*
 * Still's portrait is a circle. A structural choice no token expresses, so
 * it hangs off the chrome attribute: square crop, round mask, and a caption
 * centred under it.
 */
html[data-chrome="airy"] .bs-about__crop {
	aspect-ratio: 1;
	border-radius: 50%;
	max-inline-size: 18rem;
	margin-inline: auto;
}

html[data-chrome="airy"] .bs-about__who {
	text-align: center;
	align-items: center;
}

.bs-about__image {
	inline-size: 100%;
	block-size: 100%;
	object-fit: cover;
	display: block;
}

.bs-about__who {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 1);
	margin-block-start: calc(var(--space-unit) * 3.5);
}

.bs-about__name {
	font-family: var(--font-heading);
	font-weight: var(--heading-weight, 600);
	letter-spacing: var(--heading-tracking, -0.015em);
	text-transform: var(--heading-transform, none);
	font-size: var(--text-lg);
	line-height: 1.2;
}

.bs-about__role {
	font-size: var(--text-sm);
	color: var(--color-ink-muted);
}

.bs-about__body {
	min-inline-size: 0;
}

.bs-prose__p + .bs-prose__p {
	margin-block-start: calc(var(--space-unit) * 4);
}

.bs-credentials {
	margin-block-start: calc(var(--space-unit) * 6);
	display: flex;
	flex-wrap: wrap;
	gap: calc(var(--space-unit) * 2);
	list-style: none;
	padding: 0;
}

/*
 * Qualifications as quiet chips. The filled brand pills they replace competed
 * with the primary button for attention, and a credential is context, not a
 * call to action. The frame tokens give them Studio's 2px edge for free.
 */
.bs-credential {
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.75rem);
	letter-spacing: 0.03em;
	padding: calc(var(--space-unit) * 1.5) calc(var(--space-unit) * 3);
	border-radius: var(--radius-sm);
	border: var(--frame-width, 1px) solid var(--frame-color, var(--color-border));
	color: var(--color-ink-muted);
}

/*
 * The facts under the story: label left in the detail voice, answer right in
 * the ink, one hairline between rows. A definition list, ruled — the last
 * thing every well-made about page says.
 */
.bs-about__details {
	margin-block-start: calc(var(--space-unit) * 6);
	border-block-start: 1px solid var(--color-border);
}

.bs-about__detail {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	gap: calc(var(--space-unit) * 4);
	padding-block: calc(var(--space-unit) * 3);
	border-block-end: 1px solid var(--color-border);
}

.bs-about__detail-label {
	flex: none;
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.75rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: var(--detail-tracking, 0.08em);
	text-transform: uppercase;
	color: var(--color-ink-subtle);
}

.bs-about__detail-value {
	margin: 0;
	min-inline-size: 0;
	text-align: end;
	font-size: var(--text-sm);
	color: var(--color-ink);
	overflow-wrap: anywhere;
}

/* ---------- gallery ---------- */

/*
 * Two columns even on a phone: portfolios run long, and a stack of full-width
 * squares turns six photos into six screens of scrolling. From 48rem the
 * auto-fill takes over and the first photo of a deep set leads.
 */
.bs-gallery__grid {
	display: grid;
	gap: calc(var(--space-unit) * 2);
	grid-template-columns: repeat(2, 1fr);
}

.bs-gallery__item {
	min-width: 0;
}

/*
 * Ratios are the gallery's decision, not the figure's: square in the grids,
 * wide in the strip, varied on the wall — set here so per-variant overrides
 * (and the orphan rescue) can win.
 */
.bs-gallery__grid .bs-figure__crop,
.bs-gallery__rest .bs-figure__crop {
	aspect-ratio: 1 / 1;
}

/* Only the feature lead still draws a bare image; everything else is a figure. */
.bs-gallery__image {
	width: 100%;
	height: 100%;
	object-fit: cover;
	display: block;
}

/*
 * The phone-width version of the desktop orphan rescue: two columns strand
 * the last photo of any odd set, and a wide closing crop reads as an ending
 * where a lone half-width square reads as a mistake.
 */
@media (max-width: 47.99rem) {
	.bs-gallery__grid > .bs-gallery__item:last-child:nth-child(odd) {
		grid-column: span 2;
	}

	.bs-gallery__grid
		> .bs-gallery__item:last-child:nth-child(odd)
		.bs-figure__crop {
		aspect-ratio: 2 / 1;
	}
}

/* ---------- gallery: lead photo ---------- */

.bs-gallery__lead {
	overflow: hidden;
}

.bs-gallery__lead .bs-gallery__image {
	inline-size: 100%;
	block-size: min(38rem, 62svh);
	aspect-ratio: auto;
	object-fit: cover;
	border-radius: 0;
}

.bs-gallery__rest {
	display: grid;
	grid-template-columns: repeat(2, 1fr);
	gap: calc(var(--space-unit) * 2);
	margin-block-start: calc(var(--space-unit) * 2);
	list-style: none;
	padding: 0;
}

/* ---------- gallery: masonry ---------- */

/*
 * Reading sections stay on the ordinary 68rem measure; a portfolio is the
 * work itself and earns the same 88rem editorial canvas as full-page booking.
 * The shared gutter keeps this identical to the ordinary container until the
 * viewport is actually wide enough to use the extra space.
 */
.bs-gallery--masonry .bs-container {
	width: min(100% - calc(var(--space-unit) * 8), 88rem);
}

.bs-gallery__masonry {
	columns: 2;
	column-gap: calc(var(--space-unit) * 4);
	list-style: none;
	padding: 0;
}

.bs-gallery__masonry .bs-gallery__item {
	break-inside: avoid;
	margin-block-end: calc(var(--space-gap) * 1.25);
}

/*
 * The provisional ratio reserves a useful frame while the image is loading.
 * `auto` then hands the box to the image's intrinsic ratio as soon as its
 * dimensions are known, so the masonry keeps the provider's original crop.
 */
.bs-gallery__masonry .bs-figure__image {
	block-size: auto;
	aspect-ratio: auto 4 / 5;
}

@media (min-width: 48rem) {
	.bs-gallery__masonry {
		column-gap: var(--space-gap);
	}

	.bs-gallery__masonry .bs-gallery__item {
		margin-block-end: calc(var(--space-gap) * 1.5);
	}
}

/* ---------- gallery: strip ---------- */

/*
 * One viewport-wide row, scrolled by thumb, snapping photo to photo. Small
 * end gutters give the first and last photos breathing room while the heading
 * keeps the reading measure. A clipped trailing photo signals more to scroll.
 */
.bs-gallery--strip .bs-gallery__strip {
	display: flex;
	gap: calc(var(--space-unit) * 2);
	overflow-x: auto;
	padding-inline: calc(var(--space-unit) * 4);
	padding-block-end: calc(var(--space-unit) * 2);
	scroll-snap-type: x mandatory;
	scroll-padding-inline: calc(var(--space-unit) * 4);
	scrollbar-width: none;
	overscroll-behavior-inline: contain;
}

.bs-gallery__strip::-webkit-scrollbar {
	display: none;
}

.bs-gallery__strip .bs-gallery__item {
	flex: 0 0 min(80%, 22rem);
	scroll-snap-align: start;
}

.bs-gallery__strip .bs-figure__crop {
	aspect-ratio: 5 / 4;
}

/* Studio frames its work in ink, the way it frames its portrait. */
html[data-chrome="framed"] .bs-gallery .bs-figure__crop {
	border: var(--frame-width, 2px) solid var(--frame-color, var(--color-ink));
}

/* ---------- places ---------- */

/*
 * The "where" block: the city set as a title under the heading, then one
 * ruled row per place — the name and address on the left, what it costs to
 * be seen there on the right, and how to find the door in a quiet line
 * underneath.
 */
.bs-places__city {
	font-family: var(--font-heading);
	font-weight: var(--heading-weight, 600);
	letter-spacing: var(--heading-tracking, -0.015em);
	text-transform: var(--heading-transform, none);
	line-height: var(--heading-line, 1.15);
	font-size: var(--text-2xl);
	margin-block-end: calc(var(--space-unit) * 4);
	text-wrap: balance;
}

html[data-chrome="airy"] .bs-places__city {
	text-align: center;
}

.bs-places__list {
	border-block-start: 1px solid var(--color-border);
}

.bs-places__grid {
	display: grid;
	gap: var(--space-gap);
}

.bs-place {
	display: flex;
	justify-content: space-between;
	align-items: flex-start;
	gap: calc(var(--space-unit) * 4);
}

.bs-places__list .bs-place {
	padding-block: calc(var(--space-unit) * 3.5);
	border-block-end: 1px solid var(--color-border);
}

.bs-place__main {
	min-inline-size: 0;
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 0.75);
}

/*
 * The place's name in the body face, not the heading's: these rows are a
 * table of facts, and a tracked-capital title on each would make three rows
 * read as three sections.
 */
.bs-place__name {
	font-family: var(--font-body);
	font-weight: 500;
	font-size: var(--text-base);
	letter-spacing: 0;
	text-transform: none;
	line-height: 1.4;
	overflow-wrap: anywhere;
}

.bs-place__address {
	font-size: var(--text-sm);
	color: var(--color-ink-muted);
	text-decoration-line: underline;
	text-underline-offset: 0.2em;
	text-decoration-thickness: 1px;
	text-decoration-color: color-mix(in srgb, var(--color-ink) 30%, transparent);
	overflow-wrap: anywhere;
}

.bs-place__address:hover {
	text-decoration-color: var(--color-ink);
}

.bs-place__note {
	font-size: var(--text-sm);
	color: var(--color-ink-subtle);
	max-width: 52ch;
}

.bs-place__fee {
	flex: none;
	font-size: var(--text-base);
	color: var(--color-ink);
	font-variant-numeric: tabular-nums;
	line-height: 1.4;
}

/* ---------- questions ---------- */

/*
 * Hairline rows, a native disclosure each, and a plus that turns into a cross
 * when the row is open. No script: the browser owns the state, so the rows
 * work in the builder's script-less preview and print with every answer out.
 */
.bs-faq__list {
	border-block-start: 1px solid var(--color-border);
}

/* The ruled header supplies the first edge; each question owns its spacing. */
html:not([data-chrome="airy"]) .bs-faq .bs-section__header {
	margin-block-end: 0;
}

html:not([data-chrome="airy"]) .bs-faq .bs-section__header + .bs-faq__list {
	border-block-start: 0;
}

.bs-faq__item {
	border-block-end: 1px solid var(--color-border);
}

.bs-faq__question {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: calc(var(--space-unit) * 4);
	padding-block: calc(var(--space-unit) * 4);
	list-style: none;
	cursor: pointer;
	font-size: var(--text-base);
	line-height: 1.5;
	color: var(--color-ink);
}

.bs-faq__question::-webkit-details-marker {
	display: none;
}

.bs-faq__q {
	min-inline-size: 0;
	overflow-wrap: anywhere;
}

/*
 * The mark: two bars in the current ink. Open, the whole thing turns a
 * quarter turn and the plus becomes a cross — a transform, so nothing around
 * it moves.
 */
.bs-faq__mark {
	position: relative;
	flex: none;
	align-self: center;
	inline-size: 0.75rem;
	block-size: 0.75rem;
	color: var(--color-ink-subtle);
	transition: transform 220ms cubic-bezier(0.16, 1, 0.3, 1);
}

.bs-faq__mark::before,
.bs-faq__mark::after {
	content: "";
	position: absolute;
	inset: 0;
	margin: auto;
	background: currentColor;
}

.bs-faq__mark::before {
	inline-size: 100%;
	block-size: 1px;
}

.bs-faq__mark::after {
	inline-size: 1px;
	block-size: 100%;
}

.bs-faq__item[open] .bs-faq__mark {
	transform: rotate(45deg);
	color: var(--color-ink);
}

.bs-faq__answer {
	padding-block: 0 calc(var(--space-unit) * 4.5);
	max-width: 60ch;
	font-size: var(--text-sm);
	line-height: 1.7;
	color: var(--color-ink-muted);
}

/* Line breaks a provider typed are paragraph breaks, without the markup. */
.bs-faq__answer p {
	white-space: pre-line;
}

/*
 * The all-open style: the question as a plain row title in the body face,
 * because a list of six capitalised headings would shout.
 */
.bs-faq__question--open {
	display: block;
	font-family: var(--font-body);
	font-weight: 500;
	letter-spacing: 0;
	text-transform: none;
	cursor: default;
	padding-block-end: calc(var(--space-unit) * 1.5);
}

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

.bs-contact {
	background: var(--color-surface-alt);
}

.bs-contact__grid {
	display: grid;
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr));
	gap: calc(var(--space-gap) * 2);
	margin-block-start: var(--space-gap);
	align-items: start;
}

.bs-contact__grid > * {
	min-inline-size: 0;
}

.bs-contact__list {
	display: grid;
	margin: 0;
}

.bs-contact__item {
	display: grid;
	grid-template-columns: minmax(5rem, 0.3fr) minmax(0, 1fr);
	align-items: baseline;
	gap: calc(var(--space-unit) * 4);
	padding-block: calc(var(--space-unit) * 3);
	border-block-end: 1px solid var(--color-border);
}

.bs-contact__item:first-child {
	padding-block-start: 0;
}

.bs-contact__item:last-child {
	border-block-end: 0;
	padding-block-end: 0;
}

.bs-contact__socials {
	display: flex;
	flex-wrap: wrap;
	gap: calc(var(--space-unit) * 2) calc(var(--space-unit) * 4);
}

.bs-contact__label {
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.75rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: var(--detail-tracking, 0.12em);
	text-transform: uppercase;
	color: var(--color-ink-muted);
}

.bs-contact__value {
	font-family: var(--font-body);
	font-weight: 400;
	font-size: var(--text-base);
	line-height: 1.65;
	text-underline-offset: 0.22em;
	text-decoration-thickness: 1px;
	text-decoration-color: color-mix(in srgb, var(--color-brand) 45%, transparent);
	transition: text-decoration-color 150ms ease-out;
	overflow-wrap: anywhere;
}

a.bs-contact__value {
	text-decoration-line: underline;
}

a.bs-contact__value:hover {
	text-decoration-color: var(--color-brand);
}

.bs-contact__note {
	margin-block-start: var(--space-gap);
	padding-block-start: var(--space-gap);
	border-block-start: 1px solid var(--color-border);
}

.bs-contact__note p {
	max-inline-size: 65ch;
	margin-block-start: calc(var(--space-unit) * 2);
	white-space: pre-line;
	overflow-wrap: anywhere;
}

.bs-hours {
	width: 100%;
	table-layout: fixed;
	overflow-wrap: anywhere;
	border-collapse: collapse;
	font-family: var(--font-body);
	font-size: var(--text-base);
	line-height: 1.65;
	letter-spacing: normal;
	font-variant-numeric: tabular-nums;
}

.bs-hours__caption {
	text-align: start;
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.75rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: var(--detail-tracking, 0.12em);
	text-transform: uppercase;
	color: var(--color-ink-muted);
	margin-block-end: calc(var(--space-unit) * 3);
}

.bs-hours__row {
	border-block-end: 1px solid var(--color-border);
}

.bs-hours__row:last-child {
	border-block-end: none;
}

.bs-hours__days {
	padding-inline-end: calc(var(--space-unit) * 3);
	text-align: start;
	font-weight: 500;
	padding-block: calc(var(--space-unit) * 2);
}

.bs-hours__time {
	text-align: end;
	color: var(--color-ink-muted);
	padding-block: calc(var(--space-unit) * 2);
}

/* ---------- responsive ---------- */

@media (max-width: 30rem) {
	/* Two provider-written labels stay tappable instead of squeezing each other. */
	.bs-hero__actions {
		inline-size: 100%;
		flex-direction: column;
		align-items: stretch;
	}

	.bs-hero__actions .bs-btn {
		inline-size: 100%;
	}
}

@media (min-width: 48rem) {
	.bs-duo {
		grid-template-columns: 1.15fr 0.85fr;
	}

	.bs-info-grid {
		grid-template-columns: repeat(3, 1fr);
		gap: calc(var(--space-gap) * 2);
	}

	/* Chips wrap on the page once there is a page for them to wrap on. */
	.bs-chips {
		flex-wrap: wrap;
		inline-size: auto;
		overflow: visible;
		margin-inline: 0;
		padding-inline: 0;
	}

	.bs-chips::after {
		display: none;
	}

	/*
	 * Four fixed columns from here up. `auto-fill` sized the grid from the
	 * viewport, which made the column count unknowable — and the featured lead
	 * and the orphan rescue below only compose when a row's end is a fact.
	 */
	.bs-gallery__grid {
		grid-template-columns: repeat(4, 1fr);
	}

	/*
	 * The lead's supporting row fills whatever it has.
	 *
	 * `auto-fit` rather than a fixed four: the row's job is to close the
	 * composition under the lead, and three photographs in a four-column track
	 * leave a hole where the fourth would be. Sized so a long portfolio still
	 * wraps rather than shrinking to stamps.
	 */
	.bs-gallery__rest {
		grid-template-columns: repeat(auto-fit, minmax(min(100%, 13rem), 1fr));
	}

	.bs-gallery__strip .bs-gallery__item {
		flex-basis: min(60%, 22rem);
	}

	/* Words left, offer right — the banner's two halves stop stacking. */
	.bs-statement__banner {
		grid-template-columns: minmax(0, 1.35fr) minmax(0, 0.65fr);
		column-gap: calc(var(--space-gap) * 2);
	}

	.bs-statement__aside {
		justify-self: end;
		text-align: end;
	}

	.bs-statement__aside .bs-statement__note {
		margin-block-start: 0;
	}

	/* The statement earns display scale once the measure can hold it. */
	.bs-statement__p {
		font-size: var(--text-2xl);
	}

	.bs-statement--banner .bs-statement__p {
		font-size: var(--text-base);
	}

	/*
	 * Scoped to the plain grid, and it has to be.
	 *
	 * Unscoped, this matched the first item of the lead variant's supporting
	 * row too — so a section that already had a lead photograph promoted a
	 * second one underneath it, and the row read as a broken grid rather than
	 * as a set. The same applies to the orphan rescue below.
	 */
	.bs-gallery__grid > .bs-gallery__item:first-child:nth-last-child(n + 5) {
		grid-column: span 2;
		grid-row: span 2;
	}

	/*
	 * The rescue. With a 2×2 lead in four columns, a set of 6, 10, 14… photos
	 * strands its final photo alone on the last row, which reads as a grid
	 * that broke rather than a page that ended. Those counts are exactly the
	 * `4n + 2`-th child being last; doubling it into a wide frame under the
	 * lead closes the composition instead.
	 */
	.bs-gallery__grid
		> .bs-gallery__item:first-child:nth-last-child(n + 5)
		~ .bs-gallery__item:last-child:nth-child(4n + 2) {
		grid-column: span 2;
	}

	.bs-gallery__grid
		> .bs-gallery__item:first-child:nth-last-child(n + 5)
		~ .bs-gallery__item:last-child:nth-child(4n + 2)
		.bs-figure__crop {
		aspect-ratio: 2 / 1;
	}

	.bs-services__grid {
		grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
	}

	.bs-testimonials__grid {
		grid-template-columns: repeat(auto-fit, minmax(min(100%, 20rem), 1fr));
	}

	/*
	 * The editorial about opens out: the portrait takes the left column with
	 * its caption, the quote and the story the right. Grid areas over the
	 * one-column markup order, so nothing moves in the DOM. Rows are gapless
	 * and the story carries its own top margin, so an about with no quote
	 * closes the empty row rather than leaving a gap where a quote was not.
	 */
	.bs-about--editorial .bs-about__grid {
		display: grid;
		grid-template-columns: minmax(0, 0.38fr) minmax(0, 0.62fr);
		grid-template-areas:
			"portrait quote"
			"portrait body";
		grid-template-rows: auto 1fr;
		column-gap: calc(var(--space-gap) * 2);
		row-gap: 0;
		align-items: start;
	}

	.bs-about--editorial .bs-about__quote {
		grid-area: quote;
		font-size: var(--text-2xl);
		line-height: 1.24;
	}

	.bs-about--editorial .bs-about__portrait {
		grid-area: portrait;
		padding-inline: 0;
	}

	.bs-about--editorial .bs-about__body {
		grid-area: body;
	}

	.bs-about--editorial .bs-about__quote ~ .bs-about__body {
		margin-block-start: calc(var(--space-unit) * 6);
	}

	.bs-about--editorial .bs-about__details {
		max-width: 34rem;
	}


	.bs-places__grid {
		grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr));
	}
}

/*
 * Portrait tablets keep the split banner stacked. At this content-driven
 * threshold both the display heading and photograph have a useful column;
 * below it, making two columns only turns each half into a narrow phone.
 */
@media (min-width: 56rem) {
	.bs-hero__grid--media {
		width: min(100%, 108rem);
		grid-template-columns: 1.05fr 0.95fr;
	}

	.bs-hero__grid--media .bs-hero__copy {
		padding-inline-start: clamp(1.5rem, 4vw, 4.5rem);
		padding-block: calc(var(--space-section) * 0.6);
	}

	.bs-hero__grid--media .bs-hero__media {
		align-self: start;
	}

	.bs-hero__grid--media .bs-hero__image {
		margin-inline: auto 0;
	}
}

@media (min-width: 64rem) {
	.bs-hero__grid--media {
		padding-inline-end: clamp(1rem, 3vw, 3rem);
	}

	/*
	 * The price list borrows the booking workspace's strongest move: the
	 * heading holds a narrow left rail while the rows take the wide column.
	 * At reading widths the list is a single measured column; here it uses
	 * the page instead of leaving half of it empty.
	 *
	 * `:has` guards the split: with the heading turned off there is nothing to
	 * put in the rail, and the rows keep the single measured column. Engines
	 * without `:has` keep that column too, which degrades to correct.
	 */
	.bs-services--list .bs-container:has(.bs-section__header) {
		max-width: none;
		margin-inline: auto;
		display: grid;
		grid-template-columns: minmax(13rem, 0.55fr) minmax(0, 1.45fr);
		grid-template-rows: auto auto 1fr;
		column-gap: calc(var(--space-unit) * 14);
		align-items: start;
	}

	.bs-services--list .bs-section__header {
		position: sticky;
		inset-block-start: calc(var(--space-unit) * 24);
		grid-column: 1;
		margin-block-end: 0;
	}

	.bs-services--list .bs-section__header + .bs-section__intro {
		grid-column: 1;
		margin-block: calc(var(--space-gap) * 0.75) 0;
	}

	.bs-services--list .bs-services__list {
		grid-column: 2;
		grid-row: 1 / span 3;
	}

	/* The menu's rows get a wider price rail and more air between them. */
	.bs-service-row--menu {
		column-gap: calc(var(--space-unit) * 10);
		padding-block: calc(var(--space-unit) * 6);
	}

	.bs-service-row--menu .bs-service__desc {
		max-width: 60ch;
	}
}

/* ---------- compositions ---------- */

/*
 * The shared vocabulary `sections/compositions.ts` renders. One implementation
 * per idea; the detail voice and theme tokens carry the per-template identity.
 */

.bs-figure {
	margin: 0;
	min-width: 0;
}

.bs-figure__crop {
	overflow: hidden;
	border-radius: var(--radius-md);
}

/* ---------- shared image viewer ---------- */

.bs-lightbox__trigger {
	display: block;
	inline-size: 100%;
	block-size: 100%;
	padding: 0;
	border: 0;
	border-radius: inherit;
	background: transparent;
	color: inherit;
	text-align: inherit;
	text-decoration: none;
	cursor: zoom-in;
}

.bs-lightbox {
	inline-size: min(92vw, 80rem);
	max-inline-size: none;
	max-block-size: 92svh;
	padding: 0;
	border: 0;
	background: transparent;
	color: var(--color-ink);
	overflow: visible;
}

.bs-lightbox::backdrop {
	background: color-mix(in srgb, var(--color-ink) 72%, transparent);
	backdrop-filter: blur(8px);
}

.bs-lightbox__stage {
	position: relative;
	display: grid;
	place-items: center;
	min-block-size: 8rem;
	padding: 0;
	background: transparent;
}

.bs-lightbox__image {
	display: block;
	max-inline-size: 88vw;
	max-block-size: 84svh;
	inline-size: auto;
	block-size: auto;
	object-fit: contain;
}

.bs-lightbox__close {
	position: absolute;
	inset-block-start: calc(var(--space-unit) * 2);
	inset-inline-end: calc(var(--space-unit) * 2);
	z-index: 1;
	display: grid;
	place-items: center;
	inline-size: 2.75rem;
	block-size: 2.75rem;
	padding: 0;
	border: 1px solid var(--color-border);
	border-radius: 999px;
	background: var(--color-surface);
	color: var(--color-ink);
	cursor: pointer;
}

.bs-figure__image {
	inline-size: 100%;
	block-size: 100%;
	object-fit: cover;
	display: block;
	transition: transform 900ms cubic-bezier(0.16, 1, 0.3, 1);
}

.bs-figure:hover .bs-figure__image {
	transform: scale(1.035);
}

.bs-figure__caption {
	display: flex;
	justify-content: space-between;
	gap: calc(var(--space-unit) * 4);
	padding-block-start: calc(var(--space-unit) * 2.5);
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.75rem);
	letter-spacing: var(--detail-tracking, 0.1em);
	text-transform: uppercase;
	color: var(--color-ink-subtle);
}

.bs-figure__mark {
	font-variant-numeric: tabular-nums;
	flex-shrink: 0;
}

.bs-duo {
	display: grid;
	gap: calc(var(--space-unit) * 3);
	align-items: start;
}

.bs-caption-bar {
	display: flex;
	justify-content: space-between;
	gap: calc(var(--space-unit) * 6);
	padding-block: calc(var(--space-unit) * 2.5);
	border-block-end: 1px solid var(--color-border);
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.75rem);
	letter-spacing: var(--detail-tracking, 0.1em);
	text-transform: uppercase;
	color: var(--color-ink-subtle);
}

.bs-info-grid {
	display: grid;
	gap: calc(var(--space-gap) * 1.75);
}

.bs-info-grid__label {
	font-family: var(--font-heading);
	font-size: var(--text-lg);
	margin: 0 0 calc(var(--space-unit) * 3.5);
}

.bs-info-grid__note p {
	color: var(--color-ink-muted);
	font-size: var(--text-sm);
	max-width: 34ch;
	margin: 0 0 calc(var(--space-unit) * 2);
	text-wrap: pretty;
}

.bs-steps {
	list-style: none;
	margin: 0;
	padding: 0;
	border-block-start: 1px solid var(--color-border);
}

.bs-steps__item {
	display: grid;
	grid-template-columns: auto 1fr;
	gap: calc(var(--space-unit) * 6);
	padding-block: calc(var(--space-unit) * 4);
	border-block-end: 1px solid var(--color-border);
}

.bs-steps__n {
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.75rem);
	color: var(--color-ink-subtle);
	font-variant-numeric: tabular-nums;
	padding-block-start: 0.25em;
}

.bs-steps__body {
	max-width: 52ch;
	text-wrap: pretty;
}

/* ---------- emphasis ---------- */

/*
 * `*words*` in a heading or statement, drawn in the site's own voice. Italic
 * plus the brand color — weight or size would fight the display setting, and
 * color alone would read as a link. A face with no italic cut leans on the
 * synthesised oblique; at display sizes that reads as intent.
 */
.bs-hero__heading em,
.bs-statement__p em,
.bs-statement__heading em,
.bs-section__heading em {
	font-style: italic;
	color: var(--color-brand);
}

.bs-statement--banner em {
	color: color-mix(in srgb, var(--color-brand) 82%, var(--color-surface));
}

/*
 * There is deliberately no scroll reveal on these pages, and it is settled.
 *
 * Two implementations were built, tested, and removed — both failed the same
 * acceptance test: a full-page capture must show the whole page.
 *
 * 1. `animation-timeline: view()` (CSS only): scroll-driven animations are
 *    scrubbed, not fired — everything outside the range holds `opacity: 0`
 *    forever, so any unscrolled render (screenshot, PDF, social preview,
 *    print) was blank below the hero.
 * 2. A one-shot IntersectionObserver in a static `site.js`, hiding under an
 *    `html.bs-js` gate the script itself set: safe for readers and print,
 *    but capture-beyond-viewport renderers (headless Chromium full-page
 *    screenshots, and the preview and archival services built on them)
 *    paint off-viewport content without ever intersecting it — the tail of
 *    the page captured at `opacity: 0`.
 *
 * Both were reproduced, not theorised. The failure is structural: any
 * reveal that hides content until a viewport event shows it will blank in
 * a renderer that has no viewport events. The motion budget is spent where
 * it is safe by construction — the hero's time-based entrance, hover
 * responses, and the marquee — which is also the restraint the strongest
 * studio sites practice.
 */

/*
 * Variant stylesheet — islands and the booking flow.
 *
 * Everything the island runtime renders into a page: availability, the guided
 * booking flow, verification, review and payment, a client's own appointments.
 * See `00-base.css` for the rules of the file and how the partials are bundled.
 */

/* ---------- islands ---------- */

.bs-island {
	min-height: 3rem;
}

/*
 * `loading` is set by the island runtime, never by the renderer.
 *
 * That is deliberate and load-bearing: the only thing that knows the script is
 * running is the script. A page whose JavaScript was blocked, never downloaded,
 * or failed to parse has no `data-state` at all, and the rules below leave its
 * fallback link exactly where the snapshot put it.
 */
.bs-island[data-state="loading"] {
	/*
	 * The shape of the card that is coming, not a grey rectangle.
	 *
	 * One flat shimmering block says "empty"; a heading bar with rows under it
	 * says "a list is loading", and it holds the height the answer will need so
	 * its arrival does not shove the page. Drawn entirely in layered gradients
	 * because the island's contents at this moment are the fallback link, which
	 * must stay in the markup untouched in case the answer never comes.
	 */
	min-height: 15rem;
	border: 1px solid var(--color-border);
	border-radius: var(--radius-lg);
	/* the heading */
	background-image: linear-gradient(var(--color-surface-sunken) 0 0),
		/* three rows */
		linear-gradient(var(--color-surface-alt) 0 0),
		linear-gradient(var(--color-surface-alt) 0 0),
		linear-gradient(var(--color-surface-alt) 0 0);
	background-repeat: no-repeat;
	background-position: calc(var(--space-unit) * 5) calc(var(--space-unit) * 5),
		calc(var(--space-unit) * 5) calc(var(--space-unit) * 13),
		calc(var(--space-unit) * 5) calc(var(--space-unit) * 23),
		calc(var(--space-unit) * 5) calc(var(--space-unit) * 33);
	background-size: 45% calc(var(--space-unit) * 5),
		calc(100% - var(--space-unit) * 10) calc(var(--space-unit) * 8),
		calc(100% - var(--space-unit) * 10) calc(var(--space-unit) * 8),
		calc(100% - var(--space-unit) * 10) calc(var(--space-unit) * 8);
	animation: bs-pulse 1.6s ease-in-out infinite;
}

/*
 * A breath rather than a sweep.
 *
 * The shimmer this replaces animated `background-position` across a 400%-wide
 * gradient, which repaints the whole element every frame for the length of the
 * wait — on the mid-range phones these pages are built for, that is the most
 * expensive thing on screen while the visitor has nothing to look at. Opacity
 * composites.
 */
@keyframes bs-pulse {
	50% {
		opacity: 0.55;
	}
}

/*
 * The fallback ships visible and is taken away, rather than shipping hidden and
 * being revealed.
 *
 * Every way this can fail then degrades to the same working link: no script, a
 * blocked script, a dropped request, or Booka being down. The alternative —
 * `<noscript>` — covers only the rarest of those, and leaves an empty box for
 * all the rest.
 */
.bs-island__fallback {
	display: block;
}

/* The script is running and has asked; the skeleton speaks for it. */
.bs-island[data-state="loading"] .bs-island__fallback,
.bs-island[data-state="patching"] .bs-island__fallback,
.bs-island[data-state="ready"] .bs-island__fallback {
	display: none;
}

/*
 * It asked and did not get an answer. The link is what is left, and it works.
 *
 * The runtime prepends a notice saying so, so the visitor is not left staring
 * at a lone button wondering whether they already booked something. Without a
 * script there is no notice and no box either — just the link, which is the
 * honest shape of a page that never tried.
 */
.bs-island[data-state="error"] {
	animation: none;
	background: none;
}

.bs-island[data-state="error"] .bs-island__fallback {
	display: flex;
	justify-content: center;
}

/*
 * A re-ask keeps the screen and dims only what the answer will replace.
 *
 * `patching` is set when a visitor changes a choice on a screen that is
 * already up: the pressed states flipped optimistically, and the parts marked
 * `data-recompute` — the price bar, the times — fade while the server
 * recomputes them. Everything else stays crisp, because it is not in question.
 */
.bs-island[data-state="patching"] [data-recompute] {
	opacity: 0.45;
	pointer-events: none;
	transition: opacity 150ms ease-out;
}

/*
 * The tapped button carries the wait.
 *
 * The runtime adds this to whichever control caused a round trip that moves
 * the screen — choosing a service, holding a time, going back. The spinner is
 * a flex item at the button's end, drawn in `currentColor` so it holds up on
 * primary and ghost chrome alike.
 */
.bs-is-loading::after {
	content: "";
	inline-size: 0.85em;
	block-size: 0.85em;
	flex-shrink: 0;
	margin-inline-start: calc(var(--space-unit) * 1.5);
	border: 2px solid currentColor;
	border-inline-start-color: transparent;
	border-radius: 999px;
	animation: bs-spin 600ms linear infinite;
}

@keyframes bs-spin {
	to {
		transform: rotate(360deg);
	}
}

/* Visually hidden, audibly present — the island's status narrator lives here. */
.bs-vh {
	position: absolute;
	inline-size: 1px;
	block-size: 1px;
	overflow: hidden;
	clip-path: inset(50%);
	white-space: nowrap;
}

/*
 * A transport failure, said where the visitor is looking.
 *
 * Injected by the runtime, never by a fragment: it reports on the network
 * itself, which is the one thing no fragment can describe. The screen it sits
 * on still works, so it reads as "try again", not as a dead end.
 */
.bs-island__notice {
	width: 100%;
	font-size: var(--text-sm);
	color: var(--color-danger);
	border: 1px solid color-mix(in srgb, var(--color-danger) 35%, transparent);
	border-radius: var(--radius-md);
	background: var(--color-danger-soft);
	padding: calc(var(--space-unit) * 2.5) calc(var(--space-unit) * 3);
	margin-block-end: calc(var(--space-unit) * 3);
}

/*
 * Availability, as rendered by the island.
 *
 * Styled here rather than in the fragment because the fragment is markup with
 * no styles of its own: it lands inside a page that already has the provider's
 * theme, and inheriting it is the point — live times should look like they
 * belong to the site, not like a widget dropped onto it.
 */
.bs-slots {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 4);
	width: 100%;
}

/*
 * The day, as a heading with a line running off it.
 *
 * The times are the point of this screen and there are a lot of them; without
 * a horizon between one day and the next they read as one undifferentiated
 * field of pills. The rule costs nothing and gives the eye a place to land.
 */
.bs-slots__date {
	display: flex;
	align-items: center;
	gap: calc(var(--space-unit) * 2);
	font-size: var(--text-xs);
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--color-ink-subtle);
	margin-block-end: calc(var(--space-unit) * 2);
}

.bs-slots__date::after {
	content: "";
	flex: 1;
	block-size: 1px;
	background: var(--color-border);
}

.bs-slots__times {
	display: flex;
	flex-wrap: wrap;
	gap: calc(var(--space-unit) * 1.5);
}

/*
 * Nothing free is not an error, and it is not a dead end either.
 *
 * A provider with a full calendar has a working website; what the visitor
 * needs from this box is the sentence that tells them what to do instead.
 */
.bs-slots__empty {
	width: 100%;
	color: var(--color-ink-muted);
	font-size: var(--text-sm);
	border-radius: var(--radius-md);
	background: var(--color-surface-alt);
	padding: calc(var(--space-unit) * 4);
	text-wrap: pretty;
}


/* ---------- an island that is one line of a sentence ---------- */

/*
 * The hero's "Next: Thu 1:30" is an island with nothing to hide and nothing
 * to draw while it waits. It ships empty — no fallback, because without a
 * script the meta line simply has one item fewer — and while the runtime asks,
 * it holds a word's worth of space as a soft pill so the line does not jump
 * when the answer lands. When there is no answer, it takes up no room at all.
 */
.bs-island--inline {
	display: inline-block;
	min-height: 0;
	vertical-align: baseline;
}

.bs-island--inline[data-state="loading"] {
	min-height: 0;
	min-inline-size: 6em;
	block-size: 0.85em;
	border: 0;
	border-radius: 999px;
	background: var(--color-surface-alt);
	vertical-align: middle;
	animation: bs-pulse 1.6s ease-in-out infinite;
}

.bs-island--inline[data-state="empty"],
.bs-island--inline[data-state="error"] {
	display: none;
}

/*
 * The answer, painted in the one colour allowed to be loud.
 *
 * "Next available" is the fact a visitor came for, and the highlight — which
 * a look may set apart from the brand — is what the SPEC reserves for exactly
 * three marks: the active nav item, this one, and review stars.
 *
 * Text, not a chip. A filled pill in a cream page of hairlines is the loudest
 * thing on the screen, and it is sitting next to the headline it is supposed
 * to be a footnote to — which is exactly what the designs do *not* draw. The
 * quiet look says it in accent text; the airy look's highlight is its own near
 * black, so the same rule lands as plain ink there, which is what that design
 * shows too.
 */
.bs-next {
	display: inline-block;
	color: var(--color-highlight);
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

/*
 * The framed look is the one that floods it: a neon block, square-edged like
 * its buttons and its nav mark, in the palette's own three-touch budget.
 */
html[data-chrome="framed"] .bs-next {
	padding: 0.3em 0.6em;
	background: var(--color-highlight);
	color: var(--color-highlight-contrast);
}

/* ---------- the month, on request ---------- */

/*
 * The month grid opens under "See all dates" and closes again; it is a way of
 * scrolling the day groups further ahead, not a second picker with its own
 * answer. A tonal well rather than a framed card, so it reads as part of the
 * times step it sits inside.
 */
/*
 * The month, framed rather than filled.
 *
 * It opens inside a step whose every other card is a hairline box on the
 * paper, and a grey panel among them read as a different surface — a widget
 * dropped into the page rather than the next thing in the same list.
 */
.bs-calendar {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 3);
	width: 100%;
	border: var(--frame-width, 1px) solid var(--frame-color, var(--color-border));
	border-radius: var(--radius-sm);
	background: transparent;
	padding: calc(var(--space-unit) * 4);
	animation: bs-step-in 220ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

.bs-calendar__header {
	display: grid;
	grid-template-columns: 2.75rem minmax(0, 1fr) 2.75rem;
	align-items: center;
	gap: calc(var(--space-unit) * 2);
}

.bs-calendar__month {
	font-size: var(--text-base);
	font-weight: 600;
	letter-spacing: -0.01em;
	text-align: center;
}

.bs-calendar__nav {
	display: grid;
	place-items: center;
	inline-size: 2.75rem;
	block-size: 2.75rem;
	border: 0;
	border-radius: var(--radius-md);
	background: var(--color-surface);
	color: var(--color-ink);
	cursor: pointer;
	transition: background-color 120ms ease, color 120ms ease;
}

.bs-calendar__nav svg {
	inline-size: 1.1rem;
	block-size: 1.1rem;
}

.bs-calendar__nav:hover:not(:disabled) {
	background: var(--color-brand-soft);
	color: var(--color-brand-strong);
}

.bs-calendar__nav:disabled {
	color: var(--color-ink-subtle);
	cursor: default;
	opacity: 0.45;
}

.bs-calendar__weekdays,
.bs-calendar__days {
	display: grid;
	grid-template-columns: repeat(7, minmax(0, 1fr));
	gap: calc(var(--space-unit) * 1);
	width: 100%;
}

.bs-calendar__weekdays span {
	font-size: var(--text-xs);
	font-weight: 600;
	color: var(--color-ink-subtle);
	text-align: center;
}

.bs-calendar__day,
.bs-calendar__blank {
	justify-self: center;
	width: min(100%, 3rem);
	min-inline-size: 0;
	min-block-size: 2.75rem;
	aspect-ratio: 1;
}

.bs-calendar__day {
	position: relative;
	display: grid;
	place-items: center;
	width: 100%;
	border: 0;
	border-radius: var(--radius-md);
	background: transparent;
	color: var(--color-ink-subtle);
	font: inherit;
	font-size: var(--text-sm);
	font-variant-numeric: tabular-nums;
	cursor: default;
	transition: background-color 120ms ease, color 120ms ease, transform 120ms
		ease;
}

.bs-calendar__day--available {
	background: var(--color-brand-soft);
	color: var(--color-brand-strong);
	font-weight: 600;
	cursor: pointer;
}

/* The dot makes availability visible without relying on tint alone. */
.bs-calendar__day--available::after {
	content: "";
	position: absolute;
	inset-block-end: calc(var(--space-unit) * 1);
	inset-inline-start: 50%;
	inline-size: 0.22rem;
	block-size: 0.22rem;
	border-radius: 999px;
	background: currentColor;
	transform: translateX(-50%);
}

.bs-calendar__day--available:hover {
	background: var(--color-brand-muted);
	color: var(--color-brand-strong);
	transform: translateY(-1px);
}

.bs-calendar__day[aria-pressed="true"] {
	background: var(--color-brand);
	color: var(--color-brand-contrast);
}

.bs-calendar__day--today span {
	text-decoration: underline;
	text-decoration-thickness: 1px;
	text-underline-offset: 0.22em;
}

.bs-calendar__legend {
	display: flex;
	align-items: center;
	justify-content: center;
	gap: calc(var(--space-unit) * 1.5);
	color: var(--color-ink-muted);
	font-size: var(--text-xs);
}

.bs-calendar__legend-mark {
	inline-size: 0.35rem;
	block-size: 0.35rem;
	border-radius: 999px;
	background: var(--color-brand-strong);
}


/*
 * Nothing else in the island responds while one action is in flight.
 *
 * The button that was pressed is disabled on its own; this covers the rest, so
 * a visitor cannot start a second booking from a screen that is already
 * becoming a different screen.
 */
.bs-island[data-busy="true"] {
	pointer-events: none;
	opacity: 0.6;
}

/* ---------- the booking flow ---------- */

/*
 * One screen of the flow.
 *
 * Embedded in a page section it is a quiet card; on the booking page the
 * frame comes off and the screen is the page. Every fragment the island
 * returns — picker, screen one, auth, checkout, done — wears this same shell,
 * which is what makes six server-rendered screens feel like one flow.
 *
 * Structure inside it comes from the kicker-and-heading pairs the design
 * numbers, from hairlines, and from space. Cards and chips take their frame
 * from `--frame-*`, so a look that wants 2px ink frames gets them everywhere
 * at once and a look that wants outlines gets those.
 */
.bs-flow {
	--bs-flow-gap: calc(var(--space-unit) * 8);
	display: flex;
	flex-direction: column;
	align-items: flex-start;
	gap: var(--bs-flow-gap);
	width: 100%;
	border: var(--frame-width, 1px) solid var(--frame-color, var(--color-border));
	border-radius: var(--radius-lg);
	background: var(--color-surface);
	padding: calc(var(--space-unit) * 5);
}

/*
 * The arrival of a step.
 *
 * The one authored moment in the flow: each fragment the island fills rises a
 * few pixels into place on an exponential ease-out, which is what carries a
 * visitor from one server-rendered screen to the next instead of teleporting
 * them. It runs on `ready` only, so a screen that is merely being revised does
 * not replay it, and `prefers-reduced-motion` removes it entirely.
 */
.bs-island[data-state="ready"] > .bs-flow,
.bs-island[data-state="ready"] > .bs-bookings {
	animation: bs-step-in 260ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

@keyframes bs-step-in {
	from {
		opacity: 0;
		transform: translateY(6px);
	}
}

/*
 * The small line above a heading: "Service · 01", "Step · 02", "Times in
 * PDT", "You’re booked".
 *
 * Set in the detail voice — the third type role, the one for everything small
 * and functional — so a look that tracks its labels in a mono or in wide
 * capitals says so here without a rule per screen.
 */
.bs-kicker {
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size-fine, 0.64rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: calc(var(--detail-tracking, 0.12em) * 1.6);
	text-transform: uppercase;
	color: var(--color-ink-subtle);
	line-height: 1.4;
}

/*
 * A heading inside the flow, in the site's heading voice.
 *
 * Weight, case, tracking and line all come from the heading tokens, exactly as
 * an `h2` on the page does — a flow heading set in a different weight from the
 * page's would be the first thing on the booking page that looked borrowed.
 *
 * A step below the page's own `h2`, deliberately: "Choose location" names a
 * station of a form, not a chapter of a page, and the design sets it a size
 * down from the section headings on the site around it. Derived from the type
 * scale rather than fixed, so a compact site gets a compact flow.
 */
.bs-flow__heading {
	font-family: var(--font-heading);
	font-size: calc(var(--text-lg) * 1.2);
	font-weight: var(--heading-weight, 600);
	letter-spacing: var(--heading-tracking, -0.01em);
	text-transform: var(--heading-transform, none);
	line-height: var(--heading-line, 1.15);
	text-wrap: balance;
}

.bs-step {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 3.5);
	width: 100%;
}

.bs-step__head {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 2);
}

/* A step whose heading shares its line with a control: "See all dates". */
.bs-step__row {
	display: flex;
	align-items: flex-end;
	justify-content: space-between;
	gap: calc(var(--space-unit) * 4);
	width: 100%;
}

.bs-flow__detail {
	color: var(--color-ink-muted);
	font-size: var(--text-sm);
	margin-block-start: calc(var(--space-unit) * -4);
	max-width: 52ch;
	text-wrap: pretty;
}

.bs-flow__cta {
	width: 100%;
}

/*
 * A control that is a word, not a button.
 *
 * "See all dates", "Show more days", "Change": each opens or reverses
 * something small, and a full-weight button for every one of them would make
 * the screen shout. They wear the detail voice with a rule under them, so
 * they still read as something to press.
 */
.bs-flow__link {
	display: inline-flex;
	align-items: center;
	gap: calc(var(--space-unit) * 1.75);
	min-block-size: 2.75rem;
	border: none;
	background: none;
	padding: 0;
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.72rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: var(--detail-tracking, 0.12em);
	text-transform: uppercase;
	color: var(--color-brand-strong);
	cursor: pointer;
	white-space: nowrap;
	flex-shrink: 0;
}

/*
 * Underlined on hover and focus rather than always.
 *
 * The design sets these as tracked capitals in the accent with an icon in
 * front, which is what says "press me" at rest; a permanent rule under every
 * one of them put four underlines on a screen whose only real link is the
 * button at the bottom. The rule comes back the moment a pointer or a keyboard
 * arrives, so the affordance is never colour alone for anyone acting on it.
 */
.bs-flow__link:hover,
.bs-flow__link:focus-visible {
	color: var(--color-ink);
	text-decoration: underline;
	text-decoration-thickness: 1px;
	text-underline-offset: 0.3em;
}

/*
 * The icon on a word-sized control, sized from the word rather than in pixels
 * so it keeps its place beside capitals set at any of the three scales.
 */
.bs-flow__link-icon {
	inline-size: 1.15em;
	block-size: 1.15em;
	flex-shrink: 0;
}

/*
 * "Show more days": the one control that asks the server for more of the same.
 *
 * Full width and framed, because it is the end of a list rather than an aside
 * within one — a text link there reads as a footnote to the last day rather
 * than as the way to see the next three.
 */
.bs-flow__more {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 100%;
	min-block-size: 3rem;
	border: var(--frame-width, 1px) solid var(--frame-color, var(--color-border));
	border-radius: var(--radius-sm);
	background: transparent;
	padding: calc(var(--space-unit) * 3);
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.72rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: var(--detail-tracking, 0.12em);
	text-transform: uppercase;
	color: var(--color-brand-strong);
	cursor: pointer;
	transition: border-color 120ms ease, background-color 120ms ease;
}

.bs-flow__more:hover {
	border-color: var(--color-ink);
	background: var(--color-surface-alt);
}

/*
 * The way back, kept quiet.
 *
 * "Pick another time" and "Use a different phone" are escape hatches, not
 * alternatives to the primary action, and a second full-weight button beside
 * one is a fork in the road the visitor did not ask for. Full width so the
 * column stays aligned, no chrome so the weight stays where it belongs.
 */
.bs-flow__quiet {
	width: 100%;
	border: none;
	background: none;
	padding: calc(var(--space-unit) * 2);
	margin-block-start: calc(var(--space-unit) * -4);
	font: inherit;
	font-size: var(--text-sm);
	font-weight: 500;
	color: var(--color-ink-muted);
	cursor: pointer;
	border-radius: var(--radius-md);
	transition: color 120ms ease, background-color 120ms ease;
}

.bs-flow__quiet:hover {
	color: var(--color-ink);
	background: var(--color-surface-alt);
}

/* ---------- Service · 01: the visit ---------- */

.bs-visit {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 4);
	width: 100%;
}

.bs-svc {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 2);
	width: 100%;
	padding-block-end: calc(var(--space-unit) * 4);
	border-block-end: 1px solid var(--color-border);
}

/*
 * The service, read the way a receipt reads: what it is on the left, what it
 * costs on the right, both in the heading voice. The price is set beside the
 * name rather than under it because it is the other half of the same fact,
 * and a visitor adding an add-on watches it change in place.
 */
.bs-svc__head {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: calc(var(--space-unit) * 4);
	width: 100%;
}

.bs-svc__name {
	font-family: var(--font-heading);
	font-size: var(--text-lg);
	font-weight: var(--heading-weight, 600);
	letter-spacing: var(--heading-tracking, -0.01em);
	text-transform: var(--heading-transform, none);
	line-height: var(--heading-line, 1.15);
	text-wrap: balance;
	min-width: 0;
}

.bs-svc__price {
	font-family: var(--font-heading);
	font-size: calc(var(--text-lg) * 1.15);
	font-weight: var(--heading-weight, 600);
	line-height: 1;
	font-variant-numeric: tabular-nums;
	flex-shrink: 0;
}

/* "20 min" on the left, the way out of this service on the right. */
.bs-svc__line {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: calc(var(--space-unit) * 4);
	width: 100%;
}

/* "20 min", in the kicker's voice under the name it belongs to. */
.bs-svc__meta {
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.72rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: var(--detail-tracking, 0.12em);
	text-transform: uppercase;
	color: var(--color-ink-subtle);
	font-variant-numeric: tabular-nums;
}

/* "Change" / "Remove": a word with a chevron's job. */
.bs-svc__change {
	display: inline-flex;
	align-items: center;
	min-block-size: 2.25rem;
	border: none;
	background: none;
	padding: 0;
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size-fine, 0.64rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: var(--detail-tracking, 0.12em);
	text-transform: uppercase;
	color: var(--color-ink-muted);
	text-decoration: underline;
	text-decoration-thickness: 1px;
	text-underline-offset: 0.3em;
	cursor: pointer;
	flex-shrink: 0;
}

.bs-svc__change:hover {
	color: var(--color-ink);
}

/*
 * "Add another service": the one control on screen one that grows the visit.
 *
 * A word with a plus in front of it, in the brand — an invitation, not a
 * button competing with Continue. It used to be a dashed full-width box,
 * which drew a second empty service block on a screen that had exactly one
 * real one and made the eye stop at the outline rather than at the times.
 */
.bs-flow__add {
	display: inline-flex;
	align-items: center;
	gap: calc(var(--space-unit) * 2);
	align-self: flex-start;
	min-block-size: 2.75rem;
	border: none;
	background: none;
	padding: 0;
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.72rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: var(--detail-tracking, 0.12em);
	text-transform: uppercase;
	color: var(--color-brand-strong);
	cursor: pointer;
	transition: color 120ms ease;
}

.bs-flow__add:hover,
.bs-flow__add:focus-visible {
	color: var(--color-ink);
	text-decoration: underline;
	text-decoration-thickness: 1px;
	text-underline-offset: 0.3em;
}

/* ---------- options and add-ons ---------- */

/*
 * One labelled group of choices.
 *
 * Selection is `aria-pressed`, painted *and* marked with a check: these are
 * `data-island-patch` toggles whose state lives in the island's params, so
 * the fragment that comes back after a tap is the pressed state — and colour
 * alone is not an answer for anyone who cannot see the difference between a
 * tinted chip and a plain one.
 */
.bs-choice {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 2);
	width: 100%;
}

.bs-choice__label {
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.72rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: var(--detail-tracking, 0.12em);
	text-transform: uppercase;
	color: var(--color-ink-subtle);
}

/* The drawn mark of a made choice, in the one stroke weight the flow uses. */
.bs-check {
	inline-size: 0.9em;
	block-size: 0.9em;
	flex-shrink: 0;
}

.bs-addons {
	display: flex;
	flex-wrap: wrap;
	gap: calc(var(--space-unit) * 2);
	width: 100%;
}

/*
 * An add-on is optional and additive, so it is a chip: something to tap, not
 * a row to study. Framed by the look's frame tokens — a hairline under
 * Atelier, 2px ink under Studio, an outline under Still — and filled with the
 * look's button chrome when chosen, so a chosen add-on and the Continue button
 * are visibly the same family.
 */
.bs-addon {
	display: inline-flex;
	align-items: center;
	gap: calc(var(--space-unit) * 1.5);
	min-block-size: 2.5rem;
	border: var(--frame-width, 1px) solid var(--frame-color, var(--color-border));
	border-radius: var(--radius-sm);
	padding: calc(var(--space-unit) * 1.5) calc(var(--space-unit) * 2.75);
	font: inherit;
	font-size: var(--text-sm);
	background: transparent;
	color: inherit;
	cursor: pointer;
	transition: border-color 120ms ease, background-color 120ms ease, color 120ms
		ease;
}

.bs-addon:hover {
	border-color: var(--color-ink);
}

/*
 * Chosen is a tint and a brand frame, not a flood.
 *
 * These sit three and four to a row beside the times below them, and a row of
 * solid brand blocks makes the screen's loudest mark the add-ons rather than
 * the appointment. The check keeps the state legible where the tint is not.
 */
.bs-addon[aria-pressed="true"] {
	border-color: var(--color-brand);
	background: color-mix(
		in srgb,
		var(--color-brand) 10%,
		var(--color-surface)
	);
	color: var(--color-ink);
	font-weight: 500;
}

.bs-addon__price {
	color: var(--color-ink-subtle);
	font-variant-numeric: tabular-nums;
}

.bs-addon[aria-pressed="true"] .bs-addon__price {
	color: inherit;
}

/* ---------- Step · 02: where ---------- */

/*
 * The places, side by side.
 *
 * Three is the common case and three fit across a phone, so the cards share
 * the row rather than stacking: a visitor choosing between the studio and
 * their own home is comparing two things, and comparison wants them next to
 * each other. `flex-basis` rather than a fixed track count so a provider with
 * five places wraps onto a second row instead of squeezing five onto one.
 */
.bs-locs {
	display: flex;
	flex-wrap: wrap;
	gap: calc(var(--space-unit) * 1.5);
	width: 100%;
}

.bs-locs > li {
	display: flex;
	flex: 1 1 5.5rem;
	min-width: 0;
}

/*
 * A place, as a card: the kind drawn above the name, and what it costs to
 * come to you under it. Chosen is a brand tint and a brand frame — the same
 * mark the times wear, because to a visitor it is the same act.
 */
.bs-loc {
	display: flex;
	flex-direction: column;
	align-items: center;
	justify-content: flex-start;
	gap: calc(var(--space-unit) * 2);
	width: 100%;
	border: var(--frame-width, 1px) solid var(--frame-color, var(--color-border));
	border-radius: var(--radius-sm);
	padding: calc(var(--space-unit) * 3.5) calc(var(--space-unit) * 2);
	font: inherit;
	text-align: center;
	background: transparent;
	color: inherit;
	cursor: pointer;
	transition: border-color 120ms ease, background-color 120ms ease, color 120ms
		ease;
}

.bs-loc:hover {
	border-color: var(--color-ink);
}

/*
 * Chosen wears the highlight, the same mark the times below it wear.
 *
 * The highlight rather than the brand because a look whose brand is its ink —
 * Studio's is near-black — tints to a grey that is hard to tell from the paper
 * behind a card already wearing a 2px ink frame. The highlight defaults to the
 * brand, so a look that never set one is unchanged.
 */
.bs-loc[aria-pressed="true"] {
	border-color: var(--color-highlight, var(--color-brand));
	background: color-mix(
		in srgb,
		var(--color-highlight, var(--color-brand)) 10%,
		var(--color-surface)
	);
}

.bs-loc__icon {
	display: grid;
	place-items: center;
	color: var(--color-ink);
}

.bs-loc__icon svg {
	inline-size: 1.75rem;
	block-size: 1.75rem;
}

.bs-loc__title {
	font-size: var(--text-sm);
	line-height: 1.25;
	text-wrap: balance;
}

.bs-loc[aria-pressed="true"] .bs-loc__title {
	font-weight: 500;
}

.bs-loc__sub,
.bs-loc__fee {
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size-fine, 0.64rem);
	letter-spacing: calc(var(--detail-tracking, 0.12em) * 0.5);
	color: var(--color-ink-subtle);
	line-height: 1.35;
	margin-block-start: calc(var(--space-unit) * -1);
}

.bs-loc__fee {
	font-variant-numeric: tabular-nums;
}

/* Where that place is, said under the cards with a pin in front of it. */
.bs-loc__address {
	display: flex;
	align-items: flex-start;
	gap: calc(var(--space-unit) * 2);
	font-size: var(--text-sm);
	color: var(--color-ink-muted);
	text-wrap: pretty;
}

.bs-loc__pin {
	inline-size: 1.05em;
	block-size: 1.05em;
	flex-shrink: 0;
	margin-block-start: 0.2em;
	color: var(--color-ink-subtle);
}

/* ---------- Step · 03: when ---------- */

.bs-times__body {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 5);
	width: 100%;
}

/*
 * "See all dates" on the left, the clock the times are told in on the right.
 *
 * Both are about the list below rather than about the heading above it, so
 * they share a line under the heading instead of crowding onto it — which is
 * also the only way the zone survives a narrow phone, where a heading, a
 * control and a label on one row wrap into three.
 */
.bs-times__controls {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: calc(var(--space-unit) * 4);
	width: 100%;
	margin-block-start: calc(var(--space-unit) * -1);
}

.bs-times__zone {
	color: var(--color-ink-subtle);
	text-align: end;
}

.bs-days {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 5);
	width: 100%;
}

.bs-day {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 2);
}

/* "Today · Oct 12 ······ 7 open": the heading and the count on one line. */
.bs-day__head {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: calc(var(--space-unit) * 3);
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size-fine, 0.64rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: calc(var(--detail-tracking, 0.12em) * 1.4);
	text-transform: uppercase;
}

.bs-day__heading {
	font: inherit;
	letter-spacing: inherit;
	color: var(--color-ink);
}

.bs-day__count {
	color: var(--color-ink-subtle);
	font-variant-numeric: tabular-nums;
	white-space: nowrap;
}

/*
 * The times, on a grid rather than a wrapping row.
 *
 * A day's openings are a set of equals, and a flex wrap gives the last one of
 * an odd row a different width from every other — which reads as a difference
 * between the times. Four columns across a phone is what the design draws,
 * and `auto-fill` widens the count on a screen with room for it.
 */
.bs-day__times {
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(min(100%, 4.5rem), 1fr));
	gap: calc(var(--space-unit) * 1.5);
}

/*
 * A time a visitor can take.
 *
 * Framed like the cards above it, filled with a brand tint when chosen: the
 * mark says which time is about to be booked without turning one square of
 * the grid into the loudest thing on the page. Also the reschedule grid's
 * chip, where it books on the spot.
 */
.bs-slot {
	min-block-size: 2.75rem;
	min-inline-size: 4.5rem;
	border: var(--frame-width, 1px) solid var(--frame-color, var(--color-border));
	border-radius: var(--radius-sm);
	padding: calc(var(--space-unit) * 1.5) calc(var(--space-unit) * 2);
	font: inherit;
	font-size: var(--text-sm);
	font-variant-numeric: tabular-nums;
	background: transparent;
	color: inherit;
	cursor: pointer;
	transition: border-color 120ms ease, background-color 120ms ease, color 120ms
		ease;
}

.bs-slot:hover {
	border-color: var(--color-ink);
}

.bs-slot[aria-pressed="true"] {
	border-color: var(--color-highlight, var(--color-brand));
	background: color-mix(
		in srgb,
		var(--color-highlight, var(--color-brand)) 15%,
		var(--color-surface)
	);
	color: var(--color-ink);
	font-weight: 500;
}

.bs-slot:disabled {
	cursor: default;
	opacity: 0.5;
}

/* "20m in the chair · 9:45 to 10:05" — the choice, read back. */
.bs-times__chosen {
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size-data, 0.82rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: calc(var(--detail-tracking, 0.12em) * 0.5);
	color: var(--color-ink);
	font-variant-numeric: tabular-nums;
}

/* ---------- the bar ---------- */

/*
 * The one write on the screen, kept in reach.
 *
 * Sticks to the bottom of the viewport while its screen is in view and stops
 * at the screen's end, so it never sits on top of the footer. The same
 * translucent wash and blur as the site's own header, because it is the same
 * kind of thing: chrome that rides over content and must stay legible against
 * whatever passes beneath it.
 */
.bs-bar {
	position: sticky;
	inset-block-end: 0;
	z-index: 5;
	display: flex;
	flex-direction: column;
	align-items: stretch;
	gap: calc(var(--space-unit) * 2.5);
	width: 100%;
	border-block-start: 1px solid var(--color-border);
	background: color-mix(in srgb, var(--color-surface) 86%, transparent);
	-webkit-backdrop-filter: blur(14px);
	backdrop-filter: blur(14px);
	padding-block: calc(var(--space-unit) * 3);
	/* Clear the iPhone home indicator when the bar is at the very bottom. */
	padding-block-end: calc(var(--space-unit) * 3 + env(safe-area-inset-bottom, 0px));
}

/*
 * What the visit adds up to, said across the bar rather than stacked in its
 * corner: "Appointment" is the label for the number, and a label above its
 * own value in a two-column bar is read as a heading for the button beside it.
 */
.bs-bar__body {
	display: flex;
	align-items: baseline;
	justify-content: space-between;
	gap: calc(var(--space-unit) * 4);
	min-width: 0;
}

.bs-bar__label {
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.72rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: var(--detail-tracking, 0.12em);
	text-transform: uppercase;
	color: var(--color-ink-subtle);
	flex-shrink: 0;
}

.bs-bar__meta {
	font-size: var(--text-sm);
	font-variant-numeric: tabular-nums;
	text-align: end;
	text-wrap: balance;
}

/*
 * Continue is the width of the screen it ends.
 *
 * It is the only write on a page of reads, and the design gives it the whole
 * measure rather than a corner — which is also the only shape a thumb finds
 * without looking on a phone.
 */
.bs-bar__cta {
	width: 100%;
	min-block-size: 3.25rem;
}

/* Continue, before there is anything to continue with. */
.bs-bar__cta:disabled {
	opacity: 0.45;
	cursor: default;
}

/* ---------- booking as the page ---------- */

/*
 * The booking page: one column on a phone, two from a laptop up.
 *
 * The design draws this flow on a phone, where it is one scroll — service,
 * where, when — and that is what a phone gets. A wide screen has room to put
 * what the visit *is* beside what is *free*, which is the shape the Booka
 * app's own booking form uses: the visit stays in view, sticky, while the
 * times it produced are scrolled. Both are the same markup and the same
 * sequence; only the axis changes.
 */
.bs-section.bs-booking--page {
	padding-block: clamp(1.5rem, 3vw, 3rem) 0;
}

.bs-booking--page .bs-booking__page-shell {
	width: min(100% - calc(var(--space-unit) * 8), 44rem);
	margin-inline: auto;
}

.bs-booking__page-header {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 2);
	padding-block-end: calc(var(--space-unit) * 6);
}

/*
 * A heading only if the provider wrote one.
 *
 * Sized to the flow rather than to the page: somebody who pressed Book has
 * already been sold, and a display-sized "Book an appointment" over the first
 * step pushed the times most of a screen further down for no new information.
 */
.bs-booking__page-heading {
	font-size: calc(var(--text-lg) * 1.2);
	text-wrap: balance;
}

.bs-booking__page-intro {
	max-width: 52ch;
	font-size: var(--text-sm);
	color: var(--color-ink-muted);
	text-wrap: pretty;
}

/* The page is the card, so the screen loses its frame. */
.bs-booking--page .bs-flow {
	border: 0;
	border-radius: 0;
	background: transparent;
	padding: 0;
}

/*
 * The bar bleeds to the viewport's edges on a phone, where the column's
 * gutters would otherwise leave a strip of page scrolling past on either side
 * of a bar that is supposed to be the bottom of the screen.
 */
.bs-booking--page .bs-bar {
	margin-inline: calc(var(--space-unit) * -4);
	width: calc(100% + var(--space-unit) * 8);
	padding-inline: calc(var(--space-unit) * 4);
}

/* Room under the last screen so the bar's shadow line is never the page's. */
.bs-booking--page .bs-booking__slots,
.bs-booking--page .bs-booking__mine {
	padding-block-end: calc(var(--space-unit) * 8);
}

/*
 * The two panes, side by side on a wide screen.
 *
 * Only screen one has them: the later screens — verification, checkout, the
 * confirmation — are single questions, and splitting a single question across
 * two columns would be a layout looking for content. The bar spans both,
 * because it is the bottom of the whole screen rather than of either half.
 *
 * `--bs-flow-gap` carries the flow's own gap into the media query so the
 * column gap and the stacked gap stay one number.
 */
.bs-flow__pane {
	display: flex;
	flex-direction: column;
	gap: var(--bs-flow-gap, calc(var(--space-unit) * 8));
	width: 100%;
	min-width: 0;
}

@media (min-width: 64rem) {
	.bs-booking--page .bs-booking__page-shell {
		width: min(100% - calc(var(--space-unit) * 8), 66rem);
	}

	.bs-booking--page .bs-flow--availability {
		display: grid;
		grid-template-columns: minmax(0, 23rem) minmax(0, 1fr);
		column-gap: calc(var(--space-unit) * 14);
		row-gap: calc(var(--space-unit) * 8);
		align-items: start;
	}

	/*
	 * The visit stays put while the times scroll past it, the way the app's
	 * booking form holds the appointment in view. `top` clears the site's own
	 * sticky header, which is the only other thing on the page that rides
	 * over the content.
	 */
	.bs-booking--page .bs-flow__pane--visit {
		position: sticky;
		inset-block-start: calc(var(--space-unit) * 24);
	}

	.bs-booking--page .bs-bar {
		grid-column: 1 / -1;
		flex-direction: row;
		align-items: center;
		justify-content: space-between;
		gap: calc(var(--space-unit) * 6);
		margin-inline: 0;
		width: 100%;
		padding-inline: 0;
	}

	.bs-booking--page .bs-bar__body {
		flex: 1;
		justify-content: flex-start;
		gap: calc(var(--space-unit) * 3);
	}

	.bs-booking--page .bs-bar__meta {
		text-align: start;
	}

	.bs-booking--page .bs-bar__cta {
		width: auto;
		min-inline-size: 12rem;
	}
}

/* ---------- choosing, as one list ---------- */

/*
 * Every list of choices in the flow shares one anatomy.
 *
 * Services, service options, and saved cards are the same act — pick one of
 * these — so they are one component wearing three sets of contents, and a
 * visitor learns the interaction once. Rows sit flush with no border of their
 * own; a hairline separates them, hover fills them, and the fill is rounded so
 * the row still reads as something to press.
 */
.bs-rows {
	display: flex;
	flex-direction: column;
	width: 100%;
	list-style: none;
	padding: 0;
}

.bs-row {
	position: relative;
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: calc(var(--space-unit) * 4);
	width: 100%;
	min-block-size: 2.75rem;
	border: none;
	border-radius: var(--radius-md);
	background: transparent;
	color: inherit;
	font: inherit;
	text-align: start;
	padding: calc(var(--space-unit) * 3);
	cursor: pointer;
	transition: background-color 120ms ease, color 120ms ease;
}

/*
 * The hairline belongs to the gap between two rows, not to either row.
 *
 * Drawn as a pseudo-element inset from the edges so it stops short of the
 * rounded hover fill, and never under the first row — a line above the first
 * item is a border on the container pretending to be a divider.
 */
.bs-rows > li + li > .bs-row::before {
	content: "";
	position: absolute;
	inset-inline: calc(var(--space-unit) * 3);
	inset-block-start: 0;
	block-size: 1px;
	background: var(--color-border);
}

.bs-row:hover {
	background: var(--color-surface-alt);
}

.bs-row[aria-pressed="true"] {
	background: var(--color-brand-soft);
	color: var(--color-brand-strong);
}

/* A selected row hides the line under the next one, so the fill stays whole. */
.bs-row[aria-pressed="true"]::before,
.bs-rows > li:has(> .bs-row[aria-pressed="true"]) + li > .bs-row::before {
	opacity: 0;
}

.bs-row__body {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 0.5);
	min-width: 0;
}

.bs-row__name {
	font-weight: 600;
}

.bs-row__desc {
	font-size: var(--text-sm);
	color: var(--color-ink-muted);
	display: -webkit-box;
	-webkit-box-orient: vertical;
	-webkit-line-clamp: 2;
	overflow: hidden;
}

.bs-row[aria-pressed="true"] .bs-row__desc,
.bs-row[aria-pressed="true"] .bs-row__meta {
	color: inherit;
}

.bs-row__meta {
	display: flex;
	align-items: baseline;
	gap: calc(var(--space-unit) * 2);
	flex-shrink: 0;
	font-size: var(--text-sm);
	color: var(--color-ink-muted);
	font-variant-numeric: tabular-nums;
	text-align: end;
}

.bs-row__price {
	font-size: var(--text-base);
	font-weight: 600;
	color: var(--color-ink);
	white-space: nowrap;
}

.bs-row[aria-pressed="true"] .bs-row__price {
	color: inherit;
}

.bs-row__duration {
	white-space: nowrap;
}

/*
 * A service row stacks its price above its duration.
 *
 * Two numbers on one line at 390px is where the price starts eliding, and the
 * price is the one thing on this screen a visitor will not accept a guess at.
 */
.bs-rows--svc .bs-row__meta {
	flex-direction: column;
	align-items: flex-end;
	gap: calc(var(--space-unit) * 0.25);
}

/*
 * ...and it sits on the name's line, not the row's middle.
 *
 * At 390px a description wraps to two or three lines while the price stays
 * one, and a centred price then floats somewhere beside the description with
 * nothing to line up against. Top-aligned, every price in the list reads as a
 * column against the name it belongs to.
 */
.bs-rows--svc .bs-row {
	align-items: flex-start;
}

/*
 * A price on a service row is set in the heading voice, the way it is on the
 * service the visit already holds — the price list and the visit are the same
 * fact seen twice, and reading one in a serif and the other in a grotesk made
 * them look like two different kinds of number.
 */
.bs-rows--svc .bs-row__price {
	font-family: var(--font-heading);
	font-size: var(--text-lg);
	font-weight: var(--heading-weight, 600);
	line-height: 1.05;
}

/* The picker is a heading over one list, not a sequence of steps. */
.bs-flow--services {
	gap: calc(var(--space-unit) * 4);
}

/* ---------- verifying who is booking ---------- */

.bs-auth {
	width: 100%;
}

/* Phone or email: a two-cell track with a raised pill on the live choice. */
.bs-auth__switch {
	display: grid;
	grid-template-columns: repeat(2, minmax(0, 1fr));
	gap: calc(var(--space-unit) * 0.5);
	width: 100%;
	max-width: 20rem;
	border-radius: 999px;
	background: var(--color-surface-alt);
	padding: calc(var(--space-unit) * 0.75);
}

.bs-auth__mode {
	display: block;
}

.bs-auth__mode input {
	position: absolute;
	opacity: 0;
	pointer-events: none;
}

.bs-auth__mode span {
	display: block;
	text-align: center;
	font-size: var(--text-sm);
	font-weight: 500;
	color: var(--color-ink-muted);
	border-radius: 999px;
	padding: calc(var(--space-unit) * 1.25) calc(var(--space-unit) * 3);
	cursor: pointer;
	transition: background-color 120ms ease, color 120ms ease;
}

.bs-auth__mode input:checked + span {
	background: var(--color-surface);
	color: var(--color-ink);
	box-shadow: 0 1px 2px rgb(0 0 0 / 0.08);
}

.bs-auth__mode input:focus-visible + span {
	outline: 2px solid color-mix(in srgb, var(--color-brand) 45%, transparent);
	outline-offset: 1px;
}

.bs-auth__field {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 1);
	width: 100%;
}

.bs-auth__field > span {
	font-size: var(--text-sm);
	font-weight: 600;
}

.bs-auth__field input,
.bs-auth__field textarea {
	width: 100%;
	min-block-size: 2.75rem;
	border: 1px solid var(--color-border);
	border-radius: var(--radius-md);
	background: var(--color-surface);
	color: var(--color-ink);
	font: inherit;
	/* 16px floor: anything smaller and iOS zooms the page on focus. */
	font-size: max(var(--text-base), 1rem);
	padding: calc(var(--space-unit) * 2.5) calc(var(--space-unit) * 3);
	transition: border-color 120ms ease, box-shadow 120ms ease;
}

/*
 * The placeholder is text a visitor has to read, and the browser's own grey is
 * not a colour this theme chose — on a dark site it lands near-invisible.
 */
.bs-auth__field input::placeholder,
.bs-auth__field textarea::placeholder {
	color: var(--color-ink-subtle);
	opacity: 1;
}

.bs-auth__field input:focus,
.bs-auth__field textarea:focus {
	border-color: var(--color-brand);
	outline: 2px solid color-mix(in srgb, var(--color-brand) 25%, transparent);
	outline-offset: 1px;
}

/* A refused code says so on the field, not only in the sentence above it. */
.bs-auth__field input[aria-invalid="true"] {
	border-color: var(--color-danger);
}

/*
 * Six digits get room to be read back against the SMS they came from.
 *
 * Centred, because a short field with left-aligned text reads as a field that
 * ran out of room. The indent gives back the trailing letter-space that
 * tracking adds after the last digit, which would otherwise push the group
 * visibly left of centre.
 */
.bs-auth__code {
	font-size: var(--text-lg);
	letter-spacing: 0.35em;
	text-indent: 0.35em;
	text-align: center;
	font-variant-numeric: tabular-nums;
	max-width: 14ch;
}

.bs-auth__name {
	display: grid;
	/* Two columns while both fields can hold a name; one when they cannot. */
	grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr));
	gap: calc(var(--space-unit) * 3);
	width: 100%;
}

.bs-auth__legal {
	display: flex;
	align-items: flex-start;
	gap: calc(var(--space-unit) * 2.5);
	font-size: var(--text-sm);
	color: var(--color-ink-muted);
	cursor: pointer;
}

.bs-auth__legal input {
	margin-block-start: 0.25em;
	inline-size: 1.05em;
	block-size: 1.05em;
	flex-shrink: 0;
	accent-color: var(--color-brand);
	cursor: pointer;
}

/* Underlines belong under the text, not through its descenders. */
.bs-auth__legal a {
	text-underline-offset: 0.2em;
	text-decoration-thickness: 1px;
}

.bs-auth__reassure {
	font-size: var(--text-xs);
	color: var(--color-ink-subtle);
}

/*
 * A problem with what the visitor typed, in the theme's own danger colour.
 *
 * Boxed rather than bare so it reads as a state of the form instead of another
 * paragraph of body copy that happens to be red — and so that colour is not
 * the only thing carrying the message.
 */
.bs-auth__error {
	width: 100%;
	font-size: var(--text-sm);
	color: var(--color-danger);
	border-radius: var(--radius-md);
	background: var(--color-danger-soft);
	padding: calc(var(--space-unit) * 2.5) calc(var(--space-unit) * 3);
	text-wrap: pretty;
}
.bs-pay {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 1.5);
	width: 100%;
	border: none;
	padding: 0;
	margin: 0;
}

.bs-pay__legend {
	font-size: var(--text-xs);
	font-weight: 600;
	letter-spacing: 0.06em;
	text-transform: uppercase;
	color: var(--color-ink-subtle);
	padding: 0;
	margin-block-end: calc(var(--space-unit) * 1);
}

/*
 * A saved card is a choice like any other, so it wears the same row.
 *
 * The radio stays a real radio — it is the thing that makes one of these
 * exclusive, and replacing it with a drawn dot would mean reimplementing
 * keyboard behaviour the browser already gets right.
 */
.bs-pay__option {
	position: relative;
	display: flex;
	align-items: center;
	gap: calc(var(--space-unit) * 2.5);
	min-block-size: 2.75rem;
	border-radius: var(--radius-md);
	padding: calc(var(--space-unit) * 2.5) calc(var(--space-unit) * 3);
	font-size: var(--text-sm);
	cursor: pointer;
	transition: background-color 120ms ease, color 120ms ease;
}

.bs-pay__option + .bs-pay__option::before {
	content: "";
	position: absolute;
	inset-inline: calc(var(--space-unit) * 3);
	inset-block-start: calc(var(--space-unit) * -0.75);
	block-size: 1px;
	background: var(--color-border);
}

.bs-pay__option:hover {
	background: var(--color-surface-alt);
}

.bs-pay__option:has(input:checked) {
	background: var(--color-brand-soft);
	color: var(--color-brand-strong);
}

.bs-pay__option:has(input:checked)::before,
.bs-pay__option:has(input:checked) + .bs-pay__option::before {
	opacity: 0;
}

.bs-pay__mount {
	width: 100%;
	/* Room for Stripe's iframe to land in, so choosing "new card" does not
	   open an empty sliver that then jumps. */
	min-height: 3rem;
	margin-block-start: calc(var(--space-unit) * 1);
}

.bs-pay__error {
	font-size: var(--text-sm);
	color: var(--color-danger);
}

/* City, state, ZIP on one line where they fit; stacked where they don't. */
.bs-addr__grid {
	display: grid;
	grid-template-columns: minmax(0, 2fr) minmax(0, 1fr) minmax(0, 1fr);
	gap: calc(var(--space-unit) * 2);
	width: 100%;
}

/* ---------- checkout ---------- */

/*
 * What is being booked, in two lines, with the way back beside it.
 *
 * Sunken rather than framed, because on the booking page the screen is the
 * page and one framed card would be the only frame on it: a shift in surface
 * says "this part is settled" without drawing a box.
 */
/*
 * What was chosen, held above the checkout.
 *
 * The brand's own tint behind it, so the one block on this screen that reports
 * a decision already made looks unlike the fields below it that are still
 * asking. The tint alone rather than the canvas's tint-plus-3px-edge-rule: the
 * fill already separates it from the paper, and the bar bought nothing the
 * colour was not saying.
 */
.bs-summary {
	display: flex;
	align-items: center;
	justify-content: space-between;
	gap: calc(var(--space-unit) * 4);
	width: 100%;
	border-radius: var(--radius-sm);
	background: color-mix(
		in srgb,
		var(--color-brand) 8%,
		var(--color-surface)
	);
	padding: calc(var(--space-unit) * 4);
}

.bs-summary__body {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 1);
	min-width: 0;
}

.bs-summary__when {
	font-family: var(--font-heading);
	font-size: var(--text-lg);
	font-weight: var(--heading-weight, 600);
	letter-spacing: var(--heading-tracking, -0.01em);
	text-transform: var(--heading-transform, none);
	line-height: var(--heading-line, 1.15);
	font-variant-numeric: tabular-nums;
}

.bs-summary__where {
	font-size: var(--text-sm);
	color: var(--color-ink-muted);
	text-wrap: pretty;
}

/* The checkout's field sits closer to the summary it annotates. */
.bs-flow--review .bs-review__notes {
	margin-block-start: calc(var(--space-unit) * -3);
}

/*
 * The receipt: one line per service, per add-on, for travel, and the total.
 *
 * Hairlines between lines, tabular figures flush right, and the total in the
 * heading face — the one number a visitor is agreeing to, set in the voice the
 * page reserves for the things that matter.
 */
.bs-lines {
	display: flex;
	flex-direction: column;
	width: 100%;
	border-block-start: 1px solid var(--color-border);
}

.bs-line {
	display: flex;
	justify-content: space-between;
	align-items: baseline;
	gap: calc(var(--space-unit) * 4);
	border-block-end: 1px solid var(--color-border);
	padding-block: calc(var(--space-unit) * 3);
	font-size: var(--text-sm);
}

.bs-line dt {
	display: flex;
	align-items: baseline;
	gap: calc(var(--space-unit) * 2);
	min-width: 0;
}

.bs-line dd {
	text-align: end;
	font-variant-numeric: tabular-nums;
	flex-shrink: 0;
}

/* An add-on belongs to the service above it, and is indented to say so. */
.bs-line--addon dt {
	padding-inline-start: calc(var(--space-unit) * 4);
	color: var(--color-ink-muted);
}

.bs-line--fee dt {
	color: var(--color-ink-muted);
}

.bs-line__meta {
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.72rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: var(--detail-tracking, 0.12em);
	text-transform: uppercase;
	color: var(--color-ink-subtle);
}

.bs-line--total {
	border-block-end: 0;
	padding-block: calc(var(--space-unit) * 4) 0;
	font-family: var(--font-heading);
	font-size: var(--text-xl);
	font-weight: var(--heading-weight, 600);
	letter-spacing: var(--heading-tracking, -0.01em);
	text-transform: var(--heading-transform, none);
	line-height: var(--heading-line, 1.15);
}

/* The policy, said once as a sentence before it is listed as terms. */
.bs-policy {
	font-size: var(--text-sm);
	color: var(--color-ink-muted);
	max-width: 52ch;
	text-wrap: pretty;
}

/* ---------- done ---------- */

/*
 * The confirmation reads like a ticket: the date is the heading, because it
 * is the one thing the visitor will need again, and everything under it is
 * there to make sure they turn up in the right place.
 */
.bs-flow--done {
	gap: calc(var(--space-unit) * 5);
}

.bs-flow--done .bs-kicker {
	color: var(--color-brand-strong);
}

.bs-done__when {
	font-size: var(--text-2xl);
	font-variant-numeric: tabular-nums;
	margin-block-start: calc(var(--space-unit) * -2);
}

.bs-done__what {
	color: var(--color-ink-muted);
	margin-block-start: calc(var(--space-unit) * -3);
	text-wrap: pretty;
}

/* Where to be: the address card, framed like the location cards were. */
.bs-where {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 2);
	width: 100%;
	border: var(--frame-width, 1px) solid var(--frame-color, var(--color-border));
	border-radius: var(--radius-md);
	padding: calc(var(--space-unit) * 4);
}

.bs-where__title {
	font-family: var(--font-detail, var(--font-body));
	font-size: var(--detail-size, 0.72rem);
	font-weight: var(--detail-weight, 500);
	letter-spacing: var(--detail-tracking, 0.12em);
	text-transform: uppercase;
	color: var(--color-ink-subtle);
}

.bs-where__line {
	line-height: 1.5;
}

.bs-where__note {
	font-size: var(--text-sm);
	color: var(--color-ink-muted);
	text-wrap: pretty;
}

.bs-done__actions {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: calc(var(--space-unit) * 4);
	width: 100%;
}

/*
 * The .ics needs a script to become a download, so it appears only once the
 * runtime has said it is running. The Google link beside it is a plain link
 * and works from the snapshot's markup alone.
 */
.bs-done__ics {
	display: none;
}

html[data-bs-runtime="ready"] .bs-done__ics {
	display: inline-flex;
}

/* "Before your service": what the provider asks of the visitor, when anything. */
.bs-prep {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 2);
	width: 100%;
	border-block-start: 1px solid var(--color-border);
	padding-block-start: calc(var(--space-unit) * 5);
}

.bs-prep p {
	color: var(--color-ink-muted);
	text-wrap: pretty;
}

/* A management step that finished still gets its mark. */
.bs-flow--done .bs-flow__badge {
	display: grid;
	place-items: center;
	inline-size: calc(var(--space-unit) * 12);
	block-size: calc(var(--space-unit) * 12);
	border-radius: 999px;
	background: var(--color-brand-soft);
	color: var(--color-brand-strong);
}

.bs-flow--done .bs-flow__badge svg {
	inline-size: 55%;
	block-size: 55%;
}

/* The optional note, sized for a sentence rather than an essay. */
.bs-review__notes textarea {
	resize: vertical;
	min-block-size: 4.25rem;
	line-height: 1.5;
}

.bs-review__optional {
	font-style: normal;
	font-weight: 400;
	color: var(--color-ink-subtle);
}

/*
 * The provider's fees, and the agreement that acknowledges them.
 *
 * Quietly set apart on the alt surface rather than boxed in the danger colour:
 * a cancellation fee is ordinary business, and painting it as a warning would
 * make every checkout read like a mistake was already made. The amounts sit
 * flush right, tabular, because they are the part a visitor scans for.
 */
.bs-fees {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 2.5);
	width: 100%;
	border-radius: var(--radius-md);
	background: var(--color-surface-alt);
	padding: calc(var(--space-unit) * 3) calc(var(--space-unit) * 3.5);
}

.bs-fees__list {
	display: flex;
	flex-direction: column;
	gap: calc(var(--space-unit) * 1.25);
}

.bs-fees__row {
	display: flex;
	align-items: baseline;
	gap: calc(var(--space-unit) * 2);
	font-size: var(--text-sm);
}

.bs-fees__label {
	font-weight: 600;
}

.bs-fees__window {
	color: var(--color-ink-subtle);
	font-size: var(--text-xs);
}

.bs-fees__amount {
	margin-inline-start: auto;
	font-variant-numeric: tabular-nums;
	font-weight: 600;
}

/*
 * The one message a visitor sees when an action fails and the runtime could not
 * put a notice on screen itself.
 *
 * The fragment on screen is still the one they were looking at and the button
 * still works, so this says "that did not go through" rather than replacing a
 * booking flow with an apology. Retrying is safe: the same idempotency key goes
 * back, and the server has either recorded what it did or has nothing to
 * duplicate.
 */
.bs-island[data-action-error="true"]:not(:has([data-island-notice]))::before {
	content: "That didn’t go through. Please try again.";
	display: block;
	margin-block-end: calc(var(--space-unit) * 3);
	font-size: var(--text-sm);
	color: var(--color-danger);
	border-radius: var(--radius-md);
	background: var(--color-danger-soft);
	padding: calc(var(--space-unit) * 2.5) calc(var(--space-unit) * 3);
}

/* ---------- a customer's own appointments ---------- */

/*
 * The same list anatomy as everywhere else in the flow.
 *
 * This island used to render a bare stack of bordered boxes with no shell,
 * which made the one screen a returning customer sees the only one that did not
 * look like the rest of the site's booking.
 */
.bs-bookings {
	display: flex;
	flex-direction: column;
	width: 100%;
	border: 1px solid var(--color-border);
	border-radius: var(--radius-lg);
	background: var(--color-surface);
	padding: calc(var(--space-unit) * 2);
	list-style: none;
}

.bs-bookings__item {
	position: relative;
	display: flex;
	align-items: center;
	gap: calc(var(--space-unit) * 3);
	flex-wrap: wrap;
	padding: calc(var(--space-unit) * 3);
}

.bs-bookings__item + .bs-bookings__item::before {
	content: "";
	position: absolute;
	inset-inline: calc(var(--space-unit) * 3);
	inset-block-start: 0;
	block-size: 1px;
	background: var(--color-border);
}

.bs-bookings__detail {
	flex: 1 1 12rem;
	min-width: 0;
}

.bs-bookings__service {
	font-weight: 600;
}

.bs-bookings__when {
	font-size: var(--text-sm);
	color: var(--color-ink-muted);
	font-variant-numeric: tabular-nums;
}

.bs-bookings__status {
	font-size: var(--text-xs);
	font-weight: 600;
	color: var(--color-ink-muted);
	text-transform: capitalize;
	border: 1px solid var(--color-border);
	border-radius: 999px;
	padding: calc(var(--space-unit) * 0.75) calc(var(--space-unit) * 2);
}

.bs-bookings__status--confirmed {
	color: var(--color-brand-strong);
	border-color: var(--color-brand-muted);
	background: var(--color-brand-soft);
}

/*
 * Cancelling is quiet.
 *
 * It was a full button, which made the loudest thing on a list of appointments
 * the one action nobody arrives wanting. It stays a real, reachable control at
 * a full tap target — it just stops shouting, and only answers in the danger
 * colour once the pointer is on it.
 */
.bs-bookings__cancel {
	display: inline-flex;
	align-items: center;
	min-block-size: 2.75rem;
	padding-inline: calc(var(--space-unit) * 2);
	border: none;
	background: none;
	border-radius: var(--radius-md);
	font: inherit;
	font-size: var(--text-sm);
	font-weight: 500;
	color: var(--color-ink-subtle);
	cursor: pointer;
	transition: color 120ms ease, background-color 120ms ease;
	/*
	 * The padding is the hover fill's, not the text's. Pulling it back off the
	 * end lines this row's last word up with the status pill on the row below,
	 * which is the only right edge the eye is tracking down the list.
	 */
	margin-inline-end: calc(var(--space-unit) * -2);
}

.bs-bookings__cancel:hover {
	color: var(--color-danger);
	background: var(--color-danger-soft);
}

/*
 * An aside inside a step: the timezone the times are in, the address that is
 * coming, the fee a change may carry. Quiet by default; the fee variant keeps
 * the ink at full strength because it names money.
 */
.bs-flow__note {
	font-size: var(--text-sm);
	color: var(--color-ink-subtle);
}

.bs-flow__note--fee {
	color: var(--color-ink);
	background: var(--color-surface-alt);
	border-radius: var(--radius-md);
	padding: calc(var(--space-unit) * 2) calc(var(--space-unit) * 3);
}

/* Destructive commitment: solid, like every other destructive commit here. */
.bs-btn--danger {
	background: var(--color-danger);
	color: var(--color-surface);
}

.bs-btn--danger:hover {
	filter: brightness(0.92);
}

/* The row's verbs, gathered after the status so the name keeps the lead. */
.bs-bookings__actions {
	display: flex;
	align-items: center;
	flex-wrap: wrap;
	gap: calc(var(--space-unit) * 0.5);
}

.bs-bookings__action {
	display: inline-flex;
	align-items: center;
	min-block-size: 2.75rem;
	padding-inline: calc(var(--space-unit) * 2);
	border: none;
	background: none;
	border-radius: var(--radius-md);
	font: inherit;
	font-size: var(--text-sm);
	font-weight: 500;
	color: var(--color-ink-muted);
	cursor: pointer;
	transition: color 120ms ease, background-color 120ms ease;
}

.bs-bookings__action:hover {
	color: var(--color-brand-strong);
	background: var(--color-brand-soft);
}

/* ---------- tipping ---------- */

.bs-tips {
	display: grid;
	grid-template-columns: repeat(3, minmax(0, 1fr));
	gap: calc(var(--space-unit) * 2);
	width: 100%;
}

/*
 * Three cards, percent over price, because a tip is chosen by feel and paid
 * in dollars — both numbers have to be on the button.
 */
.bs-tip {
	display: flex;
	flex-direction: column;
	align-items: center;
	gap: calc(var(--space-unit) * 0.5);
	width: 100%;
	border: 1px solid var(--color-border);
	border-radius: var(--radius-md);
	background: var(--color-surface);
	color: inherit;
	font: inherit;
	padding: calc(var(--space-unit) * 3) calc(var(--space-unit) * 2);
	cursor: pointer;
	transition: border-color 120ms ease, background-color 120ms ease;
}

.bs-tip:hover {
	border-color: var(--color-brand);
	background: var(--color-brand-soft);
}

.bs-tip__percent {
	font-weight: 600;
	font-size: var(--text-lg);
	font-variant-numeric: tabular-nums;
}

.bs-tip__amount {
	font-size: var(--text-sm);
	color: var(--color-ink-muted);
	font-variant-numeric: tabular-nums;
}

/* ---------- small screens ---------- */

@media (max-width: 30rem) {
	.bs-addr__grid {
		grid-template-columns: minmax(0, 1fr);
	}

	.bs-calendar {
		padding: calc(var(--space-unit) * 3);
	}

	.bs-calendar__weekdays,
	.bs-calendar__days {
		gap: calc(var(--space-unit) * 0.5);
	}

	/* The heading keeps its line; the control drops under it. */
	.bs-step__row {
		flex-direction: column;
		align-items: flex-start;
		gap: calc(var(--space-unit) * 2);
	}

	/* Every location card gets the full width once two cannot share a row. */
	.bs-locs {
		grid-template-columns: minmax(0, 1fr);
	}

	.bs-bar__cta {
		min-inline-size: 7.5rem;
	}
}

/*
 * There is deliberately no scroll reveal on these pages.
 *
 * `animation-timeline: view()` was tried and removed. It is the right shape on
 * paper — no script, nothing to hydrate, and it satisfies the `script-src
 * 'self'` policy published sites serve under. But a scroll-driven animation is
 * *scrubbed* rather than fired once: an element outside the range sits at its
 * `from` state permanently, so with `fill: both` every section below the fold
 * is at `opacity: 0` until the viewport reaches it. Anything that renders the
 * page without scrolling it — a full-page screenshot, a PDF export, a social
 * preview, print — gets a blank page under the hero. That was reproduced, not
 * theorised.
 *
 * A reveal has to be one-shot and idempotent to be safe here, which means an
 * IntersectionObserver adding a class that is never removed, gated so the
 * hiding rule only exists once the script is known to be running. That is a
 * static script file this stylesheet does not currently have a partner for.
 * Until then the hero's own entrance is the page's motion, and it is safe by
 * construction: it is time-based, it finishes, and it leaves content visible.
 */
