/* base.css — fleet-shared foundation. NEVER edited per client.
   Brand comes from :root tokens (head.php) + one skin file.
   Skins may set: --radius, --shadow, --section-bg-alt, --hero-overlay,
   --btn-style hooks, heading transforms. Keep skins small. */

*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth;overflow-x:clip}
@media (prefers-reduced-motion:reduce){html{scroll-behavior:auto}}
body{overflow-x:clip;font-family:var(--body);background:var(--paper,#fff);
  color:var(--ink,#1c1c1c);line-height:1.65;-webkit-font-smoothing:antialiased}
img,video{max-width:100%;display:block}
a{color:inherit}
.wrap{max-width:1140px;margin:0 auto;padding:0 22px}
h1,h2,h3{font-family:var(--display);font-weight:800;line-height:1.08;letter-spacing:-.015em}
h1{font-size:clamp(2.2rem,5.2vw,3.6rem)}
h2{font-size:clamp(1.7rem,3.6vw,2.5rem);margin:.5rem 0 1rem}
.eyebrow{display:inline-block;font-weight:700;font-size:.8rem;letter-spacing:.14em;
  text-transform:uppercase;color:var(--primary)}
.sub{font-size:1.05rem;max-width:58ch;color:var(--muted,#4b4b4b)}
section{padding:4.4rem 0}
.section-alt{background:var(--section-bg-alt,#f6f6f4)}

/* a11y */
/* The skip link is accent-FILLED, so its label has to follow --accent-text
   like every other accent surface (.btn-accent, the form submit, the badges).
   It read `color:var(--dark)` until 2026-09-02 — the one accent surface that
   ignored the token. Harmless on a light accent, but on a site whose brand
   accent is dark (belles-hauling-moving's #C8102E) it renders dark-on-dark,
   which is a poor look on the one control that exists purely for a11y.
   Fallback is still var(--dark), so any site that sets no accent_text is
   unchanged. Found on magnolia-ridge-collective, whose gold accent measured
   4.34:1 here against 4.98:1 everywhere else on the same page. */
.skip{position:absolute;left:-9999px;top:0;background:var(--accent);
  color:var(--accent-text,var(--dark));
  padding:.6rem 1rem;font-weight:600;z-index:200}
.skip:focus{left:0}
:focus-visible{outline:3px solid var(--primary);outline-offset:3px}

/* buttons */
.btn{display:inline-flex;align-items:center;gap:.5rem;font-weight:600;font-size:.95rem;
  text-decoration:none;padding:.75rem 1.35rem;border-radius:var(--radius,10px);cursor:pointer;
  border:2px solid var(--dark);background:#fff;color:var(--dark);
  box-shadow:var(--shadow,0 2px 8px rgba(0,0,0,.12));transition:transform .12s ease}
.btn:hover{transform:translateY(-2px)}
.btn-accent{background:var(--accent);border-color:var(--accent-border,var(--dark));color:var(--accent-text,var(--dark))}
.btn-primary{background:var(--primary);border-color:var(--primary);color:#fff}
.btn-big{font-size:1.05rem;padding:.95rem 1.5rem;justify-content:center}

/* header */
.site-header{position:fixed;inset:0 0 auto 0;z-index:100;background:var(--paper,#fff);
  border-bottom:var(--header-line,1px solid #e4e4e0);display:flex;align-items:center;
  justify-content:space-between;padding:12px 22px;gap:16px}
.brand img{height:48px;width:auto}
.main-nav{display:flex;align-items:center;gap:22px}
.main-nav a{text-decoration:none;font-size:.93rem;font-weight:500}
.main-nav a:hover,.main-nav a[aria-current]{color:var(--primary)}
.hdr-cta{display:flex;align-items:center;gap:14px}
.tel-mini{font-weight:600;font-size:.9rem;text-decoration:none;white-space:nowrap}
.menu-btn{display:none;background:#fff;border:2px solid var(--dark);border-radius:8px;
  padding:.5rem .8rem;font-family:var(--display);font-weight:800;cursor:pointer}
.mobile-nav{display:none;position:fixed;top:66px;left:0;right:0;z-index:99;background:var(--paper,#fff);
  border-bottom:1px solid #e4e4e0;padding:1rem 22px 1.3rem;max-height:calc(100vh - 66px);overflow:auto}
.mobile-nav.open{display:block}
.mobile-nav a{display:block;padding:.7rem 0;font-weight:600;text-decoration:none;border-bottom:1px solid #eee}
/* Primary CTA repeated at the top of the mobile menu — the desktop .hdr-cta
   button is hidden below 860px (site-header rule below) and the mobile nav
   previously offered no equivalent, only a Call link at the very bottom.
   Higher specificity than ".mobile-nav a" (2 classes+tag beats 1 class+tag)
   so it wins over the plain-link rule above instead of fighting it. */
.mobile-nav a.mobile-cta{display:flex;justify-content:center;align-items:center;
  margin:.2rem 0 1rem;padding:.85rem 1rem;border:2px solid var(--accent-border,var(--dark));
  border-radius:var(--radius,10px);background:var(--accent);color:var(--accent-text,var(--dark))}
/* 860, not 960: with a dropdown nav the top level is only ~4 items (~350px),
   so the desktop bar fits comfortably down to here. At 960 a Windows laptop at
   125-150% display scaling reports under the breakpoint and silently gets the
   MOBILE menu — which reads as "the desktop dropdown is broken". */
@media (max-width:860px){.main-nav{display:none}.tel-mini{display:none}
  .hdr-cta .btn{display:none}.menu-btn{display:inline-flex}.brand img{height:40px}}

/* ---------- dropdown nav (parents are inferred from the URL path) ----------
   Desktop opens on :hover AND :focus-within, so keyboard users get the same
   menu without any JS. Never hover-only — that strands keyboard and switch
   users on the parent page. */
.nav-item{position:relative;display:flex;align-items:center}
.nav-item > a{text-decoration:none;font-size:.93rem;font-weight:500;
  display:inline-flex;align-items:center;gap:.32rem;padding:.5rem 0}
.nav-item > a:hover,.nav-item > a[aria-current],.nav-item > a.is-ancestor{color:var(--primary)}
.nav-caret{background:none;border:0;cursor:pointer;padding:.5rem .1rem .5rem .3rem;
  display:inline-flex;align-items:center;color:inherit;line-height:1}
.caret{width:0;height:0;border-left:4px solid transparent;border-right:4px solid transparent;
  border-top:5px solid currentColor;opacity:.55;transition:transform .15s ease}
.nav-item:hover .caret,.nav-item:focus-within .caret,.nav-item.open .caret{transform:rotate(180deg);opacity:1}
.sub-menu{position:absolute;top:100%;left:50%;transform:translate(-50%,6px);
  min-width:210px;background:var(--paper,#fff);border:1px solid #e4e4e0;
  border-radius:var(--radius,10px);box-shadow:0 12px 30px rgba(0,0,0,.14);
  padding:.5rem;opacity:0;visibility:hidden;pointer-events:none;
  transition:opacity .15s ease,transform .15s ease;z-index:120}
.sub-menu.cols2{min-width:400px;columns:2;column-gap:.4rem}
.nav-item:hover .sub-menu,.nav-item:focus-within .sub-menu,.nav-item.open .sub-menu{
  opacity:1;visibility:visible;pointer-events:auto;transform:translate(-50%,0)}
.sub-menu a{display:block;padding:.5rem .7rem;font-size:.9rem;font-weight:500;
  text-decoration:none;border-radius:7px;break-inside:avoid;white-space:nowrap}
.sub-menu a:hover,.sub-menu a[aria-current]{background:var(--section-bg-alt,#f6f6f4);color:var(--primary)}
/* hover bridge — stops the menu closing in the gap between link and panel */
.nav-item.has-sub::after{content:"";position:absolute;top:100%;left:0;right:0;height:10px}

/* ---------- mobile nav: collapsed submenus ---------- */
.m-row{display:flex;align-items:center;justify-content:space-between;gap:.5rem;
  border-bottom:1px solid #eee}
.m-row a{flex:1;border-bottom:0 !important}
.m-toggle{flex:0 0 auto;background:none;border:0;padding:.7rem .4rem;cursor:pointer;
  font-family:var(--display);font-weight:800;font-size:1.25rem;line-height:1;color:var(--primary);
  min-width:44px;min-height:44px}
.m-sub{padding-left:.9rem;border-left:2px solid var(--accent);margin:.2rem 0 .5rem}
.m-sub[hidden]{display:none}
.m-sub a{font-weight:500;font-size:.94rem;padding:.55rem 0}

/* ---------- HERO (standard: photo + scroll motion, no video weight) ---------- */
.hero{position:relative;min-height:76vh;display:flex;align-items:center;
  padding:7.5rem 0 4rem;overflow:hidden;color:#fff}
.hero-media{position:absolute;inset:-12% 0;z-index:0}
.hero-media img{width:100%;height:100%;object-fit:cover}
.hero-shade{position:absolute;inset:0;z-index:1;background:var(--hero-overlay,
  linear-gradient(180deg,rgba(10,10,12,.62),rgba(10,10,12,.38) 50%,rgba(10,10,12,.66)))}
.hero .wrap{position:relative;z-index:2;width:100%}
/* Text-shadow safety net, not a color fix: measured on yossefs-laffa-milia
   that even a deliberately-darkened --hero-overlay (see restaurant skin)
   still leaves a bright patch somewhere in most real photos (a window, a
   pale wall, a plate) — sampled at 215,206,181 there, an accent color close
   in lightness to the photo's own tones (a common case: warm accent, warm
   food/wood photography) measured 1.6:1 against it even under the darker
   overlay, nowhere near AA. A gradient can only darken the AVERAGE; it can't
   promise every accent color clears every photo everywhere text might sit.
   The shadow is what actually holds regardless of the color pairing or
   what's in the shot behind it — cheap, additive, invisible when the
   backdrop is already dark, so it costs nothing on sites that never hit
   this. h1 inherits it too since the same bright-patch risk applies. */
.hero .eyebrow{color:var(--accent);text-shadow:0 1px 3px rgba(0,0,0,.85),0 2px 14px rgba(0,0,0,.55)}
.hero h1{text-shadow:0 2px 6px rgba(0,0,0,.55),0 1px 3px rgba(0,0,0,.7)}
.hero .sub{color:rgba(255,255,255,.92);text-shadow:0 1px 4px rgba(0,0,0,.5)}
.hero-grid{display:grid;grid-template-columns:1fr;gap:2.2rem;align-items:center}
.hero-grid.has-form{grid-template-columns:1.05fr .95fr}
@media (max-width:920px){.hero-grid.has-form{grid-template-columns:1fr}}
.hero .cta-row{display:flex;flex-wrap:wrap;gap:14px;margin-top:1.8rem}

/* scroll-driven hero pan/zoom — CSS-only where supported */
@supports (animation-timeline: scroll()) {
  @media (prefers-reduced-motion: no-preference) {
    .hero-media img{animation:heroPan linear both;animation-timeline:scroll();
      animation-range:0 120vh;transform-origin:center}
    @keyframes heroPan{from{transform:scale(1.12) translateY(0)}to{transform:scale(1) translateY(6%)}}
  }
}

/* ---------- scroll reveals (IO adds .in via site.js; CSS-only upgrade below) ---------- */
@media (prefers-reduced-motion: no-preference){
  .rv{opacity:0;transform:translateY(16px);transition:opacity .55s ease,transform .55s ease}
  .rv.in{opacity:1;transform:none}
}

/* ---------- content blocks ---------- */
.grid3{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:2.3rem}
@media (max-width:960px){.grid3{grid-template-columns:repeat(2,1fr)}}
@media (max-width:620px){.grid3{grid-template-columns:1fr}}
.tile{background:#fff;border:var(--tile-border,1px solid #e4e4e0);
  border-radius:var(--radius,12px);padding:1.6rem 1.5rem;box-shadow:var(--shadow,0 2px 10px rgba(0,0,0,.06))}
.tile h3{font-size:1.08rem;margin-bottom:.5rem}
.tile p{font-size:.94rem;color:var(--muted,#4b4b4b)}
.split{display:grid;grid-template-columns:1.1fr .9fr;gap:44px;align-items:center}
@media (max-width:860px){.split{grid-template-columns:1fr}}
/* Uncropped, and capped so the photo never dominates the row.
   WIDTH-driven with a ratio-derived cap: width:100% fills the column, max-width
   stops it before it would exceed the height cap (420px desktop, 320px phones),
   and height:auto lets the ratio set the height.

   Why not the old `height:420px; width:auto`: for a LANDSCAPE ratio the derived
   width (420 x 4/3 = 560px) is wider than the .9fr column (473px at a 1140px
   wrap), so max-width:100% clamped it to 473x420 — ratio 1.127, not 1.333 — and
   object-fit:cover then quietly cropped ~18% off the SIDES of every landscape
   photo. Measured on a real render, invisible in the stylesheet. Portrait hid
   the bug for a year because 420 x 3/4 = 315px always fitted the column.
   Width-driven fits both orientations with no crop at any viewport.

   Do NOT switch to width:auto + max-height — that leaves a lazy image at 0x0
   until it loads (measured), the exact layout shift the width/height attributes
   exist to prevent. width:100% + aspect-ratio reserves the box either way. */
.split img{width:100%;max-width:calc(420px * var(--photo-ratio,3/4));
  height:auto;aspect-ratio:var(--photo-ratio,3/4);
  object-fit:cover;display:block;margin:0 auto;
  border-radius:var(--radius,12px);box-shadow:var(--shadow,0 6px 24px rgba(0,0,0,.12))}
@media (max-width:860px){.split img{max-width:calc(320px * var(--photo-ratio,3/4))}}

/* ---------- before / after pairs (cleanouts, restoration, painting, detailing) ----------
   Markup: .ba-set > div > (.ba > figure.ba-before + figure.ba-after) + .ba-cap
   Pairs must be the SAME shot from the SAME position — that is the whole point.

   PHONE PHOTOS ARE PORTRAIT. Rendered at their native 3:4 they tower: two of
   them side by side in a 1140px wrap is 750px tall, and a stack of three pairs
   on a phone is an endless scroll nobody reaches the bottom of. So we CROP to
   landscape with object-fit rather than letting the source dictate height. */
/* DO NOT crop these to landscape. Client photos come off a phone in portrait
   (3:4); forcing them into a 4:3 box throws away ~44% of the frame, which is
   exactly the half of a room that shows how full it was. The good junk-removal
   galleries all use LANDSCAPE SOURCE photos shown uncropped — so we match the
   box to the source instead of the other way round, and control height by
   limiting how wide a PAIR gets rather than by cutting the picture.
   --photo-ratio matches the client's photos: 3/4 portrait (default) or 4/3 if
   they shoot landscape. Set it ONCE per site (it drives .split img too); never
   crop against it. */
.ba-set{display:grid;gap:2.6rem;margin-top:2.3rem;justify-items:center}
/* One framed object with a hairline seam down the middle, rather than two
   separate bordered boxes floating next to each other. Same markup, same
   photos, reads as deliberate instead of as two thumbnails (2026-08-31). */
.ba{display:grid;grid-template-columns:1fr 1fr;gap:0;width:100%;max-width:680px;
  border-radius:var(--radius,12px);overflow:hidden;background:var(--dark);
  box-shadow:var(--shadow,0 6px 24px rgba(0,0,0,.12));
  transition:transform .2s ease,box-shadow .2s ease}
@media (hover:hover){.ba:hover{transform:translateY(-2px);box-shadow:0 14px 34px rgba(0,0,0,.17)}}
@media (prefers-reduced-motion:reduce){.ba{transition:none}.ba:hover{transform:none}}
.ba figure{margin:0;position:relative;min-width:0}
.ba img{width:100%;height:auto;aspect-ratio:var(--photo-ratio,3/4);object-fit:cover;
  border-radius:0;border:0;display:block}
.ba .ba-after{box-shadow:inset 3px 0 0 #fff}   /* the seam, drawn inside the frame */
.ba figcaption{position:absolute;top:12px;left:12px;z-index:2;font-family:var(--display);
  font-weight:800;font-size:.7rem;letter-spacing:.12em;text-transform:uppercase;
  padding:.34rem .7rem;border-radius:999px;background:var(--dark);color:#fff;
  box-shadow:0 1px 6px rgba(0,0,0,.28)}
.ba .ba-after figcaption{background:var(--accent);color:var(--accent-text,var(--dark))}
.ba-cap{margin-top:.7rem;font-size:.92rem;color:var(--muted,#4b4b4b)}
.ba-hint{display:none;font-size:.82rem;color:var(--muted,#4b4b4b);margin-top:.8rem}

/* ---------- comparison slider (2026-08-31) — OPT IN WITH .ba-compare ----------
   ONLY applies to <div class="ba-set ba-compare">. It is opt-in because an
   overlay slider makes a promise the photos have to keep: the two frames must
   be shot from the SAME position, or the sink jumps across the seam as you
   drag and the whole thing reads as broken. Checked against a real client's
   pairs (wfb-cleaning-solutions) — handheld phone photos taken weeks apart are
   recognisably the same room and nowhere near registered, so that site keeps
   the side-by-side above. Opt a site in only after looking at its pairs.

   PROGRESSIVE ENHANCEMENT, and deliberately so. Everything above stays exactly
   as it was; site.js adds .is-slider only when it can wire the control up. If
   the script fails, is blocked, or never runs, the side-by-side pair renders
   the way it always has — which is why this was safe to roll out to sites that
   were already live.

   The control is a real <input type="range"> stretched over the whole box.
   That is the entire trick: the browser gives us pointer dragging, click-to-
   jump, touch, keyboard arrows, Home/End and screen-reader semantics for free,
   so there is no custom pointer-event code to get wrong on a phone. JS only
   copies the value into --pos.

   The thumb is 1px wide on purpose: a normal thumb insets the value-to-pixel
   mapping by half a thumb at each end, so the divider would visibly lag the
   cursor near the edges. */
.ba.is-slider{grid-template-columns:1fr;max-width:660px;position:relative;
  border-radius:var(--radius,12px);overflow:hidden;touch-action:pan-y;
  box-shadow:var(--shadow,0 6px 24px rgba(0,0,0,.12))}
.ba.is-slider figure{grid-area:1/1;margin:0}
.ba.is-slider img{border:0;border-radius:0;display:block}
/* After is drawn on top and clipped to everything RIGHT of the handle, so the
   left half reads as "before" the way people scan a page. */
.ba.is-slider .ba-after{clip-path:inset(0 0 0 var(--pos,50%))}
.ba.is-slider .ba-before figcaption{left:12px;right:auto}
.ba.is-slider .ba-after figcaption{left:auto;right:12px}

.ba-range{position:absolute;inset:0;width:100%;height:100%;margin:0;padding:0;
  opacity:0;cursor:ew-resize;z-index:3;-webkit-appearance:none;appearance:none;background:none}
.ba-range::-webkit-slider-thumb{-webkit-appearance:none;width:1px;height:100%;background:transparent}
.ba-range::-moz-range-thumb{width:1px;height:100%;border:0;background:transparent}
.ba-range::-moz-range-track{background:transparent}

.ba-divider{position:absolute;top:0;bottom:0;left:var(--pos,50%);width:2px;
  transform:translateX(-1px);background:#fff;pointer-events:none;z-index:2;
  box-shadow:0 0 0 1px rgba(0,0,0,.22)}
.ba-grip{position:absolute;top:50%;left:50%;width:44px;height:44px;margin:-22px 0 0 -22px;
  border-radius:50%;background:#fff;box-shadow:0 2px 12px rgba(0,0,0,.34);
  display:flex;align-items:center;justify-content:center;gap:6px}
.ba-grip::before,.ba-grip::after{content:"";width:0;height:0;
  border-top:5px solid transparent;border-bottom:5px solid transparent}
.ba-grip::before{border-right:7px solid var(--dark)}
.ba-grip::after{border-left:7px solid var(--dark)}
/* Keyboard users get a visible focus ring on the grip, since the input itself
   is transparent. :focus-visible so a mouse drag does not leave it stuck on. */
.ba-range:focus-visible ~ .ba-divider .ba-grip{outline:3px solid var(--accent);outline-offset:3px}

/* Once the slider is live, one pair is ONE image tall instead of two, so the
   phone carousel is no longer earning its complexity — a plain vertical stack
   reads better and removes the horizontal-drag conflict between swiping the
   carousel and dragging the handle. site.js adds .has-slider to the set. */
@media (max-width:760px){
  .ba-set.has-slider{display:grid;overflow:visible;margin-left:0;margin-right:0;padding:0}
  .ba-set.has-slider > *{flex:none;width:100%}
  .ba-set.has-slider .ba{grid-template-columns:1fr;gap:0}
}
/* .ba-hint is a SIBLING of .ba-set, not a child of it — scoping this to
   `.ba-set.has-slider .ba-hint` silently matched nothing (found by measuring
   the rendered page, not by reading this file). site.js marks the element
   itself instead, and the drag affordance is worth showing at every width,
   not just on a phone: a slider nobody realises is interactive is just a
   cropped photo. */
.ba-hint.is-live{display:block}

/* Phone: swipe between PAIRS instead of scrolling past six tall photos.
   Within a pair, before STACKS ABOVE after rather than sitting beside it —
   two 140px thumbnails side by side on a 375px screen are too small to read,
   which defeats the point. Stacked, each photo gets the full card width and
   the comparison still works because they are adjacent. */
@media (max-width:760px){
  .ba-set{display:flex;gap:14px;overflow-x:auto;scroll-snap-type:x mandatory;
    -webkit-overflow-scrolling:touch;scrollbar-width:none;
    margin-left:-22px;margin-right:-22px;padding:0 22px 4px;scroll-padding-left:22px}
  .ba-set::-webkit-scrollbar{display:none}
  .ba-set > *{flex:0 0 86%;scroll-snap-align:start}
  /* Side by side on a phone puts each photo at ~160px, which is too small to
     read — the exact complaint that started this. Stacked, each gets the full
     card width and the pair still reads as a pair because they are adjacent
     and labelled. The carousel means only one card is on screen at a time. */
  .ba{grid-template-columns:1fr;gap:8px;max-width:none}
  .ba-hint{display:block}
}

/* ---------- photo slider (2026-09-02) — OPT IN WITH .slider ----------
   A horizontal film strip for a set of photos. Inert unless a page actually
   writes <div class="slider"><div class="slider-track">…, so every existing
   site is byte-for-byte unaffected.

   WHY UNIFORM HEIGHT AND NOT A UNIFORM BOX: the fleet rule is "match the box
   to the SOURCE — never crop to a house style", and a carousel of fixed-ratio
   cells breaks it, slicing the subject out of every photo whose shape doesn't
   match. Fixing the HEIGHT and letting width follow keeps every frame
   uncropped: portraits come out narrow, landscapes wide, and the strip still
   aligns top and bottom. It also reads more editorial than equal tiles.

   The sliding itself is native CSS scroll-snap, so touch swipe, momentum and
   trackpad gestures are the browser's own — nothing to hand-roll and nothing
   to break on a phone. site.js only adds arrows, a progress rail and keyboard
   support ON TOP; with JS off this is still a usable scrolling strip. */
.slider{position:relative;margin-top:2.2rem;--slide-h:400px}
.slider-track{display:flex;gap:14px;overflow-x:auto;overscroll-behavior-x:contain;
  scroll-snap-type:x mandatory;scroll-behavior:smooth;
  scrollbar-width:none;-ms-overflow-style:none;padding:2px 2px 6px}
.slider-track::-webkit-scrollbar{display:none}
/* snap-align START, not center. `center` cannot be satisfied by the first
   slide in a left-aligned strip — its snap position would be a negative scroll
   offset, so a mandatory container keeps yanking the scroll back to 0 and the
   arrows appear to do nothing. Caught by driving the real thing: with `center`
   a scrollLeft of 500 read back as 0; with `start` it lands. */
.slider-track > *{flex:0 0 auto;margin:0;scroll-snap-align:start}
.slider-track img{display:block;height:var(--slide-h);width:auto;max-width:none;
  border-radius:var(--radius,12px);border:var(--tile-border,1px solid #e4e4e0);
  transition:transform .35s ease,box-shadow .35s ease}
.slider-track:focus-visible{outline:3px solid var(--primary);outline-offset:4px}
@media (hover:hover){
  .slider-track img:hover{transform:translateY(-4px);box-shadow:0 14px 34px rgba(0,0,0,.16)}
}
@media (max-width:900px){.slider{--slide-h:320px}}
@media (max-width:560px){.slider{--slide-h:240px}}
/* Arrows and rail are BUILT BY site.js, so they can never sit there dead. */
.slider-nav{display:flex;align-items:center;gap:12px;margin-top:1rem}
.slider-btn{width:42px;height:42px;flex:0 0 auto;border-radius:999px;cursor:pointer;
  border:var(--tile-border,1px solid #e4e4e0);background:var(--paper,#fff);
  color:var(--ink);font:inherit;font-size:1.2rem;line-height:1;padding:0;
  display:flex;align-items:center;justify-content:center;
  transition:transform .2s ease,opacity .2s ease,background .2s ease}
.slider-btn:disabled{opacity:.3;cursor:default}
@media (hover:hover){.slider-btn:not(:disabled):hover{transform:translateY(-2px)}}
.slider-rail{flex:1;height:3px;border-radius:3px;background:rgba(0,0,0,.10);
  position:relative;overflow:hidden}
.slider-rail::after{content:"";position:absolute;top:0;bottom:0;left:0;
  width:var(--slider-w,30%);transform:translateX(var(--slider-x,0%));
  background:var(--primary);border-radius:3px;transition:transform .15s linear}
@media (prefers-reduced-motion:reduce){
  .slider-track{scroll-behavior:auto}
  .slider-track img,.slider-btn,.slider-rail::after{transition:none}
  .slider-track img:hover{transform:none}
}

/* ---------- plain fact list (services, FAQ answers) ---------- */
.ticks{list-style:none;margin-top:1rem;columns:2;column-gap:2rem}
.ticks li{font-size:.94rem;padding:.2rem 0 .2rem 1.4rem;position:relative;break-inside:avoid}
.ticks li::before{content:"";position:absolute;left:0;top:.62em;width:9px;height:9px;
  background:var(--accent);border-radius:2px}
@media (max-width:620px){.ticks{columns:1}}

/* ---------- FAQ ---------- */
.faq{margin-top:1.6rem;max-width:74ch}
.faq details{border-bottom:1px solid #dededa;padding:1rem 0}
.faq summary{font-family:var(--display);font-weight:800;font-size:1.02rem;cursor:pointer;
  list-style:none;display:flex;justify-content:space-between;gap:1rem;align-items:center}
.faq summary::-webkit-details-marker{display:none}
.faq summary::after{content:"+";color:var(--primary);font-size:1.3rem;line-height:1}
.faq details[open] summary::after{content:"–"}
.faq p{margin-top:.7rem;font-size:.95rem;color:var(--muted,#4b4b4b);max-width:68ch}

/* ---------- steps ---------- */
.steps{counter-reset:step;display:grid;grid-template-columns:repeat(4,1fr);gap:20px;margin-top:2.3rem}
@media (max-width:900px){.steps{grid-template-columns:repeat(2,1fr)}}
@media (max-width:520px){.steps{grid-template-columns:1fr}}
.steps li{list-style:none;counter-increment:step}
.steps li::before{content:counter(step);display:flex;align-items:center;justify-content:center;
  width:38px;height:38px;border-radius:50%;background:var(--accent);color:var(--accent-text,var(--dark));
  font-family:var(--display);font-weight:800;margin-bottom:.7rem}
.steps h3{font-size:1rem;margin-bottom:.35rem}
.steps p{font-size:.92rem;color:var(--muted,#4b4b4b)}

/* ---------- lead form panel ---------- */
.lead-panel{background:#fff;color:var(--ink,#1c1c1c);border-radius:var(--radius,14px);
  overflow:hidden;border:var(--tile-border,1px solid #e4e4e0);
  box-shadow:var(--shadow,0 10px 34px rgba(0,0,0,.18))}
.lead-head{padding:1rem 1.3rem;border-bottom:var(--tile-border,1px solid #e4e4e0);
  font-family:var(--display);font-weight:800;font-size:1.1rem}
.lead-form{padding:1.3rem;display:grid;gap:1rem}
.lead-form label{display:grid;gap:.35rem;font-weight:600;font-size:.86rem}
.lead-form input,.lead-form textarea{font-family:var(--body);font-size:.95rem;
  padding:.72rem .8rem;border:1.5px solid #c9c9c4;border-radius:8px;background:#fff}
.lead-form input:focus,.lead-form textarea:focus{outline:3px solid var(--primary);outline-offset:0}
.lead-form .fine{font-size:.8rem;color:var(--muted,#4b4b4b)}
.lead-form .fine a{font-weight:600}
.hp{position:absolute;left:-9999px;opacity:0;height:0;overflow:hidden}
@media (max-width:560px){
  .lead-form input,.lead-form textarea{font-size:16px;padding:.8rem .85rem} /* stops iOS zoom */
}

/* ---------- client portal ---------- */
.portal-stats{display:grid;grid-template-columns:repeat(4,1fr);gap:14px;margin-top:1.8rem}
@media (max-width:620px){.portal-stats{grid-template-columns:repeat(2,1fr)}}
.portal-stat{background:#fff;border:var(--tile-border,1px solid #e4e4e0);border-radius:var(--radius,12px);
  padding:1rem 1.1rem;text-align:center}
.portal-stat b{display:block;font-family:var(--display);font-weight:800;font-size:1.7rem;color:var(--primary)}
.portal-stat span{font-size:.8rem;color:var(--muted,#4b4b4b);text-transform:uppercase;letter-spacing:.04em}
.portal-toolbar{display:flex;align-items:center;gap:1rem;flex-wrap:wrap;margin:1.6rem 0}
.portal-toolbar input{flex:1 1 260px;max-width:420px;padding:.72rem .8rem;
  border:1.5px solid #c9c9c4;border-radius:8px;font-size:.95rem}
.portal-count{font-size:.85rem;color:var(--muted,#4b4b4b);white-space:nowrap}
.portal-ago{font-size:.78rem;color:var(--muted,#4b4b4b)}
.portal-badge-new{display:inline-block;background:var(--accent);color:var(--accent-text,var(--dark));
  font-size:.68rem;font-weight:800;text-transform:uppercase;letter-spacing:.04em;
  padding:.15rem .45rem;border-radius:5px;vertical-align:middle}
.portal-table{width:100%;border-collapse:collapse;font-size:.9rem;min-width:640px}
.portal-table th{text-align:left;font-family:var(--display);font-weight:800;font-size:.75rem;
  letter-spacing:.05em;text-transform:uppercase;padding:.6rem .7rem;border-bottom:2px solid #ddd;
  color:var(--muted,#4b4b4b)}
.portal-table td{padding:.65rem .7rem;border-bottom:1px solid #eee;vertical-align:top}
.portal-table tr:hover td{background:var(--section-bg-alt,#f6f6f4)}
.portal-table td a{text-decoration:none;font-weight:600}
@media (max-width:640px){
  /* stacked "label: value" cards instead of a horizontally-scrolled table —
     a lead list read on a phone in a truck should not need side-scrolling */
  .portal-table thead{display:none}
    /* min-width:0 is load-bearing: the desktop rule sets min-width:640px so the
     lead table can scroll, and the stacked-card layout below inherits it —
     which forces a 640px-wide block inside a 375px phone and hands the client
     a horizontally scrolling portal. Found 2026-09-02. */
.portal-table, .portal-table tbody, .portal-table tr, .portal-table td{display:block;width:100%;min-width:0}
  .portal-table tr{border:1px solid #e4e4e0;border-radius:10px;margin-bottom:.9rem;padding:.4rem .8rem}
  .portal-table td{border-bottom:0;padding:.35rem 0}
  .portal-table td::before{content:attr(data-label) ": ";font-weight:700;color:var(--muted,#4b4b4b)}
}

/* ---------- reviews ---------- */
.reviews-shell{margin-top:2rem}
.reviews-more{margin-top:1rem;font-size:.92rem}
.reviews-more a{font-weight:600;color:var(--primary)}

/* ---------- footer ---------- */
/* Kept deliberately compact: the footer is the least-read part of the page and
   every row of it pushes the real content further from the viewer. Hours are
   collapsed into day RANGES in footer.php rather than seven near-identical
   rows, which is the single biggest saving. */
/* No margin-top here (FLEET FIX, 2026-08-18): a margin renders in BODY's
   background, not the footer's --dark background, so it read as an invisible
   sliver of extra white space on every prior site only because their last
   section before the footer always happened to be plain white too (same
   color as body). The first site whose last section is .section-alt
   (carpet-boss-restoration) exposed it as a real, visible off-white/white/dark
   seam. Sections already carry 4.4rem of their own top+bottom padding
   (base.css `section{padding:4.4rem 0}`), so no extra gap was actually needed
   — removing it tightens every existing site by 2rem of white-on-white
   spacing nobody could see, and closes the visible seam here. */
.site-footer{background:var(--dark);color:#e8e8e6;padding:2.3rem 0 1rem}
/* minmax(0,1fr), not 1fr: a bare 1fr is minmax(auto,1fr), so a column holding a
   long unbreakable town name grows past its share and skews the whole row. */
.foot-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));
  gap:1.5rem 2rem;align-items:start}
.foot-areas{list-style:none;font-size:.86rem;columns:2;column-gap:1rem}
.foot-areas li{padding:.1rem 0;break-inside:avoid}
.foot-areas a{text-decoration:none}
.foot-areas a:hover{color:var(--accent)}
.foot-areas-all{font-weight:600;color:var(--accent);white-space:nowrap}
/* Stay two-up on phones — a single stacked column turns four short blocks into
   an 800px wall of footer between the last CTA and the bottom of the page.
   These overrides MUST come after the base .foot-areas rules above, or the
   base columns:2 wins on equal specificity and the phone layout skews. */
@media (max-width:760px){
  .foot-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:1.3rem 1.2rem}
  .foot-hours li{max-width:none}
  /* Areas stays a normal grid cell, single sub-column. Spanning it full width
     to keep two sub-columns was tried and MEASURED WORSE (695px vs 567px):
     the span gives it a row of its own instead of letting it sit beside
     another block. Do not "fix" this back. */
  .foot-areas{columns:1}
}
@media (max-width:340px){.foot-grid{grid-template-columns:minmax(0,1fr)}}
.foot-logo{height:32px;width:auto;margin-bottom:.6rem}
.foot-nap,.foot-area{font-size:.86rem;line-height:1.5}
.foot-area{margin-top:.35rem;color:rgba(232,232,230,.75)}
.foot-nap a{text-decoration:none;font-weight:600}
.foot-head{font-family:var(--display);display:block;margin-bottom:.45rem;font-size:.95rem}
.foot-hours{list-style:none;font-size:.86rem}
.foot-hours li{display:flex;justify-content:space-between;gap:.8rem;padding:.1rem 0;max-width:230px}
.foot-links{list-style:none;font-size:.86rem}
.foot-links li{padding:.1rem 0}
.foot-links a{text-decoration:none}
.foot-links a:hover{color:var(--accent)}
.foot-legal{margin-top:1.4rem;padding-top:.85rem;border-top:1px solid rgba(255,255,255,.15);
  font-size:.78rem;color:rgba(232,232,230,.7)}
.foot-legal a{color:inherit}

/* ---------- blog (2026-08-31) ----------
   Used by blog/index.php (listing) and blog/<slug>/index.php (a post). Only
   '/blog/' goes in $cfg['pages']; posts are listed in blog/posts.php so that
   header.php does not try to render eighteen articles as a nav dropdown.

   Base rules first, media queries at the bottom of each block — a base rule
   placed AFTER a media query silently overrides it at equal specificity
   (see "CSS ordering trap" in site-factory/CLAUDE.md). */

/* --- a post --- */
/* .site-header is position:fixed (51px tall), and a post page opens with this
   plain block rather than with a photo .hero — so it has to buy its own
   clearance the way .hero does with its 7.5rem. At 2.6rem the "All articles"
   link rendered UNDERNEATH the fixed header. Found by measuring the rendered
   page; it is invisible in the stylesheet because nothing here mentions the
   header. Do not reduce this below the header height plus breathing room. */
.post-hero{padding:7.2rem 0 1.6rem}
.post-back{font-size:.86rem;margin-bottom:.9rem}
.post-back a{text-decoration:none;font-weight:600}
.post-hero h1{font-size:clamp(1.8rem,3.6vw,2.7rem);max-width:20ch;max-width:min(20ch,100%)}
.post-meta{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;margin-top:.8rem;
  font-size:.86rem;color:var(--muted,#4b4b4b)}
.post-dot{opacity:.5}
.post-figure{margin:0 0 2.4rem}
.post-figure img{width:100%;height:auto;aspect-ratio:16/9;object-fit:cover}

/* Measured line length. An article at the full 1140px wrap is ~150 characters
   a line, which is roughly twice the width people actually read comfortably. */
.article-body{max-width:68ch;font-size:1.045rem;line-height:1.75}
.article-body > * + *{margin-top:1.15rem}
.article-body h2{font-size:clamp(1.3rem,2.2vw,1.62rem);margin-top:2.5rem;line-height:1.25}
.article-body h3{font-size:1.14rem;margin-top:1.9rem}
.article-body ul{padding-left:1.15rem}
.article-body li{margin-top:.5rem}
.article-body li::marker{color:var(--accent)}
.article-body strong{font-weight:700}
.article-body a{font-weight:600}

.post-cta{max-width:68ch;margin:3rem 0 0;padding:1.7rem 1.6rem;border-radius:var(--radius,12px);
  background:var(--section-bg-alt,#f4f6f9);border:var(--tile-border,1px solid #e4e4e0)}
.post-cta h2{font-size:1.24rem;margin-bottom:.5rem}
.post-cta p{font-size:.96rem;color:var(--muted,#4b4b4b)}
.post-cta .cta-row{margin-top:1.1rem}

.post-nav{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px;
  max-width:68ch;margin:2.2rem 0 3rem}
.post-nav a{display:block;padding:1rem 1.15rem;border-radius:var(--radius,12px);
  border:var(--tile-border,1px solid #e4e4e0);text-decoration:none;font-weight:600;
  font-size:.95rem;line-height:1.4}
.post-nav a:hover{border-color:var(--primary)}
.post-nav span{display:block;font-size:.74rem;letter-spacing:.11em;text-transform:uppercase;
  color:var(--muted,#4b4b4b);margin-bottom:.3rem;font-weight:700}
.post-nav-next{text-align:right;grid-column:2}
@media (max-width:620px){
  .post-nav{grid-template-columns:minmax(0,1fr)}
  .post-nav-next{text-align:left;grid-column:1}
}

/* --- the listing --- */
.post-list{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:22px;margin-top:22px}
.post-card{display:block;text-decoration:none;color:inherit;border-radius:var(--radius,12px);
  overflow:hidden;background:#fff;border:var(--tile-border,1px solid #e4e4e0);
  box-shadow:var(--shadow,0 2px 10px rgba(0,0,0,.06));transition:transform .18s ease,box-shadow .18s ease}
.post-card:hover{transform:translateY(-3px);box-shadow:0 10px 26px rgba(0,0,0,.13)}
.post-card-media{display:block;overflow:hidden}
.post-card-media img{width:100%;height:auto;aspect-ratio:16/9;object-fit:cover}
.post-card-body{display:block;padding:1.25rem 1.3rem 1.45rem}
.post-card-body h2,.post-card-body h3{font-size:1.06rem;line-height:1.32;margin-bottom:.5rem}
.post-card-tag{display:inline-block;font-family:var(--display);font-weight:800;font-size:.68rem;
  letter-spacing:.12em;text-transform:uppercase;padding:.3rem .66rem;border-radius:999px;
  background:var(--accent);color:var(--accent-text,var(--dark));margin-bottom:.7rem}
.post-card-excerpt{display:block;font-size:.93rem;line-height:1.6;color:var(--muted,#4b4b4b)}
.post-card .post-meta{margin-top:.85rem}

/* The newest post gets a wide card so the page has a clear entry point instead
   of eighteen identical tiles. */
.post-card-lead{display:grid;grid-template-columns:1.15fr .85fr;align-items:stretch;margin-top:2.2rem}
.post-card-lead .post-card-media{height:100%}
.post-card-lead .post-card-media img{height:100%;aspect-ratio:auto;min-height:260px}
.post-card-lead .post-card-body{align-self:center;padding:1.9rem 2rem}
.post-card-lead h2{font-size:clamp(1.25rem,2.1vw,1.6rem)}
@media (max-width:960px){
  .post-list{grid-template-columns:repeat(2,minmax(0,1fr))}
  .post-card-lead{grid-template-columns:minmax(0,1fr)}
  .post-card-lead .post-card-media img{min-height:0;aspect-ratio:16/9}
  .post-card-lead .post-card-body{padding:1.35rem 1.4rem 1.55rem}
}
@media (max-width:620px){.post-list{grid-template-columns:minmax(0,1fr)}}
