/* ============================================================================
   Brock's Pressure Washing — site-polish.css
   the wash edge · hero · service cards · overlay · call band · reviews · quote
   ----------------------------------------------------------------------------
   THE SIGNATURE lives here. Every photograph in this archive is a SINGLE file
   holding both halves of a before/after pair: landscape files split left|right,
   portrait files top/bottom, always exactly at the midpoint, dirty half first.
   Nothing is ever cut into two assets.

   How a half is shown without distorting it, everywhere on the site:

       box       overflow:hidden, whatever aspect the layout wants
       img       width:200%  height:100%  object-fit:cover      (landscape)
                 width:100%  height:200%  object-fit:cover      (portrait)
       after     shifted by -100% on that axis

   `cover` centres what it crops, so the composite's seam lands exactly on the
   box's own centre line, which is what makes the two halves land in the two
   halves of the element. Background-position arithmetic was tried first and
   stretches each half to the box's aspect ratio. Do not go back to it.

   The split axis is read at runtime from naturalHeight > naturalWidth and
   published as .is-vertical / .is-horizontal by js/bpw-site.js.
   ========================================================================= */

/* The card's diagonal has to animate, and a bare custom property cannot be
   transitioned. Registering it makes --cut a real interpolable percentage. */
@property --cut {
  syntax: "<percentage>";
  inherits: true;
  initial-value: 56%;
}

/* ---------------------------------------------------------------------------
   1 · THE WASH FIGURE — one composite, one line you can move.
   ------------------------------------------------------------------------ */
.mw-wash { --pos: 50%; position: relative; }

.mw-wash__frame {
  position: relative;
  overflow: hidden;
  background: var(--mw-navy-deep);
  aspect-ratio: 3 / 2;            /* replaced by the real half-aspect on load */
  touch-action: pan-y;
  cursor: ew-resize;
}
.mw-wash.is-vertical .mw-wash__frame { cursor: ns-resize; }

.mw-wash__layer { position: absolute; inset: 0; overflow: hidden; }
.mw-wash__layer img {
  position: absolute;
  top: 0;
  left: 0;
  width: 200%;
  height: 100%;
  /* The base reset's `img { max-width: 100% }` clamps this back to 100% and
     both layers then show the SAME centred crop of the whole composite — the
     wipe silently stops being a wipe. This line is why the layers work. */
  max-width: none;
  object-fit: cover;
  display: block;
  user-select: none;
  -webkit-user-drag: none;
}
.mw-wash__layer--after img { left: -100%; }
.mw-wash__layer--before { clip-path: inset(0 calc(100% - var(--pos)) 0 0); }

.mw-wash.is-vertical .mw-wash__layer img { width: 100%; height: 200%; left: 0; }
.mw-wash.is-vertical .mw-wash__layer--after img { top: -100%; }
.mw-wash.is-vertical .mw-wash__layer--before { clip-path: inset(0 0 calc(100% - var(--pos)) 0); }

/* THE WAND.
   The canvas holds the dirty half and is erased where you drag, so the pointer is a
   nozzle rather than a line. `pointer-events: none` because the FRAME owns the gesture —
   one element captures the pointer and the canvas is only ever drawn on. */
.mw-wash__paint {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: block;
  pointer-events: none;
  touch-action: none;
}

/* Once the canvas is live it IS the before half; the <img> underneath would otherwise
   show through every stroke. It stays in the DOM as the no-canvas fallback. */
.mw-wash.is-painting .mw-wash__layer--before { display: none; }

/* The cursor was a 34px abstract stick here until 2026-09-06 — a diagonal, a dot and
   three ticks, which reads as "draw" rather than as a wand. The wand that replaced it is
   set on the frame from JS (`WAND_CURSOR`), so the art has exactly ONE definition and a
   change to it cannot land in one of two places. This line is the floor underneath that:
   what a visitor gets before the canvas is live, or if the image is refused. */
.mw-wash.is-painting .mw-wash__frame { cursor: crosshair; }

/* The jet. A cursor cannot exist on a touch screen, so this is what shows a phone user
   where the nozzle is — the same element serves both, and only while actually washing. */
.mw-wash__jet {
  position: absolute;
  z-index: 2;
  left: 50%;
  top: 50%;
  width: 17%;
  aspect-ratio: 1;
  margin: -8.5% 0 0 -8.5%;
  border-radius: 50%;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.12s ease;
  background:
    radial-gradient(circle at 50% 50%,
      rgba(255, 255, 255, 0.85) 0%,
      rgba(80, 184, 231, 0.45) 38%,
      rgba(80, 184, 231, 0) 70%);
}
.mw-wash.is-jetting .mw-wash__jet { opacity: 1; }

