/* ---------- 00-fonts.css ---------- */
/* Self-hosted variable fonts (latin subset from Google Fonts).
   Self-hosted rather than linked: removes two third-party connections,
   makes <link rel="preload"> actually effective, works offline. */

@font-face {
  font-family: 'Inter';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/assets/fonts/inter-latin.woff2') format('woff2');
}

@font-face {
  font-family: 'Fraunces';
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url('/assets/fonts/fraunces-latin.woff2') format('woff2');
}

/* ---------- 01-tokens.css ---------- */
/* Design tokens — the single source of truth.
   Components never hardcode a colour, spacing step, duration or radius. */

:root {
  color-scheme: light dark;
  interpolate-size: allow-keywords; /* lets height:auto animate in the FAQ */

  /* --- Typography --- */
  --font-display: 'Fraunces', Georgia, 'Times New Roman', serif;
  --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;

  --text-xs: 0.75rem;    /* 12px — labels, badges */
  --text-sm: 0.875rem;   /* 14px — supporting text */
  --text-base: 1rem;     /* 16px — body */
  --text-lg: 1.25rem;    /* 20px — lead paragraph */
  --text-xl: 1.5rem;     /* 24px — H4 */
  --text-2xl: 1.953rem;  /* 31px — H3 */
  --text-3xl: 2.441rem;  /* 39px — figures and the calculator total */
  /* 52px against a 16px body is a ratio of 3.25. Below 3.0 a heading stops
     reading as a command and becomes slightly larger text, and the eye has
     nothing to catch on; above 56px on this face a two-line heading starts
     breaking badly at 390px. */
  --text-h2: 3.25rem;
  --text-4xl: 3.815rem;  /* 61px — H1 */

  --leading-tight: 1.1;
  --leading-body: 1.6;
  --tracking-tight: -0.02em;
  --tracking-caps: 0.08em;
  --measure: 65ch;

  /* --- Spacing, 4px base --- */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-12: 3rem;
  --space-16: 4rem;
  --space-24: 6rem;
  --space-32: 8rem;

  /* --- Layout --- */
  --container: 1200px;
  --container-wide: 1360px; /* header bar only — see 04-header.css */
  --gutter: var(--space-6);
  --header-height: 4.5rem;

  /* --- Colour: light ---
     Lightened at T-56. The accent moved from L 25% to L 30% with a little more
     saturation — +20% relative, the top of the 15–20% the brief asked for and
     the most it can take: this one colour is both a text colour and a fill
     under white labels, so lifting it further pulls those two pairs down from
     opposite sides. Measured at 18%, four pairs landed between 3.58 and 3.88.
     The grounds went up with it, and the ink did not: lightening text is how a
     palette gets lightened into a contrast failure. */
  --ink: #14171a;
  --ink-muted: #5a6169;
  --line: #ebedef;

  /* The boundary of a control, as distinct from the hairline between blocks.
     --line measures 1.20:1 against the page in light and 1.40:1 in dark: fine
     for a card edge, invisible on a button, which is what this token exists to
     fix (WCAG 1.4.11 asks 3:1 of a control's boundary).

     A share of the current text colour rather than a grey of its own, because
     the same button sits on the page, on a soft band, on the dark CTA band and
     over a photograph, and a fixed grey can only be right on one of them.
     Measured at 50%: 3.36:1 on the page in light, 4.82:1 in dark, higher
     everywhere else. It stays a hairline — the filled button still leads. */
  /* Engines without color-mix keep the first value. It cannot follow the text
     colour the way the mix does, so it is a middle grey that clears 3:1 on both
     of this site's grounds — worse than the real thing, and not invisible,
     which is the whole point of the token. */
  --control-line: rgb(118 124 130);
  --surface: #ffffff;
  --bg: #fdfdfb;

  --accent: #22775b;
  --accent-hover: #186249;
  --accent-soft: #eff8f5;
  --accent-contrast: #ffffff;

  --success: #2f7d5e;
  --danger: #b3261e;

  --overlay: rgb(20 23 26 / 0.45);

  /* --- Elevation --- */
  --shadow-sm: 0 1px 2px rgb(20 23 26 / 0.04);
  --shadow-md: 0 4px 16px rgb(20 23 26 / 0.06);
  --shadow-lg: 0 12px 32px rgb(20 23 26 / 0.08);

  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 16px;
  --radius-full: 999px;

  /* Depth of a diagonal section edge. Kept under --space-12 (3rem) on purpose:
     the wedge reaches into the neighbouring section's padding, never into its
     text, and --space-12 is the smallest padding any section uses. */
  --divider-h: clamp(1.25rem, 2.5vw, 2.5rem);

  /* Surface film. Two values, because one cannot serve both grounds: measured
     at a single strength the same noise moved 5 luminance levels on a light
     band and 9 on a dark one, which is 0.3% of the light mean against 4.5% of
     the dark one. The eye reads the ratio, so the darker theme gets less. */
  --noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.12'/%3E%3C/svg%3E");

  /* --- Motion --- */
  --dur-fast: 120ms;
  --dur-base: 240ms;
  --dur-slow: 420ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* The real value, wherever the engine can compute it. It has to live outside
   :root as its own block: a custom property accepts any token sequence, so a
   color-mix() an engine cannot parse is stored happily and only fails later, in
   whatever declaration uses it. @supports is the only way to ask first. */
@supports (color: color-mix(in srgb, currentColor 50%, transparent)) {
  :root {
    --control-line: color-mix(in srgb, currentColor 50%, transparent);
  }
}

/* Dark theme — token overrides only, never duplicated component rules.
   Follows the OS by default; [data-theme] on <html> forces either mode. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme='light']) {
    --noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
    --ink: #eef1f3;
    --ink-muted: #a6aeb6;
    --line: #2f363d;
    --surface: #191e22;
    --bg: #12161a;

    --accent: #82ceb5;
    --accent-hover: #9cdec8;
    --accent-soft: #1d342d;
    --accent-contrast: #0c1114;

    --success: #82ceb5;
    --danger: #f2857c;

    --overlay: rgb(4 6 8 / 0.6);

    --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.4);
    --shadow-md: 0 4px 16px rgb(0 0 0 / 0.45);
    --shadow-lg: 0 12px 32px rgb(0 0 0 / 0.5);
  }
}

:root[data-theme='dark'] {
  --noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='0.04'/%3E%3C/svg%3E");
  --ink: #eef1f3;
  --ink-muted: #a6aeb6;
  --line: #2f363d;
  --surface: #191e22;
  --bg: #12161a;

  --accent: #82ceb5;
  --accent-hover: #9cdec8;
  --accent-soft: #1d342d;
  --accent-contrast: #0c1114;

  --success: #82ceb5;
  --danger: #f2857c;

  --overlay: rgb(4 6 8 / 0.6);

  --shadow-sm: 0 1px 2px rgb(0 0 0 / 0.4);
  --shadow-md: 0 4px 16px rgb(0 0 0 / 0.45);
  --shadow-lg: 0 12px 32px rgb(0 0 0 / 0.5);
}

/* ---------- 02-base.css ---------- */
/* Reset + base elements + layout primitives. */

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

* {
  margin: 0;
}

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* keeps anchor targets clear of the sticky header */
  scroll-padding-top: calc(var(--header-height) + var(--space-4));
}

body {
  display: flex;
  flex-direction: column;
  min-height: 100vh; /* fallback for anything without dvh */
  min-height: 100dvh; /* 100vh on mobile Safari counts the chrome that is not there */
  background-color: var(--bg);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-body);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

body.has-drawer-open {
  overflow: hidden;
}

main {
  flex: 1 0 auto;
}

/* Author display rules outrank the UA sheet's [hidden] { display: none },
   so any component styled display:flex/grid stayed visible while hidden.
   One guard here beats remembering it in every component. */
[hidden] {
  display: none !important;
}

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

input,
button,
textarea,
select {
  font: inherit;
  color: inherit;
}

button {
  cursor: pointer;
}

ul,
ol {
  padding: 0;
}

:where(h1, h2, h3, h4) {
  font-family: var(--font-display);
  font-weight: 600;
  line-height: var(--leading-tight);
  text-wrap: balance;
}

h1 {
  /* clamped between two scale steps so 61px never lands on a 360px screen */
  font-size: clamp(var(--text-3xl), 6vw, var(--text-4xl));
  letter-spacing: var(--tracking-tight);
}

h2 {
  font-size: clamp(var(--text-2xl), 4vw, var(--text-h2));
  letter-spacing: var(--tracking-tight);
}

h3 {
  font-size: var(--text-2xl);
}

h4 {
  font-size: var(--text-xl);
}

p,
li {
  text-wrap: pretty;
}

:where(p, ul, ol, dl):not([class]) {
  max-width: var(--measure);
}

a {
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
  transition: color var(--dur-fast) var(--ease-in-out);
}

a:hover {
  color: var(--accent-hover);
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: var(--radius-sm);
}

hr {
  border: 0;
  border-top: 1px solid var(--line);
}

::selection {
  background: var(--accent-soft);
  color: var(--ink);
}

/* --- Layout primitives --- */

.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}

.section {
  padding-block: var(--space-16);
}

.section--tight {
  padding-block: var(--space-12);
}

.section--soft {
  background-color: var(--accent-soft);
}

/* Vertical rhythm between neighbouring blocks inside a section. Components
   (tables, checklists, card grids, prose) carry no outer margin of their own,
   so two of them in a row used to touch. Sibling margins collapse, so a
   component that already sets its own spacing keeps the larger value. */
.section > .container > * + *,
.hero > .container > * + * {
  margin-block-start: var(--space-8);
}

.stack > * + * {
  margin-block-start: var(--space-4);
}

.stack-lg > * + * {
  margin-block-start: var(--space-8);
}

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

.lead {
  font-size: var(--text-lg);
  color: var(--ink-muted);
  max-width: 55ch;
}

.eyebrow {
  display: inline-block;
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--accent);
}

.muted {
  color: var(--ink-muted);
}

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

.skip-link {
  position: absolute;
  z-index: 100;
  inset-inline-start: var(--space-4);
  inset-block-start: var(--space-4);
  padding: var(--space-3) var(--space-4);
  background: var(--surface);
  color: var(--ink);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-weight: 600;
  text-decoration: none;
  transform: translateY(-200%);
  transition: transform var(--dur-fast) var(--ease-out);
}

.skip-link:focus-visible {
  transform: none;
}

@media (min-width: 48em) {
  :root {
    --gutter: var(--space-8);
  }

  .section {
    padding-block: var(--space-24);
  }

  .section--tight {
    padding-block: var(--space-16);
  }
}

@media (min-width: 64em) {
  .section {
    padding-block: var(--space-32);
  }
}

/* Motion off: content must stay visible, never stuck at opacity 0. */
@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;
    scroll-behavior: auto !important;
  }
}

/* --- Surface texture ---
   A film of noise over every band, and a slow radial light behind a few of
   them. Neither is meant to be noticed: the test is that the background stops
   reading as a flat fill while nobody can say why.

   It is painted as a background layer on the section itself, not on a
   pseudo-element. Two reasons, both learned the hard way: .section--soft
   already spends ::before and ::after on its diagonal edges, and giving every
   section position: relative would change the painting order those edges
   depend on — a wedge only covers the block below it because that block is
   not positioned.

   The noise is one inline SVG, url-encoded rather than base64 so it still
   compresses, with its strength baked in. Dark grounds take a different value
   than light ones: the same film reads louder against dark, measured, not
   assumed. */


.hero,
.section,
.trust-bar,
.cta-band {
  background-image: var(--noise);
  background-size: 140px 140px;
}

/* --- Radial light ---
   Behind the sections a visitor is meant to stop at. Never behind a section
   that already carries a tile group or a ghost: the skill allows two moves and
   those have spent the second one. */

.section--lit {
  background-image:
    radial-gradient(60rem 34rem at 78% 0%,
      color-mix(in srgb, var(--accent) 5%, transparent) 0%,
      transparent 70%),
    var(--noise);
  background-size: auto, 140px 140px;
}

/* ---------- 03-components.css ---------- */
/* Shared components: buttons, cards, badges. */

/* --- Buttons --- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 2.75rem; /* 44px touch target */
  padding: var(--space-3) var(--space-6);
  border: 1px solid transparent;
  border-radius: var(--radius-full);
  font-size: var(--text-sm);
  font-weight: 600;
  line-height: 1.2;
  text-align: center;
  text-decoration: none;
  transition: background-color var(--dur-fast) var(--ease-in-out),
    border-color var(--dur-fast) var(--ease-in-out),
    color var(--dur-fast) var(--ease-in-out),
    transform var(--dur-fast) var(--ease-out);
}

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

.btn:disabled,
.btn[aria-disabled='true'] {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
}

.btn--primary {
  background-color: var(--accent);
  color: var(--accent-contrast);
}

.btn--primary:hover {
  background-color: var(--accent-hover);
  color: var(--accent-contrast);
}

.btn--secondary {
  background-color: transparent;
  border-color: var(--control-line);
  color: var(--ink);
}

.btn--secondary:hover {
  border-color: var(--accent);
  color: var(--accent);
}

.btn--lg {
  min-height: 3.25rem;
  padding: var(--space-4) var(--space-8);
  font-size: var(--text-base);
}

.btn--block {
  width: 100%;
}

/* --- Cards --- */

.card {
  /* contains .visually-hidden helpers: without a positioned ancestor they
     resolve against the page and push its scroll width out (see T-13F) */
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  height: 100%;
  padding: var(--space-8);
  background-color: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  /* hover and its transition live in 10-motion.css, with the rest of the states */
}

.card__title {
  font-size: var(--text-xl);
}

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

/* --- Badge --- */

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-1) var(--space-3);
  background-color: var(--accent-soft);
  color: var(--accent);
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
}

/* --- Inline contact link --- */

.link-plain {
  color: inherit;
  text-decoration: none;
}

