/* ==========================================================================
   00-fonts.css
   ========================================================================== */

/* Hanken Grotesk, self hosted. Variable 300-800, latin and latin-ext only.
   Shipped locally rather than from fonts.googleapis.com: a third party on the
   critical path costs a connection before the first glyph, and it is one more
   host that has to be up for the page to look right. */

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

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

/* ==========================================================================
   01-tokens.css
   ========================================================================== */

/* ==========================================================================
   Tokens
   ==========================================================================

   THE PALETTE IS COOL, AND THAT IS THE WHOLE IDEA.

   The obvious move for a premium cleaning brand with a rose-gold script logo is
   a warm cream ground with a clay accent. Every generated page for a service
   business looks like that, and worse, it fights the photographs: Tracy shoots
   downtown Chicago condos through floor-to-ceiling glass, so her own pictures
   are cool - grey velvet, blue-grey towels, white marble, navy, hard northern
   daylight. Warm cream behind them reads as a filter laid over someone else's
   work.

   So the ground is porcelain with a blue cast, the ink is a deep slate read off
   the navy throw in the bedroom frame, and the rose gold is reserved. It is her
   signature, not her wallpaper.

   The three rose values are SAMPLED FROM THE LOGO, not picked to taste:
   assets/source-originals/logo_main.dce00f3f.png, mean of the saturated warm
   pixels in the brush stroke, then the mean of its darkest and lightest fifths.
   If the logo is ever redrawn, re-sample rather than eyeballing a match.
   ========================================================================== */

:root {
  /* --- ground and surfaces -------------------------------------------- */
  --porcelain:    #edf1f3;  /* the page: pale, cool, the colour of clean glass */
  --porcelain-lo: #e3e9ec;  /* a half-step down, for banded sections */
  --white:        #ffffff;  /* every surface that should read as lit */
  --mist:         #dae2e6;  /* hairlines */
  --mist-soft:    #e8eef1;  /* hairlines on white */

  /* --- ink -------------------------------------------------------------
     Not #111. A tinted near-black is the tell; this one has real hue in it,
     taken from the navy throw, so text and photographs sit in one world. */
  --slate:     #16212b;
  --slate-80:  #3a464f;
  --slate-60:  #5d686f;
  --slate-45:  #6b7479;  /* 4.8:1 on white. At #7d868c it was 3.7 and failed AA */

  /* --- rose gold, sampled from the logo -------------------------------- */
  --rose-deep: #a5665a;  /* 4.9:1 on white - the only rose safe for text */
  --rose:      #dc9982;
  --rose-pale: #fbc9b3;
  --rose-wash: #fdf1ec;  /* a tint for filled chips and quiet panels */

  /* Her logo is glitter: thousands of specks of light, not a flat fill. The
     rose therefore appears as a SHEEN on exactly two things - the primary
     button and the rule under a section heading - and nowhere else.

     THE SHEEN IS BRIGHT, AND THE TEXT ON IT IS DARK. The first version ran
     from --rose-deep up to --rose-pale and set white on top, which measured
     2.35:1 at the middle of the gradient and 1.49:1 at its highlight. Both
     fail AA badly, and the CTA is the one control on the page that cannot be
     hard to read. White forces every stop down to --rose-deep or darker, and a
     rose gold that dark is just brown. So the gradient keeps the bright metal
     and the label goes slate: 6.9:1 at the darkest stop, 11:1 at the
     highlight. Dark ink on bright foil is how foil stamping has always
     worked. */
  --sheen: linear-gradient(103deg, #d08e77 0%, var(--rose) 30%, var(--rose-pale) 52%, var(--rose) 74%, #d08e77 100%);

  /* --- type -------------------------------------------------------------
     One family. The logo is already a script; a display serif beside it would
     be two voices competing for the same job. Hanken Grotesk is humanist
     enough to stay warm at heavy weights, which is what carries the headlines
     without a second face. */
  --sans: 'Hanken Grotesk', ui-sans-serif, system-ui, 'Segoe UI', sans-serif;

  /* A modular scale at 1.25, with the display sizes fluid. */
  --t-xs:   0.8125rem;
  --t-sm:   0.9375rem;
  --t-base: 1.0625rem;
  --t-md:   1.1875rem;
  --t-lg:   clamp(1.35rem, 1.1rem + 0.9vw, 1.75rem);
  --t-xl:   clamp(1.75rem, 1.3rem + 1.9vw, 2.6rem);
  --t-2xl:  clamp(2.3rem, 1.5rem + 3.4vw, 4rem);
  --t-3xl:  clamp(2.9rem, 1.5rem + 5.6vw, 6rem);

  /* --- space ------------------------------------------------------------ */
  --s-1: 0.25rem;
  --s-2: 0.5rem;
  --s-3: 0.75rem;
  --s-4: 1rem;
  --s-5: 1.5rem;
  --s-6: 2rem;
  --s-7: 3rem;
  --s-8: 4rem;
  --s-9: 6rem;
  --s-10: 8rem;

  --shell: 78rem;
  --gutter: clamp(1.25rem, 5vw, 4rem);

  /* --- shape and depth --------------------------------------------------
     Radii differ by role rather than being one value applied to everything:
     a photograph gets a generous one, a control gets a small one, a chip is a
     pill. Shadows are cool and wide, because the light in her photographs
     comes from a window, not from a lightbulb over the card. */
  --r-sm: 0.375rem;
  --r-md: 0.75rem;
  --r-lg: 1.25rem;
  --r-xl: 2rem;
  --r-pill: 999px;

  --lift-1: 0 1px 2px rgba(22, 33, 43, 0.04), 0 4px 16px -6px rgba(22, 33, 43, 0.10);
  --lift-2: 0 2px 4px rgba(22, 33, 43, 0.05), 0 18px 44px -20px rgba(22, 33, 43, 0.22);
  --lift-3: 0 4px 8px rgba(22, 33, 43, 0.06), 0 40px 80px -32px rgba(22, 33, 43, 0.30);

  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}

/* ==========================================================================
   02-base.css
   ========================================================================== */

/* ==========================================================================
   Base: reset, typography, links, focus
   ========================================================================== */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* The header is sticky, so an in-page anchor must clear it or the heading
     it jumps to lands underneath the bar. */
  scroll-padding-top: 6rem;
}