/* THE SPRAY ITSELF. The glow above says "something is here"; it does not say "water at
   pressure". A fan, aimed along the direction of travel (`--mw-jet-angle`, set by the
   pointer handler) and shedding droplets, does — and that is the whole affordance the
   operator asked for on 2026-09-03.

   Both layers are pseudo-elements on the existing jet, so the element count, the
   pointer-events: none and the z-index are unchanged. */
.mw-wash__jet::before,
.mw-wash__jet::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  pointer-events: none;
}
/* The fan: a narrow cone opening AWAY from the nozzle. `rotate` puts its axis on the
   direction of travel; the default 0deg points right, which is where atan2 starts. */
.mw-wash__jet::before {
  /* Sized and weighted from screenshots, not from taste. At 220%/0.55 it read as a smudge
     against brick; at 300%/0.92 it blanketed the picture — and a spray that hides the
     reveal defeats the thing it is celebrating. This is the pass between them: bright at
     the nozzle, gone by 88%, so the clean brick underneath stays the point. */
  width: 235%;
  height: 128%;
  transform: translate(-8%, -50%) rotate(var(--mw-jet-angle, 0deg));
  transform-origin: 8% 50%;
  background: linear-gradient(
    to right,
    rgba(255, 255, 255, 0.9) 0%,
    rgba(196, 232, 250, 0.5) 22%,
    rgba(140, 205, 240, 0.16) 55%,
    rgba(140, 205, 240, 0) 88%);
  clip-path: polygon(0% 47%, 100% 4%, 100% 96%, 0% 53%);
  filter: blur(2px);
}
/* The droplets: one repeating burst along the same axis. A second, slower copy would be
   more convincing and costs another compositing layer on a phone, which this does not
   need to earn. */
.mw-wash__jet::after {
  width: 230%;
  height: 105%;
  transform: translate(-8%, -50%) rotate(var(--mw-jet-angle, 0deg));
  transform-origin: 8% 50%;
  background-image:
    radial-gradient(circle, rgba(255,255,255,.9) 40%, rgba(255,255,255,0) 60%),
    radial-gradient(circle, rgba(255,255,255,.7) 40%, rgba(255,255,255,0) 60%),
    radial-gradient(circle, rgba(255,255,255,.8) 40%, rgba(255,255,255,0) 60%);
  background-size: 7px 7px, 5px 5px, 4px 4px;
  background-repeat: no-repeat;
  animation: mw-spit 0.5s linear infinite;
}
@keyframes mw-spit {
  0%   { background-position: 14% 44%, 14% 56%, 14% 50%; opacity: 0; }
  22%  { opacity: 1; }
  100% { background-position: 92% 30%, 88% 71%, 96% 50%; opacity: 0; }
}

/* THE WAND ITSELF — the pointer, drawn. Until 2026-09-06 this was a drifting translucent
   glow that washed nothing; the client's reading of it was that a photograph with a smudge
   moving over it does not say the photograph cleans. It is now the tool: on a mouse it
   replaces the cursor, on a phone it is what the finger is holding, and on arrival it
   makes the first stroke by itself.

   The art is a background-image set from JS so WAND_SVG stays the one definition.

   THE NOZZLE IS THE ANCHOR. The drawn tip lands at 98.2% / 50% of the art, so the element
   is translated back by exactly that and rotated about the same point — whatever `left`
   and `top` say, the NOZZLE lands there and the lance, gun and grip trail behind it. That
   is not a detail: a wand anchored at its centre would sit ON the clean patch it just
   made, and the reveal is the entire point of the control. The jet is anchored to the
   same point, so the spray leaves the nozzle and not the middle of the barrel. */
.mw-wash__wand {
  position: absolute;
  z-index: 3;
  left: 50%;
  top: 52%;
  /* Below ~130px the lance is a hairline and the tool stops reading as a tool; above
     ~230px it starts hiding the photograph it is there to clean. */
  width: clamp(132px, 34%, 232px);
  aspect-ratio: 170 / 72;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.26s ease;
  background: 50% 50% / 100% 100% no-repeat;
  transform-origin: 98.2% 50%;
  transform: translate(-98.2%, -50%)
             rotate(var(--mw-jet-angle, 0deg))
             scaleY(var(--mw-wand-flip, 1));
  filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.45));
}
.mw-wash.is-wanding .mw-wash__wand,
.mw-wash.is-demo .mw-wash__wand { opacity: 1; }

/* The prompt. Removed for good on the first stroke — a hint that returns after you have
   already used the control is noise, not help. */