.link-plain:hover {
  color: var(--accent);
  text-decoration: underline;
}

/* ---------- 04-header.css ---------- */
/* Site header: brand, primary nav with dropdowns, phone, CTA, mobile drawer.
   Mobile first — the desktop bar starts at 1100px, where all eight nav items
   plus phone and CTA finally fit on one line without crowding. */

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background-color: var(--bg);
  border-block-end: 1px solid transparent;
  transition: transform var(--dur-base) var(--ease-out),
    border-color var(--dur-fast) var(--ease-in-out),
    background-color var(--dur-fast) var(--ease-in-out);
}

/* past the fold the header needs to separate itself from the content */
.site-header.is-scrolled {
  border-block-end-color: var(--line);
  background-color: rgb(253 253 251 / 0.92);
  background-color: color-mix(in srgb, var(--bg) 92%, transparent);
  backdrop-filter: blur(8px);
}

/* scrolling down hides it, scrolling up brings it back */
.site-header.is-hidden {
  transform: translateY(-100%);
}

/* The bar carries brand + 8 nav items + phone + CTA: 1161px of content that
   does not fit the 1200px page container (1136px inside its padding). The
   header gets its own wider track instead of shrinking type or dropping the
   phone, which spec section 5 requires on desktop. */
.site-header__inner {
  max-width: var(--container-wide);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
  min-height: var(--header-height);
}

/* --- Brand --- */

.brand {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem; /* 44px hit area around a 28px mark */
  gap: var(--space-3);
  color: var(--ink);
  text-decoration: none;
  flex-shrink: 0;
}

.brand__mark {
  color: var(--accent);
  flex-shrink: 0;
}

.brand__name {
  display: block;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: var(--tracking-tight);
}

.brand__tagline {
  display: block;
  font-size: var(--text-xs);
  color: var(--ink-muted);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
}

/* --- Burger --- */

.burger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  background: none;
  border: 1px solid var(--control-line);
  border-radius: var(--radius-sm);
  color: var(--ink);
}

.burger__box {
  position: relative;
  display: block;
  width: 1.125rem;
  height: 0.75rem;
}

.burger__bar {
  position: absolute;
  inset-inline: 0;
  height: 2px;
  background-color: currentColor;
  border-radius: var(--radius-full);
  transition: transform var(--dur-base) var(--ease-out),
    opacity var(--dur-fast) var(--ease-in-out);
}

.burger__bar:nth-child(1) { top: 0; }
.burger__bar:nth-child(2) { top: 50%; transform: translateY(-50%); }
.burger__bar:nth-child(3) { bottom: 0; }

.burger[aria-expanded='true'] .burger__bar:nth-child(1) {
  transform: translateY(0.3125rem) rotate(45deg);
}

.burger[aria-expanded='true'] .burger__bar:nth-child(2) {
  opacity: 0;
}

.burger[aria-expanded='true'] .burger__bar:nth-child(3) {
  transform: translateY(-0.3125rem) rotate(-45deg);
}

/* --- Drawer (mobile) ---
   A <dialog> opened with showModal(): the browser puts it in the top layer,
   where its containing block is the viewport. The sticky header can therefore
   keep backdrop-filter and translateY — neither can capture the panel any
   more, which is what collapsed it to the header's own height before. */

.site-header__drawer {
  position: fixed;
  inset-block: 0;
  inset-inline-end: 0;
  inset-inline-start: auto;
  width: min(22rem, 88vw);
  max-width: none;
  max-height: none;
  height: 100dvh;
  margin: 0;
  padding: var(--space-6);
  overflow-y: auto;
  overscroll-behavior: contain;
  background-color: var(--surface);
  color: var(--ink);
  border: 0;
  border-inline-start: 1px solid var(--line);
  transform: translateX(100%);
  transition: transform var(--dur-base) var(--ease-out);
}

/* only [open] gets a box, so the UA rule still hides the closed dialog */
.site-header__drawer[open] {
  display: flex;
  flex-direction: column;
  gap: var(--space-8);
}

.site-header__drawer.is-open {
  transform: translateX(0);
}

/* ::backdrop did not inherit custom properties before Chrome 122 —
   the literal is the fallback, the token is used wherever it resolves */
.site-header__drawer::backdrop {
  background-color: var(--overlay, rgb(4 6 8 / 0.5));
  opacity: 0;
  transition: opacity var(--dur-base) var(--ease-in-out);
}

.site-header__drawer.is-open::backdrop {
  opacity: 1;
}

/* the burger sits behind the modal backdrop and is inert while it is open,
   so the drawer carries its own close control */
.drawer-close {
  align-self: flex-end;
  flex-shrink: 0; /* column flex shrinks height — the 44px target must not */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  background: none;
  border: 1px solid var(--control-line);
  border-radius: var(--radius-sm);
  color: var(--ink);
  transition: color var(--dur-fast) var(--ease-in-out),
    border-color var(--dur-fast) var(--ease-in-out);
}

.drawer-close:hover {
  color: var(--accent);
  border-color: var(--accent);
}

/* --- Nav --- */

.nav {
  flex-shrink: 0;
}

.nav__list,
.nav__sublist {
  list-style: none;
  margin: 0;
  padding: 0;
}

.nav__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
}

.nav__link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
  min-height: 2.75rem;
  padding: var(--space-2) var(--space-3);
  background: none;
  border: 0;
  border-radius: var(--radius-sm);
  color: var(--ink);
  font-size: var(--text-base);
  font-weight: 500;
  text-align: start;
  text-decoration: none;
  transition: color var(--dur-fast) var(--ease-in-out),
    background-color var(--dur-fast) var(--ease-in-out);
}

.nav__link:hover {
  color: var(--accent);
  background-color: var(--accent-soft);
}

.nav__link[aria-current='page'] {
  color: var(--accent);
  font-weight: 600;
}

.nav__chevron {
  flex-shrink: 0;
  transition: transform var(--dur-base) var(--ease-out);
}

.nav__item.is-open > .nav__link .nav__chevron {
  transform: rotate(180deg);
}

/* dropdown: animated with grid-template-rows, height itself is not animatable */
.nav__panel {
  display: grid;
  grid-template-rows: 0fr;
  visibility: hidden;
  transition: grid-template-rows var(--dur-base) var(--ease-out),
    visibility 0s linear var(--dur-base);
}

.nav__item.is-open > .nav__panel {
  grid-template-rows: 1fr;
  visibility: visible;
  transition: grid-template-rows var(--dur-base) var(--ease-out), visibility 0s;
}

.nav__sublist {
  min-height: 0;
  overflow: hidden;
}

.nav__sublink {
  display: flex;
  align-items: center;
  min-height: 2.75rem; /* 44px touch target */
  padding: var(--space-2) var(--space-3);
  margin-inline-start: var(--space-3);
  color: var(--ink-muted);
  font-size: var(--text-sm);
  text-decoration: none;
  border-radius: var(--radius-sm);
  transition: color var(--dur-fast) var(--ease-in-out),
    background-color var(--dur-fast) var(--ease-in-out);
}

.nav__sublink:hover,
.nav__sublink[aria-current='page'] {
  color: var(--accent);
  background-color: var(--accent-soft);
}

/* --- Header actions --- */

.site-header__actions {
  display: flex;
  flex-direction: column;
  flex-shrink: 0;
  gap: var(--space-3);
  padding-block-start: var(--space-6);
  border-block-start: 1px solid var(--line);
}

.header-phone {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 2.75rem; /* 44px touch target */
  color: var(--ink);
  font-weight: 600;
  font-size: var(--text-sm);
  text-decoration: none;
  white-space: nowrap;
}

.header-phone:hover {
  color: var(--accent);
}

.header-phone svg {
  color: var(--accent);
}

/* --- Desktop bar ---
   Starts at 1280px: the bar needs 1161px of content plus 64px of container
   padding = 1225px absolute minimum, and 1280 leaves 55px of cushion for
   fallback fonts and platform metric differences. Below that, the burger. */

@media (min-width: 80em) {
  .burger,
  .drawer-close {
    display: none;
  }

  /* the same element, now an ordinary flex row inside the header */
  .site-header__drawer {
    display: flex;
    position: static;
    flex-direction: row;
    align-items: center;
    justify-content: flex-end;
    gap: var(--space-8);
    width: auto;
    height: auto;
    padding: 0;
    overflow: visible;
    background: none;
    border: 0;
    transform: none;
    transition: none;
  }

  .nav__list {
    flex-direction: row;
    align-items: center;
    gap: 0; /* the links carry their own padding; extra gap costs 28px here */
  }

  .nav__item {
    position: relative;
  }

  .nav__link {
    min-height: 2.75rem;
    padding: var(--space-2);
    font-size: var(--text-sm);
    white-space: nowrap;
  }

  .nav__panel {
    position: absolute;
    inset-block-start: 100%;
    inset-inline-start: 0;
    display: block;
    min-width: 15rem;
    padding: var(--space-2);
    margin-block-start: var(--space-2);
    background-color: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--radius-md);
    box-shadow: var(--shadow-md);
    opacity: 0;
    transform: translateY(-6px);
    transition: opacity var(--dur-fast) var(--ease-out),
      transform var(--dur-base) var(--ease-out),
      visibility 0s linear var(--dur-base);
  }

  .nav__item.is-open > .nav__panel {
    opacity: 1;
    transform: none;
    transition: opacity var(--dur-fast) var(--ease-out),
      transform var(--dur-base) var(--ease-out), visibility 0s;
  }

  .nav__sublist {
    overflow: visible;
  }

  .nav__sublink {
    margin-inline-start: 0;
    white-space: nowrap;
  }

  .site-header__actions {
    flex-direction: row;
    align-items: center;
    gap: var(--space-4);
    padding-block-start: 0;
    border: 0;
  }
}

/* --- Theme toggle ---
   Hidden until the head bootstrap adds .js-theme. A control that cannot work
   without JavaScript must not be visible without it, and putting the class on
   <html> before the first paint means the bar is never re-laid out afterwards.

   One button, three states. The bar was measured full: brand 153 + nav 657 +
   phone + CTA left 39px free at 1280, and a 44px control needs 60px with its
   gap. Collapsing the phone to its icon on the bar frees 86px, which is what
   pays for this button. */

.theme-toggle {
  position: relative; /* holds the status helper inside — design system 6.1 */
  display: none;
}

.js-theme .theme-toggle {
  display: block;
}

.theme-toggle__button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  min-width: 2.75rem;
  min-height: 2.75rem;
  padding: 0;
  border: 1px solid var(--control-line);
  border-radius: var(--radius-full);
  background-color: var(--surface);
  color: var(--ink);
  cursor: pointer;
  transition: color var(--dur-fast) var(--ease-in-out),
    border-color var(--dur-fast) var(--ease-in-out);
}

.theme-toggle__button:hover {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
}

.theme-toggle__button:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.theme-toggle__icon {
  flex-shrink: 0;
}

/* The drawer has width to spare, so the current state is spelled out there.
   aria-hidden: the button's own label is the authority for assistive tech, and
   the visible word is repeated inside it, which keeps label-in-name honest. */
.theme-toggle__state {
  font-size: var(--text-sm);
  font-weight: 600;
}

@media (min-width: 80em) {
  .theme-toggle__state {
    display: none;
  }

  .theme-toggle__button {
    width: 2.75rem;
  }
}

@media (max-width: 79.999em) {
  .js-theme .theme-toggle {
    align-self: stretch;
  }

  .theme-toggle__button {
    width: 100%;
    justify-content: flex-start;
    padding-inline: var(--space-4);
  }
}

/* --- Phone on the desktop bar ---
   Below 80em the number is the label. On the bar it collapses to its icon, and
   the accessible name on the link carries the number instead. */

@media (min-width: 80em) {
  .header-phone {
    width: 2.75rem;
    justify-content: center;
    border-radius: var(--radius-full);
    /* It was transparent at rest and only drew a border on hover, which is the
       report this task came from: a control nobody can see until they find it.
       Hover still strengthens it — it just no longer creates it. */
    border: 1px solid var(--control-line);
    transition: border-color var(--dur-fast) var(--ease-in-out);
  }

  .header-phone:hover {
    border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
  }

  .header-phone__number {
    display: none;
  }

  .header-phone svg {
    width: 1.125rem;
    height: 1.125rem;
  }
}

@media (prefers-reduced-motion: reduce) {
  .theme-toggle__button,
  .header-phone {
    transition: none;
  }
}

/* ---------- 05-footer.css ---------- */
/* Site footer: contact block, hours, menus, service areas, legal line. */

.site-footer {
  margin-block-start: auto;
  padding-block-start: var(--space-16);
  background-color: var(--surface);
  border-block-start: 1px solid var(--line);
  font-size: var(--text-sm);
}

.site-footer__grid {
  display: grid;
  gap: var(--space-12);
  padding-block-end: var(--space-12);
}

.site-footer__col-title {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: var(--tracking-caps);
  color: var(--ink-muted);
  margin-block-end: var(--space-4);
}

.site-footer__list {
  list-style: none;
  display: grid;
  gap: var(--space-1);
}

/* 44px rows: these are tap targets on a phone, not just text links */
.site-footer__list a {
  display: flex;
  align-items: center;
  min-height: 2.75rem;
  color: var(--ink);
  text-decoration: none;
}

.site-footer__list a:hover {
  color: var(--accent);
  text-decoration: underline;
}

.site-footer__list a[aria-current='page'] {
  color: var(--accent);
  font-weight: 600;
}

.footer-contact {
  font-style: normal;
  display: grid;
  gap: var(--space-1);
  max-width: 22rem;
}

.footer-contact__row {
  display: flex;
  gap: var(--space-3);
  align-items: center;
  min-height: 2.75rem;
}

.footer-contact__row svg {
  flex-shrink: 0;
  color: var(--accent);
}

.footer-contact__row a {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
}

.footer-contact__phone {
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
}

.footer-contact__phone:hover {
  color: var(--accent);
}

