/* ===================================================================
   Ideal Window Cleaning — styles
   Light, editorial, typographic. The palette comes straight from the logo:
   navy #060645 (the favicon tile), cyan #009CDE (the word CLEANING) and white.
   Signature: the squeegee wipe (headline on load, section reveals, and the
   quote card moulding from one step into the next).
   =================================================================== */
html { scroll-behavior: smooth; }
body { margin: 0; background: #060645; }

.page {
  --frost: #F3F7FB;
  --white: #FFFFFF;
  --navy: #060645;
  --ink: #060645;
  --mid: #4F5A78;
  /* --mid, taken down until it clears 4.5:1 over the hero photograph. The van caption is the one
     piece of small text on the page that sits on a picture rather than on white, and the veil was
     thinned to let the photograph through, so it cannot borrow the body colour any more. */
  --mid-on-photo: #414A64;
  --line: #D8DFEA;
  --line-strong: #B5C0D2;
  --sky: #009CDE;
  --sky-deep: #0077AB;
  --sky-soft: #E3F4FC;
  --wet: #BFE7FA;
  --amber: #D99A1E;
  --ok: #1B7F4B;
  --warn: #B45309;
  --danger: #B42318;
  --on-navy: rgba(255, 255, 255, .74);
  /* One typeface across the whole site. Arial and Liberation Sans are metric-compatible with
     Helvetica, so a Windows or Linux visitor gets the same line breaks as a Mac one. */
  --display: 'Helvetica Neue', Helvetica, Arial, 'Liberation Sans', sans-serif;
  --body: 'Helvetica Neue', Helvetica, Arial, 'Liberation Sans', sans-serif;
  --label: 'Helvetica Neue', Helvetica, Arial, 'Liberation Sans', sans-serif;
  --ease-out: cubic-bezier(.2, .7, .2, 1);
  --ease-wipe: cubic-bezier(.77, 0, .18, 1);
  --nav-h: 72px;
  --gutter: clamp(16px, 4cqi, 64px);
  --r: 18px;

  font-family: var(--body);
  font-size: 17px;
  line-height: 1.5;
  color: var(--ink);
  background: var(--frost);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  width: 100%;
  min-width: 0;
  position: relative;
}
.page *, .page *::before, .page *::after { box-sizing: border-box; }
.page ::selection { background: var(--wet); }
.page a { color: var(--sky-deep); }
.page a:hover { color: var(--navy); }
.page :focus-visible { outline: 3px solid var(--sky); outline-offset: 3px; border-radius: 6px; }
.page [id] { scroll-margin-top: calc(var(--nav-h) + 16px); }
.page img { max-width: 100%; height: auto; }
.page .is-hidden { display: none !important; }
.vh { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }

.frame { container-type: inline-size; container-name: page; }

/* ---------- utilities ---------- */
.skip { position: absolute; left: 16px; top: -60px; z-index: 100; background: var(--navy); color: #fff; padding: 10px 14px; border-radius: 8px; text-decoration: none; }
.skip:focus { top: 16px; color: #fff; }
.progress { position: fixed; top: 0; left: 0; right: 0; height: 3px; background: var(--sky); transform-origin: 0 50%; transform: scaleX(0); z-index: 60; }
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }

.eyebrow { font: 700 12px/1.4 var(--label); letter-spacing: .14em; text-transform: uppercase; color: var(--mid); margin: 0 0 18px; }
.h2 { font-family: var(--display); font-weight: 700; font-size: clamp(32px, 4.4cqi, 58px); line-height: 1.02; letter-spacing: -.022em; margin: 0; text-wrap: balance; }

.btn { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 50px; padding: 0 24px; border-radius: 12px; font: 600 16px/1 var(--body); text-decoration: none; border: 1.5px solid transparent; cursor: pointer; transition: transform .2s var(--ease-out), background .2s, box-shadow .2s, border-color .2s; white-space: nowrap; }
/* `.page a` sets link colour; the a.btn selectors keep button labels readable on link buttons. */
.btn--primary, .page a.btn--primary { background: var(--sky); color: var(--navy); }
.btn--primary:hover, .page a.btn--primary:hover { background: #33B4E6; color: var(--navy); transform: translateY(-1px); box-shadow: 0 10px 24px -10px rgba(0, 156, 222, .7); }
.btn--ghost, .page a.btn--ghost { background: transparent; border-color: var(--line-strong); color: var(--ink); }
.btn--ghost:hover, .page a.btn--ghost:hover { border-color: var(--navy); color: var(--navy); }
.btn--sm { min-height: 42px; padding: 0 16px; font-size: 15px; border-radius: 10px; }
.linkbtn { background: none; border: 0; padding: 0; color: var(--sky-deep); font: 500 14px var(--body); cursor: pointer; text-decoration: underline; text-underline-offset: 3px; }
.linkbtn:hover { color: var(--navy); }

/* ---------- nav (navy, so the logo's white lettering reads) ---------- */
.nav { position: sticky; top: 0; z-index: 50; background: var(--navy); color: #fff; border-bottom: 1px solid transparent; transition: border-color .3s; }
/* Backdrop blur only on mouse/trackpad devices: it is a GPU-heavy effect on phones. */
@media (hover: hover) and (pointer: fine) {
  .nav { background: rgba(6, 6, 69, .92); backdrop-filter: blur(14px); -webkit-backdrop-filter: blur(14px); }
}
.nav.is-scrolled { border-color: rgba(255, 255, 255, .14); }
.nav__inner { max-width: 1280px; margin: 0 auto; padding: 0 var(--gutter); height: var(--nav-h); display: flex; align-items: center; gap: 32px; }
.brand { display: inline-flex; align-items: center; min-height: 44px; flex: 0 0 auto; }
.brand img { display: block; height: 38px; width: auto; }
.nav__links { display: flex; gap: 26px; margin-left: 8px; }
.page .nav__links a { color: var(--on-navy); text-decoration: none; font-size: 15px; font-weight: 500; position: relative; padding: 6px 0; }
.nav__links a::after { content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px; background: var(--sky); transform: scaleX(0); transform-origin: 0 50%; transition: transform .3s var(--ease-out); }
.page .nav__links a:hover { color: #fff; }
.nav__links a:hover::after { transform: scaleX(1); }
.nav__actions { margin-left: auto; display: flex; align-items: center; gap: 16px; }
.page .nav__phone { font: 700 14px var(--label); color: #fff; text-decoration: none; letter-spacing: .03em; padding: 14px 6px; white-space: nowrap; }
.page .nav__phone:hover { color: var(--sky); }
.nav__toggle { display: none; width: 44px; height: 44px; border: 1.5px solid rgba(255, 255, 255, .38); border-radius: 10px; background: transparent; cursor: pointer; position: relative; }
.nav__toggle span { position: absolute; left: 12px; right: 12px; height: 2px; background: #fff; transition: transform .3s var(--ease-out), top .3s; }
.nav__toggle span:first-child { top: 16px; }
.nav__toggle span:last-child { top: 25px; }
.menu-open .nav__toggle span:first-child { top: 20px; transform: rotate(45deg); }
.menu-open .nav__toggle span:last-child { top: 20px; transform: rotate(-45deg); }

/* ---------- hero ---------- */
/* The street photograph sits behind the whole opening block. A painted sky underneath means a
   missing or slow photograph still reads as a deliberate background rather than a hole. */
.hero { position: relative; isolation: isolate; }
.hero__bg { position: absolute; inset: 0; z-index: -1; overflow: hidden; background: linear-gradient(170deg, var(--sky-soft) 0%, #eef6fd 46%, var(--white) 100%); }
.hero__bg img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 58%; display: block; }
/* Two scrims: one across, so the headline and the card have a quiet ground; one down, so the
   photograph has ended by the time the page turns into plain white. */
/* Three washes, thin enough now that the street is genuinely a photograph rather than a texture.
   The first is a feathered pool over the headline: a picture moves - a dark roof here, a bright sky
   there - and the words have to be readable against whatever happens to land behind them, so they
   get their own ground instead of the whole picture being dimmed for their sake. The second goes
   across, so the card has a quiet left-hand side to sit against. The third goes down, so the
   photograph has ended by the time the page turns into plain white. It all lives on .hero__bg,
   which clips: a wash hung off the copy instead pushed a phone's page sideways. */
.hero__bg::after { content: ""; position: absolute; inset: 0; background:
  radial-gradient(38% 44% at 30% 32%, rgba(255,255,255,.9) 0%, rgba(255,255,255,.84) 46%, rgba(255,255,255,.48) 76%, rgba(255,255,255,0) 100%),
  linear-gradient(100deg, rgba(255,255,255,.7) 0%, rgba(255,255,255,.56) 34%, rgba(255,255,255,.22) 62%, rgba(255,255,255,.05) 100%),
  linear-gradient(180deg, rgba(255,255,255,.26) 0%, rgba(255,255,255,0) 26%, rgba(255,255,255,.42) 80%, var(--white) 100%); }
.hero__inner { max-width: 1280px; margin: 0 auto; padding: clamp(28px, 4.5cqi, 64px) var(--gutter) clamp(36px, 5cqi, 72px); display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(360px, 1fr); gap: clamp(32px, 5cqi, 80px); align-items: center; }
.hero__title { font-family: var(--display); font-weight: 700; font-size: clamp(58px, 8.6cqi, 132px); line-height: .94; letter-spacing: -.022em; margin: 0; display: flex; flex-direction: column; }
.hero__title--loc { font-size: clamp(38px, 7.2cqi, 104px); }
/* Town names are one long word ("Rickmansworth."), so let them break rather than push the page wide. */
.hero__title--loc .wipe { max-width: 100%; overflow-wrap: break-word; }
/* Helvetica has no 300 instance, so a light weight here would be a browser-faked one; 400 against
   the 700 headline is the real contrast. --sky-deep holds its contrast over the photo veil. */
.hero__question { font-family: var(--display); font-weight: 400; font-size: clamp(30px, 4.3cqi, 62px); line-height: 1.08; letter-spacing: -.012em; color: var(--sky-deep); margin: 20px 0 0; }
.hero__lead { font-size: clamp(17px, 1.45cqi, 21px); max-width: 50ch; color: var(--mid); margin: 30px 0 0; text-wrap: pretty; }

/* The van with the guarantee stamped over its corner: the same lockup that is on the fliers.
   The van is a cut-out on transparency, so it is given room rather than a frame, and it is never
   stretched - the box follows the picture's own 800x333 shape. */
.lockup { position: relative; margin: clamp(22px, 3cqi, 40px) 0 0; max-width: 520px; }
.van { margin: 0; }
.van img { display: block; width: 100%; height: auto; aspect-ratio: 800 / 318; object-fit: contain; filter: drop-shadow(0 26px 26px rgba(6, 6, 69, .22)); }
.van figcaption { margin-top: 10px; font: 700 12px/1.4 var(--label); letter-spacing: .11em; text-transform: uppercase; color: var(--mid-on-photo); }
/* `.page img { height: auto }` is element-qualified and beats a bare `.stamp`, so this rule has to
   match its weight or the stamp renders at its own 240px. */
/* The stamp goes where the photograph is empty: the wedge of clear space above the van's
   bonnet, top right. Any lower and it lands on the windscreen and reads as a mistake
   rather than a sticker on the corner of the flier. */
.page .lockup .stamp { position: absolute; top: -9%; right: -1%; width: clamp(72px, 19%, 112px); height: auto; aspect-ratio: 1; object-fit: contain; transform: rotate(-8deg); transform-origin: 100% 0; filter: drop-shadow(0 8px 14px rgba(6, 6, 69, .26)); z-index: 2; }
/* No van photograph on the server yet: the stamp stops being a corner stamp and stands on its own. */
.lockup:not(:has(.van)) .stamp { position: static; transform: rotate(-4deg); margin: 4px 0 0; }
.page .lockup:not(:has(.van)) .stamp { width: 124px; }

/* Squeegee wipe on load (only once JS is confirmed, so nothing is ever left hidden) */
.wipe { position: relative; display: inline-block; }
html.js .wipe { clip-path: inset(-6% 100% -6% 0); animation: wipeIn 1.15s var(--ease-wipe) forwards; animation-delay: var(--d, 0ms); }
html.js .wipe::after { content: ""; position: absolute; top: -4%; bottom: -4%; left: 0; width: 3px; background: var(--sky); opacity: 0; animation: blade 1.15s var(--ease-wipe) forwards; animation-delay: var(--d, 0ms); }
@keyframes wipeIn { to { clip-path: inset(-6% -2% -6% 0); } }
@keyframes blade { 0% { left: 0; opacity: 1; } 85% { opacity: 1; } 100% { left: 100%; opacity: 0; } }
html.js [data-load] { opacity: 0; transform: translateY(14px); animation: loadIn .8s var(--ease-out) forwards; animation-delay: var(--d, 0ms); }
@keyframes loadIn { to { opacity: 1; transform: none; } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

/* ---------- quote card ---------- */
/* The card sits where it is put. It used to stick under the nav, which meant that starting to
   scroll the hero slid the panel a little way down the screen on its own - a thing the reader had
   not asked for, on the one element they were most likely to be mid-thought in. */
.quote { --qp: 22px; background: var(--white); border: 1px solid var(--line); border-radius: 22px; padding: var(--qp); box-shadow: 0 40px 70px -46px rgba(6, 6, 69, .5); display: flex; flex-direction: column; }

/* Every stage lives in one grid cell. The four question stages share one floor height that site.js
   measures from the tallest of them, so tapping through them never moves the card; the details form
   is the one stage allowed past that floor, and the card opens to it once. The head above and the
   price below are outside the stack, so they never move at all. */
.quote__stage { display: grid; grid-template-areas: "stage"; position: relative; min-height: 0; overflow: hidden auto; overscroll-behavior: contain; height: var(--stage-h, auto); transition: height .34s cubic-bezier(.4, 0, .2, 1); }
.quote__step { grid-area: stage; min-width: 0; display: flex; flex-direction: column; }
/* An off stage is taken out of flow, so only the stage on screen can set the height, and painted
   out, which also takes it off the tab order and the screen reader. display:flex is needed to beat
   the browser's own [hidden] { display: none }. */
/* height:0 matters as much as the rest: an absolutely positioned child still counts towards its
   scroll container's scrollHeight, so the four questions waiting their turn - the contact form
   among them - gave the card a 330px scrollbar on every stage, with nothing to scroll to. */
html.js .quote__step:not(.is-current) { display: flex; position: absolute; top: 0; left: 0; right: 0; height: 0; overflow: hidden; visibility: hidden; pointer-events: none; }
/* The one frame in which every stage is measured at its own natural height. */
html.js .quote__stage.is-measuring { height: auto; align-items: start; transition: none; }
html.js .quote__stage.is-measuring .quote__step { height: auto; overflow: visible; }
/* Without JavaScript nothing is ever made current, so fall back to ordinary flow. */
html:not(.js) .quote__step[hidden] { display: none; }
/* Pure slack under a short stage: it holds the content at the top of the card and never adds height
   of its own. */
.quote__fill { flex: 1 1 0; min-height: 0; }

/* Step change: the squeegee wipes the old stage away and the new one in. */
.quote.is-morphing { overflow: clip; }
.quote__ghost { position: absolute; inset: 0; margin: 0; pointer-events: none; z-index: 2; background: var(--white); }
.quote__blade { position: absolute; top: 0; bottom: 0; left: 0; width: 3px; margin-left: -1.5px; background: var(--sky); opacity: 0; pointer-events: none; z-index: 3; }

/* Progress. Four beats, because the fifth stage is the receipt rather than a question. */
.prog { list-style: none; display: flex; gap: 5px; margin: 0 0 13px; padding: 0; }
.prog li { flex: 1 1 0; height: 4px; border-radius: 2px; background: var(--line); transition: background .3s var(--ease-out); }
.prog li.is-done { background: var(--sky); }
.prog li.is-now { background: var(--sky-deep); }
.prog__now { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }
.q__ask { font: 700 12px/1 var(--label); letter-spacing: .1em; text-transform: uppercase; color: var(--mid); margin: 0 0 6px; }
.q__ask--sr { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }
.q__note { margin: 10px 0 0; font-size: 13px; line-height: 1.45; color: var(--mid); }
/* Empty rather than hidden, so it can stay a live region. It should still take no room. */
.q__note:empty { display: none; }
/* Two stages are introduced by a heading only a screen reader gets, and that is where the
   focus goes on every Back. Clipped to a pixel it swallows the focus ring, so the ring goes
   round the stage instead - nothing moves, and a keyboard reader can see where they are. */
/* A step whose heading is only for screen readers has nothing visible to take the focus ring, so
   the ring goes round the stage. It has to go on the STAGE and not on the step: a ring on the step
   is clipped away by the stage it exactly fills, and a ring drawn inside the step instead is
   painted straight through the first word of every legend. overflow clips an element's children,
   never its own outline, so from here it sits in the card's padding, clear of everything. And
   :focus-visible, because this heading is focused by script on every step - a mouse user who has
   asked for nothing should not be shown a keyboard ring. */
.quote__stage:has(.q__ask--sr:focus-visible) { outline: 2px solid var(--sky-deep); outline-offset: 3px; border-radius: 14px; }
.recap { display: flex; flex-wrap: wrap; align-items: baseline; gap: 4px 10px; font-size: 13px; color: var(--mid); margin: 0 0 10px; padding: 7px 11px; background: var(--frost); border-radius: 9px; }
.fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 10px; }
.field--wide { grid-column: 1 / -1; }

/* The price and the buttons are the card's floor. They are in the same place on every stage. */
.quote__foot { margin-top: auto; }
.quote__nav { display: flex; gap: 8px; }
.btn--back { flex: 0 0 auto; padding-inline: 18px; }
.btn--next { flex: 1 1 auto; }

.quote__head { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 12px; }
.quote__title { font-family: var(--display); font-weight: 700; font-size: 23px; letter-spacing: -.015em; margin: 0; line-height: 1.1; }
.quote__tag { font: 700 11px/1 var(--label); letter-spacing: .12em; text-transform: uppercase; color: var(--navy); background: var(--sky-soft); padding: 8px 10px; border-radius: 999px; white-space: nowrap; }
.q { border: 0; margin: 0 0 10px; padding: 0; min-width: 0; }
.q__label { font: 700 12px/1 var(--label); letter-spacing: .1em; text-transform: uppercase; color: var(--mid); margin-bottom: 6px; display: block; padding: 0; }
.pills { display: grid; gap: 6px; }
.pills--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.pills--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.pills--6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
.pill { min-height: 40px; padding: 6px 6px; border-radius: 11px; border: 1.5px solid var(--line); background: var(--white); color: var(--ink); font: 500 13.5px/1.2 var(--body); cursor: pointer; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; text-align: center; transition: border-color .15s, background .15s, transform .15s var(--ease-out), box-shadow .15s; }
.pill:hover { border-color: var(--line-strong); transform: translateY(-1px); }
.pill[aria-pressed="true"] { border-color: var(--sky); background: var(--sky-soft); color: var(--navy); box-shadow: inset 0 0 0 1px var(--sky); }
.pill em { font: 700 10.5px/1 var(--label); letter-spacing: .06em; text-transform: uppercase; color: var(--sky-deep); font-style: normal; }
.pill b { font: 700 12px/1 var(--label); color: var(--mid); }
.pill[aria-pressed="true"] b, .pill[aria-pressed="true"] em { color: var(--navy); }
.pill--n { font-family: var(--label); font-size: 15px; }

/* The bigger one-off jobs, folded behind a single row so the panel keeps its height.
   Shut, the row says what is inside it; open, it becomes the panel the pills sit in. */
.jobs { margin: 0 0 12px; }
.jobs__summary { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 10px; min-height: 40px; padding: 8px 12px; box-sizing: border-box; border: 1px solid var(--line); border-radius: 12px; background: var(--frost); cursor: pointer; list-style: none; transition: border-color .15s, background .15s; }
.jobs__summary::-webkit-details-marker { display: none; }
.jobs__summary:hover { border-color: var(--line-strong); }
.jobs__label { font: 700 11px/1 var(--label); letter-spacing: .1em; text-transform: uppercase; color: var(--mid); white-space: nowrap; }
.jobs__hint { min-width: 0; text-align: right; font-size: 12.5px; color: var(--mid); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.jobs__hint.is-on { color: var(--sky-deep); font-weight: 600; }
.jobs__chev { flex: 0 0 auto; color: var(--mid); transition: transform .2s var(--ease-out); }
.jobs[open] .jobs__chev { transform: rotate(180deg); }
/* Open, the pills say what is chosen, so the shut row's one-line summary would only repeat them. */
.jobs[open] .jobs__hint { visibility: hidden; }
.jobs[open] .jobs__summary { border-radius: 12px 12px 0 0; border-bottom-color: transparent; }
.jobs__body { border: 1px solid var(--line); border-top: 0; border-radius: 0 0 12px 12px; padding: 10px 12px 12px; }
.jobs__note { margin: 8px 0 0; font-size: 12px; line-height: 1.45; color: var(--mid); }
.jobs__note--sum { color: var(--sky-deep); font-weight: 700; }
.pill--job { min-height: 44px; padding: 6px 8px; border-radius: 10px; font-size: 12.5px; }
/* The price and the guarantee together, the way the flier prints them. The seal is in the same
   place on every stage, so it reads as part of the furniture rather than a badge that keeps
   reappearing. */
.price { border-top: 1px solid var(--line); margin-top: 12px; padding-top: 11px; margin-bottom: 11px; display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 0 13px; align-items: center; }
.page .price__seal { grid-row: 1 / 3; grid-column: 2; justify-self: end; align-self: center; width: 52px; height: 52px; object-fit: contain; }
.price__main { display: flex; align-items: baseline; gap: 10px; }
.price__then { grid-column: 1; }
.price__amount { font-family: var(--display); font-weight: 700; font-size: 46px; line-height: 46px; letter-spacing: -.022em; font-variant-numeric: tabular-nums; }
.quote[data-price-state="floor"] .price__amount { font-size: 34px; letter-spacing: -.014em; }
.price__unit { font: 700 12px/1 var(--label); letter-spacing: .08em; text-transform: uppercase; color: var(--mid); }
.price__then { margin: 6px 0 0; font-size: 14.5px; min-height: 2.8em; }
.price__note { margin: 10px 0 0; font-size: 12.5px; line-height: 1.5; color: var(--mid); text-align: center; }
.price__note a { display: inline-block; padding: 2px 0; white-space: nowrap; }
.quote__alt { margin: 8px 0 0; font-size: 13px; color: var(--mid); text-align: center; }
.quote__alt a[href^="tel:"] { display: inline-block; padding: 8px 0; }
.field { margin-bottom: 10px; }
.field label { display: block; font: 500 13px/1.3 var(--body); margin-bottom: 4px; }
.quote .field { margin-bottom: 8px; }
.quote .field input { min-height: 42px; }
/* The coverage line writes itself in under the postcode as an ordinary line of text, so every field
   in the form keeps one even rhythm - a reserved gap sitting between the postcode and the notes box
   read as a hole punched in the middle of the questions. The space that line will need is held at
   the FOOT of the form instead, where an empty reserve is only the breathing room before the price,
   and the script hands it over a line at a time as the message appears. Evenly spaced fields, and
   the card still does not grow the moment somebody types a postcode. */
/* The postcode field used to hold 44px open for the coverage line it was going to be given, so
   that the card never moved. That reserve was a hole in the middle of the form that every reader
   saw, to spare one reader a 17px nudge - and the card's height is transitioned now, so the line
   arriving opens the panel by that much instead of jolting it. */
.field--pc { position: relative; }
.field label small { color: var(--mid); font-weight: 400; font-size: 12px; }
.field--notes textarea { min-height: 76px; }
.field input, .field textarea { width: 100%; min-height: 44px; padding: 9px 12px; border: 1.5px solid var(--line); border-radius: 10px; font: 400 16px var(--body); color: var(--ink); background: var(--white); resize: vertical; }
.field input:focus, .field textarea:focus { outline: none; border-color: var(--sky); box-shadow: 0 0 0 3px var(--sky-soft); }
.field.is-invalid input { border-color: var(--danger); }
.field__note { display: block; margin-top: 6px; font-size: 12.5px; color: var(--mid); line-height: 1.4; }
.field__note:empty { display: none; }
.field[data-coverage="covered"] input { border-color: var(--ok); }
.field[data-coverage="outside"] input { border-color: var(--warn); }
[data-postcode-note][data-coverage="covered"] { color: var(--ok); }
[data-postcode-note][data-coverage="outside"] { color: var(--warn); font-weight: 600; }
.dd { display: grid; gap: 7px; padding: 11px 14px; border: 1px solid var(--line); border-radius: 14px; margin-bottom: 4px; background: var(--frost); }
.dd__opt { display: flex; gap: 10px; align-items: flex-start; margin-top: 0; font-size: 13px; line-height: 1.45; cursor: pointer; }
.dd__opt input { flex: 0 0 auto; width: 20px; height: 20px; margin: 1px 0 0; accent-color: var(--sky-deep); cursor: pointer; }
.dd__opt b { display: block; margin-top: 4px; font-weight: 600; color: var(--sky-deep); }
/* `.dd p` is element-qualified and would otherwise win the font-size on both of these, so the two
   paragraphs are qualified back to the same weight and the panel sets the size it means to. */
.dd .dd__opt--later { display: block; font-size: 13px; color: var(--mid); cursor: auto; }
.dd p { margin: 0; font-size: 14.5px; }
/* Spacing inside the panel comes from the grid gap, so a hidden row (the mandate opt-in when no
   Direct Debit link is configured) leaves no gap behind it. */
.dd .dd__facts { margin: 0; font-size: 12.5px; line-height: 1.5; color: var(--mid); }
.dd__strong { font-weight: 700; color: var(--ink); }
.done__lead { font-size: 16px; margin: 0 0 14px; }
.done__dl { display: grid; grid-template-columns: auto 1fr; gap: 8px 16px; font-size: 14px; margin: 0 0 18px; }
.done__dl dt { font: 700 11px/1.6 var(--label); letter-spacing: .08em; text-transform: uppercase; color: var(--mid); }
.done__dl dd { margin: 0; }
/* A wrapper so the row can be shown or hidden as one; it hands its dt and dd back to the list grid. */
.done__also { display: contents; }
.done__fallback { margin: 0 0 14px; padding: 10px 12px; border-radius: 10px; background: #FFF4E5; color: #7A3E00; font-size: 14px; }

/* ---------- ticker ---------- */
.ticker { border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); overflow: hidden; background: var(--white); }
.ticker__track { display: flex; width: max-content; animation: marquee 40s linear infinite; }
.ticker__track span { display: inline-flex; align-items: center; font: 700 12.5px/1 var(--label); letter-spacing: .1em; text-transform: uppercase; color: var(--mid); padding: 16px 0; white-space: nowrap; }
.ticker__track span::after { content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--sky); margin: 0 28px; }
.ticker:hover .ticker__track { animation-play-state: paused; }
@keyframes marquee { to { transform: translateX(-50%); } }

/* ---------- sections ---------- */
.section { max-width: 1280px; margin: 0 auto; padding: clamp(56px, 7.5cqi, 116px) var(--gutter) 0; }
.section__head { max-width: 780px; margin-bottom: clamp(28px, 4cqi, 52px); }
.section__head--row { max-width: none; display: flex; justify-content: space-between; align-items: flex-end; gap: 24px; flex-wrap: wrap; }
.section__head--row > div:first-child { max-width: 780px; }
.section__lead { color: var(--mid); font-size: 18px; max-width: 56ch; margin: 16px 0 0; text-wrap: pretty; }
.section__tools { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

/* Sections start hidden only once the page has said it can reveal them again: html.js is set by
   the boot script in the head. Without it — no JavaScript, or a script that never arrived — the
   page is plain, still and completely readable rather than a column of blank space. */
html.js [data-reveal] { opacity: 0; transform: translateY(22px); transition: opacity .7s ease, transform .9s var(--ease-out); transition-delay: var(--d, 0ms); }
html.js [data-reveal].is-in { opacity: 1; transform: none; }
html.js [data-wipe] { clip-path: inset(0 100% 0 0 round var(--r)); transition: clip-path 1.2s var(--ease-wipe); transition-delay: var(--d, 0ms); }
html.js [data-wipe].is-in { clip-path: inset(0 0 0 0 round var(--r)); }
/* The same escape hatch for a script that loads and then fails: site.js's onerror and a watchdog
   in the head both set this, and it must come after the rules above to beat them. */
html.no-effects [data-reveal], html.no-effects [data-wipe] { opacity: 1; transform: none; clip-path: none; transition: none; }

/* panes: a 2×2 window */
.panes { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; background: var(--navy); padding: 10px; border-radius: 24px; }
.pane { --r: 14px; background: var(--white); border-radius: 14px; padding: clamp(24px, 3cqi, 40px); min-height: 230px; display: flex; flex-direction: column; justify-content: flex-end; gap: 10px; }
.pane h3 { font-family: var(--display); font-weight: 700; font-size: clamp(22px, 2.1cqi, 30px); letter-spacing: -.015em; line-height: 1.1; margin: 0; }
.pane p { margin: 0; color: var(--mid); font-size: 16px; }

/* reviews: a reel you can scroll by hand (drag, swipe, wheel, arrows) that also drifts on its own */
.reel { position: relative; margin: 0 calc(var(--gutter) * -1); padding: 8px 0 12px; }
/* Edge fades as overlays rather than mask-image: a mask over a moving strip re-rasterises every frame. */
.reel::before, .reel::after { content: ""; position: absolute; top: 0; bottom: 0; width: 7%; z-index: 2; pointer-events: none; }
.reel::before { left: 0; background: linear-gradient(90deg, var(--frost), rgba(243, 247, 251, 0)); }
.reel::after { right: 0; background: linear-gradient(270deg, var(--frost), rgba(243, 247, 251, 0)); }
.reel__track { display: flex; gap: 20px; overflow-x: auto; overflow-y: hidden; padding: 4px var(--gutter) 8px; scroll-behavior: auto; scrollbar-width: none; -ms-overflow-style: none; overscroll-behavior-x: contain; cursor: grab; -webkit-overflow-scrolling: touch; }
.reel__track::-webkit-scrollbar { display: none; }
.reel__track.is-dragging { cursor: grabbing; user-select: none; -webkit-user-select: none; }
.reel__track:focus-visible { outline: 3px solid var(--sky); outline-offset: -3px; border-radius: 12px; }
.reel__btn { width: 46px; height: 46px; border-radius: 50%; border: 1.5px solid var(--line-strong); background: var(--white); color: var(--ink); cursor: pointer; display: inline-grid; place-items: center; padding: 0; transition: border-color .15s, transform .15s var(--ease-out); }
.reel__btn:hover { border-color: var(--navy); transform: translateY(-1px); }
.reel__btn svg { display: block; }
.review { flex: 0 0 auto; width: min(440px, 78cqi); margin: 0; background: var(--white); border: 1px solid var(--line); border-radius: 20px; padding: 28px 30px; display: flex; flex-direction: column; }
.review__stars { color: var(--amber); font-size: 14px; letter-spacing: .12em; }
.review blockquote { margin: 14px 0 18px; font-family: var(--display); font-weight: 500; font-size: clamp(19px, 1.7cqi, 23px); line-height: 1.25; letter-spacing: -.01em; text-wrap: pretty; flex: 1 1 auto; }
.review figcaption { font-size: 14px; color: var(--mid); }
.review figcaption b { color: var(--ink); font-weight: 600; }
.reviews__foot { color: var(--mid); font-size: 14px; margin: 6px 0 0; }
.reviews__foot b { color: var(--ink); }

/* facts */
/* Half a million windows and ten years are worth saying and not worth shouting: at poster size they
   invited the reader to be impressed, which is a lot to ask of a figure that is simply true. Set at
   reading size - the number picked out, the words right under it, three across one rule - the same
   information is taken in at a glance and the page carries on. */
.facts { max-width: 1280px; margin: clamp(40px, 5cqi, 80px) auto 0; padding: 0 var(--gutter); }
.facts__list { list-style: none; margin: 0; padding: 22px 0 0; border-top: 2px solid var(--navy); display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px clamp(20px, 3cqi, 56px); }
.fact { font-size: clamp(15px, 1.25cqi, 18px); line-height: 1.45; color: var(--mid); text-wrap: pretty; }
.fact__n { display: block; margin-bottom: 4px; font-family: var(--display); font-weight: 700; font-size: clamp(28px, 2.9cqi, 42px); letter-spacing: -.025em; line-height: 1; color: var(--ink); font-variant-numeric: tabular-nums; }

/* team */
/* The section is the round's own faces now: one frame that flips through everybody on it, and
   beside it the three things that hold true whoever knocks. */
.round { display: grid; grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr); gap: clamp(20px, 3cqi, 44px); align-items: start; }
.faces { display: grid; gap: 3px; align-content: start; }
/* A sideways drag moves the crew; an up-and-down one is the page being scrolled, and stays that. */
.faces__frame { touch-action: pan-y; }
/* Every slide sits in the one grid cell, so the frame is as tall as the tallest of them and a flip
   is a cross-fade in place - nothing moves, nothing reflows, and the frame never jumps mid-fade. */
/* Portrait once there are photographs in it. Empty, it keeps a shallower shape: a tall blank
   box reads as a hole in the page, a shallow one as a frame waiting for its picture. */
.faces__frame { display: grid; grid-template-areas: "face"; aspect-ratio: 5 / 4; border-radius: 24px; overflow: hidden; background: var(--frost); border: 1px solid var(--line); }
.faces__frame.has-shots { aspect-ratio: 4 / 5; }
.faces__slide { grid-area: face; margin: 0; display: grid; grid-template-rows: minmax(0, 1fr) auto; min-height: 0; opacity: 0; z-index: 0; transition: opacity .7s ease; }
/* The frame coming in fades on top of the one leaving, which holds its ground underneath
   until the hand-over is done. Cross-fading both at once would show the panel through the
   middle of every flip. */
.faces__slide.is-out { opacity: 1; z-index: 1; }
.faces__slide.is-on { opacity: 1; z-index: 2; }
.faces__shot { min-height: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 12px; color: var(--mid); background: var(--frost); overflow: hidden; }
.faces__shot img { width: 100%; height: 100%; object-fit: cover; display: block; }
.faces__wait { font: 700 11px var(--label); letter-spacing: .12em; text-transform: uppercase; }
/* The name rides inside the frame so the picture and who it is stay one object through the fade. */
.faces__who { display: grid; gap: 3px; padding: 15px 18px 17px; background: var(--white); border-top: 1px solid var(--line); }
.faces__who b { font-family: var(--display); font-weight: 700; font-size: clamp(19px, 1.8cqi, 23px); letter-spacing: -.012em; line-height: 1.1; }
.faces__who span { font: 700 11.5px var(--label); letter-spacing: .08em; text-transform: uppercase; color: var(--sky-deep); }
/* An arrow either side and one bar per person: the frame reads as a crew of four you can walk
   through, not as a picture that happens to move. */
.faces__nav { display: flex; align-items: center; gap: 12px; }
.faces__arrow { flex: 0 0 auto; width: 34px; height: 34px; display: grid; place-items: center; padding: 0; border: 1px solid var(--line-strong); border-radius: 50%; background: var(--white); color: var(--navy); cursor: pointer; transition: background .2s ease, border-color .2s ease, color .2s ease; }
.faces__arrow svg { width: 17px; height: 17px; }
.faces__arrow:hover { background: var(--navy); border-color: var(--navy); color: #fff; }
.faces__pips { flex: 1 1 auto; display: flex; gap: 6px; margin: 0; padding: 0; }
/* The bar is 3px tall and a 3px target is no target at all, so the button is the height of a
   fingertip and the bar it draws sits in the middle of it. */
.faces__pip { flex: 1 1 auto; display: block; padding: 11px 0; border: 0; background: none; cursor: pointer; -webkit-appearance: none; appearance: none; }
.faces__pip::before { content: ''; display: block; height: 3px; border-radius: 2px; background: var(--line-strong); transition: background .4s ease; }
.faces__pip.is-on::before { background: var(--navy); }
.faces__pip:hover::before { background: var(--sky-deep); }
/* The frame is decoration to a screen reader; this list is where it reads the crew instead. */
.faces__sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }
.round__facts { list-style: none; margin: 0; padding: 0; display: grid; align-content: start; }
.round__facts li { display: grid; gap: 6px; padding: clamp(18px, 2cqi, 26px) 0; border-top: 1px solid var(--line); }
.round__facts li:first-child { border-top: 0; padding-top: 0; }
.round__facts li:last-child { padding-bottom: 0; }
.round__facts b { font-family: var(--display); font-weight: 700; font-size: clamp(20px, 1.9cqi, 26px); letter-spacing: -.015em; line-height: 1.15; }
.round__facts span { color: var(--mid); font-size: 16px; text-wrap: pretty; }
/* A display value of our own beats the browser's [hidden] rule, so say it again wherever we set one. */
.round[hidden], .done__also[hidden] { display: none; }

/* how it works */
.steps { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 20px; }
.step { background: var(--white); border: 1px solid var(--line); border-radius: 20px; padding: 24px 26px 28px; display: grid; gap: 10px; align-content: start; transition: border-color .2s, box-shadow .25s var(--ease-out); }
.step:hover, .step:focus-within { border-color: var(--sky); box-shadow: 0 28px 48px -34px rgba(6, 6, 69, .45); }
.step__n { font: 700 13px var(--label); color: var(--sky-deep); width: 42px; height: 42px; border: 1.5px solid var(--sky); border-radius: 50%; display: grid; place-items: center; margin-bottom: 4px; }

/* ---------- the three films ----------
   Each step draws what it says, and the drawing sits at the end of its own story: clean windows,
   a price confirmed, a payment taken. Hovering the card plays the story from the beginning, so
   the page is complete before anyone touches it and nothing loops in the corner of the eye. */
/* The drawing stays in flow and is what sizes the panel. A clip is stacked on top of it, out of
   flow from the moment the element is created, so attaching one on first hover cannot make the
   panel grow for a frame - which it did, to twice its height, while the video was in flow. */
.step__film { position: relative; margin: -4px 0 10px; border-radius: 14px; background: var(--frost); overflow: hidden; }
.film { display: block; width: 100%; height: auto; aspect-ratio: 220 / 132; }
/* Real footage, when a clip has been added, occupies the same box as the drawn film. */
.film--vid { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0; }
/* Only once a clip has frames to show does it take the drawing's place. */
.step__film.has-vid .film:not(.film--vid) { opacity: 0; }
.step__film.has-vid .film--vid { opacity: 1; }

/* One ground per film, drawn once and never swapped: a scene that inverts the whole panel
   mid-play reads as a black box dropped into a row of pale cards, not as nightfall. */
.film__ground { fill: #fff; }
.film__card, .film__pane, .film__screen, .film__bank { fill: #fff; stroke: var(--line); stroke-width: 1.5; }
.film__pill { fill: var(--frost); }
.film__hl { fill: var(--sky-soft); stroke: var(--sky); stroke-width: 1.5; }
.film__price { font: 700 30px var(--display); fill: var(--navy); letter-spacing: -.02em; }
.film__badge { fill: var(--ok); }
.film__check { fill: none; stroke: #fff; stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; }
.film__cursor { fill: var(--navy); opacity: .82; }
.film__moon { fill: #F0A800; }
.film__phone { fill: #172043; stroke: #2B3767; stroke-width: 1.5; }
.film__screen { fill: #F5F9FF; stroke: none; }
.film__bubble { fill: var(--sky); }
.film__bubble--b { fill: var(--wet); }
.film__mullion { stroke: var(--line-strong); stroke-width: 1.5; fill: none; }
.film__grime { fill: #9AA7BE; opacity: .5; }
.film__channel { fill: var(--navy); }
.film__blade { fill: var(--sky); }
.film__pole { stroke: var(--navy); stroke-width: 4.5; stroke-linecap: round; fill: none; }
.film__grip { stroke: var(--sky-deep); stroke-width: 6; stroke-linecap: round; fill: none; }
.film__van { fill: #F5F8FC; stroke: var(--line-strong); stroke-width: 1.5; stroke-linejoin: round; }
.film__vanglass { fill: var(--sky-soft); stroke: var(--line-strong); stroke-width: 1.2; stroke-linejoin: round; }
.film__vanstripe { fill: var(--sky); }
.film__wheel { fill: var(--navy); }
.film__sparks path { fill: var(--sky); }
.film__wire { stroke: var(--line-strong); stroke-width: 1.5; stroke-dasharray: 3 5; fill: none; }
.film__bankband { stroke: var(--line-strong); stroke-width: 1.5; fill: none; }
.film__coin { fill: var(--sky); }
.film__coint { font: 700 13px var(--display); fill: #fff; text-anchor: middle; }
.film__paid { font: 700 11px var(--label); letter-spacing: .12em; fill: var(--ok); text-anchor: middle; }

/* Rest: the last frame of each story. */
.fx-p1, .fx-p2, .fx-cursor, .fx-coin { opacity: 0; }
.fx-grime { transform: translateX(96px); }
.fx-squeegee { transform: translateX(96px); opacity: 0; }
.fx-night { opacity: 0; }

/* Playing: site.js adds the class on hover, keyboard focus, or - where there is no pointer to
   hover with - the first time the step scrolls into view. It comes off again at the end, which
   drops every element straight back to the resting frame above. */
.step.is-playing .fx { animation-duration: 2.4s; animation-timing-function: var(--ease-out); animation-fill-mode: both; }
.step.is-playing .fx-cursor   { animation-name: fxCursor; }
.step.is-playing .fx-hl-a     { animation-name: fxHlA; }
.step.is-playing .fx-hl-b     { animation-name: fxHlB; }
.step.is-playing .fx-p1       { animation-name: fxP1; }
.step.is-playing .fx-p2       { animation-name: fxP2; }
.step.is-playing .fx-p3       { animation-name: fxP3; }
.step.is-playing .fx-tick     { animation-name: fxPop; animation-duration: .5s; animation-delay: 1.9s; }
.step.is-playing .fx-night    { animation-name: fxNight; }
.step.is-playing .fx-day      { animation-name: fxDay; }
.step.is-playing .fx-bubble   { animation-name: fxBubble; }
.step.is-playing .fx-bubble2  { animation-name: fxBubble2; }
.step.is-playing .fx-van      { animation-name: fxVan; }
.step.is-playing .fx-squeegee { animation-name: fxSqueegee; }
.step.is-playing .fx-grime    { animation-name: fxGrime; }
.step.is-playing .fx-shine1   { animation-name: fxShine; animation-duration: .6s; animation-delay: 1.9s; }
.step.is-playing .fx-shine2   { animation-name: fxShine; animation-duration: .6s; animation-delay: 2.06s; }
.step.is-playing .fx-spark1   { animation-name: fxSpark; animation-duration: .8s; animation-delay: .1s; }
.step.is-playing .fx-spark2   { animation-name: fxSpark; animation-duration: .8s; animation-delay: .28s; }
.step.is-playing .fx-spark3   { animation-name: fxSpark; animation-duration: .8s; animation-delay: .46s; }
.step.is-playing .fx-coin     { animation-name: fxCoin; }
.step.is-playing .fx-tick2    { animation-name: fxPop; animation-duration: .5s; animation-delay: 1.85s; }
.step.is-playing .fx-paid     { animation-name: fxPaid; }

/* 1 - get your price: two taps, the figure settles, it is confirmed. */
@keyframes fxCursor {
  0%   { opacity: 0; transform: translate(34px, 62px); }
  8%   { opacity: .82; transform: translate(34px, 62px); }
  22%  { opacity: .82; transform: translate(0, 0); }
  30%  { opacity: .82; transform: translate(0, 0) scale(.86); transform-origin: 110px 41.5px; }
  38%  { opacity: .82; transform: translate(0, 0); }
  56%  { opacity: .82; transform: translate(-52px, 22px); }
  64%  { opacity: .82; transform: translate(-52px, 22px) scale(.86); transform-origin: 110px 41.5px; }
  72%  { opacity: .82; transform: translate(-52px, 22px); }
  86%, 100% { opacity: 0; transform: translate(-52px, 40px); }
}
@keyframes fxHlA { 0%, 30% { opacity: 0; } 38%, 100% { opacity: 1; } }
@keyframes fxHlB { 0%, 64% { opacity: 0; } 72%, 100% { opacity: 1; } }
/* The three figures hand over on a hard cut, never a cross-fade: two numbers dissolving through
   each other in the same place reads as a smudge, and any gap between them reads as a flicker. */
@keyframes fxP1  { 0%, 37% { opacity: 0; } 39%, 54% { opacity: 1; } 55%, 100% { opacity: 0; } }
@keyframes fxP2  { 0%, 54% { opacity: 0; } 55%, 71% { opacity: 1; } 72%, 100% { opacity: 0; } }
@keyframes fxP3  { 0%, 71% { opacity: 0; } 72%, 100% { opacity: 1; } }
@keyframes fxPop { 0% { opacity: 0; transform: scale(.4); } 70% { transform: scale(1.12); } 100% { opacity: 1; transform: scale(1); } }

/* 2 - we clean: the text the night before, then the van, then the squeegee. */
@keyframes fxNight { 0%, 30% { opacity: 1; } 42%, 100% { opacity: 0; } }
@keyframes fxDay   { 0%, 30% { opacity: 0; } 42%, 100% { opacity: 1; } }
@keyframes fxBubble  { 0%, 6% { opacity: 0; transform: translateY(6px) scale(.8); transform-origin: 86px 54px; } 16%, 100% { opacity: 1; transform: none; } }
@keyframes fxBubble2 { 0%, 16% { opacity: 0; transform: translateY(6px) scale(.8); transform-origin: 86px 70px; } 26%, 100% { opacity: 1; transform: none; } }
@keyframes fxVan     { 0%, 38% { transform: translateX(-104px); } 60%, 100% { transform: none; } }
@keyframes fxSqueegee {
  0%, 60% { transform: translateX(0); opacity: 0; }
  63%     { transform: translateX(0); opacity: 1; }
  92%     { transform: translateX(96px); opacity: 1; }
  100%    { transform: translateX(96px); opacity: 0; }
}
@keyframes fxGrime { 0%, 63% { transform: translateX(0); } 92%, 100% { transform: translateX(96px); } }
@keyframes fxShine { 0% { opacity: 0; transform: scale(.3) rotate(-30deg); } 100% { opacity: 1; transform: none; } }

/* 3 - pay after completion: it sparkles, the money goes on its own, it is marked paid. */
@keyframes fxSpark { 0% { opacity: 0; transform: scale(.2) rotate(-40deg); } 55% { opacity: 1; transform: scale(1.15) rotate(6deg); } 100% { opacity: 1; transform: none; } }
@keyframes fxCoin {
  0%, 24%  { opacity: 0; transform: translateX(0); }
  30%      { opacity: 1; transform: translateX(0); }
  70%      { opacity: 1; transform: translateX(62px); }
  80%, 100%{ opacity: 0; transform: translateX(68px) scale(.7); }
}
@keyframes fxPaid { 0%, 84% { opacity: 0; } 96%, 100% { opacity: 1; } }

/* Transforms on SVG children need an origin they can trust; percentages are unreliable there. */
.film .fx { transform-box: fill-box; }
.film .fx-cursor, .film .fx-coin, .film .fx-van, .film .fx-squeegee, .film .fx-grime { transform-box: view-box; }
.step h3 { font-family: var(--display); font-weight: 700; font-size: 25px; letter-spacing: -.015em; margin: 0; line-height: 1.1; }
.step p { margin: 0; color: var(--mid); }

/* services */

/* areas */
.areas__cta { margin: 20px 0 0; }
.check { max-width: 720px; background: var(--white); border: 1px solid var(--line); border-radius: 20px; padding: 24px; display: grid; gap: 10px; }
.check__label { font-family: var(--display); font-weight: 700; font-size: 22px; letter-spacing: -.01em; }
.check__row { display: flex; gap: 10px; flex-wrap: wrap; }
.check__row input { flex: 1 1 220px; min-height: 50px; padding: 10px 14px; border: 1.5px solid var(--line-strong); border-radius: 12px; font: 700 17px var(--label); text-transform: uppercase; color: var(--ink); background: var(--white); }
.check__row input:focus { outline: none; border-color: var(--sky); box-shadow: 0 0 0 3px var(--sky-soft); }
.check__note { margin: 0; font-size: 15px; color: var(--mid); line-height: 1.55; min-height: 1.55em; }
/* The towns the postcode turns up are the only links left in this section, and there are never
   more than a handful of them - so they can read as links without becoming a wall of blue. */
.check__note a { color: var(--sky-deep); font-weight: 700; }
.check__note[data-coverage="covered"] { color: var(--ink); }

/* area pages: breadcrumb + the neighbouring-town link grid */
.crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 0 0 18px; font: 700 12px var(--label); letter-spacing: .06em; text-transform: uppercase; color: var(--mid); }
.page .crumbs a { color: var(--mid); text-decoration: none; }
.page .crumbs a:hover { color: var(--sky-deep); }
.crumbs span { color: var(--line-strong); }
.crumbs b { color: var(--ink); font-weight: 600; }
.near { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 12px; }
.near--wide { margin-bottom: clamp(32px, 4cqi, 56px); }
.page .near a { display: grid; gap: 4px; align-content: center; min-height: 76px; padding: 14px 16px; background: var(--white); border: 1px solid var(--line); border-radius: 14px; text-decoration: none; color: var(--ink); transition: border-color .15s, transform .15s var(--ease-out); }
.page .near a:hover { border-color: var(--sky); transform: translateY(-2px); }
.near b { font-family: var(--display); font-weight: 700; font-size: 18px; letter-spacing: -.01em; line-height: 1.15; }
.near span { font: 700 11.5px var(--label); letter-spacing: .08em; text-transform: uppercase; color: var(--sky-deep); }

/* faq */
.faq__list { max-width: 840px; border-bottom: 1px solid var(--line); }
.faq details { border-top: 1px solid var(--line); }
.faq summary { list-style: none; cursor: pointer; display: flex; justify-content: space-between; align-items: center; gap: 16px; padding: 20px 0; font-family: var(--display); font-weight: 600; font-size: 20px; letter-spacing: -.005em; }
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after { content: "+"; font: 300 30px/1 var(--display); color: var(--sky); transition: transform .3s var(--ease-out); flex: 0 0 auto; }
.faq details[open] summary::after { transform: rotate(45deg); }
.faq details p { margin: 0 0 22px; color: var(--mid); max-width: 64ch; }

/* footer (navy, like the header) */
.footer { margin-top: clamp(64px, 9cqi, 140px); background: var(--navy); color: #fff; }
.footer__inner { max-width: 1280px; margin: 0 auto; padding: clamp(40px, 6cqi, 80px) var(--gutter) 40px; }
.footer__brand { display: inline-block; margin-bottom: clamp(28px, 4cqi, 48px); }
.footer__brand img { display: block; height: 44px; width: auto; }
/* One line, at every width. The wordmark is set to exactly the width it has to live in rather
   than to a guessed clamp: .footer__inner is min(100cqi, 1280px) wide with a gutter either side,
   and "We clean windows." in this face measures 8.5em, so 8.85 leaves a few per cent in hand for
   whichever of the stack the reader's machine actually has. It used to break after "We clean". */
.footer__giant { font-family: var(--display); font-weight: 700; font-size: calc((min(100cqi, 1280px) - 2 * var(--gutter)) / 8.85); white-space: nowrap; letter-spacing: -.035em; line-height: .9; margin: 0 0 clamp(32px, 5cqi, 64px); color: #fff; }
.footer__giant span { color: var(--sky); }
.footer__grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px; padding-top: 32px; border-top: 1px solid rgba(255, 255, 255, .16); }
.footer__grid h3 { font: 700 11.5px var(--label); letter-spacing: .1em; text-transform: uppercase; color: rgba(255, 255, 255, .6); margin: 0 0 10px; }
.page .footer__grid a { color: #fff; text-decoration: none; font-size: 17px; }
.page .footer__grid a.btn { color: var(--navy); font-size: 16px; }
.page .footer__grid a:not(.btn):hover { color: var(--sky); }
.footer__grid p { margin: 0; color: rgba(255, 255, 255, .8); font-size: 15px; line-height: 1.6; }
.footer__legal { margin: 40px 0 0; font-size: 12px; color: rgba(255, 255, 255, .55); line-height: 1.6; }

/* sticky bar */
.bar { position: fixed; left: 12px; right: 12px; bottom: 12px; z-index: 55; display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 10px 10px 18px; background: var(--navy); color: #fff; border-radius: 16px; box-shadow: 0 24px 48px -18px rgba(6, 6, 69, .6); transform: translateY(calc(100% + 32px)); transition: transform .45s var(--ease-out); }
.bar.is-visible { transform: none; }
.bar__price { display: grid; line-height: 1.1; min-width: 0; }
.bar__price b { font-family: var(--display); font-weight: 700; font-size: 22px; letter-spacing: -.015em; }
.bar__price span { font: 700 11px var(--label); text-transform: uppercase; letter-spacing: .08em; opacity: .8; }
.bar__price small { display: none; }
@media (min-width: 900px) {
  .bar { left: auto; right: 24px; bottom: 24px; width: 380px; }
  .bar__price small { display: block; font-size: 12px; opacity: .7; margin-top: 4px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
}

/* ---------- responsive (container queries) ---------- */
@container page (max-width: 1080px) {
  .services__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@container page (max-width: 980px) {
  .hero__inner { grid-template-columns: 1fr; align-items: start; }
  .steps { grid-template-columns: 1fr; }
  /* Stacked, a full-width portrait would be taller than the phone it is read on, so the
     frame keeps a portrait's width and the facts run underneath it. */
  .round { grid-template-columns: 1fr; }
  .faces { max-width: 400px; }
  .footer__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .nav__links { gap: 18px; }
}
/* The nav row - logo, six links, phone number and the button - wants 764px. It used to fold into
   the menu button at 720, and in the 44 pixels between the two the row ran off the edge and took
   the whole page sideways with it. It folds before it runs out of room now. */
@container page (max-width: 800px) {
  .nav__links { display: none; position: absolute; top: var(--nav-h); left: 0; right: 0; flex-direction: column; background: var(--navy); border-bottom: 1px solid rgba(255, 255, 255, .14); padding: 8px var(--gutter) 16px; gap: 0; margin: 0; }
  .page .nav__links a { padding: 12px 0; font-size: 17px; border-top: 1px solid rgba(255, 255, 255, .12); color: #fff; }
  .menu-open .nav__links { display: flex; }
  .nav__toggle { display: inline-flex; }
}
@container page (max-width: 720px) {
  .nav__phone { display: none; }
  .nav__inner { gap: 12px; }
  .brand img { height: 32px; }
  .hero__inner { padding-top: 28px; gap: 0; }
  /* One column, so the headline runs right across the picture and its pool has to run with it:
     wide and shallow, sitting over the top quarter where the words are, and open below. */
  .hero__bg::after { background:
    radial-gradient(78% 24% at 50% 13%, rgba(255,255,255,.95) 0%, rgba(255,255,255,.9) 55%, rgba(255,255,255,.62) 84%, rgba(255,255,255,.12) 100%),
    linear-gradient(100deg, rgba(255,255,255,.5) 0%, rgba(255,255,255,.4) 60%, rgba(255,255,255,.28) 100%),
    linear-gradient(180deg, rgba(255,255,255,.2) 0%, rgba(255,255,255,0) 30%, rgba(255,255,255,.42) 82%, var(--white) 100%); }
  /* On phones the price tool comes straight after the headline; the van and any lead copy follow it. */
  .hero__copy { display: contents; }
  .quote { order: 4; margin-top: 28px; }
  .hero__lead { order: 5; margin-top: 28px; }
  .lockup { order: 6; margin-top: 30px; max-width: none; }
  .footer__inner { padding-bottom: 104px; }
  .page .footer__grid a:not(.btn) { display: inline-block; padding: 6px 0; }
  .panes { grid-template-columns: 1fr; padding: 8px; gap: 8px; }
  .pane { min-height: 0; padding: 22px; }
  .pills--3 .pill { padding: 6px 4px; font-size: 13px; }
  .faces { max-width: none; }
  .services__grid, .footer__grid { grid-template-columns: 1fr; }
  .section__head--row .btn { width: 100%; }
  .section__tools { width: 100%; }
  .section__tools .btn { flex: 1 1 auto; width: auto; }
  .quote { --qp: 18px; border-radius: 18px; }
  .price__amount { font-size: 42px; line-height: 42px; }
  .check { padding: 18px; }
  .faq summary { font-size: 18px; }
  /* Two figures side by side stop being readable below this; one under the other keeps them big. */
  /* On a phone each fact reads better as its own line than as a third of a row: figure first,
     then the words, the way somebody would say it out loud. */
  .facts__list { grid-template-columns: 1fr; gap: 14px; }
  .fact { display: flex; align-items: baseline; gap: 10px; }
  .fact__n { display: inline; margin: 0; font-size: 26px; }
  /* The details stage is the one place the card carries paired inputs; at this width they stack. */
  .fields { grid-template-columns: 1fr; }
  .step { padding: 20px 20px 24px; }
}

/* Small phones (Galaxy-class, 360px). The logo, the menu button and "Get my price"
   together run past the edge at this width, so the bar tightens rather than scrolls. */
@container page (max-width: 420px) {
  .nav__inner { gap: 10px; padding: 0 14px; }
  .brand img { height: 26px; }
  .nav__actions { gap: 8px; }
  .nav__actions .btn--sm { padding: 0 12px; font-size: 14px; }
  .nav__toggle { width: 40px; height: 40px; }
  .nav__toggle span { left: 10px; right: 10px; }
}

/* ---------- static (non-scrolling canvas) and reduced motion ---------- */
.is-static .bar { display: none; }
/* site.js adds .is-paused to loops while they are scrolled out of view. */
.is-paused, .is-paused * { animation-play-state: paused !important; }
/* "Reduce motion" asks for no movement — sliding, wiping, parallax, anything that travels or
   loops on its own. It does not ask for a dead page, and a plain cross-fade is the substitute it
   expects, so the sections still arrive as you reach them. Worth knowing: iOS turns this
   preference on by itself in Low Power Mode, so a phone can land here without anyone choosing it. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  html.js .wipe { clip-path: none; animation: fadeIn .6s ease both; }
  html.js .wipe::after { display: none; }
  html.js [data-load] { transform: none; animation: fadeIn .6s ease both; }
  html.js [data-reveal] { transform: none; transition: opacity .5s ease; }
  html.js [data-wipe] { clip-path: none; opacity: 0; transition: opacity .5s ease; }
  html.js [data-wipe].is-in { opacity: 1; }
  .ticker__track { animation: none; }
  .ticker { overflow-x: auto; }
  .btn, .pill, .jobs__summary, .jobs__chev { transition: none; }
  /* These move but sat outside the block, so the preference was only half honoured. Colour still
     changes on hover — it is the travel that makes people ill, not the tint. */
  .bar { transition: none; }
  .nav__links a::after, .faq summary::after, .nav__toggle span { transition: none; }
  .btn--primary:hover, .page a.btn--primary:hover, .pill:hover,
  .page .near a:hover, .reel__btn:hover, .nav__links a:hover::after { transform: none; }
  /* The three films travel and wipe, which is exactly what this preference rules out. They keep
     their resting frame, which is the finished picture, so the step still says what it says. */
  .step.is-playing .fx { animation: none !important; }
  .step { transition: none; }
}