@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;
  }
}

body {
  margin: 0;
  background: var(--porcelain);
  color: var(--slate);
  font-family: var(--sans);
  font-size: var(--t-base);
  font-weight: 400;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;
}

h1, h2, h3, h4 {
  margin: 0;
  font-weight: 700;
  /* Headlines are the only place the type does visual work, so they are set
     tight on both axes. Body copy stays at normal tracking. */
  letter-spacing: -0.028em;
  line-height: 1.06;
  text-wrap: balance;
}

h1 { font-size: var(--t-3xl); font-weight: 800; letter-spacing: -0.038em; }
h2 { font-size: var(--t-2xl); }
h3 { font-size: var(--t-lg); letter-spacing: -0.02em; line-height: 1.18; }
h4 { font-size: var(--t-md); letter-spacing: -0.012em; line-height: 1.3; }

p { margin: 0 0 var(--s-4); max-width: 68ch; }
p:last-child { margin-bottom: 0; }

a { color: inherit; text-underline-offset: 0.2em; }

ul, ol { margin: 0; padding: 0; }

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

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

strong { font-weight: 700; }

:focus-visible {
  outline: 2.5px solid var(--rose-deep);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}
:focus:not(:focus-visible) { outline: none; }

::selection { background: var(--rose-pale); color: var(--slate); }

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

.skip-link {
  position: absolute;
  top: var(--s-3);
  left: var(--s-3);
  z-index: 200;
  padding: var(--s-3) var(--s-5);
  background: var(--slate);
  color: var(--white);
  border-radius: var(--r-md);
  font-weight: 600;
  text-decoration: none;
  transform: translateY(-160%);
  transition: transform 180ms var(--ease);
}
.skip-link:focus { transform: translateY(0); }

/* ==========================================================================
   03-layout.css
   ========================================================================== */

/* ==========================================================================
   Layout: shell, sections, section headings
   ==========================================================================

   Everything is left-aligned against one rail. Centred columns are the default
   for a service page and they make a page read as a brochure; holding a single
   left edge from the logo down through every heading is what makes it read as
   a document someone laid out on purpose.
   ========================================================================== */

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

.section { padding-block: clamp(var(--s-8), 9vw, var(--s-10)); }
.section-tight { padding-block: clamp(var(--s-7), 6vw, var(--s-9)); }
.section-paper { background: var(--porcelain-lo); }
.section-white { background: var(--white); }

/* A section heading and its rule. The rule is one of only two places the
   rose-gold sheen appears - it is the signature, so it has to stay rare. */
.head { margin-bottom: var(--s-7); }
.head__rule {
  width: 3.25rem;
  height: 3px;
  margin-bottom: var(--s-5);
  border-radius: 2px;
  background: var(--sheen);
}
.head h2 { max-width: 20ch; }
.head__sub {
  margin-top: var(--s-4);
  max-width: 56ch;
  color: var(--slate-60);
  font-size: var(--t-md);
}

/* Two-column: a heading rail on the left, content on the right. Collapses to
   one column early, because a 1fr/1fr split at tablet width leaves both halves
   too narrow to read. */
.split {
  display: grid;
  gap: clamp(var(--s-6), 5vw, var(--s-9));
  align-items: start;
}
/* A heading set at the full display size inside a 20rem rail broke "What is
   Cleaning with Cooley" over four lines. The rail is wider, and a heading in
   one steps down a size - it is a rail, not a hero. */
.split__rail h2 { font-size: var(--t-xl); }

@media (min-width: 60rem) {
  .split { grid-template-columns: minmax(0, 24rem) minmax(0, 1fr); }
  .split--wide { grid-template-columns: minmax(0, 1fr) minmax(0, 1.35fr); }
  .split__rail { position: sticky; top: 7rem; }
}

