/* ===========================================================================
   GLEAM & GLOW CLEANING
   ---------------------------------------------------------------------------
   One stylesheet, no framework, no build step.

   THE PALETTE IS NOT INVENTED. Every colour below was sampled out of her own
   business card (tools/sample-brand.py prints them):

     #F9F5F4  the card's blush-white ground
     #3C414D  the ink of the "Gleam & Glow Cleaning" serif
     #8A4D5E  the deepest rose in the "Cleaning Services" script
     #CFA4B1  the body of the pink vacuum in her logo

   Everything else here is a tint or shade of those four. The one colour from
   outside the brand is WhatsApp green, used on exactly one button, because a
   recognisable green WhatsApp button is worth more to her than a perfectly
   consistent palette.

   THE MOTIFS, and why each one is here rather than being decoration:

     THE ARCH      Images are arch-topped. It is the shape of the round mirrors
                   and the freestanding bath in her own photographs, and it
                   reads instantly as bathroom-and-boutique rather than as
                   yet another rectangle of stock photography.

     THE GLEAM     A specular streak that travels diagonally across headings,
                   buttons and photographs. It is the business name made
                   literal, and it is the whole reason the site does not need
                   to shout: light moving across a clean surface IS the
                   product.

   Every animation in here is switched off wholesale under
   prefers-reduced-motion at the bottom of the file.
   ========================================================================= */

@import url('tokens.css');

/* =========================================================================
   RESET
   ========================================================================= */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Anchored sections must clear the sticky header. */
  scroll-padding-top: 5.5rem;

  /* `clip`, not `hidden`, and it is load-bearing.
     *
     THE BUG THIS FIXES: the closed mobile nav is parked off the right edge
     with translateX(101%). A position:fixed element is normally laid out
     against the viewport and cannot extend the document — but `backdrop-filter`
     on an ancestor makes that ancestor the containing block for fixed
     descendants. The header gets `backdrop-filter` the moment it goes solid,
     so from that point the parked nav counted as page content and the document
     became ~1.9x the viewport wide. It showed up on the gallery page at every
     width, because that page has no hero and the header is solid from the
     first paint.
     *
     `overflow-x: hidden` here would fix the width but turn the root into a
     scroll container, which silently breaks `position: sticky` on the header.
     `clip` clips without creating one. Browsers too old to know it ignore the
     line and fall back to body's `overflow-x: hidden`, which stops the page
     being swipeable sideways even though it still measures wide. */
  overflow-x: clip;
}

body {
  margin: 0;
  background: var(--cream);
  color: var(--ink);
  font-family: var(--sans);
  font-weight: 300;
  font-size: clamp(1rem, .95rem + .25vw, 1.125rem);
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;

  /* `clip`, matching html above, and this line is why the header sticks.
     *
     THE BUG: this was `overflow-x: hidden`, and `hidden` makes body a SCROLL
     CONTAINER. `position: sticky` sticks to its nearest scrolling ancestor,
     so the header was sticking to a box that never scrolls — it slid off the
     top of the window and never came back, on every page, at every width.
     *
     The note on html says overflow-x there is `clip` for exactly this reason
     and that body's `hidden` is only a fallback for browsers that do not know
     `clip`. That was half right: the fallback was also silently overriding
     the fix in the browsers that DID. Overflow does not propagate from body
     to the viewport when the root's own overflow is `clip`, so body kept its
     scroll container either way.
     *
     `clip` clips identically without creating one. The @supports block below
     restores the old `hidden` where `clip` is unknown, which is the same
     trade the html rule already makes: a very old browser gets the sideways
     scroll stopped and loses the sticky header. */
  overflow-x: clip;
}

@supports not (overflow: clip) {
  body { overflow-x: hidden; }
}

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

button, input, select, textarea { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: 0; padding: 0; }

a { color: var(--rose); text-decoration-thickness: 1px; text-underline-offset: .2em; }
a:hover { color: var(--rose-deep); }

h1, h2, h3, h4 {
  font-family: var(--display);
  font-weight: 300;
  line-height: 1.04;
  letter-spacing: -.005em;
  margin: 0;
  /* Serif display type at these sizes needs the kerning it ships with. */
  font-kerning: normal;
  font-variant-ligatures: common-ligatures;
}

p { margin: 0 0 1.15em; }
p:last-child { margin-bottom: 0; }

::selection { background: var(--blush-light); color: var(--rose-deep); }

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

/* =========================================================================
   PRIMITIVES
   ========================================================================= */

.wrap {
  width: min(100% - (var(--pad) * 2), var(--max));
  margin-inline: auto;
}
.wrap--narrow { --max: 940px; }

.section { padding-block: var(--section); position: relative; }

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

.skip-link {
  position: absolute; top: -100%; left: var(--pad); z-index: 999;
  background: var(--rose); color: var(--white);
  padding: .75rem 1.25rem; border-radius: 0 0 var(--radius) var(--radius);
  text-decoration: none;
}
.skip-link:focus { top: 0; color: var(--white); }

/* ---- the eyebrow ------------------------------------------------------- */
.eyebrow {
  /* .8rem, not the .7rem this started at. At .7 it rendered 11.2px, and a
     heavily letterspaced uppercase label that small is genuinely hard to read
     on a phone. */
  font-size: .8rem;
  font-weight: 400;
  letter-spacing: .32em;
  text-transform: uppercase;
  color: var(--rose);
  margin: 0 0 1.4rem;
  display: flex;
  align-items: center;
  gap: .9rem;
}
.eyebrow::before {
  content: '';
  width: clamp(1.5rem, 4vw, 3rem);
  height: 1px;
  background: var(--blush);
  flex: none;
}
.eyebrow--center { justify-content: center; }
.eyebrow--center::after {
  content: '';
  width: clamp(1.5rem, 4vw, 3rem);
  height: 1px;
  background: var(--blush);
  flex: none;
}

/* ---- headings ---------------------------------------------------------- */
.h-display {
  font-size: clamp(3rem, 10.5vw, 8.5rem);
  line-height: .94;
  letter-spacing: -.02em;
}
.h1 { font-size: clamp(2.4rem, 6vw, 4.6rem); }
.h2 { font-size: clamp(2rem, 4.6vw, 3.5rem); }
.h3 { font-size: clamp(1.4rem, 2.2vw, 1.9rem); }

/* The italic rose half of a two-tone heading. */
.accent {
  font-style: italic;
  color: var(--rose);
  font-weight: 400;
}

.lede {
  font-size: clamp(1.05rem, 1rem + .4vw, 1.3rem);
  color: var(--ink-soft);
  max-width: var(--max-text);
  line-height: 1.65;
}
.lede--center { margin-inline: auto; text-align: center; }

.section-head { margin-bottom: clamp(2.5rem, 5vw, 4.5rem); }
.section-head--center { text-align: center; }
.section-head--center .lede { margin-inline: auto; }

/* Heading on the left, standfirst on the right, sharing a baseline. Exists to
   break the centred-eyebrow-over-centred-heading-over-centred-paragraph
   rhythm that every section on this page used to share. It collapses to
   stacked below the split, where two columns would leave both too narrow. */
.section-head--split {
  display: grid;
  gap: clamp(1.25rem, 3vw, 3.5rem);
  align-items: end;
}
@media (min-width: 860px) {
  .section-head--split { grid-template-columns: 1.1fr .9fr; }
  .section-head--split .lede { padding-bottom: .4rem; }
}

/* =========================================================================
   THE GLEAM
   -------------------------------------------------------------------------
   A specular streak that travels across a surface. Used on the hero, on
   photographs as they arrive, and across buttons on hover.

   Built with a skewed gradient rather than a rotated pseudo-element so it
   stays a single compositor-friendly transform — this runs on phones, and a
   sweep that stutters is worse than no sweep at all.
   ========================================================================= */