.site-footer__brand {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem; /* 44px hit area around a 24px mark */
  gap: var(--space-3);
  color: var(--ink);
  text-decoration: none;
  margin-block-end: var(--space-4);
}

.site-footer__blurb {
  color: var(--ink-muted);
  margin-block-end: var(--space-6);
  max-width: 30ch;
}

.site-footer__legal {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-6);
  justify-content: space-between;
  align-items: center;
  padding-block: var(--space-6);
  border-block-start: 1px solid var(--line);
  color: var(--ink-muted);
  font-size: var(--text-xs);
}

/* The demonstration note takes the whole last line rather than competing for
   space with the copyright: it is a sentence, not a label, and on a phone the
   three-way flex row would break it into fragments. */
.site-footer__demo {
  flex-basis: 100%;
  margin: 0;
  max-width: var(--measure);
}


@media (min-width: 40em) {
  .site-footer__grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 64em) {
  .site-footer__grid {
    grid-template-columns: 1.4fr repeat(3, 1fr);
    gap: var(--space-8);
  }
}

/* ---------- 06-sections.css ---------- */
/* Page-level sections shared by every page: hero, trust bar, guarantee,
   final CTA band, and the empty slots waiting for copy and components. */

/* --- Hero --- */

.hero {
  padding-block: var(--space-16) var(--space-12);
}

.hero__inner {
  max-width: 46rem;
}

.hero__title {
  margin-block-end: var(--space-6);
}

.hero__lead {
  font-size: var(--text-lg);
  color: var(--ink-muted);
  max-width: 34rem;
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin-block-start: var(--space-8);
}

.hero__note {
  margin-block-start: var(--space-4);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

@media (min-width: 48em) {
  .hero {
    padding-block: var(--space-24) var(--space-16);
  }
}

/* --- Trust bar --- */

.trust-bar {
  border-block: 1px solid var(--line);
  background-color: var(--surface);
}

.trust-bar__list {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-8) var(--space-6);
  list-style: none;
  padding-block: var(--space-8);
  margin: 0;
}

.trust-bar__figure {
  display: block;
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: 600;
  line-height: var(--leading-tight);
}

.trust-bar__label {
  display: block;
  margin-block-start: var(--space-1);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

@media (min-width: 48em) {
  .trust-bar__list {
    grid-template-columns: repeat(4, 1fr);
  }
}

/* --- Section headings --- */

.section__head {
  max-width: var(--measure);
  margin-block-end: var(--space-12);
}

.section__head > * + * {
  margin-block-start: var(--space-4);
}

/* --- Guarantee --- */

.guarantee {
  max-width: 44rem;
  margin-inline: auto;
  padding: var(--space-12);
  background-color: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  text-align: center;
}

.guarantee__title {
  margin-block-end: var(--space-6);
}

.guarantee__note {
  margin-block-start: var(--space-4);
  color: var(--ink-muted);
}

.guarantee__text {
  font-size: var(--text-lg);
  line-height: var(--leading-body);
  margin-inline: auto;
  max-width: 38rem;
}

/* --- Final CTA band --- */

.cta-band {
  background-color: var(--accent-soft);
}

.cta-band__inner {
  max-width: 44rem;
  margin-inline: auto;
  text-align: center;
}

.cta-band__text {
  margin-block-start: var(--space-4);
  font-size: var(--text-lg);
  color: var(--ink-muted);
  margin-inline: auto;
  max-width: 34rem;
}

.cta-band__actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-4);
  margin-block-start: var(--space-8);
}

.cta-band__note {
  margin-block-start: var(--space-4);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

/* --- Hero with a supporting image ---
   Text first on mobile, side by side from 64em. The image keeps its own
   aspect ratio so the reserved box never collapses before the file loads. */

.hero--media .hero__grid {
  display: grid;
  gap: var(--space-8);
  align-items: center;
}

.hero--media .hero__inner {
  max-width: 34rem;
}

.figure {
  margin: 0;
  border-radius: var(--radius-lg);
  overflow: hidden;
  border: 1px solid var(--line);
  background-color: var(--surface);
}

.figure img {
  display: block;
  width: 100%;
  height: auto;
}

.figure--wide img {
  aspect-ratio: 16 / 9;
  object-fit: cover;
}

.figure--portraitish img {
  aspect-ratio: 4 / 3;
  object-fit: cover;
}

/* A frame for photographs whose meaning sits at the edges. The neighbourhood
   shots are 3:2 and were composed that way on purpose — the toy basket, the
   child's drawing and the open garage door all live in the outer sixth, and a
   4:3 crop removes exactly the detail that makes the picture belong to that
   page. So this frame takes the file's own shape and crops nothing. */
.figure--scene img {
  aspect-ratio: 3 / 2;
  object-fit: cover;
}

.hero__figure {
  margin-block-start: var(--space-12);
}

@media (min-width: 64em) {
  .hero--media .hero__grid {
    grid-template-columns: 1fr 1fr;
    gap: var(--space-16);
  }

  .hero--media .hero__figure {
    margin-block-start: 0;
  }
}

/* Full-width image band. No padding on top so the image sits under the hero it
   belongs to, but the block below it still needs room to breathe. */
.media-band {
  padding-block: 0 var(--space-12);
}

/* --- Section archetypes ---
   A page is read as a rhythm of forms, not as a list of blocks, and a form is
   width, ground and role together. Twelve sections that differed only in
   background colour read as twelve of the same thing, however much ornament
   each one carried.

   So the rhythm here is width: prose keeps the reading measure, a grid and a
   table take the wide track, a quiet section takes air instead, and a split
   sends its picture off the side of the screen. */

.section--grid > .container,
.section--wide > .container {
  max-width: var(--container-wide);
}

/* One sentence and room around it. The guarantee already had the sentence; it
   had the padding of a section holding a twelve-row table. */
.section--quiet {
  padding-block: calc(var(--space-24) * 1.5);
}

@media (min-width: 64em) {
  .section--quiet {
    padding-block: calc(var(--space-32) * 1.35);
  }
}

/* --- Split ---
   Text leads, the picture supports and runs to the edge of the viewport. Not
   50/50: equal columns decline to say which one matters.

   Built as a full-bleed grid with named lines rather than a negative margin in
   vw. 50vw counts the scrollbar, and a negative margin in vw is exactly how a
   page ends up 15px draggable — the named track simply ends where the screen
   does. */

@media (min-width: 64em) {
  .split {
    display: grid;
    /* The first track is the margin between the screen and the container line,
       so the copy starts exactly where every other section's copy starts. The
       ratio is then taken across what is left — from that line to the edge of
       the screen — which is what a reader actually compares. Sizing the two
       columns inside the container instead leaves the picture the whole outer
       margin as a bonus, and at 1920 the "7/5" measures 44/56. */
    grid-template-columns:
      [full-start] max(var(--gutter), calc(50% - var(--container) / 2 + var(--gutter)))
      [text-start] 7fr
      [text-end] 5fr [full-end];
    align-items: center;
  }

  .split__text {
    grid-column: text-start / text-end;
    /* The air before the picture lives inside this column, not in a grid gap.
       A gap sits between every pair of tracks, including the margin track and
       this one, and pushed the copy 48px past the line every other section
       starts on — measured, not guessed. */
    padding-inline-end: var(--space-12);
  }

  .split__media {
    grid-column: text-end / full-end;
    align-self: stretch;
  }

  .split__media img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    border-start-start-radius: var(--radius-lg);
    border-end-start-radius: var(--radius-lg);
  }
}

@media (max-width: 63.999em) {
  .split__media {
    margin-block-start: var(--space-8);
  }

  .split__media img {
    width: 100%;
    height: auto;
    border-radius: var(--radius-lg);
  }
}

/* Below the split breakpoint the section has no container of its own, so the
   two halves carry the gutter themselves. */
@media (max-width: 63.999em) {
  .split__text,
  .split__media {
    padding-inline: var(--gutter);
  }
}

/* ---------- 07-calculator.css ---------- */
/* Price calculator: chip radio groups, checkbox add-ons, result panel. */

.calc {
  display: grid;
  gap: var(--space-8);
  align-items: start;
}

.calc__form {
  display: grid;
  gap: var(--space-8);
}

.calc__group {
  border: 0;
  padding: 0;
  margin: 0;
  min-inline-size: 0; /* fieldset default keeps it from shrinking in a grid */
}

.calc__legend {
  padding: 0;
  margin-block-end: var(--space-3);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--ink-muted);
}

.calc__group.is-disabled .calc__legend,
.calc__group.is-disabled .chip {
  opacity: 0.45;
}

.calc__hint {
  margin-block-start: var(--space-3);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

/* --- Chips (radio groups) --- */

.chips {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 2.75rem;
  min-width: 2.75rem;
  justify-content: center;
  padding: var(--space-2) var(--space-4);
  background-color: var(--surface);
  border: 1px solid var(--control-line);
  border-radius: var(--radius-full);
  font-size: var(--text-sm);
  font-weight: 500;
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-in-out),
    background-color var(--dur-fast) var(--ease-in-out),
    color var(--dur-fast) var(--ease-in-out);
}

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

.chip__hint {
  font-size: var(--text-xs);
  color: var(--ink-muted);
}

input:checked + .chip {
  background-color: var(--accent);
  border-color: var(--accent);
  color: var(--accent-contrast);
}

input:checked + .chip .chip__hint {
  color: inherit;
  opacity: 0.85;
}

input:focus-visible + .chip {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

input:disabled + .chip {
  cursor: not-allowed;
  opacity: 0.45;
}

/* --- Add-ons --- */

.calc__extras {
  display: grid;
  gap: var(--space-2);
}

.check {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-height: 2.75rem;
  padding: var(--space-2) var(--space-4);
  background-color: var(--surface);
  border: 1px solid var(--control-line);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  cursor: pointer;
  transition: border-color var(--dur-fast) var(--ease-in-out);
}

.check:hover {
  border-color: var(--accent);
}

.check__input {
  position: absolute;
  opacity: 0;
  width: 1px;
  height: 1px;
}

.check__box {
  position: relative;
  flex-shrink: 0;
  width: 1.25rem;
  height: 1.25rem;
  border: 1px solid var(--control-line);
  border-radius: var(--radius-sm);
  background-color: var(--bg);
  transition: background-color var(--dur-fast) var(--ease-in-out),
    border-color var(--dur-fast) var(--ease-in-out);
}

.check__box::after {
  content: '';
  position: absolute;
  inset-block-start: 0.15rem;
  inset-inline-start: 0.4rem;
  width: 0.3rem;
  height: 0.6rem;
  border: solid var(--accent-contrast);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg) scale(0);
  transition: transform var(--dur-fast) var(--ease-out);
}

.check__input:checked + .check__box {
  background-color: var(--accent);
  border-color: var(--accent);
}

.check__input:checked + .check__box::after {
  transform: rotate(45deg) scale(1);
}

.check__input:focus-visible + .check__box {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.check__text {
  flex: 1;
}

.check__price {
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

/* --- Result panel --- */

.calc__result {
  padding: var(--space-8);
  background-color: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
}

.calc__result-label {
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--ink-muted);
}

.calc__price {
  font-family: var(--font-display);
  font-size: var(--text-4xl);
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: var(--tracking-tight);
  font-variant-numeric: tabular-nums;
  margin-block-start: var(--space-2);
}

/* the new figure rises into place: ~200ms, transform and opacity only */
.calc__price.is-updated {
  animation: price-in var(--dur-base) var(--ease-out);
}

@keyframes price-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

/* With motion off the keyframes are dropped entirely rather than shortened:
   a 0.01ms animation still starts at opacity 0, and a price that flashes
   invisible is exactly the failure the motion skill warns about. */
@media (prefers-reduced-motion: reduce) {
  .calc__price.is-updated {
    animation: none;
  }
}

.calc__was {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-block-start: var(--space-2);
  color: var(--ink-muted);
  font-variant-numeric: tabular-nums;
}

.calc__lines {
  list-style: none;
  display: grid;
  gap: var(--space-2);
  margin-block: var(--space-6);
  padding-block-start: var(--space-6);
  border-block-start: 1px solid var(--line);
  font-size: var(--text-sm);
}

.calc__lines li {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  color: var(--ink-muted);
}

.calc__lines li span:last-child {
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

.calc__result .btn {
  margin-block-start: var(--space-4);
}

.calc--compact .calc__price {
  font-size: var(--text-3xl);
}

@media (min-width: 64em) {
  .calc {
    grid-template-columns: 1.3fr 1fr;
    gap: var(--space-16);
  }

  .calc__result {
    position: sticky;
    top: calc(var(--header-height) + var(--space-6));
  }
}

/* ---------- 08-form.css ---------- */
/* Booking form: fields, inline errors, confirmation panel. */

.booking {
  max-width: 46rem;
}

.booking__estimate {
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-2);
  margin-block-end: var(--space-8);
  padding: var(--space-3) var(--space-6);
  background-color: var(--accent-soft);
  border-radius: var(--radius-full);
  color: var(--ink);
  font-size: var(--text-sm);
}

.booking__estimate strong {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-variant-numeric: tabular-nums;
}

.form {
  display: grid;
  gap: var(--space-6);
}

.form__row {
  display: grid;
  gap: var(--space-6);
}

.field {
  display: grid;
  gap: var(--space-2);
  min-inline-size: 0;
}

.field--group {
  border: 0;
  padding: 0;
  margin: 0;
}

.field__label {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  padding: 0;
  font-size: var(--text-sm);
  font-weight: 600;
}

.field__sublabel {
  font-size: var(--text-xs);
  color: var(--ink-muted);
}

.field__required {
  font-size: var(--text-xs);
  font-weight: 500;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--ink-muted);
}

.field__input {
  width: 100%;
  min-height: 2.75rem;
  padding: var(--space-3) var(--space-4);
  background-color: var(--surface);
  border: 1px solid var(--control-line);
  border-radius: var(--radius-md);
  color: var(--ink);
  font-size: var(--text-base);
  transition: border-color var(--dur-fast) var(--ease-in-out),
    box-shadow var(--dur-fast) var(--ease-in-out);
}