.mw-wash__hint {
  position: absolute;
  z-index: 3;
  left: 50%;
  bottom: 12%;
  transform: translateX(-50%);
  padding: 0.5rem 0.9rem;
  background: rgba(14, 34, 51, 0.86);
  color: var(--mw-white);
  font-family: var(--mw-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 700;
  white-space: nowrap;
  pointer-events: none;
  transition: opacity 0.25s ease;
}
.mw-wash.is-touched .mw-wash__hint,
.mw-wash.is-washed .mw-wash__hint { opacity: 0; }
.mw-wash.is-hinting .mw-wash__hint { animation: mw-nudge 1.6s ease-in-out 2; }
@keyframes mw-nudge {
  0%, 100% { transform: translateX(-50%) translateY(0); }
  50%      { transform: translateX(-50%) translateY(-0.35rem); }
}

/* Keyboard parity for a surface that cannot have a slider. Two real buttons reach both
   ends of the comparison, which is what the control is for. */
.mw-wash__acts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5rem;
  margin-top: 0.6rem;
}
.mw-wash__btn {
  font-family: var(--mw-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 700;
  padding: 0.55rem 0.9rem;
  min-height: 2.75rem;
  border: 2px solid var(--mw-navy);
  background: var(--mw-white);
  color: var(--mw-navy);
  cursor: pointer;
}
.mw-wash__btn--go { background: var(--mw-burgundy); border-color: var(--mw-burgundy); color: var(--mw-white); }
.mw-wash__btn:focus-visible { outline: 3px solid var(--mw-gold); outline-offset: 3px; }
.mw-wash__status {
  margin: 0;
  font-family: var(--mw-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--mw-ink-muted);
}
/* The hero's figure sits on navy, so the controls inherit its ground rather than the
   page's. Screenshot-caught: the status read navy-on-navy and was invisible until the
   pixels were looked at. */
.mw-hero__fig .mw-wash__acts { padding: 0 0.9rem 0.9rem; }
.mw-hero__fig .mw-wash__status { color: rgba(255, 255, 255, 0.82); }
.mw-hero__fig .mw-wash__btn { border-color: var(--mw-white); }

/* Corner tags. Burgundy names the dirty half; navy names the clean one. */
.mw-wash__tag {
  position: absolute;
  z-index: 2;
  font-family: var(--mw-mono);
  font-size: 0.625rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  font-weight: 700;
  padding: 0.4rem 0.7rem;
  color: var(--mw-white);
  pointer-events: none;
}
.mw-wash__tag--before { top: 0; left: 0; background: var(--mw-burgundy); }
.mw-wash__tag--after  { bottom: 0; right: 0; background: var(--mw-navy); }
/* Both keep a floor so the pair always reads as a legend — an AFTER label that is
   completely invisible until you wash does not tell you there is an after to find. */
.mw-wash.is-painting .mw-wash__tag--before { opacity: max(0.5, calc((100 - var(--washed, 0)) / 100)); }
.mw-wash.is-painting .mw-wash__tag--after  { opacity: max(0.5, calc(var(--washed, 0) / 100)); }
.mw-wash__tag { transition: opacity 0.2s linear; }

.mw-wash__cap { margin: 0.6rem 0 0; }

.mw-wash.is-failed .mw-wash__frame { display: none; }

/* ---------------------------------------------------------------------------
   2 · HERO — the proof, at full width, before any claim.
   ------------------------------------------------------------------------ */
/* EVERY half in this archive is a vertical phone photograph (0.67–0.83). So
   the figure column gets a WIDTH and the frame derives its HEIGHT from the
   half's real aspect-ratio, which js/bpw-site.js sets inline. Nothing is
   cropped and nothing is stretched. Giving the frame a definite height
   instead — the obvious way to make a hero "fill" — squeezes a portrait half
   into a letterbox slice of itself, which is how the first version of this
   hero ended up showing lawn instead of the driveway. */
.mw-hero {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(15rem, 34vw, 30rem);
  align-items: stretch;
  background: var(--mw-navy);
}
.mw-hero__plate {
  background: var(--mw-navy);
  color: var(--mw-white);
  padding: clamp(1.75rem, 4vw, 3.25rem) clamp(1.25rem, 3vw, 2.75rem);
  /* Bleed to the left page edge, but keep the text on the shell's grid. */
  padding-left: max(clamp(1.25rem, 3vw, 2.75rem), calc((100vw - 1180px) / 2));
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 1.1rem;
}
.mw-hero__title {
  font-family: var(--mw-display);
  text-transform: uppercase;
  font-size: clamp(2.25rem, 5.2vw, 4rem);
  line-height: 0.92;
  letter-spacing: 0.005em;
}
.mw-hero__title em { font-style: normal; color: var(--mw-gold); display: block; }
.mw-hero__lede { font-size: 1.0625rem; color: rgba(255, 255, 255, 0.9); max-width: 46ch; margin: 0; }
.mw-hero__actions { display: flex; gap: 0.75rem; flex-wrap: wrap; margin-top: 0.35rem; }
.mw-hero__hint {
  margin: 0;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-family: var(--mw-mono);
  font-size: 0.625rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--mw-gold);
}
.mw-hero__fig { position: relative; background: var(--mw-navy-deep); }
/* Definite height + the half's real aspect-ratio (set inline by the script)
   means the WIDTH is derived, so the photograph is never cropped and the
   column is exactly as wide as the picture. */