.grid { display: grid; gap: var(--s-5); }
@media (min-width: 48rem) { .grid--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 62rem) { .grid--4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (min-width: 48rem) and (max-width: 61.999rem) { .grid--4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

.stack > * + * { margin-top: var(--s-4); }
.stack-lg > * + * { margin-top: var(--s-6); }

/* A page whose first section IS the explorer: the h1 steps down from the
   display scale, because it is a working heading over a control, not a
   statement the way "Who I am" is. */
.head__h1 { font-size: var(--t-2xl); }

/* ==========================================================================
   04-chrome.css
   ========================================================================== */

/* ==========================================================================
   Header, navigation, footer
   ========================================================================== */

.header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: rgba(255, 255, 255, 0.82);
  backdrop-filter: saturate(160%) blur(14px);
  -webkit-backdrop-filter: saturate(160%) blur(14px);
  border-bottom: 1px solid transparent;
  transition: border-color 260ms var(--ease), background 260ms var(--ease);
}
/* The rule only appears once the page has moved, so the header sits on the
   hero photograph without a line cutting across it. */
.header[data-scrolled='true'] {
  border-bottom-color: var(--mist);
  background: rgba(255, 255, 255, 0.94);
}

.header__inner {
  display: flex;
  align-items: center;
  gap: var(--s-5);
  min-height: 4.75rem;
}

.brand { display: flex; align-items: center; flex: 0 0 auto; text-decoration: none; }
/* The header uses the ROUND badge, not the script wordmark, and that is a
   decision about aspect ratio rather than taste. The wordmark is 650x425: three
   stacked lines of script with a deep C loop and a Cooley descender. Fit its
   height into a header and it is 98px wide and illegible; make it legible and
   it is 118px tall and the header is not a header any more. The badge is 1:1,
   so it reads at 52px. The script wordmark gets the footer, which has room.

   The name beside it is set in the site's own face. A clean grotesque next to
   an ornate glitter badge is the contrast doing the work - two ornate things
   would fight. */
.brand { gap: var(--s-3); }
.brand img { width: 44px; height: 44px; }
@media (min-width: 48rem) { .brand img { width: 52px; height: 52px; } }

.brand__name {
  display: none;
  color: var(--slate);
  font-size: 1.0625rem;
  font-weight: 700;
  letter-spacing: -0.028em;
  white-space: nowrap;
}
/* "with" is the small word in her own lettering too - it sits between the two
   big lines at a fraction of their size. */
.brand__name em {
  font-style: normal;
  font-weight: 500;
  color: var(--slate-45);
}
.brand__name { display: inline; }

/* Under ~30rem the bar cannot hold badge + name + a phone button with a number
   in it + the menu. The number is the thing that compresses best: an icon-only
   call button keeps the most important action one tap away and gives the name
   back its room. The accessible name still reads the whole number. */
@media (max-width: 29.999rem) {
  .header__cta .btn--primary { padding: 0.6rem; }
  .header__cta .btn--primary span { display: none; }
  .header__cta .btn--primary svg { width: 1.25rem; height: 1.25rem; }
}
@media (max-width: 20rem) { .brand__name { display: none; } }

.nav { display: none; margin-left: auto; align-items: center; gap: var(--s-1); }
@media (min-width: 56rem) { .nav { display: flex; } }

.nav a {
  padding: var(--s-2) var(--s-4);
  border-radius: var(--r-pill);
  color: var(--slate-80);
  font-size: var(--t-sm);
  font-weight: 600;
  text-decoration: none;
  transition: background 160ms var(--ease), color 160ms var(--ease);
}
.nav a:hover { background: var(--porcelain); color: var(--slate); }
.nav a[aria-current='page'] { color: var(--slate); background: var(--porcelain); }

.header__cta { margin-left: auto; display: flex; align-items: center; gap: var(--s-3); }
@media (min-width: 56rem) { .header__cta { margin-left: var(--s-4); } }

/* --- the mobile panel ----------------------------------------------------
   A details/summary disclosure rather than a scripted drawer: it opens with
   JS disabled, it is keyboard-operable for free, and there is no focus trap to
   get wrong. */
.menu { position: relative; }
@media (min-width: 56rem) { .menu { display: none; } }

.menu > summary {
  display: grid;
  place-items: center;
  width: 2.75rem; height: 2.75rem;
  border-radius: var(--r-md);
  list-style: none;
  color: var(--slate);
}
.menu > summary::-webkit-details-marker { display: none; }
.menu > summary:hover { background: var(--porcelain); }
.menu[open] > summary { background: var(--porcelain); }

.menu__panel {
  position: absolute;
  right: 0;
  top: calc(100% + var(--s-3));
  min-width: 14rem;
  padding: var(--s-3);
  background: var(--white);
  border: 1px solid var(--mist);
  border-radius: var(--r-lg);
  box-shadow: var(--lift-2);
}
.menu__panel a {
  display: block;
  padding: var(--s-3) var(--s-4);
  border-radius: var(--r-md);
  font-weight: 600;
  text-decoration: none;
}
.menu__panel a:hover { background: var(--porcelain); }

/* --- footer ------------------------------------------------------------- */
.footer {
  background: var(--slate);
  color: rgba(255, 255, 255, 0.72);
  padding-block: var(--s-8) var(--s-6);
}
.footer a { color: var(--white); }

.footer__top {
  display: grid;
  gap: var(--s-7);
  padding-bottom: var(--s-7);
  border-bottom: 1px solid rgba(255, 255, 255, 0.14);
}
@media (min-width: 52rem) {
  .footer__top { grid-template-columns: minmax(0, 1.3fr) repeat(2, minmax(0, 1fr)); gap: var(--s-6); }
}

/* The round badge, not the script wordmark. The wordmark's lettering is
   near-black on transparent, so on this footer it disappeared entirely - and a
   white-inverted version would then put white script over a pale rose brush,
   which is no better. The badge has its own fill and reads on any ground. */
.footer__mark { width: 84px; height: 84px; margin-bottom: var(--s-5); }

.footer h3 {
  margin-bottom: var(--s-4);
  color: var(--white);
  font-size: var(--t-sm);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: none;
}

.footer__list { list-style: none; }
.footer__list li + li { margin-top: var(--s-2); }
.footer__list a { text-decoration: none; opacity: 0.82; transition: opacity 150ms var(--ease); }
.footer__list a:hover { opacity: 1; text-decoration: underline; }

.footer__contact a {
  font-size: var(--t-md);
  font-weight: 600;
  text-decoration: none;
}
.footer__contact a:hover { text-decoration: underline; }

.footer__social { display: flex; gap: var(--s-2); margin-top: var(--s-5); }
.footer__social a {
  display: grid;
  place-items: center;
  width: 2.5rem; height: 2.5rem;
  border: 1px solid rgba(255, 255, 255, 0.22);
  border-radius: var(--r-pill);
  transition: background 160ms var(--ease), border-color 160ms var(--ease);
}
.footer__social a:hover { background: rgba(255, 255, 255, 0.12); border-color: rgba(255, 255, 255, 0.4); }
.footer__social svg { width: 1.05rem; height: 1.05rem; }

.footer__bottom {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-4);
  align-items: center;
  justify-content: space-between;
  padding-top: var(--s-5);
  font-size: var(--t-sm);
}
.footer__bottom a { text-decoration: none; opacity: 0.82; }
.footer__bottom a:hover { opacity: 1; text-decoration: underline; }