.field__input:hover {
  border-color: color-mix(in srgb, var(--accent) 40%, var(--line));
}

.field__input:focus-visible {
  outline: none;
  border-color: var(--accent);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 25%, transparent);
}

.field__input--area {
  min-height: 8rem;
  resize: vertical;
  line-height: var(--leading-body);
}

/* the error is carried by text as well as colour, never colour alone */
.field__input.is-invalid {
  border-color: var(--danger);
}

.field__error {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  color: var(--danger);
  font-size: var(--text-sm);
}

.field__error::before {
  content: '!';
  flex-shrink: 0;
  display: inline-grid;
  place-items: center;
  width: 1.125rem;
  height: 1.125rem;
  border: 1px solid currentColor;
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: 700;
}

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

/* --- Confirmation --- */

.booking__done {
  padding: var(--space-12);
  background-color: var(--surface);
  border: 1px solid var(--line);
  border-inline-start: 3px solid var(--success);
  border-radius: var(--radius-lg);
  animation: confirm-in var(--dur-base) var(--ease-out);
}

.booking__done:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
}

.booking__done-title {
  margin-block-end: var(--space-4);
}

@keyframes confirm-in {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: none; }
}

/* motion off: the confirmation must be visible instantly, never faded out */
@media (prefers-reduced-motion: reduce) {
  .booking__done {
    animation: none;
  }
}

.booking__summary {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: var(--space-2) var(--space-6);
  margin-block-start: var(--space-8);
  padding-block-start: var(--space-6);
  border-block-start: 1px solid var(--line);
  font-size: var(--text-sm);
}

.booking__summary dt {
  color: var(--ink-muted);
}

.booking__summary dd {
  margin: 0;
  overflow-wrap: anywhere; /* a 2000-character note must not widen the page */
}

@media (min-width: 48em) {
  .form__row {
    grid-template-columns: 1fr 1fr;
  }
}

/* ---------- 09-content.css ---------- */
/* Content components introduced with the home page copy: card grids, steps,
   prose blocks, comparison table, reason list, neighbourhood list, FAQ.
   Cards reuse .card from 03-components.css — only the inner parts are new. */

.card-grid {
  list-style: none;
  display: grid;
  gap: var(--space-6);
  padding: 0;
  margin: 0;
}

.card-grid > li {
  display: flex;
}

.card-grid > li > * {
  width: 100%;
}

.card__price {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: 600;
  color: var(--accent);
}

.card__link {
  margin-block-start: auto;
  padding-block-start: var(--space-4);
  font-size: var(--text-sm);
  font-weight: 600;
  text-decoration: none;
}

.card__link::after {
  content: ' \2192'; /* arrow travels with the label, so it never wraps alone */
}

.card__link:hover {
  text-decoration: underline;
}

/* --- Quote cards --- */

.card--quote {
  gap: var(--space-6);
}

.card__quote {
  margin: 0;
  font-size: var(--text-lg);
  line-height: var(--leading-body);
}

.card__cite {
  margin-block-start: auto;
  font-size: var(--text-sm);
  font-weight: 600;
}

.card__meta {
  display: block;
  font-weight: 400;
  color: var(--ink-muted);
}

/* --- Steps --- */

.steps {
  list-style: none;
  display: grid;
  gap: var(--space-8);
  padding: 0;
  margin: 0;
  counter-reset: step;
}

.step {
  display: grid;
  gap: var(--space-3);
  padding-block-start: var(--space-6);
  border-block-start: 2px solid var(--accent);
}

.step__number {
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: var(--tracking-caps);
  color: var(--accent);
}

.step__title {
  font-size: var(--text-xl);
}

.step__text {
  color: var(--ink-muted);
  max-width: 34ch;
}

/* --- Prose blocks --- */

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

.prose > * + * {
  margin-block-start: var(--space-6);
}

.section__aside {
  margin-block-start: var(--space-8);
  color: var(--ink-muted);
}

/* --- Comparison table --- */

.table-wrap {
  position: relative; /* contains .visually-hidden helpers so they cannot widen the page */
  overflow-x: auto; /* a wide table scrolls inside its own box, never the page */
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background-color: var(--surface);
}

.table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
  min-width: 34rem;
}

.table th,
.table td {
  padding: var(--space-4) var(--space-6);
  text-align: start;
  vertical-align: top;
  border-block-end: 1px solid var(--line);
}

.table thead th {
  font-size: var(--text-xs);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--ink-muted);
  white-space: nowrap;
}

.table tbody th {
  font-weight: 600;
  white-space: nowrap;
}

.table tbody tr:last-child th,
.table tbody tr:last-child td {
  border-block-end: 0;
}

/* Only the us-versus-them table tints a column. Service tables compare our own
   options, where highlighting one of them would be arbitrary. */
.table--compare thead th:nth-child(2),
.table--compare tbody td:nth-child(2) {
  background-color: var(--accent-soft);
}

.table--compare thead th:nth-child(2) {
  color: var(--accent);
}

.table--compare tbody td:nth-child(3) {
  color: var(--ink-muted);
}

/* --- Reasons ("who we're not for") --- */

.reasons {
  list-style: none;
  display: grid;
  gap: var(--space-8);
  padding: 0;
  margin: 0;
}

.reason {
  padding-inline-start: var(--space-6);
  /* a rule that carries meaning — it is what groups the reason — so it is held
     to the control threshold, not to the hairline one */
  border-inline-start: 2px solid var(--control-line);
}

.reason__title {
  font-size: var(--text-xl);
  margin-block-end: var(--space-3);
}

.reason__text {
  color: var(--ink-muted);
  max-width: var(--measure);
}

/* --- Neighbourhood list --- */

.area-list {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  padding: 0;
  margin: 0;
}

.area-list li {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  padding: var(--space-2) var(--space-4);
  border: 1px solid var(--line);
  border-radius: var(--radius-full);
  background-color: var(--surface);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

.area-list a {
  color: var(--ink);
  text-decoration: none;
}

.area-list li:has(a):hover {
  border-color: var(--accent);
}

.area-list a:hover {
  color: var(--accent);
}

/* --- FAQ --- */

.faq {
  display: grid;
  gap: var(--space-2);
  max-width: var(--measure);
}

.faq__item {
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background-color: var(--surface);
}

.faq__question {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  min-height: 2.75rem;
  padding: var(--space-4) var(--space-6);
  font-weight: 600;
  cursor: pointer;
  list-style: none; /* the default triangle is replaced below */
}

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

.faq__question::after {
  content: '';
  flex-shrink: 0;
  width: 0.6rem;
  height: 0.6rem;
  border-inline-end: 2px solid var(--accent);
  border-block-end: 2px solid var(--accent);
  transform: rotate(45deg) translate(-0.15rem, -0.15rem);
  transition: transform var(--dur-base) var(--ease-out);
}

.faq__item[open] .faq__question::after {
  transform: rotate(-135deg) translate(-0.15rem, -0.15rem);
}

.faq__question:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
  border-radius: var(--radius-md);
}

.faq__answer {
  padding: 0 var(--space-6) var(--space-6);
  color: var(--ink-muted);
}

/* Where ::details-content is supported the panel slides open; everywhere else
   it simply appears, which is the same content either way. */
.faq__item::details-content {
  block-size: 0;
  overflow: hidden;
  transition: block-size var(--dur-base) var(--ease-out),
    content-visibility var(--dur-base) allow-discrete;
}

.faq__item[open]::details-content {
  block-size: auto;
}

@media (prefers-reduced-motion: reduce) {
  .faq__item::details-content {
    transition: none;
  }
}

@media (min-width: 48em) {
  .card-grid {
    grid-template-columns: repeat(2, 1fr);
  }

  .steps {
    grid-template-columns: repeat(3, 1fr);
  }

  .reasons {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 64em) {
  .card-grid {
    grid-template-columns: repeat(4, 1fr);
  }

  .card-grid--two {
    grid-template-columns: repeat(2, 1fr);
  }
}

/* --- Checklists (service pages) --- */

.checklist {
  display: grid;
  /* minmax(0, 1fr), not the implicit 1fr. A bare 1fr is minmax(auto, 1fr) and
     auto is min-content, so the column keeps the width of the longest
     unbreakable thing inside it — an email address, here — and pushes the page
     sideways: measured 284px of column in a 257px container at 320px wide. */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-8);
}

.checklist__title {
  font-size: var(--text-lg);
  margin-block-end: var(--space-4);
}

.list-check,
.list-plain {
  list-style: none;
  display: grid;
  gap: var(--space-2);
  padding: 0;
  margin: 0;
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

/* The marker sits outside the flow, and the item's own content is an ordinary
   line of text.

   It used to be a two-column grid, which works only while an item holds exactly
   one node. Every list on the site did — until the 404 page, where an item is a
   link followed by a sentence: in a grid those are two items, so the sentence
   dropped onto the next row and into the 1.25rem marker column, one word per
   line. Wrapping the text in a span would have fixed that page and left the
   trap for the next one. */
.list-check li,
.list-plain li {
  position: relative;
  padding-inline-start: calc(1.25rem + var(--space-3));
}

.list-check li::before,
.list-plain li::before {
  content: '';
  position: absolute;
  inset-inline-start: 0;
}

/* the tick is aligned to the first line, not to the middle of the item */
.list-check li::before {
  inset-block-start: 0.28em;
  inset-inline-start: 0.3rem;
  width: 0.45rem;
  height: 0.8rem;
  border: solid var(--accent);
  border-width: 0 2px 2px 0;
  transform: rotate(45deg);
}

.list-plain li::before {
  inset-block-start: 0.7em;
  width: 0.7rem;
  border-block-start: 1px solid var(--ink-muted);
}

@media (min-width: 48em) {
  .checklist {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-12) var(--space-8);
  }
}

/* A quote card standing on its own rather than inside a grid */
.card--single {
  max-width: 46rem;
}

/* --- Rating summary (reviews page) --- */

.rating {
  display: grid;
  gap: var(--space-2);
  max-width: 34rem;
}

.rating__score {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  font-weight: 600;
  color: var(--accent);
}

.rating__number {
  font-variant-numeric: tabular-nums;
}

.rating__count {
  color: var(--ink-muted);
  font-size: var(--text-sm);
  margin-block-end: var(--space-4);
}

.rating__rows {
  list-style: none;
  display: grid;
  gap: var(--space-2);
  padding: 0;
  margin: 0;
}