.mw-hero__fig .mw-wash__frame { width: 100%; height: auto; }
.mw-hero__cap {
  position: absolute;
  left: 0;
  bottom: 0;
  z-index: 2;
  margin: 0;
  padding: 0.5rem 0.9rem;
  background: var(--mw-white);
  color: var(--mw-ink-muted);
}

@media (max-width: 900px) {
  .mw-hero { grid-template-columns: minmax(0, 1fr); }
  .mw-hero__plate { padding-left: clamp(1.25rem, 5vw, 2.75rem); order: 2; }
  .mw-hero__fig { order: 1; }
  .mw-hero__fig .mw-wash__frame { height: auto; width: 100%; }
}

/* ---------------------------------------------------------------------------
   3 · PROOF STRIP — four facts in the trade's own ticket type.
   Replaces four icon cards that each occupied 140px of a phone screen.
   ------------------------------------------------------------------------ */
.mw-proof { border-bottom: 1px solid var(--mw-rule); background: var(--mw-white); }
.mw-proof__row {
  display: flex;
  flex-wrap: wrap;
  gap: 0.6rem 2.25rem;
  padding-block: 0.9rem;
}
.mw-proof__item {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  font-family: var(--mw-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--mw-ink-muted);
}
.mw-proof__item .mw-ico { color: var(--mw-burgundy); }

/* ---------------------------------------------------------------------------
   4 · SERVICE CARDS — THE SIGNATURE.
   Nine cards. Each one IS a before/after: one real job photograph cut by the
   wash edge, so the card shows the product without being asked. No border,
   no radius, no shadow — a photograph with a plate bolted under it.
   ------------------------------------------------------------------------ */
.mw-cards {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: clamp(0.75rem, 1.6vw, 1.25rem);
}

.mw-card {
  --cut: 56%;
  position: relative;
  display: flex;
  flex-direction: column;
  text-decoration: none;
  color: inherit;
  background: var(--mw-navy);
  transition: --cut 0.45s ease;
}
.mw-card:hover, .mw-card:focus-visible { --cut: 24%; }

.mw-card__field {
  position: relative;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--mw-navy-deep);
}
.mw-card__layer { position: absolute; inset: 0; overflow: hidden; }
.mw-card__layer img {
  position: absolute;
  top: 0;
  left: 0;
  width: 200%;
  height: 100%;
  max-width: none;   /* see the note on .mw-wash__layer img */
  object-fit: cover;
  display: block;
}
.mw-card__layer--after img { left: -100%; }
.mw-card__layer--before {
  clip-path: polygon(0 0, var(--cut) 0, calc(var(--cut) - var(--mw-skew)) 100%, 0 100%);
}
.mw-card.is-vertical .mw-card__layer img { width: 100%; height: 200%; left: 0; }
.mw-card.is-vertical .mw-card__layer--after img { top: -100%; }

/* The edge, drawn as its own clipped band so it tracks --cut exactly and
   stays true at any card size — no rotation, no trigonometry. */
.mw-card__edge {
  position: absolute;
  inset: 0;
  background: var(--mw-burgundy);
  clip-path: polygon(
    var(--cut) 0,
    calc(var(--cut) + 0.9%) 0,
    calc(var(--cut) + 0.9% - var(--mw-skew)) 100%,
    calc(var(--cut) - var(--mw-skew)) 100%
  );
  pointer-events: none;
}

/* The four surfaces with no job photo yet get the icon as their field, plus
   the service's own one-line description — saying what the work is beats a
   stock photograph of someone else's fence, and beats an empty box. */
.mw-card__blank {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.9rem;
  padding: 1rem;
  text-align: center;
  background: var(--mw-navy-deep);
  color: rgba(255, 255, 255, 0.45);
}
/* The icon is the field, so it stays quiet; the line under it is real text
   and gets a real text colour. */
.mw-card__blank .mw-ico { color: rgba(255, 255, 255, 0.42); }
.mw-card__blank span {
  color: rgba(255, 255, 255, 0.9);
  font-family: var(--mw-mono);
  font-size: 0.625rem;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  line-height: 1.6;
}
.mw-card__blank span {
  font-family: var(--mw-mono);
  font-size: 0.625rem;
  line-height: 1.5;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.72);
  max-width: 24ch;
}
@media (max-width: 420px) { .mw-card__blank span { display: none; } }