@keyframes gleam-sweep {
  0%   { transform: translateX(-130%) skewX(-18deg); opacity: 0; }
  8%   { opacity: 1; }
  92%  { opacity: 1; }
  100% { transform: translateX(230%) skewX(-18deg); opacity: 0; }
}

.gleam { position: relative; overflow: hidden; isolation: isolate; }
.gleam::after {
  content: '';
  position: absolute;
  inset-block: -20%;
  left: 0;
  width: 45%;
  pointer-events: none;
  z-index: 3;
  background: linear-gradient(
    100deg,
    transparent 0%,
    rgba(255, 255, 255, .05) 30%,
    rgba(255, 255, 255, .55) 50%,
    rgba(255, 255, 255, .05) 70%,
    transparent 100%
  );
  transform: translateX(-130%) skewX(-18deg);
  opacity: 0;
}
/* Fired by site.js once the element is on screen, so the sweep is seen. */
.gleam.is-gleaming::after { animation: gleam-sweep 1.5s var(--ease-soft) forwards; }
/* And again, slowly, on the hero only. */
.gleam--loop.is-gleaming::after {
  animation: gleam-sweep 2.2s var(--ease-soft) 1s infinite;
  animation-delay: 1s;
}

/* =========================================================================
   BUTTONS
   ========================================================================= */

.btn {
  --btn-bg: var(--rose);
  --btn-fg: var(--white);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .7rem;
  padding: 1.05rem 2rem;
  background: var(--btn-bg);
  color: var(--btn-fg);
  border: 1px solid var(--btn-bg);
  border-radius: 999px;
  font-size: .82rem;
  font-weight: 400;
  letter-spacing: .16em;
  text-transform: uppercase;
  text-decoration: none;
  position: relative;
  overflow: hidden;
  isolation: isolate;
  transition: transform .5s var(--ease), box-shadow .5s var(--ease),
              background-color .35s var(--ease-soft), color .35s var(--ease-soft),
              border-color .35s var(--ease-soft);
  white-space: nowrap;
}
.btn:hover, .btn:focus-visible {
  color: var(--btn-fg);
  transform: translateY(-2px);
  box-shadow: var(--shadow);
}
/* The gleam crosses the button on hover. */
.btn::after {
  content: '';
  position: absolute;
  inset-block: -50%;
  left: 0;
  width: 40%;
  z-index: -1;
  background: linear-gradient(100deg, transparent,
              rgba(255, 255, 255, .38) 50%, transparent);
  transform: translateX(-150%) skewX(-18deg);
  transition: transform .75s var(--ease-soft);
}
.btn:hover::after, .btn:focus-visible::after { transform: translateX(260%) skewX(-18deg); }

.btn svg { width: 1.15em; height: 1.15em; flex: none; }

.btn--ghost {
  --btn-bg: transparent;
  --btn-fg: var(--ink);
  border-color: var(--blush);
}
.btn--ghost:hover, .btn--ghost:focus-visible {
  --btn-fg: var(--rose-deep);
  border-color: var(--rose);
}

.btn--light {
  --btn-bg: transparent;
  --btn-fg: var(--white);
  border-color: rgba(255, 255, 255, .55);
  backdrop-filter: blur(6px);
}
.btn--light:hover, .btn--light:focus-visible {
  --btn-bg: var(--white);
  --btn-fg: var(--rose-deep);
  border-color: var(--white);
}

.btn--wa {
  --btn-bg: var(--whatsapp);
  --btn-fg: #fff;
  border-color: var(--whatsapp);
}
.btn--wa:hover, .btn--wa:focus-visible {
  --btn-bg: #1eb85a;
  border-color: #1eb85a;
}

.btn--lg { padding: 1.25rem 2.6rem; font-size: .88rem; }
.btn--block { display: flex; width: 100%; }

.btn-row {
  display: flex;
  flex-wrap: wrap;
  gap: .9rem;
  align-items: center;
}

/* A text link that draws its own underline on hover. */
.link-draw {
  --c: var(--rose);
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  /* Padded to a 44px tall tap target. As a bare inline link it measured 27px,
     which is a fiddly thing to hit with a thumb and it is the only route to
     the full gallery from the front page. */
  padding-top: .55rem;
  min-height: 44px;
  font-size: .8rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--c);
  position: relative;
  padding-bottom: .5rem;
}
.link-draw::after {
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right;
  transition: transform .55s var(--ease);
}
.link-draw:hover::after, .link-draw:focus-visible::after {
  transform: scaleX(1);
  transform-origin: left;
}
.link-draw svg { width: 1em; height: 1em; transition: transform .45s var(--ease); }
.link-draw:hover svg { transform: translateX(4px); }

/* =========================================================================
   NOTICE BAR
   ========================================================================= */

.notice {
  background: var(--rose-deep);
  color: var(--white);
  text-align: center;
  font-size: .82rem;
  letter-spacing: .06em;
  padding: .7rem var(--pad);
  position: relative;
  z-index: 60;
}
.notice a { color: var(--white); text-decoration-color: rgba(255, 255, 255, .5); }
.notice[hidden] { display: none; }

/* =========================================================================
   HEADER
   ========================================================================= */

.header {
  position: sticky;
  top: 0;
  z-index: 50;
  transition: background-color .45s var(--ease-soft), box-shadow .45s var(--ease-soft),
              backdrop-filter .45s var(--ease-soft);
}
/* Over the hero it is transparent with white type; the moment the page
   scrolls past the hero it becomes a solid cream bar.
   *
   ⚠  THE FROSTING IS ON A PSEUDO-ELEMENT, NOT ON THE HEADER, and that is
      load-bearing — it is the fix for a bug where the mobile menu only opened
      while the page was scrolled to the very top.
      *
      An element with `backdrop-filter` becomes the CONTAINING BLOCK for its
      `position: fixed` descendants. The mobile nav panel is fixed, and it is
      a child of this header. So the moment the header went solid and gained
      the filter, the nav stopped being positioned against the viewport and
      started being positioned against the header — a bar about 60px tall.
      `inset: 0 0 0 auto` then sized the whole menu to that bar, and
      `translateX(101%)` parked it just off the right-hand end of it. Tapping
      the button still worked, the nav still got `.is-open`, and nothing
      visible happened.
      *
      At scroll zero there is no `.is-stuck`, no filter, and no containing
      block — which is exactly why it worked at the top of the page and
      nowhere else, and why it looked like an intermittent fault.
      *
      A pseudo-element can carry the filter safely: it has no descendants of
      its own to capture, so the header stops being a containing block and the
      nav is measured against the viewport again. The note on `html` further
      up already described this containing-block behaviour — it just did not
      connect it to the nav.
      *
      Regression test: tools/test-browser.py, "mobile nav opens when scrolled". */
.header.is-stuck {
  box-shadow: 0 1px 0 rgba(207, 164, 177, .35);
}
.header::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  background: rgba(249, 245, 244, .88);
  backdrop-filter: saturate(160%) blur(14px);
  transition: opacity .45s var(--ease-soft);
}
.header.is-stuck::before { opacity: 1; }

.header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  padding-block: 1rem;
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: .85rem;
  text-decoration: none;
  color: inherit;
}
.brand__mark { width: 27px; height: auto; flex: none; }
.brand__name {
  font-family: var(--display);
  font-size: 1.5rem;
  line-height: 1;
  letter-spacing: .01em;
  white-space: nowrap;
}
.brand__name em { font-style: italic; color: var(--rose); }
.brand__sub {
  display: block;
  font-family: var(--sans);
  font-size: .55rem;
  letter-spacing: .3em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-top: .25rem;
}

/* White type while the header floats over the hero photograph. */
.header:not(.is-stuck) .brand,
.header:not(.is-stuck) .nav__link { color: var(--white); }
.header:not(.is-stuck) .brand__sub { color: rgba(255, 255, 255, .7); }
.header:not(.is-stuck) .brand__name em { color: var(--blush-light); }
.header:not(.is-stuck) .nav__link::after { background: var(--white); }
.header:not(.is-stuck) .nav-toggle__bar { background: var(--white); }