/* --- Folowise build credit -------------------------------------------------
   One instance, in the legal row. Measured on --slate #16212b:
   #a8aeb3 is 7.28:1 and #ffffff is 16.32:1, both well clear of 4.5:1. */
.credit {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  color: #a8aeb3;
}
.credit__mark {
  display: block;
  width: 18px;
  height: 18px;
  flex: none;
}
.footer__bottom .credit__brand {
  color: #fff;
  font-weight: 600;
  opacity: 1;
  text-decoration: none;
  border-bottom: 1px solid rgba(255, 255, 255, 0.35);
}
.footer__bottom .credit__brand:hover {
  border-bottom-color: #fff;
  text-decoration: none;
}
@media (max-width: 640px) {
  .credit { min-height: 44px; }
}

/* ==========================================================================
   05-components.css
   ========================================================================== */

/* ==========================================================================
   Buttons, chips, cards, badges
   ========================================================================== */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s-3);
  padding: 0.95rem var(--s-6);
  border: 1px solid transparent;
  border-radius: var(--r-pill);
  font-size: var(--t-base);
  font-weight: 700;
  letter-spacing: -0.01em;
  text-decoration: none;
  white-space: nowrap;
  transition: transform 160ms var(--ease), box-shadow 220ms var(--ease),
              background 180ms var(--ease), border-color 180ms var(--ease), color 180ms var(--ease);
}
.btn:active { transform: translateY(1px); }
.btn svg { width: 1.1em; height: 1.1em; flex: 0 0 auto; }

/* The primary button carries the rose-gold sheen. It is the second and last
   place that gradient is allowed; everything else that needs emphasis uses
   weight or the slate. The sheen slides on hover - that is the glitter in the
   logo doing something, rather than a colour fade. */
.btn--primary {
  position: relative;
  background: var(--sheen);
  background-size: 220% 100%;
  background-position: 18% 0;
  /* Slate, not white - see the --sheen note in 01-tokens.css. */
  color: var(--slate);
  /* A hairline of the deep rose, because a light button on the porcelain
     ground has no edge of its own. */
  border-color: rgba(165, 102, 90, 0.42);
  box-shadow: var(--lift-1);
  transition: background-position 520ms var(--ease-out), transform 160ms var(--ease), box-shadow 220ms var(--ease);
}
.btn--primary:hover { background-position: 82% 0; box-shadow: var(--lift-2); }