.rating__row {
  display: grid;
  grid-template-columns: 4.5rem 1fr 3rem;
  align-items: center;
  gap: var(--space-3);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

.rating__bar {
  height: 0.5rem;
  border-radius: var(--radius-full);
  background-color: var(--line);
  overflow: hidden;
}

.rating__fill {
  display: block;
  height: 100%;
  background-color: var(--accent);
}

/* 352, 20, 5, 2 and 1 of 380 reviews */
.rating__fill--5 { width: 92.6%; }
.rating__fill--4 { width: 5.3%; }
.rating__fill--3 { width: 1.3%; }
.rating__fill--2 { width: 0.5%; }
.rating__fill--1 { width: 0.3%; }

.rating__value {
  text-align: end;
  font-variant-numeric: tabular-nums;
}

/* --- Review cards --- */

.card__rating {
  color: var(--accent);
  letter-spacing: 0.1em;
}

/* Unfilled stars are decorative (the rating is also in text for screen readers),
   but they still need to read as star outlines rather than vanish. */
.card__rating-empty {
  color: color-mix(in srgb, var(--ink-muted) 55%, var(--surface));
}

.card__reply {
  margin-block-start: var(--space-6);
  padding-block-start: var(--space-6);
  border-block-start: 1px solid var(--line);
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

.card__reply-label {
  font-weight: 600;
  color: var(--ink);
  margin-block-end: var(--space-2);
}

.review-filter {
  display: grid;
  gap: var(--space-6);
}

.review-filter__count,
.review-filter__empty {
  margin-block-start: var(--space-6);
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

/* --- Misc content bits --- */

.checklist__heading {
  margin-block: var(--space-12) var(--space-4);
  font-size: var(--text-xl);
}

.stat-list {
  border: 0;
}

.contact-line {
  font-size: var(--text-lg);
  font-weight: 600;
  /* an address has no spaces to break at, and on a 320px phone it is wider than
     the column it lives in */
  overflow-wrap: anywhere;
}

.contact-block {
  font-style: normal;
  line-height: var(--leading-body);
}

/* --- Eyebrow ---
   The label above a section heading. Part of the typographic system rather
   than an ornament: it names what the heading already says, in two to four
   words, so the eye can place a section before reading it.

   The 24px rule is the only drawn element. It is small on purpose — an eyebrow
   that reads as a graphic has stopped being typography. */

.eyebrow {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: 0;
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--ink-muted);
}

.eyebrow::before {
  content: '';
  flex-shrink: 0;
  width: 1.5rem;
  height: 2px;
  background-color: var(--accent);
}

/* Centred blocks — the guarantee and the closing call to action — centre the
   label with its rule rather than hanging it off the left edge of a centred
   paragraph, which reads as a mistake. */
.eyebrow--center {
  justify-content: center;
  margin-block-end: var(--space-3);
}

/* The heading follows its own label closely: the pair reads as one unit, and
   the section's own rhythm above and below is untouched. */
.section__head > .eyebrow + * {
  margin-block-start: var(--space-3);
}

/* --- Numbers as objects ---
   The figures were already on the page, sitting in a row of plain text. A
   border and some space around each one turns four sentences into four things
   the eye can count. Nothing here invents a number. */

.trust-bar__list > li {
  padding: var(--space-6);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background-color: var(--bg);
}

.trust-bar__list .trust-bar__figure {
  /* Fluid, because a fixed 31px display face sets the minimum width of its
     own grid track: "Licensed" cannot break, so at a 345px client width the
     two columns demanded 336px of a 297px track and the page could be dragged
     sideways by 15px. The clamp lets the figure give way before the grid does. */
  font-size: clamp(var(--text-lg), 5.5vw, var(--text-2xl));
}

/* --- Ghost numbers ---
   A single figure set large behind the words, at six per cent. It is felt as
   depth rather than read as a number: at this opacity it never competes with
   the text on top, and below 64em there is no room for it to be anything but
   in the way, so it goes. */

.has-ghost {
  position: relative; /* the ghost is absolute inside it — design system 6.1 */
}

.ghost {
  display: none;
}

@media (min-width: 64em) {
  .ghost {
    display: block;
    position: absolute;
    inset-block-start: var(--space-12);
    /* anchored to its right edge, so it grows leftwards and can never widen
       the page — the section is not clipped, because the soft bands' diagonal
       edges live outside their own box */
    inset-inline-end: var(--gutter);
    z-index: 0;
    font-family: var(--font-display);
    font-size: calc(var(--text-4xl) * 2.5);
    font-weight: 600;
    line-height: 1;
    letter-spacing: var(--tracking-tight);
    color: var(--ink);
    opacity: 0.06;
    pointer-events: none;
    user-select: none;
  }

  /* the words keep their own layer above it */
  .has-ghost > .container {
    position: relative;
    z-index: 1;
  }
}

/* --- Pull quote ---
   One sentence lifted out of a long passage and set large, hanging past the
   left edge of the text column.

   aria-hidden, and deliberately not a <blockquote>: the sentence is already in
   the paragraph beside it, so a screen reader would otherwise hear it twice,
   and it is not a quotation from anywhere — it is the same page raising its
   voice for one line. */

.pull {
  margin-block: var(--space-8);
  padding-inline-start: var(--space-6);
  border-inline-start: 2px solid var(--accent);
  font-family: var(--font-display);
  font-size: var(--text-xl);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  color: var(--ink);
}

/* The outdent only where there is room for it: below 64em the container has
   nothing to spare and the quote would push the page sideways. */
@media (min-width: 64em) {
  .pull {
    margin-inline-start: calc(var(--space-8) * -1);
    padding-inline-start: var(--space-8);
  }
}

/* --- Arc ---
   One shape for the whole site: a quarter of a thin circle, echoing the radius
   the cards already use. It is drawn as a full ring and three quarters of it
   are clipped away, which costs one declaration and keeps the curve true.

   Its own element rather than a pseudo, because the two sections that would
   host it — the soft bands — already spend ::before and ::after on their
   diagonal edges. */

.arc {
  display: none;
}

@media (min-width: 64em) {
  .arc {
    display: block;
    position: absolute;
    z-index: 0;
    inline-size: 20rem;
    block-size: 20rem;
    border: 1.5px solid var(--accent);
    border-radius: 50%;
    clip-path: inset(0 0 50% 50%); /* the top-right quarter, nothing else */
    opacity: 0.06;
    pointer-events: none;
  }

  /* Anchored inside the corner, not pulled out of it. A negative inset on a
     full-width section widens the document — measured at 64px of horizontal
     overflow on three pages — and the section cannot be clipped, because the
     soft bands draw their diagonal edges outside their own box. */
  .arc--end {
    inset-block-end: 0;
    inset-inline-end: 0;
  }

  .has-arc > .container {
    position: relative;
    z-index: 1;
  }
}

/* --- Overlap ---
   One per page, and on this site it is the row of figures riding up over the
   bottom of the hero photograph. The tiles carry their own opaque background,
   so nothing about their contrast depends on what is behind them.

   The strip has to be positioned to paint above the band: the band is
   positioned itself, and without this the later, unpositioned sibling would
   sit underneath it. */

@media (min-width: 64em) {
  .trust-bar--over {
    position: relative;
    z-index: 1;
    background-color: transparent;
    border-block-start-color: transparent;
    margin-block-start: calc(var(--space-8) * -1);
  }

  /* The strip's own top padding would swallow the whole pull, leaving the
     tiles exactly where they started. Drop it, and the --space-8 becomes
     --space-8 of tile actually standing on the photograph. */
  .trust-bar--over .trust-bar__list {
    padding-block-start: 0;
  }
}

/* ---------- 10-motion.css ---------- */
/* Motion: section reveals and hover states.
   Only transform and opacity are animated — both are composited, so nothing
   here forces a layout pass. The reveal styles apply only when the inline
   bootstrap in <head> has added .js-motion, so a page without JavaScript (or
   with motion turned off) renders everything visible from the first paint. */

.js-motion [data-reveal] {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out);
  transition-delay: var(--reveal-delay, 0ms);
}

.js-motion [data-reveal].is-visible {
  opacity: 1;
  transform: none;
}

/* children of a revealed grid ride the parent's transition with a short offset */
.js-motion [data-reveal] > [data-reveal-child] {
  transition: inherit;
  transition-delay: var(--reveal-delay, 0ms);
}

/* --- Hover: the same language everywhere --- */

.card {
  transition: transform var(--dur-base) var(--ease-out),
    box-shadow var(--dur-base) var(--ease-out),
    border-color var(--dur-base) var(--ease-out);
}

.card-grid > li:hover .card--single {
  transform: translateY(-4px);
  box-shadow: var(--shadow-md);
  border-color: color-mix(in srgb, var(--accent) 35%, var(--line));
}

.card-grid > li:hover .card:not(.card--quote) {
  transform: translateY(-4px);
  box-shadow: var(--shadow-md);
  border-color: color-mix(in srgb, var(--accent) 35%, var(--line));
}

/* a picture inside a card or figure leans in slightly on hover */
.figure img,
.card img {
  transition: transform var(--dur-slow) var(--ease-out);
}

.figure:hover img,
.card-grid > li:hover .card img {
  transform: scale(1.04);
}

/* the arrow in a card link steps forward with the eye */
.card__link::after {
  display: inline-block;
  transition: transform var(--dur-fast) var(--ease-out);
}

.card__link:hover::after,
.card-grid > li:hover .card__link::after {
  transform: translateX(4px);
}

.btn:active {
  transform: scale(0.985);
}

/* --- Activity line --- */

.activity {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  margin-block-start: var(--space-6);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

.activity__dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: var(--radius-full);
  background-color: var(--success);
  flex-shrink: 0;
}

/* Motion off: nothing moves, nothing waits, nothing is left invisible. */
@media (prefers-reduced-motion: reduce) {
  .js-motion [data-reveal],
  .js-motion [data-reveal] > [data-reveal-child] {
    opacity: 1;
    transform: none;
    transition: none;
  }

  .figure img,
  .card img,
  .card__link::after {
    transition: none;
  }

  .figure:hover img,
  a.card:hover img,
  .card-grid > li:hover .card img,
  .card__link:hover::after,
  .card-grid > li:hover .card__link::after {
    transform: none;
  }
}

/* ---------- 11-compare.css ---------- */
/* Comparison slider. The wash and the divider move with --split; the labels
   never get clipped, they are shown or hidden by side. */

.compare {
  margin: 0;
  --split: 50%;
}

.compare__frame {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background-color: var(--surface);
  touch-action: pan-y; /* vertical scrolling still works while dragging sideways */
  user-select: none; /* a drag should not paint a text selection across the caption */
  cursor: ew-resize;
}

/* The photographs take no pointer events of their own. Without this, pressing
   on one starts the browser's native image drag, which fires pointercancel and
   kills the slider one move in — the divider jumps once and then stops dead.
   Costs nothing: everything interactive here is the frame and the handle. */
.compare__image {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  pointer-events: none;
  user-select: none;
}

/* The right of the divider is the deep clean: a light accent wash marks it
   without hiding the room underneath. */
.compare__wash {
  position: absolute;
  inset: 0;
  clip-path: inset(0 0 0 var(--split));
  background-color: rgb(34 119 91 / 0.32);
  background-color: color-mix(in srgb, var(--accent) 32%, transparent);
  mix-blend-mode: multiply;
  pointer-events: none;
}

.compare__badge {
  position: absolute;
  inset-block-start: var(--space-4);
  z-index: 2;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-full);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  white-space: nowrap;
  pointer-events: none;
}

.compare__badge--standard {
  inset-inline-start: var(--space-4);
  background-color: var(--surface);
  color: var(--ink);
}

.compare__badge--deep {
  inset-inline-end: var(--space-4);
  background-color: var(--accent);
  color: var(--accent-contrast);
}

/* --- Points --- */

.compare__points {
  list-style: none;
  margin: 0;
  padding: 0;
  position: absolute;
  inset: 0;
  pointer-events: none;
}

.compare__point {
  position: absolute;
  inset-inline-start: var(--x);
  inset-block-start: var(--y);
  transform: translate(-50%, -50%);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-full);
  background-color: rgb(255 255 255 / 0.92);
  background-color: color-mix(in srgb, var(--surface) 92%, transparent);
  border: 1px solid var(--line);
  color: var(--ink);
  font-size: var(--text-xs);
  font-weight: 600;
  white-space: nowrap;
  backdrop-filter: blur(4px);
}

.compare__point::before {
  content: '';
  width: 0.4rem;
  height: 0.4rem;
  border-radius: var(--radius-full);
  background-color: var(--accent);
  flex-shrink: 0;
}

.compare__point[data-side='deep'] {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--line));
}

/* Only once the script runs do points start hiding; without it every label
   stays on screen, which is still a readable picture of both services. */
.compare.is-interactive .compare__point {
  opacity: 0;
  transform: translate(-50%, -50%) scale(0.94);
  transition: opacity var(--dur-fast) var(--ease-out), transform var(--dur-fast) var(--ease-out);
}

.compare.is-interactive .compare__point.is-shown {
  opacity: 1;
  transform: translate(-50%, -50%);
}

/* --- Divider and handle --- */

.compare__divider {
  position: absolute;
  inset-block: 0;
  inset-inline-start: var(--split);
  width: 2px;
  margin-inline-start: -1px;
  background-color: var(--surface);
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--ink) 25%, transparent);
  pointer-events: none;
  z-index: 3;
}

.compare__handle {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: var(--split);
  z-index: 4;
  width: 2.75rem;
  height: 2.75rem;
  margin: -1.375rem 0 0 -1.375rem;
  display: grid;
  place-items: center;
  border-radius: var(--radius-full);
  background-color: var(--surface);
  /* The one control on the site that sits on a photograph, and a photograph is
     neither theme: --control-line follows the text colour, which measured 2.56:1
     against a mid-tone kitchen in light and 1.64:1 in dark. So the handle keeps
     a hairline of its own that does not depend on the theme at all — a dark ring
     drawn outside the white disc. Against any ground the pair reads: light disc,
     dark ring. */
  border: 0;
  box-shadow: 0 0 0 1px rgb(12 17 20 / 0.55), var(--shadow-md);
  color: var(--ink);
  cursor: ew-resize;
}

.compare__handle:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
}

.compare__arrows {
  font-size: var(--text-lg);
  line-height: 1;
  letter-spacing: -0.1em;
  transform: rotate(90deg);
}

.compare__caption {
  margin-block-start: var(--space-4);
  max-width: var(--measure);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

@media (prefers-reduced-motion: reduce) {
  .compare.is-interactive .compare__point {
    transition: none;
  }
}

/* Small screens: a label pinned to a point on the photo would hang past the
   frame, so the labels leave the picture and stack in two columns instead —
   standard on the left, deep on the right. The photo becomes the backdrop and
   the divider still drives which column is showing. */
@media (max-width: 47.99em) {
  .compare__points {
    display: grid;
    grid-template-columns: 1fr 1fr;
    align-content: start;
    gap: var(--space-2);
    padding: 3.25rem var(--space-3) var(--space-3);
  }

  .compare__point {
    position: static;
    transform: none;
    font-size: 0.6875rem;
    padding: var(--space-1) var(--space-2);
    max-width: 100%;
    white-space: normal;
  }

  .compare__point[data-side='standard'] {
    grid-column: 1;
    justify-self: start;
  }

  .compare__point[data-side='deep'] {
    grid-column: 2;
    justify-self: end;
    text-align: end;
  }

  .compare.is-interactive .compare__point {
    transform: none;
    scale: 0.96;
  }

  /* the base .is-shown rule is more specific than a plain .compare__point one,
     and a media query adds no specificity — so it has to be overridden by name */
  .compare.is-interactive .compare__point.is-shown {
    transform: none;
    scale: 1;
  }

  .compare__badge {
    font-size: 0.625rem;
    padding: var(--space-1) var(--space-3);
  }
}

/* --- Real before/after pairs ---
   Two photographs of the same room from the same camera position, the second
   revealed by the divider. The pair keeps the frame's native 3:2 so neither
   half is cropped: a crop here would move the walls as the divider passes and
   the whole point is that nothing moves except the mess. */

.compare--pair .compare__image {
  aspect-ratio: 3 / 2;
}

.compare--pair .compare__image--after {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  clip-path: inset(0 0 0 var(--split));
}

/* The "after" badge is a plain surface chip on a pair: the accent fill belongs
   to the schematic slider, where the right-hand side is a service level rather
   than a photograph. */
.compare--pair .compare__badge--deep {
  background-color: var(--surface);
  color: var(--ink);
}

/* ---------- 12-carousel.css ---------- */
/* Reviews carousel. The track is a scroll-snap list first and a carousel
   second: with no JavaScript it is still a readable, scrollable row of cards,
   which matters more than the dots do. */

.carousel {
  position: relative;
}

.carousel__track {
  list-style: none;
  display: flex;
  gap: var(--space-6);
  margin: 0;
  padding: 0 0 var(--space-2);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  scroll-padding-inline-start: 0;
  /* The bar is hidden, the scrolling is not: keyboard, wheel, trackpad, touch
     and snapping all still work. The dots are this carousel's position
     indicator, and a second one drawn by the browser under the cards only
     looked like a stray line. */
  scrollbar-width: none;
}

.carousel__track::-webkit-scrollbar {
  display: none;
}

/* Only once the script is running is the row something you can grab. Without
   it the cursor would promise a drag that nothing implements. */
.carousel.is-enhanced .carousel__track {
  cursor: grab;
}

/* Held down. Snapping is suspended for the length of the gesture — a snap
   fighting a finger reads as the track refusing to move — and selection is
   suppressed so a drag across a quote does not paint it blue. At rest the text
   selects normally, because a review someone wants to quote should be
   copyable. */
.carousel__track.is-dragging {
  cursor: grabbing;
  scroll-snap-type: none;
  scroll-behavior: auto;
  user-select: none;
}

/* An image inside a card would start the browser's own drag and cancel the
   pointer mid-gesture — the same defect the comparison slider had. */
.carousel__track img {
  -webkit-user-drag: none;
  user-select: none;
}

.carousel__track:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: var(--radius-lg);
}