.nav { display: flex; align-items: center; gap: clamp(1.25rem, 2.5vw, 2.5rem); }
.nav__link {
  position: relative;
  text-decoration: none;
  color: var(--ink);
  font-size: .78rem;
  letter-spacing: .17em;
  text-transform: uppercase;
  padding-block: .3rem;
  transition: color .3s var(--ease-soft);
}
.nav__link::after {
  content: '';
  position: absolute;
  left: 0; bottom: 0;
  width: 100%; height: 1px;
  background: var(--rose);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform .5s var(--ease);
}
.nav__link:hover::after, .nav__link:focus-visible::after,
.nav__link[aria-current='page']::after {
  transform: scaleX(1);
  transform-origin: left;
}
.header .btn { padding: .8rem 1.5rem; font-size: .72rem; }

/* While the header floats over the hero photograph, the button borrows the
   ghost treatment so it reads as part of the image rather than as a solid
   chip stuck on top of it. It becomes the rose button the moment the header
   goes solid. */
.header:not(.is-stuck) .header__inner > .nav > .btn {
  background: transparent;
  border-color: rgba(255, 255, 255, .55);
  color: var(--white);
  backdrop-filter: blur(6px);
}
.header:not(.is-stuck) .header__inner > .nav > .btn:hover {
  background: var(--white);
  border-color: var(--white);
  color: var(--rose-deep);
}

.nav-toggle { display: none; width: 44px; height: 44px; position: relative; }
.nav-toggle__bar {
  position: absolute;
  left: 11px;
  width: 22px; height: 1.5px;
  background: var(--ink);
  transition: transform .45s var(--ease), opacity .25s var(--ease-soft),
              background-color .3s var(--ease-soft);
}
.nav-toggle__bar:nth-child(1) { top: 17px; }
.nav-toggle__bar:nth-child(2) { top: 22px; }
.nav-toggle__bar:nth-child(3) { top: 27px; }
.nav-toggle[aria-expanded='true'] .nav-toggle__bar:nth-child(1) { transform: translateY(5px) rotate(45deg); }
.nav-toggle[aria-expanded='true'] .nav-toggle__bar:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded='true'] .nav-toggle__bar:nth-child(3) { transform: translateY(-5px) rotate(-45deg); }

@media (max-width: 900px) {
  .nav-toggle { display: block; }
  .nav {
    position: fixed;
    inset: 0 0 0 auto;
    width: min(88vw, 360px);
    background: var(--cream);
    flex-direction: column;
    align-items: stretch;
    justify-content: center;
    gap: .5rem;
    padding: 5rem var(--pad) var(--pad);
    transform: translateX(101%);
    transition: transform .6s var(--ease);
    box-shadow: var(--shadow-lg);
    overflow-y: auto;
  }
  .nav.is-open { transform: none; }
  .header:not(.is-stuck) .nav__link { color: var(--ink); }
  .nav__link {
    font-size: 1rem;
    padding-block: 1rem;
    border-bottom: 1px solid var(--cream-deep);
  }
  .nav__link::after { display: none; }
  .nav .btn {
    margin-top: 1.5rem;
    /* The shared .header .btn rule shrinks these for the desktop bar. In the
       full-screen mobile panel they are the primary action and were coming
       out 28px tall. */
    padding: 1.1rem 1.5rem;
    font-size: .8rem;
  }
}

/* =========================================================================
   HERO
   ========================================================================= */

.hero {
  position: relative;
  min-height: min(100svh, 900px);
  display: grid;
  /* An EXPLICIT track, and it matters. With the default single `auto` column,
     the track sizes itself to its content, so the `width: min(100% - pad*2, …)`
     on the .wrap inside had no definite width to take a percentage of and fell
     back to max-content. At 320px that made the hero's inner column 356px —
     wider than the phone — and pushed the headline and the quote button 16px
     off the right edge, where `overflow: hidden` quietly cut them off.
     minmax(0, 1fr) gives the track a definite size and also stops a long
     unbroken word from being able to stretch it. */
  grid-template-columns: minmax(0, 1fr);
  align-items: end;
  overflow: hidden;
  isolation: isolate;

  /* The header floats over the photograph rather than sitting above it, so
     the hero is pulled up by exactly the header's height and given the same
     amount back as padding.

     --header-h is measured and set by site.js, because this height is not
     knowable from CSS: it depends on the brand lockup, which wraps
     differently at different widths and in different fonts. The fallback
     below is only ever used for the instant before the script runs, and is
     close enough that nothing visibly jumps. */
  margin-top: calc(-1 * var(--header-h, 5.5rem));
  padding-top: var(--header-h, 5.5rem);
}

.hero__media {
  position: absolute;
  inset: 0;
  z-index: -2;
  overflow: hidden;
}
.hero__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 38%;
  /* Slow drift. Scale headroom so the pan never exposes an edge. */
  animation: hero-drift 26s var(--ease-soft) infinite alternate;
}
@keyframes hero-drift {
  from { transform: scale(1.06) translate3d(0, 0, 0); }
  to   { transform: scale(1.14) translate3d(-1.5%, -1.5%, 0); }
}

/* Two scrims, doing different jobs. The first darkens the bottom so the
   headline is legible on any photograph she swaps in; the second tints the
   whole frame rose so a cool bathroom photo still feels like her brand. */
.hero__media::after {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to top, rgba(38, 24, 29, .82) 0%,
                            rgba(38, 24, 29, .38) 38%,
                            rgba(38, 24, 29, .10) 66%,
                            rgba(38, 24, 29, .30) 100%),
    linear-gradient(115deg, rgba(138, 77, 94, .30), rgba(60, 65, 77, .18));
}

.hero__inner {
  position: relative;
  z-index: 1;
  padding-block: clamp(3rem, 9vw, 7rem);
  color: var(--white);
}
.hero .eyebrow { color: var(--blush-light); }
.hero .eyebrow::before { background: rgba(255, 255, 255, .5); }

.hero__title {
  margin: 0 0 1.6rem;
  max-width: 15ch;
  text-shadow: 0 2px 40px rgba(38, 24, 29, .45);
}
.hero__title .accent { color: var(--blush-light); }

.hero__intro {
  color: rgba(255, 255, 255, .88);
  max-width: 48ch;
  margin-bottom: 2.4rem;
  font-size: clamp(1.02rem, .95rem + .4vw, 1.25rem);
  text-shadow: 0 1px 24px rgba(38, 24, 29, .5);
}

/* The line-by-line reveal. Each line sits in an overflow-hidden box and
   rises into it, which reads as type being set rather than as a fade. */
.reveal-line { display: block; overflow: hidden; }
.reveal-line > span {
  display: block;
  transform: translateY(110%);
  transition: transform 1.1s var(--ease);
}
.is-ready .reveal-line > span { transform: none; }
.is-ready .reveal-line:nth-child(2) > span { transition-delay: .12s; }

.hero__scroll {
  position: absolute;
  left: 50%;
  bottom: 1.75rem;
  transform: translateX(-50%);
  z-index: 2;
  display: grid;
  justify-items: center;
  gap: .6rem;
  color: rgba(255, 255, 255, .75);
  font-size: .62rem;
  letter-spacing: .3em;
  text-transform: uppercase;
  text-decoration: none;
}
.hero__scroll:hover { color: var(--white); }
.hero__scroll span.line {
  width: 1px; height: 46px;
  background: linear-gradient(to bottom, rgba(255, 255, 255, .8), transparent);
  position: relative;
  overflow: hidden;
}
.hero__scroll span.line::after {
  content: '';
  position: absolute;
  inset-inline: 0;
  height: 40%;
  background: var(--white);
  animation: scroll-cue 2.4s var(--ease-soft) infinite;
}
@keyframes scroll-cue {
  0%   { transform: translateY(-100%); }
  60%, 100% { transform: translateY(340%); }
}