.btn--solid { background: var(--slate); color: var(--white); }
.btn--solid:hover { background: #0e1720; box-shadow: var(--lift-2); }

.btn--ghost { background: transparent; border-color: var(--mist); color: var(--slate); }
.btn--ghost:hover { border-color: var(--slate-45); background: var(--white); }

.btn--onphoto {
  background: rgba(255, 255, 255, 0.92);
  color: var(--slate);
  backdrop-filter: blur(6px);
}
.btn--onphoto:hover { background: var(--white); box-shadow: var(--lift-2); }

.btn--sm { padding: 0.6rem var(--s-5); font-size: var(--t-sm); }
.btn--block { width: 100%; }

.btn-row { display: flex; flex-wrap: wrap; gap: var(--s-3); }

/* --- card ---------------------------------------------------------------- */
.card {
  padding: var(--s-6);
  background: var(--white);
  border: 1px solid var(--mist-soft);
  border-radius: var(--r-lg);
  box-shadow: var(--lift-1);
}
.card h3 { margin-bottom: var(--s-3); }
.card p { color: var(--slate-60); font-size: var(--t-sm); line-height: 1.66; }

.card__icon {
  display: grid;
  place-items: center;
  width: 3rem; height: 3rem;
  margin-bottom: var(--s-5);
  background: var(--rose-wash);
  border: 1px solid #f6ddd2;
  border-radius: var(--r-md);
  color: var(--rose-deep);
}
.card__icon svg { width: 1.5rem; height: 1.5rem; stroke-width: 1.9; }

/* --- chip (used by the service filter and the quote builder) ------------- */
.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s-2);
  padding: 0.55rem var(--s-4);
  background: var(--white);
  border: 1px solid var(--mist);
  border-radius: var(--r-pill);
  color: var(--slate-80);
  font-size: var(--t-sm);
  font-weight: 600;
  transition: border-color 150ms var(--ease), background 150ms var(--ease),
              color 150ms var(--ease), box-shadow 150ms var(--ease);
}
.chip:hover { border-color: var(--slate-45); color: var(--slate); }
.chip[aria-pressed='true'],
.chip[aria-selected='true'] {
  background: var(--slate);
  border-color: var(--slate);
  color: var(--white);
}

.chip-row { display: flex; flex-wrap: wrap; gap: var(--s-2); }

/* --- prose (about, covid, privacy) --------------------------------------- */
.prose { max-width: 68ch; }
.prose p { font-size: var(--t-md); line-height: 1.72; color: var(--slate-80); }
.prose h3 { margin-top: var(--s-7); margin-bottom: var(--s-3); }
.prose h3:first-child { margin-top: 0; }
.prose ul { margin: 0 0 var(--s-5); padding-left: 1.15rem; }
.prose li { margin-bottom: var(--s-2); color: var(--slate-80); }
.prose a { color: var(--rose-deep); font-weight: 600; }

.prose dl { margin: 0 0 var(--s-5); }
.prose dt { margin-top: var(--s-4); font-weight: 700; color: var(--slate); }
.prose dd { margin: var(--s-1) 0 0; color: var(--slate-80); }

/* A signature block. The name gets weight, the role does not. */
.signature { margin-top: var(--s-6); padding-top: var(--s-5); border-top: 1px solid var(--mist); }
.signature strong { display: block; font-size: var(--t-md); }
.signature span { color: var(--slate-60); font-size: var(--t-sm); }

/* --- trust badges -------------------------------------------------------- */
.badges {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: clamp(var(--s-5), 4vw, var(--s-7));
}
.badges a, .badges div { display: block; }
.badges img { width: auto; }
.badges .badge-bonded img { height: 82px; }
.badges .badge-bbb img { height: 42px; }
.badges .badge-coc img { height: 58px; }

/* The four reasons are 18, 18, 63 and 55 words. Across four columns the long
   two became 13-line ribbons beside a 5-line one - readable, but it looked
   like a fault. Two columns give each a 30rem measure and the raggedness
   stops being the first thing you see. */
.reasons .card p { font-size: var(--t-base); }
@media (min-width: 62rem) { .reasons { gap: var(--s-6); } }

/* --- BBB rating badge -----------------------------------------------------
   Text, not the BBB seal: the seal is the Accredited Business mark and she is
   not accredited, so BBB serves a 1x1 pixel in its place. */
.badges .badge-bbb {
  display: inline-flex;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-4);
  border: 1px solid var(--mist);
  border-radius: var(--r-md, 10px);
  background: var(--white);
  text-decoration: none;
  color: var(--slate);
}
.badges .badge-bbb:hover { border-color: var(--slate-45); }
.badges .badge-bbb__rating {
  font-size: var(--t-lg);
  font-weight: 800;
  line-height: 1;
  color: var(--slate);
}
.badges .badge-bbb__text {
  font-size: var(--t-xs);
  line-height: 1.25;
  color: var(--slate-60);
  /* the two lines are set by the <br>; without this the flex row shrinks it
     and the phrase breaks mid-way as "A+ Better Business / Bureau rating" */
  white-space: nowrap;
}
.badges .badge-bbb__rating { flex: none; }

/* ==========================================================================
   06-hero.css
   ========================================================================== */