.mw-card__plate {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.7rem 0.85rem;
  background: var(--mw-navy);
  color: var(--mw-white);
}
.mw-card:hover .mw-card__plate,
.mw-card:focus-visible .mw-card__plate { background: var(--mw-burgundy); }
.mw-card__plate .mw-ico { color: var(--mw-gold); }
.mw-card:hover .mw-card__plate .mw-ico { color: var(--mw-white); }
.mw-card__name {
  font-weight: 700;
  font-size: 0.9375rem;
  line-height: 1.2;
  letter-spacing: -0.005em;
}
.mw-card__count {
  margin-left: auto;
  font-family: var(--mw-mono);
  font-size: 0.625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--mw-gold);
  white-space: nowrap;
}
.mw-card:hover .mw-card__count { color: var(--mw-white); }

@media (max-width: 900px) { .mw-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 420px) {
  .mw-card__plate { padding: 0.55rem 0.6rem; gap: 0.4rem; }
  .mw-card__name { font-size: 0.8125rem; }
  .mw-card__count { font-size: 0.5625rem; }
}

/* ---------------------------------------------------------------------------
   5 · OVERLAY — one dialog, hash-routed, serving the service brief and the
   photo view. nginx 301s every retired /services/<slug> URL to #service/<slug>,
   so these routes are load-bearing for print collateral and inbound links.
   ------------------------------------------------------------------------ */
.mw-overlay {
  border: 0;
  padding: 0;
  margin: 0;
  max-width: none;
  max-height: none;
  width: 100%;
  height: 100%;
  background: transparent;
  overflow: hidden;
}
.mw-overlay::backdrop { background: rgba(11, 26, 43, 0.86); }

.mw-overlay__panel {
  position: relative;
  width: min(1180px, 100%);
  max-height: 100%;
  margin: auto;
  overflow-y: auto;
  overscroll-behavior: contain;
  background: var(--mw-navy);
  color: var(--mw-white);
  display: grid;
  /* The stage is the NARROWER column. Every half in this archive is a vertical
     phone photograph, so a wide picture column would be mostly empty navy
     while the brief beside it — lede, method, what's included, facts — ran on
     for another screenful. */
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
}
.mw-overlay__close {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 5;
  width: 3rem;
  height: 3rem;
  display: grid;
  place-items: center;
  border: 0;
  background: var(--mw-burgundy);
  color: var(--mw-white);
  cursor: pointer;
}
.mw-overlay__close:hover { background: var(--mw-burgundy-deep); }

.mw-overlay__stage {
  background: var(--mw-navy-deep);
  display: flex;
  flex-direction: column;
  padding-top: 1rem;
}
.mw-overlay__figwrap { position: relative; }

/* Cap the picture by HEIGHT and let its aspect-ratio derive the width, so the
   caption and the rail of the other jobs stay on screen with it. Uncapped, a
   0.67 half at the stage's full width ran 1030px tall and pushed the rail, the
   caption and both phone numbers below the fold of a scrolling panel. */
.mw-overlay .mw-wash__frame {
  height: min(56vh, 30rem);
  width: auto;
  max-width: 100%;
  margin-inline: auto;
}

/* Prev / next sit on the image, not under it, so the whole rail is reachable
   without scrolling past the picture. */
.mw-overlay__step {
  position: absolute;
  /* Centred on the PHOTOGRAPH, not on the wrapper. The wrapper grew when the wand's
     controls landed under the frame, and `top: 50%` quietly walked the arrows below the
     picture they page through. The frame's height is declared right above, so the arrows
     use the same number rather than a second guess at it. */
  top: calc(min(56vh, 30rem) / 2);
  transform: translateY(-50%);
  z-index: 3;
  width: 2.75rem;
  height: 3.5rem;
  display: grid;
  place-items: center;
  border: 0;
  background: rgba(20, 51, 86, 0.88);
  color: var(--mw-white);
  cursor: pointer;
}
.mw-overlay__step:hover { background: var(--mw-burgundy); }
.mw-overlay__step--prev { left: 0; }
.mw-overlay__step--next { right: 0; }
.mw-overlay__step--prev .mw-ico { transform: rotate(180deg); }
.mw-overlay__step[hidden] { display: none; }

.mw-overlay__cap {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding: 0.8rem 1rem;
  color: rgba(255, 255, 255, 0.82);
}
.mw-overlay__cap .mw-meta { color: rgba(255, 255, 255, 0.66); }

/* The other jobs on this surface. */
.mw-rail {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 5.5rem;
  gap: 0.5rem;
  overflow-x: auto;
  padding: 0 1rem 1rem;
  scrollbar-width: thin;
}
.mw-rail__item {
  position: relative;
  padding: 0;
  border: 2px solid transparent;
  background: var(--mw-navy);
  aspect-ratio: 1 / 1;
  overflow: hidden;
  cursor: pointer;
}
/* A rail thumb shows the CLEAN half only. A square centred crop of the whole
   composite lands on the seam, so the strip was a row of before/after joins
   rather than a row of finished jobs. */