@media (max-width: 640px) {
  .hero { min-height: 92svh; }
  .hero__scroll { display: none; }
}

/* =========================================================================
   THE MARQUEE
   -------------------------------------------------------------------------
   A slow band of outlined serif words. It does two things at once: it lists
   what she does in a form nobody reads as a list, and its movement carries
   the eye from the hero into the page.
   ========================================================================= */

.marquee {
  --speed: 42s;
  overflow: hidden;
  padding-block: clamp(1.75rem, 4vw, 3rem);
  background: var(--cream);
  border-block: 1px solid var(--cream-deep);
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.marquee__track {
  display: flex;
  width: max-content;
  animation: marquee var(--speed) linear infinite;
}
.marquee:hover .marquee__track { animation-play-state: paused; }
@keyframes marquee { to { transform: translateX(-50%); } }

.marquee__group {
  display: flex;
  align-items: center;
  flex: none;
}
.marquee__item {
  font-family: var(--display);
  font-size: clamp(1.6rem, 4.5vw, 3.2rem);
  line-height: 1;
  padding-inline: clamp(1rem, 2.5vw, 2rem);
  color: var(--rose);
  white-space: nowrap;
}
/* Alternate words are outlined rather than filled, so the band has rhythm. */
.marquee__item:nth-child(even) {
  color: transparent;
  -webkit-text-stroke: 1px var(--blush);
  font-style: italic;
}
.marquee__dot {
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--blush);
  flex: none;
}

/* =========================================================================
   ARCH-TOPPED FIGURES
   ========================================================================= */

.figure {
  position: relative;
  overflow: hidden;
  background: var(--cream-warm);
  border-radius: var(--radius-lg);
}
.figure--arch { border-radius: 999px 999px var(--radius-lg) var(--radius-lg); }
.figure--arch-soft { border-radius: 14rem 14rem var(--radius-lg) var(--radius-lg); }

.figure img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform 1.4s var(--ease);
}
.figure--zoom:hover img { transform: scale(1.05); }

/* Photographs arrive by wiping upward rather than fading. A fade on a
   photograph looks like a slow connection; a wipe looks intentional. */
.figure--reveal img {
  clip-path: inset(100% 0 0 0);
  transform: scale(1.08);
  transition: clip-path 1.3s var(--ease), transform 1.6s var(--ease);
}
.is-in .figure--reveal img,
.figure--reveal.is-in img {
  clip-path: inset(0 0 0 0);
  transform: scale(1);
}

.figure__caption {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  padding: 2.5rem 1.25rem 1.1rem;
  background: linear-gradient(to top, rgba(38, 24, 29, .78), transparent);
  color: var(--white);
  font-size: .78rem;
  letter-spacing: .12em;
  text-transform: uppercase;
}

/* =========================================================================
   SERVICES
   ========================================================================= */

.services__grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
  gap: clamp(1.75rem, 3.5vw, 3rem);
}

.service {
  display: grid;
  gap: 1.4rem;
  grid-template-rows: auto 1fr;
}
.service__figure {
  aspect-ratio: 3 / 4;
  border-radius: 12rem 12rem var(--radius-lg) var(--radius-lg);
}
.service__title { margin-bottom: .6rem; }
.service__body { color: var(--ink-soft); font-size: .98rem; margin: 0; }

/* A hairline that draws itself in when the card arrives. */
.service__rule {
  height: 1px;
  background: var(--blush);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 1.1s var(--ease) .2s;
  margin-bottom: 1.1rem;
}
.is-in .service__rule { transform: scaleX(1); }

/* =========================================================================
   BEFORE / AFTER
   -------------------------------------------------------------------------
   A draggable wipe. Both photographs are always in the DOM at full size and
   the top one is clipped, so there is no swapping, no loading gap and no
   flash — and it works with a keyboard, because the handle is a real range
   input sitting invisibly on top.
   ========================================================================= */

.ba {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--ink);
  box-shadow: var(--shadow-lg);
  touch-action: pan-y;         /* let the page scroll vertically over it */
  --pos: 50%;
}
.ba__img {
  display: block;
  width: 100%;
  height: auto;
}
.ba__after {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  clip-path: inset(0 0 0 var(--pos));
}
.ba__range {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  margin: 0;
  opacity: 0;
  cursor: ew-resize;
  -webkit-appearance: none;
  appearance: none;
  background: none;
}
.ba__range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 56px; height: 100vh;
  cursor: ew-resize;
}
.ba__range::-moz-range-thumb {
  width: 56px; height: 100vh;
  border: 0; background: none; cursor: ew-resize;
}
.ba__range:focus-visible + .ba__handle { box-shadow: 0 0 0 3px var(--rose); }

.ba__handle {
  position: absolute;
  top: 0; bottom: 0;
  left: var(--pos);
  width: 2px;
  background: var(--white);
  transform: translateX(-1px);
  pointer-events: none;
  box-shadow: 0 0 20px rgba(0, 0, 0, .35);
  border-radius: 2px;
}
.ba__grip {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  width: 54px; height: 54px;
  border-radius: 50%;
  background: var(--white);
  display: grid;
  place-items: center;
  color: var(--rose);
  box-shadow: var(--shadow);
}
.ba__grip svg { width: 22px; height: 22px; }

.ba__tag {
  position: absolute;
  top: 1rem;
  padding: .4rem .95rem;
  border-radius: 999px;
  font-size: .64rem;
  letter-spacing: .22em;
  text-transform: uppercase;
  backdrop-filter: blur(8px);
  pointer-events: none;
}
.ba__tag--before { left: 1rem; background: rgba(38, 24, 29, .6); color: var(--white); }
.ba__tag--after  { right: 1rem; background: rgba(255, 255, 255, .9); color: var(--rose-deep); }

.ba-block {
  display: grid;
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
  grid-template-columns: 1fr;
}
@media (min-width: 900px) {
  .ba-block { grid-template-columns: 1fr 1.15fr; }
  .ba-block--flip .ba-block__text { order: 2; }
}

/* =========================================================================
   HOW IT WORKS
   -------------------------------------------------------------------------
   Three steps on the deep rose band. The numerals are large and set behind
   the text rather than stacked above it in a neat little column — the old
   version of this section was four evenly-spaced 01/02/03/04 cards, which is
   the single most template-looking arrangement on the web.
   ========================================================================= */

.steps {
  background: var(--rose-deep);
  color: var(--cream);
  position: relative;
  overflow: hidden;
}
.steps .eyebrow { color: var(--blush-light); }
.steps .eyebrow::before, .steps .eyebrow::after { background: rgba(231, 208, 214, .45); }
.steps .lede { color: rgba(249, 245, 244, .78); }

/* Her own logo mark, huge and barely there. It is the only ornament on the
   site that is not a photograph, and it is hers. */
.steps__mark {
  position: absolute;
  right: -4%;
  bottom: -12%;
  width: min(34vw, 340px);
  opacity: .09;
  pointer-events: none;
  /* No filter. This used to be the pink vacuum PNG faked white with
     grayscale + brightness; the reversed SVG is already cream, and running
     the old filter over it flattens every glint out of the mark. */
}

.steps__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: clamp(2rem, 4vw, 3.25rem);
  position: relative;
  counter-reset: step;
}
@media (min-width: 800px) {
  .steps__list { grid-template-columns: repeat(3, 1fr); }
}

.step {
  position: relative;
  padding-top: clamp(2.25rem, 4vw, 3rem);
  counter-increment: step;
}
/* The rule above each step draws itself in as the row arrives, left to right,
   which reads as a sequence rather than as three unrelated boxes. */