/* ==========================================================================
   The hero, and the film over it
   ==========================================================================

   A cleaning company sells one moment: you open the door and the room is done.
   So the hero is that moment, and it is the visitor who causes it. The
   photograph sits underneath in focus; over it is a canvas holding a hazed,
   desaturated, milky copy of the same frame - a window that wants cleaning.
   Moving the pointer wipes it away.

   This is the only non-trivial motion on the site. Everything else is quiet on
   purpose, because one thing that does something is worth more than eight
   sections that fade up on scroll.

   With no JavaScript, no canvas is painted and the sharp photograph is simply
   the hero. Nothing is lost but the trick.
   ========================================================================== */

.hero {
  position: relative;
  isolation: isolate;
  min-height: min(88svh, 50rem);
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  background: var(--slate);
}

.hero__media { position: absolute; inset: 0; z-index: 0; }
.hero__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 56% 50%;
}

/* The film. Pointer events pass through to the page so the canvas never
   steals a scroll or a click on the buttons sitting over it. */
.hero__film {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  /* Until the script has painted a frame there is nothing to show, and an
     un-painted canvas would flash as a transparent rectangle. */
  opacity: 0;
  transition: opacity 700ms var(--ease);
}
.hero__film[data-ready='true'] { opacity: 1; }

/* A scrim under the copy only. The photograph stays untouched everywhere else,
   which is the point of shooting the real homes. */
.hero::after {
  content: '';
  position: absolute;
  inset: auto 0 0 0;
  z-index: 2;
  height: 68%;
  pointer-events: none;
  background: linear-gradient(to top,
    rgba(10, 17, 23, 0.55) 0%,
    rgba(10, 17, 23, 0.28) 34%,
    rgba(10, 17, 23, 0) 100%);
}
@media (min-width: 64rem) {
  .hero::after {
    inset: 0 auto 0 0;
    width: 62%;
    height: auto;
    background: linear-gradient(to right,
      rgba(10, 17, 23, 0.5) 0%,
      rgba(10, 17, 23, 0.22) 55%,
      rgba(10, 17, 23, 0) 100%);
  }
}

.hero__inner {
  position: relative;
  z-index: 3;
  width: 100%;
  padding-block: clamp(var(--s-7), 10vh, var(--s-10));
}

/* The copy sits on white. Text laid straight onto a photograph is a gamble at
   the best of times, and here the thing behind it CHANGES as the film clears -
   so it would be a gamble twice. */
.hero__card {
  max-width: 40rem;
  padding: clamp(var(--s-6), 4vw, var(--s-8));
  background: rgba(255, 255, 255, 0.975);
  backdrop-filter: blur(22px) saturate(150%);
  -webkit-backdrop-filter: blur(22px) saturate(150%);
  border-radius: var(--r-xl);
  box-shadow: var(--lift-3);
}

/* Capped below --t-3xl. The display scale is sized for a full-bleed heading;
   inside a card at this width it has to stop sooner or it takes four lines and
   shoves the buttons past the fold. */
.hero__card h1 {
  margin-bottom: var(--s-5);
  max-width: 14ch;
  font-size: clamp(2.3rem, 1.6rem + 2.6vw, 3.6rem);
}

.hero__lead {
  max-width: 44ch;
  margin-bottom: var(--s-6);
  color: var(--slate-60);
  font-size: var(--t-md);
  line-height: 1.6;
}

.hero__phone {
  display: block;
  margin-top: var(--s-5);
  color: var(--slate-60);
  font-size: var(--t-sm);
}
.hero__phone a { color: var(--slate); font-weight: 700; text-decoration: none; }
.hero__phone a:hover { text-decoration: underline; }

/* The hint. It says what to do, then gets out of the way for good once the
   visitor has done it - a prompt that keeps prompting is nagging. */
.hero__hint {
  position: absolute;
  z-index: 3;
  right: var(--gutter);
  /* Clear of the COVID strip, which is fixed to the bottom of the viewport and
     was sitting straight across this. */
  bottom: calc(var(--s-6) + 3.25rem);
  display: none;
  align-items: center;
  gap: var(--s-3);
  padding: var(--s-3) var(--s-5);
  background: rgba(255, 255, 255, 0.9);
  backdrop-filter: blur(10px);
  border-radius: var(--r-pill);
  color: var(--slate-80);
  font-size: var(--t-sm);
  font-weight: 600;
  box-shadow: var(--lift-1);
  transition: opacity 500ms var(--ease), transform 500ms var(--ease);
}
.hero__hint[data-show='true'] { display: flex; }

/* Never on a narrow screen. film.js already hides it for a coarse pointer -
   the sweep runs by itself there - but a narrow window with a mouse attached
   still matched, and the hint then sat straight over the hero card. There is
   also no honest wording for it on touch: "move your cursor" is wrong, and
   "drag" is a promise the scroll-safe version deliberately does not keep. */
@media (max-width: 47.999rem) {
  .hero__hint[data-show='true'] { display: none; }
}
.hero__hint[data-show='false'] { opacity: 0; transform: translateY(8px); pointer-events: none; }
.hero__hint svg { width: 1.15rem; height: 1.15rem; color: var(--rose-deep); }