.mw-rail__item img {
  position: absolute;
  top: 0;
  left: -100%;
  width: 200%;
  height: 100%;
  max-width: none;
  object-fit: cover;
  display: block;
}
.mw-rail__item.is-vertical img { width: 100%; height: 200%; left: 0; top: -100%; }
.mw-rail__item[aria-current="true"] { border-color: var(--mw-gold); }
.mw-rail__item:hover { border-color: var(--mw-white); }

.mw-overlay__copy {
  padding: clamp(1.5rem, 3vw, 2.5rem);
  display: flex;
  flex-direction: column;
  gap: 1rem;
}
.mw-overlay__eyebrow {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--mw-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--mw-gold);
}
.mw-overlay__title {
  font-family: var(--mw-display);
  text-transform: uppercase;
  font-size: clamp(1.75rem, 3.4vw, 2.6rem);
  line-height: 0.96;
}
.mw-overlay__lede { font-size: 1.0625rem; color: rgba(255, 255, 255, 0.92); margin: 0; }
.mw-overlay__body { color: rgba(255, 255, 255, 0.8); font-size: 0.9375rem; }
.mw-overlay__body p { margin: 0 0 0.85rem; }

.mw-overlay__sub {
  font-family: var(--mw-mono);
  font-size: 0.625rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--mw-gold);
  border-top: 1px solid rgba(255, 255, 255, 0.25);
  padding-top: 0.9rem;
}
.mw-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 0.5rem; }
.mw-list li {
  display: flex;
  gap: 0.6rem;
  font-size: 0.9375rem;
  color: rgba(255, 255, 255, 0.9);
}
.mw-list li::before { content: "—"; color: var(--mw-gold); flex: none; }

.mw-facts { margin: 0; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 0.5rem 1rem; }
.mw-facts dt {
  font-family: var(--mw-mono);
  font-size: 0.625rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--mw-gold);
  padding-top: 0.15rem;
}
.mw-facts dd { margin: 0; font-size: 0.9375rem; color: rgba(255, 255, 255, 0.9); }

.mw-overlay__actions {
  display: flex;
  gap: 0.75rem;
  flex-wrap: wrap;
  border-top: 1px solid rgba(255, 255, 255, 0.25);
  padding-top: 1.1rem;
  margin-top: auto;
}

@media (max-width: 900px) {
  .mw-overlay__panel { grid-template-columns: minmax(0, 1fr); }
  .mw-overlay__copy { padding-bottom: 2rem; }
}

/* ---------------------------------------------------------------------------
   6 · CALL BAND — burgundy, both numbers, the largest type on the page.
   ------------------------------------------------------------------------ */
.mw-callband { background: var(--mw-burgundy); color: var(--mw-white); }
.mw-callband__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.5rem 2.5rem;
  flex-wrap: wrap;
  padding-block: clamp(1.75rem, 4vw, 2.75rem);
}
.mw-callband__kicker {
  font-family: var(--mw-mono);
  font-size: 0.6875rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--mw-gold);
  margin: 0 0 0.85rem;
}
.mw-callband .mw-lines { gap: 1rem 3rem; }
.mw-callband .mw-lines__num {
  font-family: var(--mw-display);
  font-size: clamp(1.75rem, 4.4vw, 2.75rem);
  line-height: 1;
  letter-spacing: 0.01em;
}
.mw-callband__note { margin: 1rem 0 0; font-size: 0.875rem; color: rgba(255, 255, 255, 0.86); }

/* ---------------------------------------------------------------------------
   7 · REVIEWS — ruled rows. These were four white boxes; a quotation does not
   need a container to be a quotation.
   ------------------------------------------------------------------------ */
/* Three short quotes across, then the long one full width. A two-column grid
   left the third quote beside an empty cell. */
.mw-reviews { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0 clamp(1.75rem, 3.5vw, 3rem); }
.mw-review { padding-block: 1.5rem; border-top: 1px solid var(--mw-rule); }
.mw-review--wide { grid-column: 1 / -1; }
.mw-review__stars { display: flex; gap: 0.15rem; color: var(--mw-gold); margin-bottom: 0.6rem; }
.mw-review__stars .mw-ico { width: 0.9rem; height: 0.9rem; }

/* THE STARS ARE THE LINK. Operator, 2026-09-06: the Google listing should be reachable
   by clicking the reviews and the stars, not only from a button. The stars are what a
   visitor reads as "reviews", so they are the thing that should respond.

   `a.mw-review__stars` still needs `display: flex` — the base rule sets it, but an <a>
   is inline by default and the cascade order here is what keeps the row a row.

   A 0.9rem star row is 14px tall, which is under the 24px WCAG 2.2 AA target floor the
   2026-09-04 pass established for standalone actions. `min-height` alone would only move
   the box; the padding is what puts the extra area under the thumb without moving the
   stars off the baseline they share with the quote beneath them. */