.step::before {
  content: '';
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  background: rgba(231, 208, 214, .3);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 1s var(--ease);
}
.is-in .step::before { transform: scaleX(1); }
.is-in .step:nth-child(2)::before { transition-delay: .12s; }
.is-in .step:nth-child(3)::before { transition-delay: .24s; }

.step__n {
  position: absolute;
  /* Straddling the hairline rather than sitting under it. Below the rule the
     numeral shared a line with the step title and the two ran into each
     other; hung across the rule it reads as a marker on the sequence and
     leaves the title clean. */
  top: 0;
  right: 0;
  transform: translateY(-42%);
  font-family: var(--display);
  font-size: clamp(3rem, 5.5vw, 4.25rem);
  line-height: 1;
  color: var(--blush);
  opacity: .22;
  pointer-events: none;
}
.step__title {
  font-family: var(--display);
  font-weight: 400;
  font-size: clamp(1.5rem, 2.4vw, 1.9rem);
  color: var(--white);
  margin: 0 0 .6rem;
  position: relative;
}
.step__body {
  color: rgba(249, 245, 244, .74);
  font-size: .97rem;
  margin: 0;
  max-width: 34ch;
  position: relative;
}

/* =========================================================================
   CREDENTIAL BADGES
   Only rendered for the ones she has actually confirmed.
   ========================================================================= */

.badges {
  display: flex;
  flex-wrap: wrap;
  gap: .7rem;
  margin-top: 2rem;
}
.badge {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  padding: .5rem 1rem .5rem .8rem;
  border: 1px solid var(--blush);
  border-radius: 999px;
  font-size: .72rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--rose-deep);
  background: var(--blush-pale);
}
.badge svg { width: 1rem; height: 1rem; color: var(--rose); }
.steps .badge {
  border-color: rgba(231, 208, 214, .35);
  background: rgba(255, 255, 255, .07);
  color: var(--cream);
}
.steps .badge svg { color: var(--blush); }

/* =========================================================================
   GALLERY
   ========================================================================= */

.gallery {
  display: grid;
  gap: clamp(.6rem, 1.4vw, 1.1rem);

  /* Column counts are FIXED per breakpoint rather than auto-filled, and that
     is the whole point: the front page shows twelve photographs, and twelve
     divides exactly by 2, 3 and 4. Auto-fill picked five columns on a wide
     screen and left two stranded on a final row, which does not read as a
     gallery — it reads as something broken. */
  grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 700px)  { .gallery { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1100px) { .gallery { grid-template-columns: repeat(4, 1fr); } }
.gallery__item {
  position: relative;
  display: block;
  aspect-ratio: 3 / 4;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--cream-warm);
  cursor: zoom-in;
  border: 0;
  padding: 0;
  width: 100%;
}
.gallery__item img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform 1.2s var(--ease), filter .6s var(--ease-soft);
}
.gallery__item:hover img, .gallery__item:focus-visible img { transform: scale(1.06); }
.gallery__item::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to top, rgba(109, 59, 72, .5), transparent 55%);
  opacity: 0;
  transition: opacity .5s var(--ease-soft);
}
.gallery__item:hover::after, .gallery__item:focus-visible::after { opacity: 1; }

/* NOT staggered, and that was tried first. Giving every seventh tile a taller
   aspect ratio was meant to stop the grid reading as a spreadsheet. What it
   actually does is size each grid ROW to its tallest tile and leave the
   shorter ones sitting in a pocket of white space — which reads as broken
   rather than as rhythm.
   *
   These are forty portrait photographs of clean rooms. Uniform, generously
   spaced and all the same shape is the more expensive-looking answer, and the
   interest comes from the photographs themselves. */

/* =========================================================================
   LIGHTBOX
   ========================================================================= */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: rgba(30, 20, 24, .94);
  backdrop-filter: blur(10px);
  display: grid;
  grid-template-rows: auto 1fr auto;
  opacity: 0;
  pointer-events: none;
  transition: opacity .4s var(--ease-soft);
}
.lightbox.is-open { opacity: 1; pointer-events: auto; }
.lightbox__bar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem var(--pad);
  color: rgba(255, 255, 255, .7);
  font-size: .75rem;
  letter-spacing: .18em;
  text-transform: uppercase;
}
.lightbox__stage {
  display: grid;
  place-items: center;
  padding: 0 var(--pad);
  min-height: 0;
}
.lightbox__stage img {
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
}
.lightbox__nav {
  display: flex;
  justify-content: center;
  gap: 1rem;
  padding: 1.25rem var(--pad) 2rem;
}
.lightbox__btn {
  width: 52px; height: 52px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .3);
  color: var(--white);
  display: grid;
  place-items: center;
  transition: background-color .3s var(--ease-soft), border-color .3s var(--ease-soft);
}
.lightbox__btn:hover { background: rgba(255, 255, 255, .14); border-color: var(--white); }
.lightbox__btn svg { width: 20px; height: 20px; }

/* =========================================================================
   REVIEWS
   Hidden entirely until there is a real one. See config.js.
   ========================================================================= */

.reviews__grid {
  display: grid;
  /* Capped at 30rem and centred, rather than 1fr. She will very likely have
     exactly one review to start with, and with 1fr a single card stretches
     the full width of the page and reads as a mistake. This way one card is a
     card, and three still fill the row. */
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 290px), 30rem));
  justify-content: center;
  gap: clamp(1.5rem, 3vw, 2.5rem);
}
.review {
  background: var(--white);
  border-radius: var(--radius-lg);
  padding: clamp(1.75rem, 3vw, 2.5rem);
  box-shadow: var(--shadow-sm);
  display: grid;
  align-content: start;
  gap: 1.2rem;
  position: relative;
}
.review__quote {
  font-family: var(--display);
  font-size: 3.5rem;
  line-height: .6;
  color: var(--blush);
  height: 1.2rem;
}
.review__text {
  font-family: var(--display);
  font-size: 1.3rem;
  line-height: 1.45;
  color: var(--ink);
  margin: 0;
}
.review__who {
  font-size: .75rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--rose);
}
.review__who span { color: var(--ink-faint); }

/* =========================================================================
   THE QUOTE FORM
   ========================================================================= */

.quote { background: var(--cream-warm); }

.quote__panel {
  background: var(--white);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  overflow: hidden;
  display: grid;
  /* minmax(0, …) on both tracks, not 1fr. A bare `1fr` is `minmax(auto, 1fr)`,
     and that `auto` floor means the track cannot shrink below its content's
     min-content width — so the long "07443 404231 on WhatsApp" row in the
     aside stretched the whole panel past the edge of a 320 and 360px screen.
     A zero minimum lets the track shrink and the text wrap instead. */
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 940px) {
  .quote__panel { grid-template-columns: minmax(0, .85fr) minmax(0, 1.15fr); }
}

.quote__aside {
  background: var(--rose-deep);
  color: var(--cream);
  padding: clamp(2rem, 4vw, 3.25rem);
  display: grid;
  align-content: space-between;
  gap: 2rem;
  position: relative;
  overflow: hidden;
}
.quote__aside .h2 { color: var(--white); }
.quote__aside p { color: rgba(249, 245, 244, .78); }

.quote__mark {
  position: absolute;
  right: -12%;
  bottom: 12%;
  width: min(52%, 230px);
  opacity: .11;
  pointer-events: none;
}
/* Below the split it sits behind a much shorter column and starts crowding
   the text, so it goes. */
@media (max-width: 939px) { .quote__mark { display: none; } }

.quote__direct {
  display: grid;
  gap: .9rem;
  position: relative;
}
.quote__direct a {
  display: flex;
  align-items: center;
  gap: .9rem;
  color: var(--white);
  text-decoration: none;
  font-size: 1.05rem;
  /* The number and the words after it have to be allowed to wrap on a narrow
     phone rather than forcing the panel wider than the screen. */
  flex-wrap: wrap;
  min-width: 0;
  padding: .85rem 1.1rem;
  border: 1px solid rgba(231, 208, 214, .3);
  border-radius: 999px;
  transition: background-color .35s var(--ease-soft), border-color .35s var(--ease-soft);
}
.quote__direct a:hover { background: rgba(255, 255, 255, .1); border-color: var(--blush); }
.quote__direct svg { width: 1.2rem; height: 1.2rem; flex: none; color: var(--blush); }

