/* Teo Exterior Cleaning — teoexteriorcleaning.co.uk
   Hand-written, no framework and no web fonts, so a phone loads it in one request.
   Colours are taken from the logo (azure #0092E4) and checked for WCAG AA contrast:
   white on #0B76BD = 4.84:1, #076A9E on white = 5.89:1, white on #072A45 = 14.72:1. */

:root {
  --azure: #0092e4;
  --azure-ink: #076a9e;
  --azure-btn: #0b76bd;
  --azure-btn-hover: #0a6aa8;
  --tint: #eaf6fe;
  --navy: #072a45;
  --navy-hover: #0a4e7a;
  --ink: #0f1720;
  --muted: #55677a;
  --line: #e2e9f0;
  --line-strong: #cbd8e4;
  --star: #b45309;
  --white: #fff;
  --radius: 14px;
  --radius-lg: 20px;
  --radius-xl: 28px;
  --shadow-sm: 0 1px 2px rgba(15, 23, 32, .05), 0 2px 10px rgba(15, 23, 32, .05);
  --shadow-md: 0 2px 6px rgba(15, 23, 32, .06), 0 18px 48px rgba(15, 23, 32, .10);
  --wrap: 1200px;
  --gutter: clamp(20px, 4vw, 40px);
  --pad-section: clamp(56px, 7vw, 112px);
  --pad-tight: clamp(24px, 3vw, 40px);
  --measure: 64ch;
  --display: -apple-system, "SF Pro Display", "Segoe UI Variable Display", system-ui, "Helvetica Neue", Arial, sans-serif;
  --text: -apple-system, system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}

* { box-sizing: border-box; }

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

body {
  margin: 0;
  font-family: var(--text);
  color: var(--ink);
  background: var(--white);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; height: auto; display: block; }
a { color: var(--azure-ink); }
a:hover { color: var(--azure-btn-hover); }

h1, h2, h3 { font-family: var(--display); color: var(--ink); margin: 0 0 .5em; text-wrap: balance; }
h1 { font-size: clamp(2.25rem, 5.2vw, 4.1rem); line-height: 1.03; letter-spacing: -.03em; }
h2 { font-size: clamp(1.75rem, 3.2vw, 2.6rem); line-height: 1.08; letter-spacing: -.025em; }
h3 { font-size: 1.3rem; line-height: 1.16; letter-spacing: -.015em; }
p { margin: 0 0 1rem; text-wrap: pretty; }