a.mw-review__stars {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  width: fit-content;
  padding-right: 0.35rem;
  margin-bottom: 0.35rem;
  text-decoration: none;
  border-radius: 2px;
  transition: color 0.15s ease, transform 0.15s ease;
}
a.mw-review__stars > span { display: flex; gap: 0.15rem; }
/* Gold on gold cannot signal anything, so the hover state moves rather than recolours:
   the row lifts a hair and warms. The focus ring is the same one every other control on
   this site uses. */
a.mw-review__stars:hover { color: var(--mw-gold-bright, #F0C93B); transform: translateY(-1px); }
a.mw-review__stars:focus-visible { outline: 3px solid var(--mw-gold); outline-offset: 3px; }

/* The head aside is prose everywhere else on the site; here it is a destination. It
   keeps the aside's type and gets a rule under it so it reads as a link without
   becoming a second button competing with the real one below the grid. */
a.mw-head__aside.mw-gplink {
  text-decoration: none;
  border-bottom: 1px solid var(--mw-gold);
  padding-bottom: 2px;
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  transition: color 0.15s ease;
}
a.mw-head__aside.mw-gplink:hover { color: var(--mw-gold); }
a.mw-head__aside.mw-gplink:focus-visible { outline: 3px solid var(--mw-gold); outline-offset: 3px; }

.mw-reviews__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.75rem 1.25rem;
  margin-top: 1.75rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--mw-rule);
}
.mw-reviews__foot .mw-meta { margin: 0; }

@media (prefers-reduced-motion: reduce) {
  a.mw-review__stars { transition: none; }
  a.mw-review__stars:hover { transform: none; }
}
.mw-review__quote { margin: 0 0 0.6rem; font-size: 1.0625rem; line-height: 1.55; color: var(--mw-ink); }
.mw-review__by { margin: 0; }

@media (max-width: 980px) { .mw-reviews { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 720px) { .mw-reviews { grid-template-columns: minmax(0, 1fr); } }

/* ---------------------------------------------------------------------------
   8 · QUOTE — the form, and the three steps that lead to it.
   ------------------------------------------------------------------------ */
.mw-quote { display: grid; grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); gap: clamp(1.75rem, 4vw, 3.5rem); }
.mw-quote__aside { display: flex; flex-direction: column; gap: 1.5rem; }

.mw-steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 1rem; }
.mw-steps li { display: grid; grid-template-columns: 2.25rem minmax(0, 1fr); gap: 0.85rem; }
.mw-steps b {
  font-family: var(--mw-mono);
  font-size: 0.875rem;
  color: var(--mw-burgundy);
  border-top: 2px solid var(--mw-burgundy);
  padding-top: 0.3rem;
}
.mw-steps span { font-size: 0.9375rem; color: var(--mw-ink-muted); }
.mw-steps strong { display: block; color: var(--mw-ink); }

.mw-who { display: flex; gap: 1rem; align-items: flex-start; border-top: 1px solid var(--mw-rule); padding-top: 1.25rem; }
.mw-who__mark {
  width: 3.5rem;
  height: 3.5rem;
  flex: none;
  border-radius: 50%;
  background: var(--mw-white) center/86% no-repeat
    url("/assets/images/0000-00-00.artifact-image.brocks_pressure_washing.brand-mark.avif");
  box-shadow: 0 0 0 2px var(--mw-gold);
}
.mw-who__name { font-weight: 700; }

@media (max-width: 860px) { .mw-quote { grid-template-columns: minmax(0, 1fr); } }

/* ---------------------------------------------------------------------------
   8b · ABOUT — the person, with a portrait that cannot break
   ------------------------------------------------------------------------ */

/* THE PORTRAIT IS TWO BACKGROUND LAYERS, and that is the whole trick.

   A photograph of Brock is the point of this plate, and until one is taken there
   is no honest thing to put in an <img alt="Brock Miller">: a missing file draws
   a broken-image icon in one browser and the alt text in another, and inventing
   a stand-in portrait of a real person is not on the table at all.

   Layered backgrounds solve it without a line of JavaScript. A background layer
   that 404s simply does not paint — the layer beneath it shows through. So the
   photograph sits on top and the brand mark sits under it, and the plate is a
   finished thing either way. The day the file lands at the path below it appears
   on its own; nothing here has to change and nothing has to be switched on.

   The identity is carried in the <figcaption> as real text, so the person's name
   is never something only a sighted visitor gets. That is why the div itself is
   role="presentation" rather than an img with a label that would be wrong in one
   of its two states. */