.quote__form { padding: clamp(1.75rem, 4vw, 3.25rem); }

.field { display: grid; gap: .5rem; margin-bottom: 1.25rem; }
.field__label {
  font-size: .7rem;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--ink-soft);
}
.field input, .field select, .field textarea {
  width: 100%;
  padding: .95rem 1.1rem;
  border: 1px solid var(--cream-deep);
  border-radius: var(--radius);
  background: var(--cream);
  font-size: 1rem;
  font-weight: 300;
  transition: border-color .3s var(--ease-soft), background-color .3s var(--ease-soft);
}
.field input:focus, .field select:focus, .field textarea:focus {
  outline: none;
  border-color: var(--rose);
  background: var(--white);
}
.field textarea { resize: vertical; min-height: 7rem; line-height: 1.6; }
.field select {
  appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath fill='none' stroke='%238a4d5e' stroke-width='1.4' d='M1 1.5 6 6.5l5-5'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 1.1rem center;
  background-size: 12px;
  padding-right: 2.75rem;
}
.field-row {
  display: grid;
  gap: 0 1.25rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 200px), 1fr));
}

.quote__alt {
  margin: 1.25rem 0 0;
  padding-top: 1.25rem;
  border-top: 1px solid var(--cream-deep);
  text-align: center;
  font-size: .95rem;
  color: var(--ink-soft);
}
.quote__alt strong { color: var(--rose-deep); font-weight: 400; }
.quote__alt .link-draw { margin-left: .4rem; }

.quote__note {
  font-size: .82rem;
  color: var(--ink-faint);
  margin: 1rem 0 0;
  text-align: center;
}

/* The live preview of the message that is about to be sent. Removing the
   surprise is the point — people are far more willing to press a button that
   opens WhatsApp when they can already see exactly what it will say. */
.quote__preview {
  margin-top: 1.5rem;
  background: #e6ded8;
  border-radius: var(--radius-lg);
  padding: 1.1rem;
}
.quote__preview-label {
  font-size: .78rem;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: .6rem;
}
.quote__bubble {
  background: #dcf8c6;
  border-radius: 10px 10px 10px 2px;
  padding: .8rem 1rem;
  font-size: .92rem;
  line-height: 1.55;
  color: #111b21;
  white-space: pre-wrap;
  word-break: break-word;
  box-shadow: 0 1px 1px rgba(0, 0, 0, .12);
}

/* =========================================================================
   THE PICKER — her price bands, as something a customer taps
   -------------------------------------------------------------------------
   Replaced a "Your property" text box. Tap targets, not typing: this is used
   on a phone, one-handed, by somebody who is deciding whether to bother
   messaging at all. Every option is a full-width row with a 56px target.
   ========================================================================= */

.picker { border: 0; padding: 0; margin: 0 0 1.25rem; min-width: 0; }
.picker legend { padding: 0; }

.picker__opts { display: grid; gap: .5rem; }
@media (min-width: 560px) {
  .picker__opts--split { grid-template-columns: 1fr 1fr; }
}

.opt {
  display: flex;
  align-items: center;
  gap: .85rem;
  min-height: 56px;
  padding: .7rem 1rem;
  background: var(--cream);
  border: 1px solid var(--cream-deep);
  border-radius: var(--radius-lg);
  cursor: pointer;
  transition: border-color .25s var(--ease-soft), background-color .25s var(--ease-soft);
}
.opt:hover { border-color: var(--blush); }

/* The input is the control and must stay focusable — it is moved out of sight
   rather than removed, and the visible tick is drawn by ::before on the label
   so the whole row is the target. */
.opt input {
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0;
  pointer-events: none;
}
.opt::before {
  content: '';
  flex: none;
  width: 22px; height: 22px;
  border: 1px solid var(--blush);
  border-radius: 6px;
  background: var(--white);
  transition: background-color .2s var(--ease-soft), border-color .2s var(--ease-soft);
}
/* A radio reads as a circle, a checkbox as a square. People know what each
   means without being told they can only pick one. */
.opt:has(input[type="radio"])::before { border-radius: 50%; }

.opt:has(input:checked) {
  border-color: var(--rose);
  background: var(--blush-pale);
}
.opt:has(input:checked)::before {
  background: var(--rose) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 8.5l3.2 3.2L13 5' fill='none' stroke='%23fff' stroke-width='2.2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/13px no-repeat;
  border-color: var(--rose);
}
.opt:has(input:focus-visible) { outline: 2px solid var(--rose); outline-offset: 2px; }

.opt__body { flex: 1; min-width: 0; display: grid; }
.opt__name { font-size: .98rem; }
.opt__detail { font-size: .8rem; color: var(--ink-faint); line-height: 1.35; }

.opt__price {
  font-family: var(--display);
  font-size: 1.35rem;
  color: var(--rose-deep);
  white-space: nowrap;
}
.opt__price i {
  font-family: var(--sans);
  font-style: normal;
  font-size: .58rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.picker__total {
  margin: -.4rem 0 1.25rem;
  padding: .85rem 1.1rem;
  background: var(--blush-pale);
  border-radius: var(--radius-lg);
  color: var(--rose-deep);
  font-size: .95rem;
}
.picker__total strong { font-family: var(--display); font-size: 1.4rem; font-weight: 400; }

/* `:has()` is what makes the whole row a target without a line of script. Very
   old browsers ignore it, so the tick never fills in — the radio still works,
   the message still composes, and the form is merely plainer. */
@supports not selector(:has(*)) {
  .opt::before { display: none; }
  .opt input { position: static; width: auto; height: auto; opacity: 1; pointer-events: auto; }
}

/* =========================================================================
   PRICING  (off by default)
   ========================================================================= */

.prices { display: grid; gap: 0; max-width: 780px; margin-inline: auto; }
.price-row {
  display: flex;
  align-items: baseline;
  gap: 1rem;
  padding: 1.25rem 0;
  border-bottom: 1px solid var(--cream-deep);
}
.price-row__label { font-family: var(--display); font-size: 1.5rem; }
.price-row__note { color: var(--ink-faint); font-size: .85rem; display: block; }
.price-row__dots { flex: 1; border-bottom: 1px dotted var(--blush); translate: 0 -.35em; }
.price-row__price {
  font-family: var(--display);
  font-size: 1.6rem;
  color: var(--rose);
  white-space: nowrap;
}
.price-row__unit { font-family: var(--sans); font-size: .75rem; color: var(--ink-faint); }

/* =========================================================================
   FAQ
   ========================================================================= */

.faq { max-width: 860px; margin-inline: auto; }
.faq__item { border-bottom: 1px solid var(--cream-deep); }
.faq__q {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem;
  padding: 1.5rem 0;
  text-align: left;
  font-family: var(--display);
  font-size: clamp(1.15rem, 2vw, 1.5rem);
  color: var(--ink);
  transition: color .3s var(--ease-soft);
}
.faq__q:hover { color: var(--rose); }
.faq__icon {
  flex: none;
  width: 26px; height: 26px;
  position: relative;
}
.faq__icon::before, .faq__icon::after {
  content: '';
  position: absolute;
  inset: 50% 0 auto 0;
  height: 1px;
  background: var(--rose);
  transition: transform .45s var(--ease);
}
.faq__icon::after { transform: rotate(90deg); }
.faq__q[aria-expanded='true'] .faq__icon::after { transform: rotate(0); }

.faq__a {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows .5s var(--ease);
}
.faq__q[aria-expanded='true'] + .faq__a { grid-template-rows: 1fr; }
.faq__a > div { overflow: hidden; }
.faq__a p {
  color: var(--ink-soft);
  padding-bottom: 1.5rem;
  max-width: var(--max-text);
}

/* =========================================================================
   TERMS & CONDITIONS
   -------------------------------------------------------------------------
   A page somebody reads to the end before handing over a door key, so it is
   set as a document rather than as marketing: one column, generous leading,
   and headings that can be scanned for the clause you are looking for.
   ========================================================================= */

.terms__intro,
.terms__closing {
  font-family: var(--display);
  font-size: clamp(1.2rem, 1rem + .8vw, 1.6rem);
  line-height: 1.5;
  color: var(--rose-deep);
  max-width: 46ch;
}
.terms__intro { margin-bottom: clamp(2.5rem, 5vw, 3.5rem); }
.terms__closing {
  margin-top: clamp(2.5rem, 5vw, 3.5rem);
  padding-top: clamp(1.75rem, 3vw, 2.5rem);
  border-top: 1px solid var(--blush);
  font-style: italic;
}

.terms__list { display: grid; gap: clamp(1.75rem, 3.5vw, 2.75rem); }

.terms__block {
  display: grid;
  gap: .5rem;
  /* Headings sit beside the clause above the split rather than on top of it,
     which is what lets somebody run an eye down the left edge for the one
     they want instead of reading the whole page. */
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 800px) {
  .terms__block { grid-template-columns: minmax(0, 15rem) minmax(0, 1fr); gap: 2.5rem; }
}

.terms__h {
  font-family: var(--sans);
  font-size: .78rem;
  font-weight: 400;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--rose);
  margin: 0;
  padding-top: .35rem;
}