.carousel__track > li {
  flex: 0 0 min(88%, 26rem);
  scroll-snap-align: start;
  display: flex;
}

.carousel__track > li > * {
  width: 100%;
}

/* --- Dots --- */

.carousel__dots {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  margin-block-start: var(--space-6);
}

.carousel__dot {
  /* 44px of touch target around an 8px dot: the dot is what you see, the
     button is what you hit */
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  border: 0;
  background: none;
  display: grid;
  place-items: center;
  cursor: pointer;
}

.carousel__dot::before {
  content: '';
  width: 0.5rem;
  height: 0.5rem;
  border-radius: var(--radius-full);
  /* the dot has no border: its fill IS its boundary, so it takes the control
     colour rather than the hairline grey it used to share with card edges */
  background-color: var(--control-line);
  transition: background-color var(--dur-fast) var(--ease-in-out),
    transform var(--dur-fast) var(--ease-out);
}

.carousel__dot:hover::before {
  background-color: color-mix(in srgb, var(--accent) 60%, var(--line));
}

.carousel__dot.is-active::before {
  background-color: var(--accent);
  transform: scale(1.4);
}

.carousel__dot:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -6px;
  border-radius: var(--radius-full);
}

.carousel__hint {
  margin-block-start: var(--space-4);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

@media (min-width: 48em) {
  .carousel__track > li {
    flex-basis: min(46%, 26rem);
  }
}

@media (min-width: 64em) {
  .carousel__track > li {
    flex-basis: calc((100% - var(--space-6) * 2) / 3);
  }
}

@media (prefers-reduced-motion: reduce) {
  .carousel__track {
    scroll-behavior: auto;
  }

  .carousel__dot::before {
    transition: none;
  }
}

/* ---------- 13-parallax.css ---------- */
/* Parallax on framed photographs.

   The picture inside a frame is drawn taller than the frame and slid the other
   way as the page scrolls, so it travels about 15% slower than the text beside
   it. Nothing else moves: one composited transform per frame, written from JS.

   Desktop only. Below 64em the frame is nearly as wide as the viewport and a
   visitor sees one band at a time, so the two layers never separate enough to
   read as depth — the phone would pay for the work and get nothing back.

   Everything here sits behind .js-motion, which the head bootstrap adds only
   when prefers-reduced-motion is not set, so motion-off never reaches these
   rules at all. */

@media (min-width: 64em) {
  .js-motion [data-parallax] {
    position: relative;
  }

  /* The image leaves the flow, so the frame needs a height of its own. These
     are the same ratios the image carried in 06-sections.css. */
  .js-motion .figure--wide[data-parallax] {
    aspect-ratio: 16 / 9;
  }

  .js-motion .figure--portraitish[data-parallax] {
    aspect-ratio: 4 / 3;
  }

  .js-motion [data-parallax] > img {
    position: absolute;
    inset-inline: 0;
    /* the extra height is split evenly above and below, so an image at rest
       sits exactly where it did before the slack was added */
    inset-block-start: calc(var(--parallax-slack, 0px) / -2);
    width: 100%;
    height: calc(100% + var(--parallax-slack, 0px));
    object-fit: cover;
    will-change: transform;

    /* Only scale transitions. The parallax offset is written to transform on
       every frame and has to land instantly: a transition on transform here
       would put the picture a third of a second behind the scroll. */
    transition: scale var(--dur-slow) var(--ease-out);
  }

  /* the hover zoom moves to the independent scale property so it can coexist
     with a transform that JS owns */
  .js-motion [data-parallax]:hover > img {
    scale: 1.04;
  }
}

/* ---------- 14-dividers.css ---------- */
/* Diagonal edges between bands.

   A soft band keeps its own square background and grows two triangles, one
   above and one below, filled with background-color: inherit — the section's
   own colour, whatever the theme resolved it to. The union of box and triangle
   is a sheared rectangle, so the visible boundary runs from the box edge on one
   side to --divider-h past it on the other.

   Two consequences worth stating, because both were the point:
   - no rule anywhere names the colour of a neighbouring block, so dark theme
     needs no second version of any of this;
   - the triangles are inset-inline: 0 on a positioned parent, so they can never
     be wider than the section and cannot cause horizontal overflow.

   position: relative also makes the section a containing block for anything
   absolute inside it — see 6.1 in the design system. */

.section--soft,
.cta-band {
  position: relative;
}

.section--soft::before,
.section--soft::after,
.cta-band::before {
  content: '';
  position: absolute;
  inset-inline: 0;
  height: var(--divider-h);
  background-color: inherit;
  pointer-events: none;
}

/* above the band: a right-hand triangle, so the top edge climbs to the right */
.section--soft::before,
.cta-band::before {
  bottom: 100%;
  clip-path: polygon(0 100%, 100% 0, 100% 100%);
}

/* below it: the mirrored triangle, keeping both edges parallel */
.section--soft::after {
  top: 100%;
  clip-path: polygon(0 0, 100% 0, 0 100%);
}

/* The final CTA is the last block on every page. A triangle hanging below it
   would add its height to the scrollable area for nothing, so that band is
   sheared at the top only. */

/* ---------- 15-plan.css ---------- */
/* Floor plan on /whats-included/.

   Inline SVG, no raster and no sprite: one drawing, zero network requests, and
   the colours are inherited rather than baked in. Everything visual lives on
   classes here, which is what makes the dark theme free — the plan has no
   second set of rules anywhere.

   Stroke weight is 2 user units on every line, matching the icon set. The
   drawing is capped at 44rem so it renders close to 1:1 and the strokes do not
   thicken into something heavier than the icons beside them. */

.plan {
  margin: 0;
}

.plan__scroll {
  position: relative; /* a scrolling box is a containing block — design system 6.1 */
  /* the box hugs the drawing instead of stretching to the container: a 44rem
     plan floating in a 70rem card reads as a mistake */
  max-width: calc(44rem + var(--space-6) * 2);
  margin-inline: auto;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background-color: var(--surface);
  padding: var(--space-6);
  scrollbar-width: thin;
  scrollbar-color: var(--line) transparent;
}

.plan__scroll:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.plan__svg {
  display: block;
  width: 100%;
  /* Below this the labels stop being labels. A narrow screen scrolls the plan
     sideways instead, which keeps the drawing honest at any width. */
  min-width: 38rem;
  max-width: 44rem;
  height: auto;
  margin-inline: auto;
  font-family: var(--font-body);
}

/* --- The drawing --- */

.plan__wall,
.plan__line {
  fill: none;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

.plan__wall {
  stroke: var(--ink);
}

.plan__line {
  stroke: var(--ink-muted);
}

.plan__room {
  fill: var(--ink-muted);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
}

.plan__label {
  fill: var(--ink);
  font-size: 12px;
}

/* filled: every visit. ring: only a deep clean reaches it. */
.plan__dot {
  fill: var(--accent);
}

.plan__dot--deep {
  fill: var(--surface);
  stroke: var(--accent);
  stroke-width: 2;
}

/* --- Legend and caption --- */

.plan__legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3) var(--space-6);
  list-style: none;
  margin: var(--space-4) 0 0;
  padding: 0;
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

.plan__legend li {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
}

.plan__key {
  width: 0.625rem;
  height: 0.625rem;
  border-radius: var(--radius-full);
  background-color: var(--accent);
  flex-shrink: 0;
}

.plan__key--deep {
  background-color: transparent;
  box-shadow: inset 0 0 0 2px var(--accent);
}

.plan__hint,
.plan__caption {
  margin-block-start: var(--space-4);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

/* the hint is only true while the plan is wider than its box */
@media (min-width: 45em) {
  .plan__hint {
    display: none;
  }
}

/* --- Room zones ---
   One transparent shape per room, so a page can light the room it is talking
   about. The fade is dropped by the global reduced-motion rule in 02-base,
   which is !important and needs no local copy. On a page that only reads the plan nothing changes: the fill stays
   at zero and the drawing is the drawing. */

.plan__zone {
  fill: var(--accent);
  fill-opacity: 0;
  transition: fill-opacity var(--dur-base) var(--ease-in-out);
}

.plan__zone.is-on {
  fill-opacity: 0.1;
}

/* --- Sticky pair ---
   The plan holds still while the rooms go past it. Two columns, 5/7 — the
   drawing supports, the list leads — and no fixed heights anywhere: the plan
   sticks to the top of the viewport under the header and the column beside it
   is simply taller.

   No ancestor of the sticky column may have overflow: hidden. That is why the
   band component uses clip; hidden would make a scroll container here and the
   plan would scroll away with everything else. */

.room + .room,
.room__more {
  margin-block-start: var(--space-16);
}

.room__title {
  margin-block-end: var(--space-4);
}

@media (min-width: 64em) {
  .sticky-pair__inner {
    display: grid;
    grid-template-columns: 5fr 7fr;
    gap: var(--space-12);
    align-items: start;
  }

  .sticky-pair__aside {
    position: sticky;
    /* clear of the sticky header, with air above the drawing */
    top: var(--space-24);
  }

  /* one room per screenful, so exactly one is ever the current one */
  .room + .room {
    margin-block-start: var(--space-32);
  }
}

/* ---------- 16-band.css ---------- */
/* Full-width hero band.

   The photograph runs edge to edge and the copy sits on top of its left third.
   Two decisions here were measured rather than chosen:

   1. The text is dark on a light veil, not light on a dark one. The left of the
      frame holds a blown-out window next to a dark picture frame — luminance
      from 0.06 to 1.00 in the same zone — so no plain text colour survives it
      and a scrim is unavoidable. Measured over the actual pixels of that zone:
      a light veil at 0.72 leaves the worst pixel at 8.7:1 with --ink, while a
      dark veil needs 0.62 just to reach 5:1 with paper-coloured text, by which
      point the photograph behind the text is gone. The light veil also keeps
      the room bright, which is what the picture is selling.

   2. One veil serves both themes because it is painted in --bg and the text in
      --ink. In dark mode both flip together and the ratio holds. */

.band {
  /* How strongly the veil covers the copy. One number, because the right value
     depends on the photograph underneath and was measured per band, not chosen. */
  --veil: 74%;

  position: relative; /* the media is absolute inside it — design system 6.1 */
  display: grid;
  align-items: center;
  min-height: clamp(26rem, 33vw, 40rem);
  /* clip, not hidden: hidden makes a scroll container, and a scroll container
     breaks position: sticky inside it — which the room plan will need. */
  overflow: clip;
  background-color: var(--surface); /* what shows while the image is decoding */
}

.band__media {
  position: absolute;
  inset: 0;
}

.band__image {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 45%;
}

/* The veil covers the copy and nothing else.

   It is tied to the text column, not to a percentage of the viewport, because
   those two part company: the copy is capped at 32rem from the container edge,
   so at 1024px it reaches 56% of the screen and at 1920px only 47%. A gradient
   in viewport percentages was measured failing in dark mode at exactly that
   seam — the last words sat where the veil had already faded and the blown-out
   window came through at rgb(137,140,141), 2.98:1 against light ink.

   So the panel ends where the text ends, plus one fade, at every width. It runs
   past the top and bottom of the band and is clipped by it, which is what keeps
   its horizontal edges soft and its vertical edges invisible. */

.band__inner {
  position: relative; /* the veil is absolute inside it — design system 6.1 */
  isolation: isolate; /* without a stacking context here the z-index: -1 veil
                         escapes and paints behind the photograph instead of
                         over it — measured as no veil at all */
  padding-block: var(--space-16);
}

.band__inner::before,
.band__inner::after {
  content: '';
  position: absolute;
  inset-block: -50vh; /* clipped by the band: no hard top or bottom edge */
  z-index: -1;
}

.band__inner::before {
  inset-inline-start: -100vw;
  width: calc(100vw + var(--gutter) + 32rem);
  /* the veil is what makes the headline readable over a photograph: without
     color-mix this has to be a flat wash rather than nothing */
  background-color: rgb(253 253 251 / 0.74);
  background-color: color-mix(in srgb, var(--bg) var(--veil), transparent);
}

.band__inner::after {
  inset-inline-start: calc(var(--gutter) + 32rem);
  width: 9rem;
  background: linear-gradient(
    to right,
    color-mix(in srgb, var(--bg) var(--veil), transparent),
    transparent
  );
}

.band .hero__title,
.band .hero__lead,
.band .hero__note,
.band .activity {
  max-width: 32rem;
}

.band .hero__lead,
.band .hero__note,
.band .activity {
  color: var(--ink); /* --ink-muted has nothing to spare over a photograph */
}

/* The secondary button is transparent everywhere else, which works over a flat
   background and fails over a photograph: its border disappears into the frame
   and the label sits on whatever happens to be behind it. On the band it gets
   the page background back. */
.band .btn--secondary {
  background-color: var(--bg);
  /* it kept the hairline grey when the rest of the site moved to the control
     token — measured 1.05:1 against the veiled photograph in light and 1.22:1
     in dark, the weakest control edge left on the site */
  border-color: var(--control-line);
}

.band .btn--secondary:hover {
  background-color: var(--surface);
}

/* overflow: clip reached Safari later than the rest. hidden does the same job
   for these two bands — the reason clip was chosen is that it does not create a
   scroll container, and nothing inside either band is sticky. So an engine
   without clip gets hidden and loses nothing here; anywhere a sticky child
   appears later, this fallback is the thing to check first. */
@supports not (overflow: clip) {
  .band,
  .band--plain {
    overflow: hidden;
  }
}

/* --- Ken Burns, on the hero only ---
   A pan across a picture that is already enlarged, not a zoom.

   The zoom was the obvious version and it stepped. Four per cent over twenty
   seconds is about two thousandths of scale per frame, and a scaling composited
   layer is re-rasterised whenever it drifts far enough from the scale it was
   drawn at: every re-raster snaps. Measured on the zoom, with no dropped frames
   at all — 600 frames, median 8.3ms, none over 20ms — the picture still moved
   1041 pixels in a median frame and 2908 in the worst. Smooth timing, visible
   steps.

   A translation never re-rasterises: the compositor moves the layer it already
   has. So the scale is constant and only the offset animates, and the easing is
   linear — over twenty seconds an ease reads as the picture stopping at the
   ends rather than as a decision.

   The travel is also larger and the duration shorter than the zoom's: three per
   cent each way in eighteen seconds rather than four per cent in twenty. Any
   motion this slow moves a fraction of a pixel per frame, and a fraction of a
   pixel is where a compositor without sub-pixel interpolation snaps — so the
   less time the picture spends below that threshold, the better. It is still a
   drift: about nine pixels a second on a 2400px photograph.

   Gated on .js-motion, so a page with motion turned off never starts it. */

.js-motion .band--hero .band__image {
  animation: hero-drift 18s linear infinite alternate;
  will-change: transform;
}

@keyframes hero-drift {
  /* the scale stays put in both frames — only the offset moves */
  from { transform: scale(1.08) translate3d(-3%, 0, 0); }
  to { transform: scale(1.08) translate3d(3%, 0, 0); }
}

/* --- Below the desktop band ---
   A 3:1 frame on a phone is a letterbox, and copy over it would sit on the
   busiest part of the picture. So the image becomes a normal block with a
   taller crop, and the words go underneath where contrast is not a question. */

@media (max-width: 63.999em) {
  .band {
    display: block;
    min-height: 0;
  }

  .band__media {
    position: static;
  }

  .band__image {
    aspect-ratio: 3 / 2;
    height: auto;
    object-position: 62% 50%; /* keeps the island and the person on it in frame */
  }

  /* no text over the picture down here, so no veil either */
  .band__inner::before,
  .band__inner::after {
    content: none;
  }

  .band__inner {
    padding-block: var(--space-12);
  }
}

@media (prefers-reduced-motion: reduce) {
  .band__image {
    animation: none;
  }
}

/* --- The parallaxing band ---
   background-attachment: fixed is the obvious way to do this and the wrong
   one: iOS Safari ignores it and paints a static, badly cropped photograph
   instead. So the picture is an ordinary layer that JS slides, which is the
   same machinery 13-parallax.css already uses for framed photographs.

   The one line that has to be here: 13-parallax.css gives every [data-parallax]
   position: relative, and that selector outweighs .band__media. The media then
   stops being a full-size layer, collapses to zero height, and the slack comes
   out as 0px — the parallax looks wired up and does nothing. */

@media (min-width: 64em) {
  .js-motion .band--fixed .band__media[data-parallax] {
    position: absolute;
    inset: 0;
  }
}

/* ---------- 17-calendar.css ---------- */
/* Availability calendar. States are never colour-only — border style, an
   inline glyph and the accessible label all carry the distinction too.

   Cell borders use --control-line, not --line: a state boundary is a
   meaningful graphic (WCAG 1.4.11, 3:1), not the decorative card divider
   --line is calibrated for (measured ~1.2:1 against these surfaces). */

.avail {
  display: grid;
  /* An implicit grid column defaults to auto — min-content, the same trap
     as a flex item's default min-width: auto one level up. No child
     min-width could shrink a column never allowed below its widest row
     (T74-01: the nav). */
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-6);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: var(--space-6);
  min-width: 0; /* a grid item's default auto min-width is its content's, not the track's */
  position: relative; /* containment for the dialog's helpers, see design-system 6.1 */
}

.avail__head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-6);
}