.mw-bio {
  display: grid;
  grid-template-columns: minmax(0, 20rem) minmax(0, 1fr);
  gap: clamp(1.5rem, 4vw, 3rem);
  align-items: start;
}
.mw-bio__fig { margin: 0; }
.mw-bio__portrait {
  aspect-ratio: 4 / 5;
  border-radius: 2px;
  background-color: var(--mw-navy-deep);
  background-image:
    url("/assets/images/0000-00-00.artifact-image.brocks_pressure_washing.brock_miller-portrait.avif"),
    url("/assets/images/0000-00-00.artifact-image.brocks_pressure_washing.brand-mark.avif");
  background-repeat: no-repeat, no-repeat;
  /* cover for the photograph; the mark is a mark and must not be cropped. */
  background-size: cover, 52%;
  background-position: 50% 28%, 50% 50%;
  box-shadow: 0 10px 30px rgba(20, 51, 86, 0.22);
}
.mw-bio__cap {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
  margin-top: 0.9rem;
  /* Burgundy is the BEFORE colour on this site and it carries meaning, so it
     marks the person the way it marks the dirty half — as a claim, not a tint. */
  border-left: 3px solid var(--mw-burgundy);
  padding-left: 0.75rem;
}
.mw-bio__name {
  font-family: var(--mw-display);
  font-size: 1.5rem;
  line-height: 1;
  text-transform: uppercase;
  letter-spacing: 0.01em;
}
.mw-prose--lead { font-size: 1.125rem; }
.mw-bio__copy .mw-prose + .mw-prose { margin-top: 1rem; }
.mw-bio__copy .mw-who { margin-top: 1.75rem; }

@media (max-width: 760px) {
  .mw-bio { grid-template-columns: minmax(0, 1fr); }
  /* A 4:5 portrait at full phone width eats the fold before a word is read.
     Wider and shorter keeps the face and the first paragraph on one screen. */
  .mw-bio__portrait { aspect-ratio: 3 / 2; background-position: 50% 24%, 50% 50%; }
  .mw-bio__fig { max-width: 30rem; }
}


/* ---------------------------------------------------------------------------
   9 · SERVICE AREAS — contact page. Two territories, named.
   ------------------------------------------------------------------------ */
.mw-areas { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: clamp(1.5rem, 3vw, 2.5rem); }
.mw-area { border-top: 2px solid var(--mw-navy); padding-top: 1rem; }
.mw-area__name { font-weight: 800; font-size: 1.125rem; color: var(--mw-navy); margin-bottom: 0.15rem; }
.mw-area__towns { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.9rem; padding: 0; list-style: none; }
.mw-area__towns li {
  font-family: var(--mw-mono);
  font-size: 0.625rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--mw-burgundy);
  border: 1px solid var(--mw-burgundy);
  padding: 0.3rem 0.5rem;
}
@media (max-width: 720px) { .mw-areas { grid-template-columns: minmax(0, 1fr); } }

/* ---------------------------------------------------------------------------
   10 · MOTION
   ------------------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .mw-card { transition: none; }
  .mw-card:hover, .mw-card:focus-visible { --cut: 24%; }
  .mw-wash.is-hinting .mw-wash__hint { animation: none; }
  .mw-wash__jet { transition: none; }
  /* Someone who asked for less motion still gets the jet and the wand — those only ever
     appear under their own pointer — but the spray stops spitting, and the unattended
     arrival stroke does not run at all. That suppression is in JS (`demoWillRun`),
     because the stroke is canvas paint, not an animation this block could stop.

     What this no longer does is `display: none` the wand. That was right while the wand
     was a self-running decoration; it is the POINTER now, and hiding somebody's cursor
     because they asked for less motion makes the control worse for them, not calmer. */
  .mw-wash__jet::after { animation: none; }
  .mw-wash__wand { transition: none; }
}

/* TAP TARGETS — WCAG 2.2 AA (2.5.8). Measured 2026-09-04 at a 390px viewport:
   each selector below drew shorter than 24px. Links inside running sentences are
   exempt and are deliberately left alone; these are standalone ACTIONS, where a
   missed tap is a dead end rather than a re-read. The type size does not change —
   only the area that responds to a thumb. */

/* "Text this number" drew 246x16 — the primary contact action on a phone, and the
   one a wet thumb is most likely to reach for. */
.mw-lines__sms,
.mw-footer__production {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
}

/* Same 24px floor, but block-level. `inline-flex` was fine while each footer
   column held at most ONE of these; the About link added a second and the two
   ran together on one line ("Contact Back to the work"). `flex` puts them back
   one per line and `fit-content` keeps the hit area on the words rather than
   spanning the whole column, which would make the empty space beside a link
   navigate. */
.mw-footer__line--small {
  display: flex;
  width: fit-content;
  align-items: center;
  min-height: 24px;
}