.terms__points {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: .75rem;
  color: var(--ink-soft);
  max-width: var(--max-text);
}
.terms__points li { position: relative; padding-left: 1.6rem; }
/* Her glint as the bullet — the same shape the logo is built from. */
.terms__points li::before {
  content: '';
  position: absolute;
  left: 0;
  top: .62em;
  width: .55rem;
  height: .55rem;
  background: var(--blush);
  clip-path: polygon(50% 0%, 58% 42%, 100% 50%, 58% 58%, 50% 100%, 42% 58%, 0% 50%, 42% 42%);
}

@media print {
  .terms__block { grid-template-columns: 1fr; gap: .4rem; break-inside: avoid; }
  .terms__intro, .terms__closing { font-size: 1rem; }
}

/* =========================================================================
   ABOUT / CARD
   ========================================================================= */

.about {
  display: grid;
  gap: clamp(2rem, 5vw, 4.5rem);
  align-items: center;
  grid-template-columns: 1fr;
}
@media (min-width: 900px) { .about { grid-template-columns: .9fr 1.1fr; } }

.about__card {
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow);
  overflow: hidden;
  background: var(--white);
  transform: rotate(-2deg);
  transition: transform .8s var(--ease);
}
.about__card:hover { transform: rotate(0deg) scale(1.02); }

/* =========================================================================
   FOOTER
   ========================================================================= */

.footer {
  background: var(--ink);
  color: rgba(249, 245, 244, .72);
  padding-block: clamp(3.5rem, 7vw, 5.5rem) 2rem;
  font-size: .92rem;
}
.footer a { color: var(--blush-light); text-decoration: none; }
.footer a:hover { color: var(--white); text-decoration: underline; }

.footer__top {
  display: grid;
  gap: clamp(2rem, 5vw, 4rem);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr));
  padding-bottom: 3rem;
  border-bottom: 1px solid rgba(249, 245, 244, .12);
}
.footer__brand { display: grid; gap: 1.25rem; align-content: start; }
.footer__mark { width: 44px; }
.footer__name {
  font-family: var(--display);
  font-size: 1.75rem;
  color: var(--white);
  line-height: 1.1;
}
.footer__name em { font-style: italic; color: var(--blush); }

.footer__h {
  font-size: .68rem;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--blush);
  margin-bottom: 1.1rem;
}
.footer__list { list-style: none; margin: 0; padding: 0; display: grid; gap: .6rem; }

.footer__bottom {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding-top: 2rem;
  font-size: .78rem;
  color: rgba(249, 245, 244, .5);
}

/* Her door into the owner panel.
   Deliberately QUIET but not hidden. It is the last thing in the footer, it
   is smaller than the copyright line, and a customer scrolling past has no
   reason to touch it — but she can always find it from any page on her phone
   without having to remember a URL. Sized to a 44px tap target, because she
   is going to be pressing it one-handed. */
.owner-link {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  min-height: 44px;
  padding: 0 1rem;
  border: 1px solid rgba(249, 245, 244, .18);
  border-radius: 999px;
  color: rgba(249, 245, 244, .55);
  font-size: .72rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  text-decoration: none;
  transition: color .3s var(--ease-soft), border-color .3s var(--ease-soft),
              background-color .3s var(--ease-soft);
}
.owner-link:hover, .owner-link:focus-visible {
  color: var(--white);
  border-color: var(--blush);
  background: rgba(249, 245, 244, .06);
  text-decoration: none;
}
.owner-link svg { width: 13px; height: 13px; flex: none; }

.socials { display: flex; gap: .75rem; }
.socials a {
  width: 40px; height: 40px;
  border: 1px solid rgba(249, 245, 244, .2);
  border-radius: 50%;
  display: grid;
  place-items: center;
  transition: background-color .35s var(--ease-soft), border-color .35s var(--ease-soft),
              transform .35s var(--ease);
}
.socials a:hover {
  background: var(--rose);
  border-color: var(--rose);
  transform: translateY(-2px);
}
.socials svg { width: 18px; height: 18px; }

/* =========================================================================
   STICKY MOBILE BAR
   -------------------------------------------------------------------------
   On a phone the WhatsApp button is never more than a thumb away. This is the
   single highest-converting element on the site and it costs nothing on
   desktop, where it is not rendered at all.
   ========================================================================= */

.mobile-bar {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 90;
  display: none;
  gap: .6rem;
  padding: .6rem;
  padding-bottom: max(.6rem, env(safe-area-inset-bottom));
  background: rgba(249, 245, 244, .93);
  backdrop-filter: blur(14px);
  border-top: 1px solid var(--cream-deep);
  transform: translateY(110%);
  transition: transform .5s var(--ease);
}
.mobile-bar.is-up { transform: none; }
.mobile-bar .btn { flex: 1; padding: .95rem 1rem; font-size: .74rem; }

@media (max-width: 760px) {
  .mobile-bar { display: flex; }
  /* Nothing at the bottom of the page may hide behind it. */
  body { padding-bottom: 4.5rem; }
}

/* =========================================================================
   SCROLL REVEALS
   -------------------------------------------------------------------------
   Applied by site.js via IntersectionObserver. The `no-js` guard matters: if
   JavaScript fails, `is-in` is never added, so without it every revealing
   element on the site would stay invisible forever. The default is therefore
   VISIBLE, and the hidden state is only applied once JS has confirmed it is
   running.
   ========================================================================= */

.js .reveal {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 1s var(--ease-soft), transform 1s var(--ease);
}
.js .reveal.is-in { opacity: 1; transform: none; }

.js .reveal--stagger > * {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity .9s var(--ease-soft), transform .9s var(--ease);
}
.js .reveal--stagger.is-in > * { opacity: 1; transform: none; }
.js .reveal--stagger.is-in > *:nth-child(1) { transition-delay: .00s; }
.js .reveal--stagger.is-in > *:nth-child(2) { transition-delay: .08s; }
.js .reveal--stagger.is-in > *:nth-child(3) { transition-delay: .16s; }
.js .reveal--stagger.is-in > *:nth-child(4) { transition-delay: .24s; }
.js .reveal--stagger.is-in > *:nth-child(5) { transition-delay: .32s; }
.js .reveal--stagger.is-in > *:nth-child(6) { transition-delay: .40s; }