/* ==========================================================================
   07-sections.css
   ========================================================================== */

/* ==========================================================================
   Rooms strip, Tracy, service explorer, quote builder, trust
   ========================================================================== */

/* --- the rooms -----------------------------------------------------------
   A scroll-snap strip rather than a carousel with dots and arrows. Four
   photographs is not enough content to justify hiding three of them behind a
   control, and a strip shows the visitor immediately that there is more. */
.rooms {
  list-style: none;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: min(82vw, 34rem);
  gap: var(--s-5);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  /* The strip is full-bleed but its first frame starts on the same left rail as
     every heading above it, or the page has two left edges.

     scroll-padding is not decoration here. `scroll-snap-type: mandatory` snaps
     the first item to the SCROLLPORT edge, which scrolls straight past the
     padding-inline and parks the photo at x=0 - the alignment was correct in
     the layout and thrown away by the snap. */
  --rail: max(var(--gutter), (100% - var(--shell)) / 2 + var(--gutter));
  padding-inline: var(--rail);
  scroll-padding-inline: var(--rail);
  padding-bottom: var(--s-5);
  scrollbar-width: thin;
  scrollbar-color: var(--mist) transparent;
}
.rooms::-webkit-scrollbar { height: 6px; }
.rooms::-webkit-scrollbar-thumb { background: var(--mist); border-radius: 3px; }

.room { scroll-snap-align: start; }
.room figure { margin: 0; }
.room img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: var(--r-lg);
  background: var(--porcelain-lo);
}
.room figcaption {
  margin-top: var(--s-3);
  color: var(--slate-60);
  font-size: var(--t-sm);
  font-weight: 600;
}

/* --- Tracy --------------------------------------------------------------- */
.tracy { display: grid; gap: clamp(var(--s-6), 5vw, var(--s-8)); align-items: center; }
@media (min-width: 56rem) { .tracy { grid-template-columns: minmax(0, 18rem) minmax(0, 1fr); } }

.tracy__portrait {
  width: min(16rem, 60vw);
  aspect-ratio: 1;
  border-radius: var(--r-pill);
  object-fit: cover;
  box-shadow: var(--lift-2);
}

.tracy blockquote {
  margin: 0;
  font-size: var(--t-lg);
  font-weight: 500;
  line-height: 1.42;
  letter-spacing: -0.018em;
  color: var(--slate);
  max-width: 34ch;
}

/* --- the service explorer ------------------------------------------------ */
.explorer__filters { margin-bottom: var(--s-6); }