/* A wrapping flex container's own intrinsic width is measured as if it
   never wrapped — so without this, the fieldset (sized to content) claims
   room for "Every 2 weeks" next to the other two chips unbroken, wider than
   a 360px card, even though .chips itself wraps fine once given a width to
   wrap inside. flex-grow plus min-width: 0 gives it that width to shrink
   into instead of dictating one from its own content. */
.avail__head [data-avail-freq] {
  flex: 1 1 auto;
  min-width: 0;
}

.avail__nav {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.avail__step {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  flex-shrink: 0; /* the select gives way first, not these */
  border: 1px solid var(--control-line);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--ink);
  cursor: pointer;
}

.avail__step svg { width: 18px; height: 18px; }

.avail__step:hover { border-color: var(--accent); color: var(--accent); }
.avail__step:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.avail__step:disabled { opacity: 0.4; cursor: not-allowed; }

/* Select arrow: masked ::after on a wrapper, not background-image on the
   select. A data: URI is its own document — currentColor/var() inside one
   never see the page cascade and resolve to the SVG's own default, invisible
   in dark. A mask paints with background-color, which the cascade does
   reach. ::after on <select> itself is unreliable across engines. One CSS
   variable holds the encoded SVG so it is not duplicated for -webkit-. */
.avail__jump-wrap {
  position: relative;
  display: inline-block;
  min-width: 0;
  --chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E");
}

.avail__jump-wrap::after {
  content: '';
  position: absolute;
  top: 50%;
  right: var(--space-3);
  translate: 0 -50%;
  width: 16px;
  height: 16px;
  background-color: var(--ink);
  -webkit-mask: var(--chevron) center / 16px no-repeat;
  mask: var(--chevron) center / 16px no-repeat;
  pointer-events: none;
}

.avail__jump {
  appearance: none;
  min-width: 0;
  max-width: 11rem;
  height: 44px;
  padding-inline: var(--space-3) var(--space-8);
  border: 1px solid var(--control-line);
  border-radius: var(--radius-md);
  background-color: var(--surface);
  color: var(--ink);
  font: inherit;
}

.avail__jump:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.avail__summary {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-2);
  min-width: 0;
  overflow-wrap: anywhere;
  font-size: var(--text-lg);
  color: var(--ink);
}

.avail__summary span { min-width: 0; overflow-wrap: anywhere; }

.avail__month-label {
  margin: 0 0 var(--space-4);
  font-size: var(--text-lg);
  font-weight: 600;
  min-width: 0;
}

.avail__weekdays,
.avail__grid {
  display: grid;
  grid-template-columns: repeat(7, 1fr);
  gap: var(--space-1);
}

.avail__weekdays {
  margin-block-end: var(--space-2);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--ink-muted);
  text-align: center;
}

.avail__closed-mark { color: var(--accent); font-weight: 700; }

.avail__note {
  margin: calc(var(--space-2) * -1) 0 0;
  font-size: var(--text-xs);
  color: var(--ink-muted);
}

.avail__cell {
  position: relative;
  display: grid;
  place-items: center;
  aspect-ratio: 1;
  /* WCAG 2.5.8 asks 24px, not the 44px seen elsewhere on the site — a
     7-column grid inside a fixed 24px page gutter cannot hold 44px cells at
     320px (the container leaves ~272px, and 7 × 44 alone is 308). min-height
     equal to min-width, both above the 24px floor, lets the track really
     shrink: pairing aspect-ratio with a min on only one axis was what forced
     the square back up to 44 regardless of the track (T74-01). */
  min-width: 36px;
  min-height: 36px;
  border: 1px solid var(--control-line);
  border-radius: var(--radius-sm);
  background: var(--surface);
  font: inherit;
  font-size: var(--text-sm);
  color: var(--ink);
}

.avail__cell--pad { visibility: hidden; }

.avail__cell.is-past { color: var(--ink-muted); opacity: 0.6; }

/* Booked and closed read identically at a glance if only the class differs
   — same border style, same fill (T69-02). Closed gets a hatch, booked
   stays a flat dashed tile: two different graphics, not one graphic with
   two names. */
.avail__cell.is-booked {
  border-style: dashed;
  color: var(--ink-muted);
  background: var(--line);
}

.avail__cell.is-closed {
  border-style: solid;
  color: var(--ink-muted);
  background-color: var(--surface);
  background-image: repeating-linear-gradient(45deg, var(--line) 0 3px, transparent 3px 6px);
}

.avail__cell.is-free {
  cursor: pointer;
}

.avail__cell.is-free:hover,
.avail__cell.is-free:focus-visible {
  border-color: var(--accent);
  background: var(--accent-soft);
  color: var(--accent);
}

.avail__cell.is-yours {
  border-color: var(--accent);
  border-width: 2px;
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 700;
}

.avail__cell.is-shifted::after {
  content: '\21BA';
  position: absolute;
  top: 1px;
  right: 2px;
  font-size: 0.6rem;
  font-weight: 400;
}

.avail__cell:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.avail__legend {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  list-style: none;
  padding: 0;
  margin: 0;
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

.avail__legend-item { position: relative; padding-inline-start: var(--space-6); }

.avail__legend-item::before {
  content: '';
  position: absolute;
  inset-inline-start: 0;
  top: 50%;
  translate: 0 -50%;
  width: var(--space-4);
  height: var(--space-4);
  border-radius: var(--radius-sm);
  border: 1px solid var(--control-line);
}

.avail__legend-item.is-yours::before { border-color: var(--accent); border-width: 2px; background: var(--accent-soft); }
.avail__legend-item.is-shifted::before { border-style: dashed; border-color: var(--accent); }
.avail__legend-item.is-booked::before { border-style: dashed; background: var(--line); }
.avail__legend-item.is-closed::before {
  border-style: solid;
  background-image: repeating-linear-gradient(45deg, var(--line) 0 2px, transparent 2px 4px);
}

/* CSS Grid does not need role="row" wrappers to lay out — display: contents
   keeps every cell a direct grid item — but Chrome drops the whole grid
   (and its aria-label) from the accessibility tree without them (T69-05):
   an ARIA grid requires row children to be a valid grid at all. */
.avail__row { display: contents; }

/* ---------- Confirmation dialog ---------- */

.avail__dialog {
  margin: auto; /* the site's global reset zeroes margin; a modal <dialog>
    needs it back to center in inset: 0 — without it the browser's own
    default centering never applies and it renders at 0,0 (T69-03). */
  border: 0;
  border-radius: var(--radius-lg);
  padding: var(--space-8);
  max-width: 26rem;
  width: calc(100% - var(--space-8));
  background: var(--surface);
  color: var(--ink);
  box-shadow: var(--shadow-lg);
}

.avail__dialog::backdrop { background: var(--overlay); }

.avail__dialog h3 { margin: 0 0 var(--space-3); }
.avail__dialog p { margin: 0 0 var(--space-6); color: var(--ink-muted); }

.avail__dialog-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
}

@media (min-width: 48em) {
  .avail__cell { font-size: var(--text-base); }
}

/* No room left for the card's own side padding at 7 columns this narrow —
   the grid bleeds to the card's own edge (border stays as the frame) so
   every spare pixel goes to the cells instead of sitting in a margin. */
@media (max-width: 26em) {
  .avail { padding: var(--space-4) var(--space-3); }
  .avail__weekdays, .avail__grid {
    gap: 2px;
    margin-inline: calc(var(--space-3) * -1);
  }
}

@media (max-width: 22.5em) {
  .avail { padding-inline: var(--space-2); }
  .avail__weekdays, .avail__grid {
    gap: 1px;
    margin-inline: calc(var(--space-2) * -1);
  }
  /* 7 × 36 + 6 × 1px gap doesn't fit this box (T78-02) — the 36px floor was
     a comfort minimum, not a real one; dropping it here lets 1fr divide the
     exact available width with nothing left over to overflow with. */
  .avail__cell { min-width: 0; min-height: 0; }
}

/* ---------- Home-page strip: nearest open days, no grid ---------- */

/* The neighbouring .section--soft draws its wedge on bottom: 100% — outside
   its own box, over whatever sits above it, up to --divider-h deep at one
   edge (14-dividers.css). It is a clip-path, not a box, so no rectangle
   test ever sees the overlap; the fix is to start this strip's own content
   below the wedge's deepest point rather than to remove a divider the whole
   site's section rhythm depends on.

   Both edges, not just the top (T88): .section--soft draws a mirrored wedge
   on top: 100% as well, so a neighbour below reaches up over this strip by
   the same --divider-h. */
.next-slots {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3) var(--space-6);
  min-width: 0;
  margin-block: var(--divider-h);
  padding: var(--space-4) var(--space-6);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--surface);
}

.next-slots__label {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  min-height: 44px; /* the row's own height, so the label centres against the slots */
  margin: 0;
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--ink-muted);
}

.next-slots__list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  list-style: none;
  padding: 0;
  margin: 0;
  flex: 1 1 auto;
  min-width: 0;
}

.next-slots__slot {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding-inline: var(--space-3);
  border: 1px solid var(--control-line);
  border-radius: var(--radius-md);
  color: var(--ink);
  text-decoration: none;
  font-size: var(--text-sm);
}

.next-slots__slot:hover,
.next-slots__slot:focus-visible {
  border-color: var(--accent);
  color: var(--accent);
  background: var(--accent-soft);
}

.next-slots__slot:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

.next-slots__more {
  flex-shrink: 0;
  display: inline-flex;
  align-items: center;
  /* A flex item is blockified whatever its own display says, so the inline
     exemption from the target-size rule does not apply to this one — it
     needs the height itself (T83-03). */
  min-height: 44px;
  margin-inline-start: auto;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--accent);
  text-decoration: none;
}