/* =========================================================================
   THE SPARKLE LAYER
   -------------------------------------------------------------------------
   A handful of slow motes drifting upward behind the page. Deliberately
   sparse — this is a cleaning company, not a snow globe, and the moment it
   reads as glitter it has ruined the thing it was meant to support.
   ========================================================================= */

.sparkles {
  position: fixed;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: .55;
}

/* =========================================================================
   UTILITIES
   ========================================================================= */

.bg-warm { background: var(--cream-warm); }
.bg-white { background: var(--white); }
.text-center { text-align: center; }
[hidden] { display: none !important; }

/* =========================================================================
   REDUCED MOTION
   -------------------------------------------------------------------------
   Everything decorative stops. The reveals resolve to their finished state
   rather than being left mid-transition, so nothing is ever stuck invisible.
   ========================================================================= */

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

  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
  }

  .js .reveal,
  .js .reveal--stagger > * { opacity: 1 !important; transform: none !important; }
  .reveal-line > span { transform: none !important; }
  .figure--reveal img { clip-path: none !important; transform: none !important; }
  .service__rule { transform: scaleX(1) !important; }
  .hero__media img { animation: none !important; transform: scale(1.06); }
  .marquee__track { animation: none !important; }
  .gleam::after { display: none !important; }
  .sparkles { display: none !important; }
  .hero__scroll span.line::after { animation: none !important; }
}

/* =========================================================================
   WHAT'S INCLUDED
   -------------------------------------------------------------------------
   Her comparison table. One <table>, two layouts, no duplicate markup:

   ·  PHONE (default). The tab strip shows, and every column cell is hidden
      except the one whose index matches [data-col] on the wrapper. Written
      out to four columns so she can add one without touching CSS.
   ·  WIDE (≥780px). The tab strip is display:none, all columns show, and the
      head row sticks under the site header so row 19 still knows which
      column it is in.

   ⚠  THE SCROLL WRAPPER'S overflow IS PHONE-ONLY, ON PURPOSE. An element
      with overflow-x on one axis computes the other axis to auto, which
      makes it a scroll container — and a sticky child sticks to its nearest
      scroll container, not the viewport. Put overflow on .incl__scroll at
      all widths and the sticky head silently stops working. Same family of
      bug as the one that killed the sticky site header; see HANDOVER.md.
   ========================================================================= */

.incl {
  background: var(--white);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-sm);
  padding: clamp(1rem, 3vw, 2rem);
}

/* ---- the phone-only switcher ---- */
.incl__tabs {
  display: flex;
  gap: .35rem;
  padding: .3rem;
  margin-bottom: 1.25rem;
  background: var(--cream-warm);
  border-radius: 999px;
}

.incl__tab {
  flex: 1 1 0;
  min-width: 0;
  padding: .7rem .35rem;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--ink-soft);
  font-family: var(--sans);
  font-size: .8rem;
  font-weight: 500;
  letter-spacing: .02em;
  cursor: pointer;
  transition: background-color .3s var(--ease-soft), color .3s var(--ease-soft);
}

.incl__tab[aria-selected="true"] {
  background: var(--rose);
  color: var(--white);
  box-shadow: var(--shadow-sm);
}

.incl__table {
  width: 100%;
  border-collapse: separate;   /* collapse drops sticky borders in Safari */
  border-spacing: 0;
  font-family: var(--sans);
  text-align: left;
}

/* The first head cell is a label for the row headers below it, which is what
   a screen reader wants, but printing "Included" over a column of task names
   reads as a heading for the wrong thing. Kept in the accessibility tree and
   taken out of the picture. */
.incl__task-h { font-size: 0; color: transparent; }

.incl__col,
.incl__task-h {
  position: sticky;
  top: var(--header-h, 5.5rem);
  z-index: 1;
  background: var(--white);
  padding: .9rem .5rem 1rem;
  box-shadow: inset 0 -1px 0 var(--blush-light);
}

.incl__col {
  text-align: center;
  width: 5.5rem;
}

.incl__col-name {
  display: block;
  color: var(--rose);
  font-size: .72rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  line-height: 1.35;
}

.incl__task {
  padding: .85rem .75rem .85rem 0;
  font-size: .95rem;
  font-weight: 400;
  line-height: 1.4;
  color: var(--ink);
  box-shadow: inset 0 -1px 0 var(--blush-pale);
}

.incl__cell {
  padding: .85rem .5rem;
  text-align: center;
  box-shadow: inset 0 -1px 0 var(--blush-pale);
}

.incl__table tbody tr:last-child .incl__task,
.incl__table tbody tr:last-child .incl__cell { box-shadow: none; }

/* Everything she does include, in the brand colour. Everything she does not,
   as a dash the eye slides straight over — a column of red crosses next to
   the cheapest clean she sells would sell it as the wrong answer. */
.incl__tick {
  width: 1.15rem;
  height: 1.15rem;
  color: var(--rose);
  vertical-align: middle;
}

.incl__no {
  display: inline-block;
  color: var(--blush);
  font-size: 1rem;
  line-height: 1;
}

.incl__note {
  margin-top: 1.5rem;
  font-family: var(--sans);
  font-size: .82rem;
  color: var(--ink-faint);
  text-align: center;
}

@media (max-width: 779px) {
  .incl__scroll { overflow-x: auto; }

  /* No head row at all on a phone. With one column showing, it printed
     "DEEP CLEAN" directly under a button already reading "Deep" — the same
     word twice, costing a line of screen. Every cell still carries its own
     "Included — Deep clean" for a screen reader, so nothing is lost by it. */
  .incl__table thead { display: none; }

  /* Which means the switcher is now the ONLY thing naming the column, and
     twenty-one rows is far enough to scroll that it must not leave with
     them. It rides down under the site header instead. The white ring is a
     spread shadow rather than a border: it paints out the rows passing
     behind the pill's rounded corners without adding a box of its own. */
  .incl__tabs {
    position: sticky;
    /* Flush to the header, not below it: the ring then extends UP behind the
       header rather than leaving a half-centimetre slot for a row of the
       table to show through. */
    top: var(--header-h, 5.5rem);
    z-index: 2;
    box-shadow: 0 0 0 .6rem var(--white), var(--shadow-sm);
  }

  .incl__col,
  .incl__cell { display: none; }

  .incl[data-col="0"] [data-c="0"],
  .incl[data-col="1"] [data-c="1"],
  .incl[data-col="2"] [data-c="2"],
  .incl[data-col="3"] [data-c="3"] { display: table-cell; }

  /* One column left, so it can afford to be wider than 5.5rem. */
  .incl__col { width: 7rem; }
}

@media (min-width: 780px) {
  .incl__tabs { display: none; }
  .incl__col { width: 8rem; }
}

/* =========================================================================
   PRINT
   Someone will print this to put through a door. Make it survive.
   ========================================================================= */

@media print {
  .header, .mobile-bar, .sparkles, .marquee, .hero__scroll,
  .lightbox, .nav-toggle, .notice, .owner-link,
  .incl__tabs { display: none !important; }
  /* Every column, no switcher, and nothing stuck to a viewport that a sheet
     of paper does not have. */
  .incl { box-shadow: none; padding: 0; }
  .incl__col, .incl__cell { display: table-cell !important; }
  .incl__col, .incl__task-h { position: static; }
  .incl__table { break-inside: auto; }
  .incl__table tr { break-inside: avoid; }
  body { background: #fff; color: #000; padding: 0; }
  .hero { min-height: auto; color: #000; }
  .hero__media { display: none; }
  .hero__inner { color: #000; }
  .hero__title, .hero__intro { text-shadow: none; color: #000; }
  .section { padding-block: 1.5rem; }
  .steps, .footer { background: #fff !important; color: #000 !important; }
  .steps__mark { display: none; }
  a { color: #000; }
}