.explorer__list {
  display: grid;
  gap: var(--s-3);
  list-style: none;
}
@media (min-width: 40rem) { .explorer__list { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (min-width: 62rem) { .explorer__list { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

/* Each service is a toggle, not a label: picking one here builds the summary
   in the quote section below, so the whole list is one continuous task rather
   than a menu you read and then forget. */
.svc {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  width: 100%;
  padding: var(--s-4) var(--s-5);
  background: var(--white);
  border: 1px solid var(--mist-soft);
  border-radius: var(--r-md);
  text-align: left;
  font-weight: 600;
  transition: border-color 160ms var(--ease), box-shadow 200ms var(--ease), background 160ms var(--ease);
}
.svc:hover { border-color: var(--rose); box-shadow: var(--lift-1); }
.svc[aria-pressed='true'] { border-color: var(--rose-deep); background: var(--rose-wash); }

.svc__box {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  width: 1.4rem; height: 1.4rem;
  border: 1.5px solid var(--mist);
  border-radius: var(--r-sm);
  background: var(--white);
  color: var(--white);
  transition: background 160ms var(--ease), border-color 160ms var(--ease);
}
.svc__box svg { width: 0.85rem; height: 0.85rem; opacity: 0; transition: opacity 140ms var(--ease); }
.svc[aria-pressed='true'] .svc__box { background: var(--rose-deep); border-color: var(--rose-deep); }
.svc[aria-pressed='true'] .svc__box svg { opacity: 1; }

.svc[hidden] { display: none; }

.explorer__empty { color: var(--slate-60); }

/* --- the quote builder --------------------------------------------------- */
.quote {
  display: grid;
  gap: clamp(var(--s-6), 4vw, var(--s-8));
  align-items: start;
}
@media (min-width: 64rem) { .quote { grid-template-columns: minmax(0, 1fr) minmax(0, 24rem); } }

.field { margin-bottom: var(--s-6); }
.field > .field__label {
  display: block;
  margin-bottom: var(--s-3);
  font-size: var(--t-sm);
  font-weight: 700;
  color: var(--slate);
}
.field__help {
  margin-top: var(--s-3);
  /* slate-45 is 4.8:1 on WHITE but only 4.20:1 on the porcelain ground this
     actually sits on. slate-60 is 5.03:1 here and 4.66:1 on the banded tint. */
  color: var(--slate-60);
  font-size: var(--t-xs);
}

.stepper { display: flex; align-items: center; gap: var(--s-2); }
.stepper button {
  display: grid;
  place-items: center;
  width: 2.5rem; height: 2.5rem;
  background: var(--white);
  border: 1px solid var(--mist);
  border-radius: var(--r-pill);
  color: var(--slate);
  font-size: var(--t-md);
  font-weight: 700;
  transition: border-color 150ms var(--ease), background 150ms var(--ease);
}
.stepper button:hover:not(:disabled) { border-color: var(--slate-45); }
.stepper button:disabled { opacity: 0.38; cursor: not-allowed; }
.stepper output {
  min-width: 3.5rem;
  text-align: center;
  font-size: var(--t-md);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* The summary panel. It sticks on a wide screen because it is the result of
   everything above it - scrolling away from your own answers is what makes a
   builder feel like a form. */
.summary {
  padding: var(--s-6);
  background: var(--slate);
  color: var(--white);
  border-radius: var(--r-lg);
  box-shadow: var(--lift-2);
}
@media (min-width: 64rem) { .summary { position: sticky; top: 7rem; } }

.summary h3 { color: var(--white); margin-bottom: var(--s-4); }
.summary dl { margin: 0 0 var(--s-6); }
/* Scoped to the definition list on purpose. As `.summary div` this also caught
   the button stack below it and laid three full-width buttons across one row,
   which pushed the document 176px wider than the viewport. */
.summary dl > div {
  display: flex;
  gap: var(--s-4);
  justify-content: space-between;
  padding: var(--s-3) 0;
  border-bottom: 1px solid rgba(255, 255, 255, 0.14);
}
.summary dl > div:last-child { border-bottom: 0; }
.summary dt { color: rgba(255, 255, 255, 0.64); font-size: var(--t-sm); }
.summary dd { margin: 0; text-align: right; font-weight: 600; font-size: var(--t-sm); }
.summary dd em { font-style: normal; color: rgba(255, 255, 255, 0.5); font-weight: 400; }

.summary__note {
  margin-top: var(--s-4);
  color: rgba(255, 255, 255, 0.58);
  font-size: var(--t-xs);
  line-height: 1.55;
}
.summary .btn--ghost { border-color: rgba(255, 255, 255, 0.3); color: var(--white); }
.summary .btn--ghost:hover { background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.5); }

/* --- closing call -------------------------------------------------------- */
.callout {
  display: grid;
  gap: var(--s-6);
  align-items: center;
  padding: clamp(var(--s-7), 6vw, var(--s-9));
  background: var(--white);
  border-radius: var(--r-xl);
  box-shadow: var(--lift-2);
}
@media (min-width: 56rem) { .callout { grid-template-columns: minmax(0, 1fr) auto; gap: var(--s-8); } }
.callout h2 { max-width: 18ch; }
.callout p { margin-top: var(--s-4); color: var(--slate-60); }

/* ==========================================================================
   08-notice.css
   ========================================================================== */

/* ==========================================================================
   The COVID-19 notice
   ==========================================================================

   The original site ran this as a bar pinned to the bottom of every page with
   a DISMISS button beside it. It is kept, because it points at a statement
   naming two real certifications, but it is a quiet strip rather than a
   dialog - it is information, and it was never worth interrupting anyone for.

   It ships `hidden` and the script reveals it. That way a visitor who has
   already dismissed it this session never sees it flash in before the script
   can remove it.
   ========================================================================== */

.notice {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 90;
  background: var(--slate);
  color: rgba(255, 255, 255, 0.86);
  box-shadow: 0 -8px 30px -12px rgba(22, 33, 43, 0.45);
}

.notice__inner {
  display: flex;
  align-items: center;
  gap: var(--s-4);
  padding-block: var(--s-3);
  /* Clear the home indicator on a phone. */
  padding-bottom: calc(var(--s-3) + env(safe-area-inset-bottom, 0px));
}

.notice p { margin: 0; font-size: var(--t-sm); }
.notice a { color: var(--white); font-weight: 600; text-underline-offset: 0.22em; }

.notice__close {
  margin-left: auto;
  flex: 0 0 auto;
  width: 2.25rem; height: 2.25rem;
  display: grid;
  place-items: center;
  background: transparent;
  border: 1px solid rgba(255, 255, 255, 0.28);
  border-radius: var(--r-pill);
  color: var(--white);
  font-size: 1.25rem;
  line-height: 1;
  transition: background 150ms var(--ease), border-color 150ms var(--ease);
}
.notice__close:hover { background: rgba(255, 255, 255, 0.12); border-color: rgba(255, 255, 255, 0.5); }

/* A dismissible strip at the bottom of the viewport will cover the last line
   of the page otherwise. */
body:has(.notice:not([hidden])) .footer { padding-bottom: var(--s-9); }

/* The summary panel's fallback textarea. It is only ever unhidden for the
   instant execCommand('copy') needs a real selection, so it must not be able
   to push the layout around while it is there. */
.summary textarea {
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0;
  pointer-events: none;
}

/* The "nothing chosen yet" state in the summary reads as a prompt rather than
   as an answer. */
.summary [data-out].is-empty { color: rgba(255, 255, 255, 0.5); font-weight: 400; }