.wrap { width: 100%; max-width: var(--wrap); margin: 0 auto; padding: 0 var(--gutter); }
.section { padding: var(--pad-section) 0; }
.section--tight { padding: var(--pad-tight) 0; }
.section--photo { padding: 0; overflow: hidden; background: var(--navy); }
.section--photo picture, .section--photo img { display: block; width: 100%; }
.section--photo img { height: clamp(300px, 43vw, 620px); object-fit: cover; object-position: center 48%; }
.section--tint { background: var(--tint); }
.section--navy { background: var(--navy); color: #dfeaf3; }
.section--navy h2, .section--navy h3 { color: var(--white); }
.eyebrow {
  text-transform: uppercase; letter-spacing: .1em; font-size: .78rem;
  font-weight: 700; color: var(--azure-ink); margin: 0 0 .6rem;
}
.section--navy .eyebrow { color: #7fd4f5; }
.lede { font-size: clamp(1.075rem, 1.35vw, 1.25rem); color: var(--muted); max-width: var(--measure); }
.section--navy .lede { color: #c7d8e6; }
.section__head { max-width: 46rem; margin-bottom: clamp(28px, 3.5vw, 48px); }
.center { text-align: center; }
.center .lede { margin-left: auto; margin-right: auto; }

/* ---------- buttons ---------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  min-height: 48px; padding: .8rem 1.3rem; border-radius: 999px;
  font-weight: 650; font-size: 1rem; text-decoration: none; border: 2px solid transparent;
  cursor: pointer; transition: background .15s ease, color .15s ease, border-color .15s ease;
}
.btn--primary { background: var(--azure-btn); color: var(--white); }
.btn--primary:hover { background: var(--azure-btn-hover); color: var(--white); }
.btn--ghost { border-color: rgba(255, 255, 255, .7); color: var(--white); background: rgba(255, 255, 255, .08); }
.btn--ghost:hover { background: var(--white); color: var(--navy); }
.btn--outline { border-color: var(--azure-btn); color: var(--azure-btn); background: var(--white); }
.btn--outline:hover { background: var(--azure-btn); color: var(--white); }
.btn--wide { width: 100%; }

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

.skip {
  position: absolute; left: -9999px; top: 0; background: var(--navy); color: var(--white);
  padding: .7rem 1rem; z-index: 100;
}
.skip:focus { left: 12px; top: 12px; }

/* ---------- header ---------- */
.header {
  position: sticky; top: 0; z-index: 50; background: rgba(255, 255, 255, .96);
  border-bottom: 1px solid var(--line); backdrop-filter: saturate(180%) blur(8px);
}
.header__inner { display: flex; align-items: center; gap: 1rem; min-height: 70px; }
.brand { display: flex; align-items: center; gap: .7rem; text-decoration: none; flex: 0 0 auto; min-height: 24px; }
.brand__rule { width: 28px; height: 4px; border-radius: 999px; background: var(--azure); flex: 0 0 auto; }
.brand__name { display: block; font-weight: 750; font-size: 1.06rem; color: var(--ink); line-height: 1.05; letter-spacing: -.02em; }
.brand__sub { display: block; margin-top: .22rem; font-weight: 500; font-size: .75rem; color: var(--muted); letter-spacing: .01em; }

.nav { margin-left: auto; display: flex; align-items: center; gap: clamp(.65rem, 1.2vw, 1.1rem); }
.nav a { color: var(--ink); text-decoration: none; font-weight: 550; font-size: .95rem; white-space: nowrap; }
.nav a:hover { color: var(--azure-ink); }
.header .btn { min-height: 44px; padding: .6rem 1.1rem; font-size: .95rem; white-space: nowrap; }
/* .nav a is more specific than .btn--primary and used to turn this text dark (3.72:1 on blue).
   Pin both states here so the header's main conversion action remains AA. */
.header .btn--primary, .header .btn--primary:hover { color: var(--white); }
.nav__phone { display: inline-flex; align-items: center; gap: .4rem; font-weight: 700; color: var(--navy); text-decoration: none; white-space: nowrap; }

.burger { display: none; margin-left: auto; }
.burger summary {
  list-style: none; cursor: pointer; border: 1px solid var(--line); border-radius: 10px;
  width: 46px; height: 42px; display: grid; place-items: center;
}
.burger summary::-webkit-details-marker { display: none; }
.burger summary span, .burger summary span::before, .burger summary span::after {
  content: ""; display: block; width: 20px; height: 2px; background: var(--ink); position: relative;
}
.burger summary span::before { position: absolute; top: -6px; }
.burger summary span::after { position: absolute; top: 6px; }
.burger__panel {
  position: absolute; left: 0; right: 0; top: 100%; background: var(--white);
  border-bottom: 1px solid var(--line); box-shadow: var(--shadow-md); padding: 1rem var(--gutter) 1.4rem;
  display: grid; gap: .35rem;
}
.burger__panel a { padding: .7rem 0; border-bottom: 1px solid var(--line); text-decoration: none; font-weight: 600; }
.burger__panel .btn { margin-top: .6rem; }

/* ---------- hero ---------- */
.hero { background: var(--white); overflow: hidden; }
.hero__grid {
  display: grid; grid-template-columns: minmax(0, 1.08fr) minmax(0, .92fr);
  gap: clamp(24px, 4vw, 56px); align-items: center;
}
.hero__copy { padding: clamp(40px, 6vw, 84px) 0; max-width: 36rem; }
.hero__copy h1 { margin-bottom: .38em; }
.hero__promise { font-size: clamp(1.075rem, 1.35vw, 1.25rem); color: var(--muted); max-width: 34rem; }
.hero__media {
  position: relative; margin: clamp(24px, 4vw, 44px) 0; border-radius: var(--radius-xl);
  overflow: hidden; box-shadow: var(--shadow-md); min-height: min(70vh, 640px);
}
.hero__media img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center 42%; }
.hero__chip {
  position: absolute; left: 16px; bottom: 16px; margin: 0; color: var(--ink);
  background: rgba(255, 255, 255, .94); border-radius: 999px; padding: .55rem .95rem;
  font-size: .86rem; box-shadow: var(--shadow-sm); backdrop-filter: blur(10px);
}
.hero__chip strong { color: var(--azure-ink); font-size: 1.03rem; }
.ticks { list-style: none; padding: 0; margin: 1.4rem 0 1.8rem; display: grid; gap: .55rem; }
.ticks li { display: flex; gap: .6rem; align-items: flex-start; color: var(--muted); }
/* Light text only where the background is dark. These selectors are more specific than the
   rule above, so a tick list inside a dark band stays readable on a white page and vice versa. */
.section--navy .ticks li { color: #eaf4fa; }
.ticks svg { flex: 0 0 auto; margin-top: .28rem; }
.hero__actions { display: flex; flex-wrap: wrap; gap: .8rem; align-items: center; }
.hero__actions .btn--ghost { color: var(--navy); border-color: var(--line-strong); background: var(--white); }
.hero__actions .btn--ghost:hover { color: var(--white); border-color: var(--navy); background: var(--navy); }
.hero__note { margin-top: 1.1rem; font-size: .92rem; color: var(--muted); }

/* Interior service/area pages keep a cinematic cover; only the home page uses the split hero. */
.hero--cover { position: relative; color: var(--white); min-height: 560px; display: grid; align-items: center; }
.hero--cover::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(105deg, rgba(4, 26, 44, .91) 0%, rgba(4, 26, 44, .76) 46%, rgba(4, 26, 44, .30) 100%);
}
.hero--cover .hero__media { position: absolute; inset: 0; margin: 0; min-height: 0; border-radius: 0; box-shadow: none; }
.hero--cover .hero__media img { object-position: center; }
.hero--cover .hero__inner { position: relative; z-index: 2; padding: clamp(64px, 9vw, 112px) var(--gutter); width: 100%; max-width: var(--wrap); margin: 0 auto; }
.hero--cover h1 { color: var(--white); max-width: 12ch; }
.hero--cover p:not(.eyebrow) { color: #e7f1f8; max-width: 34rem; font-size: 1.15rem; }
.hero--cover .hero__actions .btn--ghost { color: var(--white); border-color: rgba(255,255,255,.72); background: rgba(255,255,255,.08); }
.hero--cover .hero__actions .btn--ghost:hover { color: var(--navy); background: var(--white); }

/* ---------- trust bar ---------- */
.trust { background: var(--navy); color: #dfeaf3; }
.trust__grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1.2rem; padding: 1.6rem 0; }
.trust__item { display: flex; gap: .7rem; align-items: flex-start; font-size: .96rem; }
.trust__item svg { flex: 0 0 auto; margin-top: .15rem; }
.trust__item strong { display: block; color: var(--white); font-size: 1rem; }

/* ---------- cards ---------- */
.grid { display: grid; gap: 1.4rem; }
.grid--2 { grid-template-columns: repeat(2, 1fr); }
.grid--3 { grid-template-columns: repeat(3, 1fr); }
.grid--4 { grid-template-columns: repeat(4, 1fr); }
.grid--services { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: clamp(16px, 2.4vw, 32px); }
/* An odd number of cards in a two-column grid leaves the last one beside an empty cell, which is
   what made the areas page look unfinished at the bottom. Only an odd count is affected, so the
   paired bands on the home page are untouched. */
.grid--2 > :last-child:nth-child(odd) { grid-column: 1 / -1; }

.card {
  background: var(--white); border: 1px solid var(--line); border-radius: var(--radius-lg);
  overflow: hidden; box-shadow: var(--shadow-sm); display: flex; flex-direction: column;
  transition: transform .2s ease, box-shadow .2s ease;
}
.card:hover { transform: translateY(-3px); box-shadow: var(--shadow-md); }
.card__media { display: block; overflow: hidden; }
.card img { aspect-ratio: 3 / 2; object-fit: cover; width: 100%; }
.card:hover img { transform: scale(1.025); }
.card__body { padding: 1.2rem 1.3rem 1.4rem; display: flex; flex-direction: column; flex: 1; }
.card h3 { margin-bottom: .35rem; font-size: 1.5rem; }
.card p { color: var(--muted); font-size: .98rem; }
.card__price {
  display: inline-block; background: none; color: var(--azure-ink); font-weight: 750;
  padding: 0; font-size: .72rem; margin-bottom: .7rem; letter-spacing: .12em; text-transform: uppercase;
}
.card__included { color: var(--ink) !important; font-size: .9rem !important; font-weight: 600; }
.card__link { margin-top: auto; font-weight: 650; text-decoration: none; }
.card__link::after { content: " →"; }

.panel { background: var(--white); border: 1px solid var(--line); border-radius: var(--radius-lg); padding: 1.5rem; box-shadow: var(--shadow-sm); }
.panel--tint { background: var(--tint); border-color: #cfe6f8; }
/* The owner and his son sit side by side, two 3:4 portraits that match the files exactly, so the
   pair reads as a two-person family round instead of one face beside a branded van. The 3:4 box is
   the images' own ratio: anything taller would let object-fit crop the person out of frame. */
/* minmax(0, 1fr) and min-width:0 are the same guard .grid--services and .footer__grid already use.
   Measured at 390px with both portraits decoded the files did not in fact overflow (doc=390), but an
   <img> grid item keeps min-width:auto, so the 900px file is free to become the track's floor on a
   browser that sizes replaced elements that way. The guard costs nothing and removes the risk. */
.owner__photos { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: clamp(.6rem, 1.4vw, 1rem); }
.owner__photos img { width: 100%; min-width: 0; height: auto; aspect-ratio: 3 / 4; object-fit: cover;
                     border-radius: var(--radius-xl); }

/* ---------- before / after ----------
   The divider is a clip-path driven by --pos, so both photographs keep exactly the same size
   and position and nothing drifts or squashes as the customer drags. The range input lies over
   the whole frame at zero opacity: dragging anywhere on the picture works natively on mouse and
   touch, and it stays one focusable control with arrow keys, Home and End for anyone not using a
   pointer. No custom drag code and no library. */
.ba { position: relative; border-radius: var(--radius-xl); overflow: hidden; aspect-ratio: 880 / 586;
      background: var(--tint); box-shadow: var(--shadow-md); user-select: none; }
/* The pairs are wrapped in <picture> for the AVIF source, so the wrapper has to take the inset and
   the image stays inside the same containing block — otherwise the divider's clip-path would not
   clip the "before" picture and the two frames would drift apart as the handle moves. */
.ba picture { position: absolute; inset: 0; display: block; }
.ba__img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.ba__before { position: absolute; inset: 0; clip-path: inset(0 calc(100% - var(--pos, 50%)) 0 0); }
.ba__handle {
  position: absolute; top: 0; bottom: 0; left: var(--pos, 50%); width: 3px; background: var(--white);
  box-shadow: 0 0 0 1px rgba(4, 26, 44, .12); transform: translateX(-50%); pointer-events: none;
}
.ba__grip {
  position: absolute; top: 50%; left: 50%; width: 54px; height: 54px; border-radius: 50%;
  transform: translate(-50%, -50%); background: var(--white); box-shadow: var(--shadow-md);
  display: grid; place-items: center; color: var(--navy);
}
.ba__grip svg { width: 24px; height: 24px; }
.ba__tag {
  position: absolute; top: 14px; font-size: .74rem; font-weight: 700; letter-spacing: .1em;
  text-transform: uppercase; background: rgba(4, 26, 44, .72); color: var(--white);
  padding: .34rem .62rem; border-radius: 999px; backdrop-filter: blur(6px);
}
.ba__tag--before { left: 14px; }
.ba__tag--after { right: 14px; }
.ba input[type="range"] { position: absolute; inset: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: ew-resize; }
.ba:focus-within .ba__grip { outline: 3px solid var(--azure); outline-offset: 3px; }
.ba__hint {
  position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%); margin: 0;
  background: rgba(4, 26, 44, .72); color: var(--white); font-size: .82rem; padding: .32rem .72rem;
  border-radius: 999px; backdrop-filter: blur(6px); transition: opacity .3s ease; pointer-events: none;
}
.ba[data-touched] .ba__hint { opacity: 0; }
.ba__note { margin: .8rem 0 0; font-size: .95rem; color: var(--muted); text-align: center; }

/* ---------- gallery ---------- */
.editorial-gallery { display: grid; grid-template-columns: repeat(12, 1fr); grid-auto-rows: 90px; gap: 14px; }
.editorial-gallery figure { margin: 0; position: relative; overflow: hidden; border-radius: var(--radius-lg); background: var(--tint); }
/* A 12-column mosaic that has to tile exactly. Rows 1-4 are 7 + 5 with the 5 split in two, rows 5-7
   are 4 + 4 + 4. The earlier spans (7x5, 5x3, 5x4, 4x3, 4x3, 4x3) left a two-row hole under the
   large tile and pushed the bottom row out of line, which is what made the section look unfinished. */
.editorial-gallery figure:nth-child(1) { grid-column: span 7; grid-row: span 4; }
.editorial-gallery figure:nth-child(2) { grid-column: span 5; grid-row: span 2; }
.editorial-gallery figure:nth-child(3) { grid-column: span 5; grid-row: span 2; }
.editorial-gallery figure:nth-child(4) { grid-column: span 4; grid-row: span 3; }
.editorial-gallery figure:nth-child(5) { grid-column: span 4; grid-row: span 3; }
.editorial-gallery figure:nth-child(6) { grid-column: span 4; grid-row: span 3; }
.editorial-gallery img { width: 100%; height: 100%; object-fit: cover; transition: transform .25s ease; }
.editorial-gallery figure:hover img { transform: scale(1.02); }
.editorial-gallery figcaption {
  position: absolute; left: 12px; right: 12px; bottom: 12px; width: fit-content; max-width: calc(100% - 24px);
  color: var(--white); background: rgba(7, 42, 69, .84); padding: .4rem .65rem; border-radius: 9px;
  font-size: .8rem; line-height: 1.3; backdrop-filter: blur(8px);
}

/* ---------- job photo strip ---------- */
/* Photographs from real jobs. A plain three-up grid rather than the recent-work mosaic: every
   picture keeps its own 3:4 crop, the caption sits under it in ordinary text, and nothing here can
   disturb the six hand-tuned gallery spans. */
.photo-strip { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-top: clamp(20px, 3vw, 34px); }
.photo-strip figure { margin: 0; }
.photo-strip img {
  display: block; width: 100%; height: auto; aspect-ratio: 3 / 4; object-fit: cover;
  border-radius: var(--radius-lg); background: var(--tint);
}
.photo-strip figcaption { margin-top: .5rem; font-size: .9rem; color: var(--muted); }

/* ---------- reviews ---------- */
.review { background: var(--white); border: 1px solid var(--line); border-radius: var(--radius-lg); padding: 1.4rem; }
.stars { color: var(--star); letter-spacing: .12em; margin-bottom: .5rem; }
.review blockquote { margin: 0 0 .7rem; font-size: 1.02rem; }
.review cite { font-style: normal; color: var(--muted); font-size: .93rem; }
/* The Google review link is the whole call to action of that card, not a link inside a sentence,
   so it has to meet the 24px minimum target size on its own. */
.review cite a { display: inline-flex; align-items: center; min-height: 24px; font-weight: 650; }
/* While there are no confirmed reviews the invitation is a line inside the owner band rather than a
   band of its own: the owner band is where a customer has just read who they are hiring. */
.review-line { margin: 1.3rem 0 0; max-width: 62ch; color: #c7d8e6; font-size: .96rem; }
.review-line strong { color: var(--white); }
.review-line a { color: #7fd4f5; font-weight: 650; padding: .25rem .12rem; }
.review-line a:hover { color: var(--white); }

/* ---------- steps ---------- */
.steps { counter-reset: s; display: grid; gap: 1.2rem; grid-template-columns: repeat(3, 1fr); }
.step { position: relative; padding-left: 3.4rem; }
.step::before {
  counter-increment: s; content: counter(s); position: absolute; left: 0; top: 0;
  width: 2.5rem; height: 2.5rem; border-radius: 50%; background: var(--azure-btn); color: var(--white);
  display: grid; place-items: center; font-weight: 750;
}
.step h3 { margin-bottom: .3rem; }
.step p { color: var(--muted); margin: 0; }

/* ---------- faq ---------- */
.faq { border-top: 1px solid var(--line); }
.faq details { border-bottom: 1px solid var(--line); }
.faq summary {
  cursor: pointer; min-height: 48px; padding: 1.05rem 2rem 1.05rem 0; font-weight: 650; position: relative; list-style: none;
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: "+"; position: absolute; right: .3rem; top: 50%; transform: translateY(-50%);
  color: var(--azure-ink); font-weight: 700; font-size: 1.3rem;
}
.faq details[open] summary::after { content: "–"; }
.faq details p { color: var(--muted); margin: 0 0 1.1rem; max-width: 68ch; }

/* ---------- forms ---------- */
.form { display: grid; gap: .9rem; }
/* The phone number and the WhatsApp link are the most valuable taps on the site, and they sit in a
   sentence: a slightly larger line and padded links widen the target without moving any text. */
.form small { font-size: .95rem; }
.form small a { padding: .28rem .12rem; }
/* A table row header is a target, not inline prose: WCAG 2.5.8 asks for 24px and a bare line box
   on the price table measures 20. */
.panel table th a, .panel table td a { display: inline-flex; align-items: center; min-height: 24px; }
.price-panel { overflow-x: auto; }
.form label { font-weight: 600; font-size: .95rem; }
.form input, .form select, .form textarea {
  width: 100%; padding: .8rem .9rem; border: 1px solid #cbd8e4; border-radius: 10px;
  font: inherit; color: var(--ink); background: var(--white); min-height: 48px;
}
.form textarea { min-height: 110px; resize: vertical; }
/* In-page quote CTAs land below the sticky header. The status line reserves its own row, so an
   empty-field message does not push the submit button and phone links down after a customer taps. */
.panel#quote { scroll-margin-top: 90px; }
.quote-status { display: block; min-height: 24px; color: var(--navy); font-weight: 650; }
.form .row { display: grid; gap: .9rem; grid-template-columns: 1fr 1fr; }
.form small { color: var(--muted); }

/* ---------- areas ---------- */
.breadcrumb { border-bottom: 1px solid var(--line); background: #f7fafc; }
.breadcrumb ol { display: flex; flex-wrap: wrap; align-items: center; gap: .4rem; min-height: 42px;
  margin: 0 auto; padding-top: .45rem; padding-bottom: .45rem; list-style: none;
  color: var(--muted); font-size: .86rem; }
.breadcrumb li { display: inline-flex; align-items: center; gap: .4rem; }
.breadcrumb li + li::before { content: "/"; color: #91a0ae; }
.breadcrumb a { color: var(--azure-ink); font-weight: 650; text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }
.breadcrumb [aria-current="page"] { color: var(--ink); }
/* The one sentence per area that links the services it actually books. It carries the only
   contextual service-to-area link on the page, so it must read as part of the copy. */
.local-booked { color: var(--muted); }
.local-booked a { color: var(--azure-ink); font-weight: 650; padding: .2rem .1rem; }
.chips { display: flex; flex-wrap: wrap; gap: .6rem; padding: 0; list-style: none; margin: 0; }
.chips a {
  display: inline-block; padding: .55rem 1rem; border-radius: 999px; background: var(--white);
  border: 1px solid var(--line); text-decoration: none; font-weight: 600; color: var(--ink);
}
.chips a:hover { border-color: var(--azure-btn); color: var(--azure-ink); }

/* ---------- customer portal ---------- */
.portal-main { min-height: calc(100vh - 70px); background: #f3f7fa; padding: clamp(28px, 5vw, 72px) var(--gutter); }
.portal-shell { width: min(100%, 980px); margin: 0 auto; }
.portal-brand { display: flex; align-items: center; gap: .75rem; margin-bottom: 2rem; }
.portal-brand strong { display: block; font-size: 1.05rem; }
.portal-brand span:not(.brand__rule) { display: block; color: var(--muted); font-size: .82rem; }
.portal-state { min-height: 480px; display: grid; place-content: center; justify-items: center; text-align: center; max-width: 40rem; margin: 0 auto; }
.portal-state h1 { margin-top: 1rem; }
.portal-state p { color: var(--muted); max-width: 36rem; }
.portal-state__actions { display: flex; flex-wrap: wrap; gap: .75rem; justify-content: center; margin-top: 1rem; }
.portal-spinner { width: 42px; height: 42px; border: 3px solid #cfe6f8; border-top-color: var(--azure-btn); border-radius: 50%; animation: portal-spin .8s linear infinite; }
.portal-lock { width: 48px; height: 48px; border-radius: 50%; display: grid; place-items: center; color: var(--white); background: var(--navy); box-shadow: var(--shadow-sm); }
@keyframes portal-spin { to { transform: rotate(360deg); } }
.portal-welcome { background: var(--navy); color: var(--white); padding: clamp(28px, 5vw, 52px); border-radius: var(--radius-xl); box-shadow: var(--shadow-md); }
.portal-welcome h1 { color: var(--white); }
.portal-welcome p { color: #cfe0ec; margin-bottom: .4rem; }
.portal-welcome small { color: #9fc2da; }
.portal-grid { display: grid; grid-template-columns: 1.25fr 1fr; gap: 18px; margin-top: 18px; }
.portal-card { background: var(--white); border-radius: var(--radius-lg); padding: clamp(22px, 3vw, 32px); box-shadow: var(--shadow-sm); }
.portal-card--next { grid-row: span 2; background: linear-gradient(145deg, #fff 0%, var(--tint) 100%); }
.portal-card--balance { background: var(--navy); color: #dfeaf3; }
.portal-card--balance .eyebrow { color: #7fd4f5; }
.portal-card--balance p { color: #c7d8e6; }
.portal-money { display: block; margin-top: 2.5rem; color: var(--azure-ink); font-size: clamp(1.6rem, 4vw, 2.6rem); }
.portal-balance { display: block; color: var(--white); font-size: clamp(2.2rem, 6vw, 4.4rem); line-height: 1; margin: .4rem 0 1rem; letter-spacing: -.04em; }
.portal-actions { margin-top: 18px; background: var(--white); border-radius: var(--radius-lg); padding: clamp(22px, 3vw, 32px); box-shadow: var(--shadow-sm); }
.portal-actions .btn { margin: .3rem .45rem .2rem 0; }
.portal-footer { text-align: center; background: #f3f7fa; color: var(--muted); padding: 1.2rem var(--gutter) 2rem; font-size: .88rem; }
.portal-footer a { color: var(--azure-ink); }
[hidden] { display: none !important; }

/* ---------- footer ---------- */
.footer { background: var(--navy); color: #c7d8e6; padding: clamp(40px, 6vw, 64px) 0 1.5rem; }
/* The footer's column labels are top-level sections of the footer landmark. As h3 they made the
   404 page jump straight from its h1 to a footer h3, which fails an accessibility audit. */
.footer h2 { color: var(--white); font-size: 1rem; letter-spacing: .02em; }
.footer a { color: #cfe3f1; text-decoration: none; }
.footer a:hover { color: var(--white); text-decoration: underline; }
.footer__grid { display: grid; grid-template-columns: 1.4fr 1fr 1fr 1fr; gap: 2rem; }
.footer ul { list-style: none; padding: 0; margin: 0; display: grid; gap: .45rem; font-size: .95rem; }
/* WCAG 2.5.8 wants at least 24x24 CSS px on every target; the bare line box here is under that. */
.footer ul a, .footer p a { display: inline-flex; align-items: center; min-height: 28px; }
.footer__bottom {
  margin-top: 2.2rem; padding-top: 1.2rem; border-top: 1px solid rgba(255, 255, 255, .14);
  font-size: .87rem; display: flex; flex-wrap: wrap; gap: .6rem 1.4rem; justify-content: space-between;
}
/* The badge is drawn as a white-and-azure lockup with a dark silhouette — the artwork was made for a
   dark field, so it sits straight on the navy footer, where the white lettering reads. A white card
   behind it washes "EXTERIOR CLEANING" out; shrinking it into the header is forbidden by the suite. */
.footer__logo { margin: 0 0 1.1rem; }
.footer__logo img { display: block; width: 208px; height: auto; }
.footer__wordmark { margin-bottom: 1rem; }
.footer__wordmark strong { display: block; color: var(--white); font-size: 1.15rem; }
.footer__wordmark span { display: block; color: #9fc2da; font-size: .82rem; margin-top: .15rem; }

/* ---------- sticky mobile call bar ---------- */
.callbar { display: none; }
@media (max-width: 900px) {
  .callbar {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 60; display: grid;
    grid-template-columns: 1fr 1fr 1fr; background: var(--white);
    border-top: 1px solid var(--line); box-shadow: 0 -4px 16px rgba(15, 23, 32, .10);
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .callbar a {
    padding: .7rem .3rem .75rem; text-align: center; text-decoration: none; font-weight: 650;
    font-size: .84rem; color: var(--navy); display: grid; gap: .1rem; justify-items: center;
  }
  .callbar a + a { border-left: 1px solid var(--line); }
  .callbar a strong { font-size: .9rem; }
  .callbar svg { width: 20px; height: 20px; }
  body { padding-bottom: calc(69px + env(safe-area-inset-bottom, 0px)); }
}

/* ---------- responsive ---------- */
@media (max-width: 1240px) and (min-width: 1081px) {
  .nav__phone { display: none; }
}
@media (max-width: 1080px) {
  .nav { display: none; }
  .burger { display: block; }
  .hero__grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 980px) {
  .trust__grid, .grid--4 { grid-template-columns: repeat(2, 1fr); }
  .grid--3, .steps { grid-template-columns: 1fr; }
  .footer__grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 820px) {
  .grid--2, .grid--services { grid-template-columns: 1fr; }
  .hero__grid { display: flex; flex-direction: column; gap: 0; padding: 0; max-width: none; }
  .hero__copy { order: 2; max-width: none; padding: 42px var(--gutter) 52px; }
  .hero__media { order: 1; width: 100%; min-height: 0; height: clamp(240px, 42vh, 360px); margin: 0; border-radius: 0; box-shadow: none; }
  .hero__media img { object-position: center 45%; }
  .hero__chip { left: var(--gutter); bottom: 14px; }
  /* Six photographs in two equal columns is three full rows. A full-width first tile left the sixth
     tile stranded beside an empty cell, so every tile is the same size here. */
  .editorial-gallery { grid-template-columns: repeat(2, 1fr); grid-auto-rows: 200px; }
  .editorial-gallery figure:nth-child(n) { grid-column: span 1; grid-row: span 1; }
  .photo-strip { grid-template-columns: repeat(2, 1fr); }
  .hero--cover { min-height: 520px; display: grid; }
  .hero--cover .hero__media { position: absolute; width: auto; height: auto; min-height: 0; margin: 0; border-radius: 0; }
  .hero--cover .hero__inner { padding: 72px var(--gutter); }
  .portal-grid { grid-template-columns: 1fr; }
  .portal-card--next { grid-row: auto; }
}
@media (max-width: 560px) {
  body { font-size: 16.5px; }
  h1 { font-size: clamp(2.25rem, 11vw, 2.75rem); }
  .trust__grid { grid-template-columns: 1fr; }
  /* Two columns instead of four stacked lists, with the wordmark and contact details across both.
     Four lists in one column made the footer 1066px of a 10700px phone page. */
  .footer__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.8rem 1.2rem; }
  .footer__grid > div:first-child { grid-column: 1 / -1; }
  /* Three link lists in two columns would leave the third one beside an empty cell. */
  .footer__grid > div:last-child { grid-column: 1 / -1; }
  /* Four stacked service cards cost 2548px on a phone: a wider photo crop and tighter padding. */
  .section:not(.section--tight):not(.section--photo) { padding: 44px 0; }
  .card img { aspect-ratio: 2 / 1; }
  .card__body { padding: 1.05rem 1.15rem 1.2rem; }
  /* A 16:9 comparison on a 350px screen gives each side 175px, which is too little to see what
     changed. A squarer frame shows more of the gutter and makes the drag worth doing. */
  .ba { aspect-ratio: 4 / 3; }
  .form .row { grid-template-columns: 1fr; }
  .hero__actions .btn { width: 100%; }
  /* The desktop price table forces its explanatory column into a 120px strip on a 390px phone.
     Keep real table semantics in the HTML, but present each row as a labelled price card here. */
  .price-panel { padding: 0; border: 0; background: transparent; box-shadow: none; overflow: visible !important; }
  .price-table, .price-table tbody, .price-table tr, .price-table th, .price-table td {
    display: block; width: 100%;
  }
  .price-table caption { display: block; width: 100%; }
  .price-table thead { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
  .price-table tr { margin-bottom: 12px; padding: 18px; border: 1px solid var(--line);
    border-radius: var(--radius-lg); background: var(--white); box-shadow: var(--shadow-sm); }
  .price-table th { padding: 0 0 .8rem !important; font-size: 1.12rem; }
  .price-table td { display: grid; grid-template-columns: 5rem minmax(0, 1fr); gap: .65rem;
    padding: .65rem 0; border-top: 1px solid var(--line); }
  .price-table td::before { content: attr(data-label); color: var(--muted); font-weight: 700; font-size: .82rem;
    letter-spacing: .04em; text-transform: uppercase; }
  .brand__sub { display: none; }
  .brand__rule { width: 22px; }
  .portal-main { padding-left: 16px; padding-right: 16px; }
  .portal-welcome { border-radius: var(--radius-lg); }
  .portal-state__actions .btn, .portal-actions .btn { width: 100%; margin-right: 0; }
}
/* ---------- motion ----------
   Scroll-driven reveal, driven entirely by CSS: no observer, no library, nothing on the main
   thread. Gated behind @supports, so a browser without scroll timelines shows plain content, and
   behind prefers-reduced-motion, so nobody who asked for less movement gets any.

   The reveal deliberately moves and scales without touching opacity. A view() timeline is
   INACTIVE while its subject is entirely outside the scrollport, and an inactive timeline makes
   the animation not apply at all, so an opacity reveal would either do nothing or leave content
   invisible. Transform-only cannot hide anything, and because the timeline becomes active the
   moment an element starts entering, the movement plays exactly when the customer sees it. In a
   full-page screenshot every element is inside one tall viewport, so all of them read as settled. */
@keyframes reveal-in {
  from { transform: translateY(22px) scale(.99); }
  to   { transform: none; }
}
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    [data-reveal] {
      animation: reveal-in linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 55%;
    }
  }
}
/* The hero arrives on its own, before any scrolling: a short, choreographed entrance for the
   words and then the photograph. Like everything else here it moves without fading — a frozen or
   throttled animation must never be able to leave the first screen invisible, and a transform
   that has not started yet is just a slightly offset element. */
@keyframes hero-in {
  from { transform: translateY(24px); }
  to   { transform: none; }
}
@media (prefers-reduced-motion: no-preference) {
  .hero__copy { animation: hero-in .8s cubic-bezier(.22, .68, 0, 1) both; }
  .hero__media { animation: hero-in .9s cubic-bezier(.22, .68, 0, 1) .12s both; }
}
/* The header settles into a translucent, blurred bar once the page has moved. Pure scroll
   timeline, so there is no scroll listener anywhere. */
@keyframes header-settle {
  from { box-shadow: 0 0 0 rgba(4, 26, 44, 0); background: rgba(255, 255, 255, .96); }
  to   { box-shadow: 0 10px 30px rgba(4, 26, 44, .07), 0 1px 0 var(--line);
         background: rgba(255, 255, 255, .86); }
}
@supports (animation-timeline: scroll()) {
  .header { animation: header-settle linear both; animation-timeline: scroll(root); animation-range: 0 140px; }
}
/* Cards, tiles and links answer the pointer, which is most of the difference between a picture
   of a job and something that feels like a real, finished product. */
.card { transition: transform .25s ease, box-shadow .25s ease, border-color .25s ease; }
.card:hover { border-color: #cfe6f8; }
.card:hover img { transform: scale(1.035); }
.card__link { transition: gap .2s ease, color .2s ease; }
.card__link:hover { gap: .55rem; }
.chips a { transition: border-color .2s ease, transform .2s ease, box-shadow .2s ease; }
.chips a:hover { transform: translateY(-2px); box-shadow: var(--shadow-sm); }
.footer a { transition: color .2s ease; }
.faq summary { transition: color .2s ease; }
.faq summary:hover { color: var(--azure-ink); }

/* Thumbs need more room than a cursor. On touch devices every footer link, gallery link and card
   link is a 44px row, which is the size Apple and Google both measure against. */
@media (pointer: coarse) {
  .footer ul { gap: .1rem; }
  .footer ul a, .footer p a { min-height: 44px; }
  .card__link { display: inline-flex; align-items: center; min-height: 44px; }
  .callbar a { min-height: 52px; }
}
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { transition: none !important; }
}