.next-slots__more:hover,
.next-slots__more:focus-visible { text-decoration: underline; }
.next-slots__more:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ---------- 18-sticky-cta.css ---------- */
/* The sticky booking bar, phones and tablets only.

   It exists because of a measurement: the header's own button is hidden below
   1280px — burger only at 360, 390, 768 and 1024 — so on a phone there is no
   call to action on screen at all once the hero has scrolled away. Above that
   width the header carries it and this bar would be a second copy.

   Three things it must not do, in order of how badly they read:
   - cover the end of the page. The body gets exactly the bar's height back as
     padding, so the footer's last line still clears it.
   - repeat an action that is already on screen. When the booking form or the
     closing call to action is in view, the bar steps out of the way.
   - flash. The reveal class is set in the head, before the first paint, so the
     bar starts off-screen rather than appearing and then sliding. */

.sticky-cta {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  /* under the header, which is 50 and holds the drawer inside its own stacking
     context: a bar above it would float over an open menu */
  z-index: 40;

  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);

  padding: var(--space-3) var(--gutter);
  /* the home indicator on an iPhone sits inside the viewport: without this the
     button's lower half is under it */
  padding-block-end: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));

  background-color: var(--surface);
  border-block-start: 1px solid var(--control-line);
  box-shadow: 0 -4px 16px rgb(12 17 20 / 0.08);
}

/* The height the page has to give back — safe area included, once.

   The bar's own padding already reserves env(safe-area-inset-bottom), so its
   measured height carries the home indicator inside it. The script writes that
   measured height here, which means anything adding env() again on top of this
   token reserves the same 34px twice. The fallback below is the only place the
   inset appears in this file, and it exists for the case where no script ran to
   measure anything. */
:root {
  --sticky-cta-h: calc(4.25rem + env(safe-area-inset-bottom, 0px));
}

.sticky-cta__note {
  margin: 0;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink);
  /* the promise gives way before the button does */
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.sticky-cta__action {
  flex-shrink: 0;
}

/* Reveal. .js-sticky is added in the head script, so a page with scripting
   starts with the bar off-screen and never shows it in the wrong place; a page
   without scripting keeps it on screen from the first frame. */
.js-sticky .sticky-cta {
  transform: translateY(110%);
  transition: transform var(--dur-base) var(--ease-out);
  will-change: transform;
}

.js-sticky .sticky-cta.is-shown {
  transform: none;
}

/* An open drawer is already a full-screen choice of where to go; the bar
   underneath it is noise, and on a short phone it overlaps the drawer's own
   footer. */
body.has-drawer-open .sticky-cta {
  opacity: 0;
  pointer-events: none;
}

/* Above the header's own breakpoint the bar is not merely hidden — it is not
   there, so it cannot take focus or be read out. */
@media (min-width: 80em) {
  .sticky-cta {
    display: none;
  }
}

@media (max-width: 79.999em) {
  body {
    padding-block-end: var(--sticky-cta-h);
  }

  /* An anchor that lands at the very bottom of the viewport would land under
     the bar. The header already reserves its own height at the top; this is the
     same reservation at the other end. */
  html {
    scroll-padding-bottom: var(--sticky-cta-h);
  }
}

@media (max-width: 22.5em) {
  /* at 360px the promise and the button cannot both keep their room; the
     button is the point of the bar, so the promise goes */
  .sticky-cta__note {
    display: none;
  }

  .sticky-cta {
    justify-content: center;
  }

  .sticky-cta__action {
    width: 100%;
  }
}

@media (prefers-reduced-motion: reduce) {
  .js-sticky .sticky-cta {
    transition: none;
  }
}

/* ---------- 19-rates.css ---------- */
/* Rate cards on /pricing/. Three, with the middle one raised — the shape this
   niche settled on because a reader picks between three things rather than
   reading fifteen cells. The table stays underneath: the cards answer what a
   clean costs, the table what YOUR home costs.

   The prices live in the markup, with the reasoning; this file only places
   them. */

.rates {
  display: grid;
  gap: var(--space-6);
  margin-block-start: var(--space-12);
}

.rate {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-8) var(--space-6);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  background-color: var(--surface);
}

.rate--lead {
  border-color: var(--accent);
  box-shadow: var(--shadow-md);
}

/* The badge is bounded by the card, by construction.

   It used to be nowrap with no maximum, which for an absolutely positioned box
   means it keeps its one-line width whatever the card does: at 768 the card was
   213.7px and the badge 255.2px, computed right: -67.5px, and it drew itself
   over 42.5px of the card beside it. Nothing warned, because an out-of-flow box
   pushes nothing.

   So: a maximum tied to the card's own width, and permission to wrap. A shorter
   line would have fixed this width; only the maximum fixes every width. */
.rate__badge {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: var(--space-6);
  max-width: calc(100% - var(--space-6) * 2);
  transform: translateY(-50%);
  padding: var(--space-1) var(--space-3);
  border-radius: var(--radius-full);
  background-color: var(--accent);
  color: var(--accent-contrast);
  font-size: var(--text-xs);
  font-weight: 600;
  line-height: var(--leading-tight, 1.25);
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  text-wrap: balance;
}

.rate__name {
  margin: 0;
  font-size: var(--text-xl);
}

.rate__price {
  display: flex;
  align-items: baseline;
  gap: var(--space-2);
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  font-weight: 600;
  line-height: 1;
  color: var(--ink);
}

.rate__unit {
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: 400;
  color: var(--ink-muted);
}

.rate__text {
  margin: 0;
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

.rate__list {
  display: grid;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
  font-size: var(--text-sm);
}

.rate__list li {
  display: flex;
  gap: var(--space-2);
}

.rate__action {
  margin-block-start: auto;
}

@media (min-width: 48em) {
  .rates {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    /* stretch, not start: every card takes the height of the tallest content,
       and the raised one then adds its own padding on top of that. With start,
       each card was as tall as its own text, and at exactly 768 the third card
       came out 8px taller than the card that is supposed to lead. */
    align-items: stretch;
  }

  .rate--lead {
    margin-block: calc(var(--space-4) * -1);
    padding-block: calc(var(--space-8) + var(--space-4));
  }
}

/* ---------- 20-team.css ---------- */
/* The team block on /about/, and the van band under it.

   Four square portraits, name, role, one line each. No cards: a border around a
   face turns a person into a product tile, and the photographs already have
   their own edges. What holds the row together is the shared crop and the
   alignment, not a box.

   The photographs are 1:1 by construction — aspect-ratio here rather than trust
   in the file — so a replacement that is 4:3 crops instead of breaking the row. */

.team {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-12) var(--space-6);
  margin: var(--space-12) 0 0;
  padding: 0;
  list-style: none;
}

.team__photo {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border-radius: var(--radius-lg);
  background-color: var(--surface);
}

.team__name {
  margin: var(--space-4) 0 0;
  font-size: var(--text-xl);
}

.team__role {
  margin: var(--space-1) 0 0;
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: var(--tracking-caps);
  text-transform: uppercase;
  color: var(--accent);
}

.team__note {
  margin: var(--space-2) 0 0;
  color: var(--ink-muted);
  font-size: var(--text-sm);
}

.team__note--wide {
  max-width: var(--measure);
  margin-block-start: var(--space-12);
}

@media (min-width: 30em) {
  .team {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 64em) {
  .team {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

/* --- The van --- */

.band--plain {
  display: block;
  min-height: 0;
  overflow: clip;
  background-color: var(--surface);
  /* room under the caption. Without it the caption's last line sits exactly on
     the boundary of the next section, and .section--soft draws its diagonal
     edge OUTSIDE its own box, over whatever is above it — measured 33px of the
     caption under the wedge at 1440, 13,164 painted pixels. Same defect as
     T-50, caught by the check written for it. */
  padding-block-end: var(--space-12);
}

.band__figure {
  margin: 0;
}

.band__photo {
  display: block;
  width: 100%;
  height: auto;
  /* the band crop, not the file's: a 3.38:1 photograph is a band on a wide
     screen and a letterbox on a phone, so the phone gets a taller slice of it */
  aspect-ratio: 21 / 9;
  object-fit: cover;
}

.band__caption {
  max-width: var(--container);
  margin: var(--space-4) auto 0;
  padding-inline: var(--gutter);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

@media (max-width: 47.999em) {
  .band__photo {
    aspect-ratio: 3 / 2;
  }
}

/* ---------- 21-badges.css ---------- */
/* The trust cluster.

   It is a strip, not another container section, and that is the point of it
   being here at all. /pricing/ ran eight container-width sections in a row with
   nothing alternating but the background colour — which the layout notes call
   twelve identical sections rather than six pairs. The cluster breaks that run
   with a different width and a different ground, and carries information while
   doing it.

   Five items, one line each, no borders between them: a row of boxed badges is
   a table of contents, a row of marks is a signature. The icons are anchors —
   five distinct silhouettes so the eye can count them at a glance — and the
   words carry every claim. */

.badge-strip {
  background-color: var(--accent-soft);
  /* --space-12, not --space-8: the section below is soft, and a soft section
     draws its diagonal edge OUTSIDE its own box, over whatever is above it.
     Measured 32px of padding against a 36px wedge — the last badge sat under
     it. The token comment on --divider-h says the same thing from the other
     side: the wedge is kept under --space-12 so it lands in padding. */
  padding-block: var(--space-12);
}

.badges {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-4) var(--space-8);
  max-width: var(--container-wide);
  margin: 0 auto;
  padding-inline: var(--gutter);
  list-style: none;
}

.badge-item {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--ink);
}

.badge-item__icon {
  flex-shrink: 0;
  width: 1.5rem;
  height: 1.5rem;
  color: var(--accent);
}

.badge-item__text {
  min-width: 0;
}

/* Two columns as soon as a line fits twice, five across when the row does not
   force a line to wrap. The count is not hard-coded to five: auto-fit keeps the
   strip honest if an item is ever added or removed. */
@media (min-width: 36em) {
  .badges {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 64em) {
  .badges {
    grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
    gap: var(--space-6);
  }
}

/* ---------- 23-route.css ---------- */
/* The route diagram on /service-areas/.

   A line with stops on it, drawn entirely in CSS on an ordinary list: the dot
   is a ::before on the stop, the connector is a ::after that runs to the next
   one. Nothing here is an image or a script, so the diagram survives both a
   missing stylesheet (a legible list of names and times) and a missing script
   (it never had one).

   Vertical first, because that is how a route reads on a phone and how a list
   is laid out anyway. The horizontal arrangement is the wide-screen variant,
   not the base. */

.route {
  margin: var(--space-12) 0 0;
}

.route__line {
  display: grid;
  gap: var(--space-8);
  margin: 0;
  padding: 0;
  list-style: none;
  counter-reset: stop;
}

.route__stop {
  position: relative;
  padding-inline-start: var(--space-8);
}

/* the stop itself */
.route__stop::before {
  content: '';
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0.45em;
  width: 0.875rem;
  height: 0.875rem;
  border-radius: var(--radius-full);
  background-color: var(--accent);
  /* a ring in the page colour keeps the dot legible where the connector runs
     behind it */
  box-shadow: 0 0 0 3px var(--bg);
}

/* the connector to the next stop */
.route__stop:not(:last-child)::after {
  content: '';
  position: absolute;
  inset-inline-start: calc(0.875rem / 2 - 1px);
  inset-block-start: 0.45em;
  width: 2px;
  height: calc(100% + var(--space-8));
  background-color: var(--accent);
  opacity: 0.45;
}

.route__leg {
  margin: 0 0 var(--space-2);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

.route__leg-time {
  font-weight: 600;
  color: var(--ink);
}

.route__name {
  display: inline-block;
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: 600;
  color: var(--ink);
}

a.route__name {
  text-decoration: underline;
  text-decoration-color: var(--accent);
  text-underline-offset: 4px;
  text-decoration-thickness: 2px;
}

a.route__name:hover {
  color: var(--accent);
}

a.route__name:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 4px;
  border-radius: var(--radius-sm);
}

/* The neighbourhoods beside a stop. Not links, and they say why in the caption
   — the difference has to be visible before anyone tries to click one, so they
   are plain text at the muted weight with a hollow marker. */
.route__near {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-6);
  margin: var(--space-2) 0 0;
  padding: 0;
  list-style: none;
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

.route__near li {
  position: relative;
  padding-inline-start: var(--space-4);
}

.route__near li::before {
  content: '';
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0.55em;
  width: 0.5rem;
  height: 0.5rem;
  border-radius: var(--radius-full);
  border: 2px solid var(--accent);
  opacity: 0.75;
}

.route__near-time {
  color: var(--ink-muted);
  white-space: nowrap;
}

.route__caption {
  max-width: var(--measure);
  margin: var(--space-8) 0 0;
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

/* Wide screens: the same list, laid along a horizontal line. The connector
   turns and runs to the right, the times sit above it, and each stop keeps its
   own neighbourhoods underneath. */
@media (min-width: 64em) {
  .route__line {
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: var(--space-8);
    align-items: start;
  }

  .route__stop {
    padding-inline-start: 0;
    padding-block-start: var(--space-8);
  }

  .route__stop::before {
    inset-block-start: 1.55rem;
    inset-inline-start: 0;
  }

  .route__stop:not(:last-child)::after {
    inset-block-start: calc(1.55rem + 0.875rem / 2 - 1px);
    inset-inline-start: 0.875rem;
    width: calc(100% + var(--space-8) - 0.875rem);
    height: 2px;
  }

  .route__leg {
    position: absolute;
    inset-block-start: 0;
    inset-inline-start: 0;
    margin: 0;
  }

  /* the first stop has no leg above it, so its name lines up with the rest */
  .route__stop:first-child {
    padding-block-start: var(--space-8);
  }

  .route__name {
    margin-block-start: var(--space-4);
  }

  .route__near {
    display: grid;
    gap: var(--space-1);
  }
}
