/* ---- tokens.css ---- */
/* ============================================================================
   TOKENS — the per-client file.

   To re-skin this design for a different business, this is usually the ONLY
   file you touch. Everything else is written against these variables.

   DSP Painting is mostly monochrome: warm near-black grounds, white type, and
   photography carrying the colour. The original site never changed Porto's
   default blue, so the accent slot here holds the warm near-black the design
   actually uses. The blue itself is back on two surfaces only — the services
   box under the home hero and the "free estimate" band — because the client
   asked for those to match the original (2026-09-24). It is --blue below; the
   nav and buttons stay monochrome.

   Type and spacing use clamp() so they scale smoothly between a small phone
   (~360px) and the 1140px container. No breakpoints, no jumps.
   Formula used throughout:  clamp(MIN, <intercept>rem + <slope>vw, MAX)
   ========================================================================= */

:root {
  /* ---- brand ----------------------------------------------------------
     --accent is the warm near-black measured off the live site's hero and
     "Owner operated" bands (#2B2A28). Buttons invert against their ground:
     dark pill on light sections, white pill on dark ones. */
  --accent: #2b2a28;
  --accent-dark: #171614;
  --accent-soft: #4a4741;

  /* Measured on the original 2026-09-25: the services box is a vertical
     gradient from #0169fe to #0088cc, and its three icons are white, black and
     red (#ff0000), one per card. The estimate band is flat #0169fe. */
  --blue: #0169fe;
  --blue-deep: #0088cc;
  --icon-black: #000000;
  --icon-red: #ff0000;

  /* ---- neutrals ------------------------------------------------------- */
  --ink: #212529;          /* headings */
  --ink-alt: #2b2a28;      /* nav, footer, dark bands */
  --body: #6b6b6b;         /* body copy — see note below */
  --muted: #969696;
  --white: #ffffff;
  --on-dark: #ffffff;
  --on-dark-muted: rgba(255, 255, 255, 0.8);   /* #FFFFFFCC on the original */
  --grey-band: #ededed;
  --grey-soft: #f7f7f7;
  --line: #e6e6e6;

  /* The original sets body copy to Porto's default #777777, which is 4.48:1 on
     white — just under the 4.5:1 WCAG AA minimum. Darkened to #6b6b6b (5.3:1).
     Recorded in the README as a deliberate fix, not a silent change. */

  /* ---- type ----------------------------------------------------------- */
  --font: "Poppins", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  /* Second family, used once: the owner's pull-quote on /about/ is set in
     Marcellus on the original. Georgia is the closest widely-installed serif,
     so the quote still reads as a quote if the webfont never arrives. */
  --font-quote: "Marcellus", Georgia, "Times New Roman", serif;

  /* Every max below is a value MEASURED off the live site in Chrome at
     innerWidth 1054 (getComputedStyle), not read out of a stylesheet. The min
     is chosen to scale sensibly to ~360px. */

  --fs-body: 0.875rem;                                          /* 14px  measured */
  --fs-lead: 0.9625rem;                                         /* 15.4px measured — service-card copy */
  --fs-hero-prefix: clamp(2.125rem, 1.1rem + 4.5vw, 3.9375rem); /* 34 → 63   measured 63px */
  --fs-hero-rotator: clamp(1.75rem, 1.4rem + 1.55vw, 2.1rem);   /* 28 → 33.6 measured 33.6px */
  /* Measured on the original: 26px at 390, 30.8px at 768 and flat above.
     (Was 30 → 40px, which ran the inner-page title 30% oversized on 14 pages.) */
  --fs-h1-banner: clamp(1.625rem, 1.3155rem + 1.27vw, 1.925rem);  /* 26 → 30.8 */
  --fs-h2: clamp(1.75rem, 1.25rem + 2.2vw, 2.40625rem);         /* 28 → 38.5 measured 38.5px */
  --fs-h3: clamp(1.375rem, 1.15rem + 1vw, 1.75rem);             /* 22 → 28   measured 28px, service title */
  --fs-h4: clamp(1.0625rem, 1.02rem + 0.2vw, 1.25rem);          /* 17 → 20 */
  --fs-sub: clamp(1.25rem, 1.05rem + 1.1vw, 1.59375rem);        /* 20 → 25.5 measured 25.5px, portfolio tile title */
  --fs-counter: clamp(2.75rem, 1.5rem + 5.5vw, 4.6625rem);      /* 44 → 74.6 measured 74.62px */
  --fs-counter-label: 1.3125rem;                                /* 21px  measured */
  --fs-label: 1.05rem;                                          /* 16.8px measured — checklist / eyebrow */
  --fs-small: 0.8125rem;                                        /* 13px */

  --lh-body: 1.857;        /* 26px at 14px — measured */
  --lh-heading: 1.25;
  --lh-tight: 1;           /* the display headings are all line-height 1 */
  --lh-rotator: 1.5;       /* measured 50.4px at 33.6px */

  /* Display tracking is uniformly -0.05em across the site: -1.68px at 33.6,
     -1.925px at 38.5, -1.4px at 28, -1.275px at 25.5 — all exactly -0.05em. */
  --ls-display: -0.05em;
  --ls-h1: -0.05em;
  --ls-h2: -0.05em;
  --ls-h3: -0.05em;
  --ls-body: -0.015em;     /* measured -0.24px at 16px on testimonial names */
  --ls-label: 2px;         /* measured on the checklist items */

  /* ---- spacing -------------------------------------------------------- */
  --section-y: clamp(3.75rem, 2.5rem + 5.5vw, 7.5rem);          /* 60 → 120 */
  --section-y-tight: clamp(2.5rem, 1.75rem + 3.33vw, 4.375rem); /* 40 → 70 */
  --gap: clamp(1.25rem, 1rem + 1.1vw, 1.875rem);                /* 20 → 30 */

  --container: 1140px;
  --gutter: 15px;

  /* ---- grid minimums --------------------------------------------------
     These drive auto-fit grids. A column can never be narrower than this,
     so columns drop away on their own as the viewport shrinks — no media
     query needed. Tuned so the 1140px container yields 2 / 3 / 4 columns. */
  --col-min-2: 420px;
  --col-min-3: 300px;
  --col-min-4: 220px;
  --col-min-gallery: 290px;

  /* ---- chrome ---------------------------------------------------------
     MEASURED on the live site at innerWidth 1054:
       header logo img   175 x 193      (aspect 0.907)
       top utility bar    58px tall
       main header row   241px tall
       header total      299px tall
     The logo is the value most often got wrong, and the original's own markup
     gets it wrong: the <img> carries width="175" height="46" against a file
     whose real aspect is 0.907, so the browser ignores it and the attributes
     only cause layout shift. 175x193 is what actually renders. */
  --logo-w: 175px;        /* measured; stepped at the breakpoints below, not fluid */
  --logo-h: 193px;
  --topbar-h: 58px;
  --topbar-bg: #1c1f23;   /* measured full-width on the original */
  --topbar-bg-home: rgba(0, 0, 0, 0.15);   /* over the home hero photo, measured */
  --ink-deep: #1f1f1f;    /* measured: the About page's closing band */
  --header-h: 241px;
  --header-h-stuck: 72px;

  --radius: 10px;          /* cards */
  --radius-pill: 200px;    /* buttons are fully rounded on this site */

  --shadow-card: 0 0 21px -5px rgba(0, 0, 0, 0.2);
  --shadow-card-hover: 0 6px 28px -4px rgba(0, 0, 0, 0.26);

  --transition: 0.2s ease;
  --transition-slow: 0.5s ease;

  /* ---- the one structural breakpoint ---------------------------------
     Only used where layout genuinely changes rather than merely resizing:
     the nav collapsing to a drawer, and the split colour bands stacking. */
  --bp-nav: 991px;
}

/* The logo does not scale fluidly on the original — it steps, and the steps are
   tied to the same widths as the container change. Reproduced literally.
   (A clamp() on vw put it at 137px where the live site renders 175px.) */
/* Measured on the original at 390 / 768 / 992 / 1054 / 1100 / 1164 / 1280 /
   1440 / 1600 / 1920: it is 110px at ≤768 and 175px at every width above,
   never larger. An earlier `min-width: 1164px { --logo-w: 250px }` step came
   from a note, not a measurement, and rendered the logo 43% oversized on every
   desktop screen. There is no third step. */
@media (max-width: 991px) { :root { --logo-w: 110px; } }

/* ---- base/fonts.css ---- */
/* Self-hosted fonts, from assets/fonts/ — no request leaves this origin.

   These are exactly the @font-face rules Google Fonts served on 2026-09-22 for
     css2?family=Marcellus&family=Poppins:wght@400;500;600;700&display=swap
   (fetched with a desktop Chrome User-Agent, which is what gets woff2), with
   only the url() rewritten to a local file. Same families, weights, styles,
   unicode-range and font-display, so the browser picks the same face for every
   character it did before.

   A browser downloads only a subset whose unicode-range the page uses:
   measured on / and /about/, that is Poppins's latin file in each weight, plus
   Marcellus's latin file on /about/ (the owner's pull-quote). The latin-ext
   and devanagari files are there for any character outside it.

   Weight 300 (.pf-item__meta, the portfolio tiles' category labels) has no
   face, here or on Google: the browser uses the nearest, 400, as before. */

/* latin-ext */
@font-face {
  font-family: 'Marcellus';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/marcellus-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* latin */
@font-face {
  font-family: 'Marcellus';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/marcellus-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* devanagari */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/poppins-400-devanagari.woff2') format('woff2');
  unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
}

/* latin-ext */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/poppins-400-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* latin */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/assets/fonts/poppins-400-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* devanagari */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/assets/fonts/poppins-500-devanagari.woff2') format('woff2');
  unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
}

/* latin-ext */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/assets/fonts/poppins-500-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* latin */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url('/assets/fonts/poppins-500-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* devanagari */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/assets/fonts/poppins-600-devanagari.woff2') format('woff2');
  unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
}

/* latin-ext */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/assets/fonts/poppins-600-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* latin */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 600;
  font-display: swap;
  src: url('/assets/fonts/poppins-600-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* devanagari */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/assets/fonts/poppins-700-devanagari.woff2') format('woff2');
  unicode-range: U+0900-097F, U+1CD0-1CF9, U+200C-200D, U+20A8, U+20B9, U+20F0, U+25CC, U+A830-A839, U+A8E0-A8FF, U+11B00-11B09;
}

/* latin-ext */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/assets/fonts/poppins-700-latin-ext.woff2') format('woff2');
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

/* latin */
@font-face {
  font-family: 'Poppins';
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url('/assets/fonts/poppins-700-latin.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ---- base/reset.css ---- */
/* ---- reset & document defaults ------------------------------------------ */

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

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

  /* The mobile nav drawer is `position: fixed` and parked off-screen right
     when closed. A fixed element is positioned against the viewport, not the
     body, so `body { overflow-x: hidden }` does NOT clip it — the responsive
     sweep caught 96px of horizontal scroll on every page at 360px because of
     exactly this.

     `clip` rather than `hidden` on purpose: `overflow: hidden` makes this a
     scroll container, which breaks `position: sticky` on the header. `clip`
     does not, and it still removes the off-screen drawer from the scrollable
     area.

     This clip is the ONLY horizontal clip on the page, and it has to stay that
     way. `body` previously also carried `overflow-x: hidden`; once `html` is
     `clip`, body's overflow no longer propagates to the viewport, so body
     itself became a scroll container — and the sticky header then stuck to a
     box that never scrolls, riding off-screen (measured: top -1147px at
     scrollY 1200). Do not reintroduce an overflow on `body`. */
  overflow-x: clip;
}

body {
  margin: 0;
  font-family: var(--font);
  font-size: var(--fs-body);
  font-weight: 400;
  line-height: var(--lh-body);
  color: var(--body);
  background: var(--white);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* No overflow here on purpose — see the note on `html` above. */
}

img {
  max-width: 100%;
  height: auto;
  display: block;
  border: 0;
}

a {
  color: var(--accent);
  text-decoration: none;
  transition: color var(--transition);
}
a:hover { color: var(--accent-dark); }

p { margin: 0 0 1rem; }
p:last-child { margin-bottom: 0; }

/* Both default to `margin: 1em 40px` in every browser. Left alone, a quotation
   or a captioned photo is indented 40px on each side, which on a 360px phone
   costs a quarter of the readable width. */
blockquote, figure { margin: 0 0 1rem; }

ul, ol { margin: 0 0 1rem; padding-left: 1.25rem; }

/* Keyboard focus was invisible in the original theme. */
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

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

.skip-link {
  position: absolute;
  top: -60px;
  left: 12px;
  z-index: 2000;
  padding: 10px 18px;
  background: var(--ink-alt);
  color: #fff;
  font-weight: 600;
  transition: top var(--transition);
}
.skip-link:focus { top: 12px; color: #fff; }

/* ---- base/typography.css ---- */
/* ---- typography ----------------------------------------------------------
   All sizes are fluid via clamp(), so there is not a single font-size media
   query anywhere in this stylesheet. Text scales smoothly from 320px to 4K.
   -------------------------------------------------------------------------- */

h1, h2, h3, h4, h5, h6 {
  margin: 0 0 1rem;
  color: var(--ink);
  font-weight: 600;
  line-height: var(--lh-heading);
}

/* Small tracked label above a heading — "CALL NOW" over "FREE ESTIMATE".
   Measured from the original: 16.8px, weight 400, 2px tracking, uppercase. */
.eyebrow {
  display: inline-block;
  margin: 0 0 10px;
  color: var(--accent);
  font-size: var(--fs-label);
  font-weight: 400;
  line-height: 1.5;
  letter-spacing: var(--ls-label);
  text-transform: uppercase;
}
.on-dark .eyebrow,
.section--dark .eyebrow,
.section--photo .eyebrow { color: var(--on-dark); }

.section-title {
  margin: 0 0 18px;
  font-size: var(--fs-h2);
  font-weight: 700;              /* measured w700, not 600 */
  line-height: var(--lh-tight);  /* measured 38.5px at 38.5px */
  letter-spacing: var(--ls-h2);
  color: var(--ink);
}

.section-intro {
  max-width: 700px;
  margin: 0 auto;
  font-size: var(--fs-body);
  line-height: var(--lh-body);
  color: var(--body);
}

.section-head { margin-bottom: clamp(2rem, 1.5rem + 2.2vw, 3.125rem); }
.section-head--center { text-align: center; }

.lead { font-size: var(--fs-lead); line-height: 1.82; }

/* The previous client's zigzag rule is gone — DSP's design has no such motif,
   and it carried a hardcoded colour, which a component must never do.

   The angled band divider lives in components/shape-divider.css. */

/* ---- base/layout.css ---- */
/* ---- layout primitives ---------------------------------------------------
   The grids here use auto-fit + minmax, so they reflow on their own at every
   width. There are no media queries in this file, and none are needed.
   -------------------------------------------------------------------------- */

.container {
  width: 100%;
  max-width: var(--container);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

.section { padding: var(--section-y) 0; position: relative; }
.section--tight { padding: var(--section-y-tight) 0; }
.section--grey { background: var(--grey-band); }
.section--soft { background: var(--grey-soft); }
.section--dark { background: var(--ink-alt); }

.row {
  display: flex;
  flex-wrap: wrap;
  margin: 0 -15px;
}
.row > * { padding: 0 15px; }

/* min(…, 100%) stops a column from ever being wider than its container,
   which is what would otherwise cause horizontal scroll on small phones. */
.grid { display: grid; gap: var(--gap); }
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(var(--col-min-2), 100%), 1fr)); }
.grid--3 { grid-template-columns: repeat(auto-fit, minmax(min(var(--col-min-3), 100%), 1fr)); }
.grid--4 { grid-template-columns: repeat(auto-fit, minmax(min(var(--col-min-4), 100%), 1fr)); }

/* Two-column content splits (text beside an image). Collapse by width, not
   by breakpoint: below ~760px the pair no longer fits and stacks itself. */
.split {
  display: grid;
  gap: clamp(2rem, 1rem + 4vw, 3.75rem);
  align-items: center;
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
}

/* ---- components/button.css ---- */
/* ---- buttons & arrow links ----------------------------------------------

   DSP's buttons are fully rounded pills. The original renders them via Porto's
   `btn btn-xl btn-light btn-full-rounded` / `btn-dark` classes:

     border-radius : 200px   (fully rounded — 12 occurrences in the page CSS)
     font-size     : 1.1em, weight 700
     padding       : 20px 48px   (the xl size used for every hero/section CTA)

   Two fills only, and they invert against their ground: a white pill on the
   dark photographic bands, a near-black pill on light sections. There is no
   third colour — this brand has no chromatic accent.

   ⚠️ Hover states are NOT measured. Query 3 of the effects audit needs the
   browser, which was unavailable. What is here is a plain tonal shift; Porto
   may apply a transform or shadow as well. Confirm before sign-off.
   ------------------------------------------------------------------------- */

.btn {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 20px 48px;
  border: 0;
  border-radius: var(--radius-pill);
  font-family: inherit;
  font-size: var(--fs-lead);   /* measured 15.4px */
  font-weight: 700;
  line-height: 1.4286;         /* measured 22px at 15.4px */
  text-align: center;
  cursor: pointer;
  transition: background-color var(--transition), color var(--transition),
              box-shadow var(--transition);
}

/* Dark pill — the default on light sections. */
.btn--dark { background: var(--accent); color: var(--white); }
.btn--dark:hover { background: var(--accent-dark); color: var(--white); }

/* White pill — for use on the dark photographic bands. */
.btn--light { background: var(--white); color: var(--accent); }
.btn--light:hover { background: var(--grey-band); color: var(--accent); }

/* Outline, for a secondary action on a light ground. */
.btn--outline {
  background: transparent;
  color: var(--accent);
  box-shadow: inset 0 0 0 2px var(--accent);
}
.btn--outline:hover { background: var(--accent); color: var(--white); }

/* Smaller size — the testimonials rail CTA uses it. */
.btn--md { padding: 13px 30px; font-size: var(--fs-body); }

.btn svg { flex: none; }
.btn--icon:hover svg { transform: translateX(4px); }
.btn svg { transition: transform 0.25s ease; }

/* Keyboard focus must stay visible on both grounds. */
.btn:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
}
.btn--light:focus-visible,
.on-dark .btn:focus-visible {
  outline-color: var(--white);
}

/* "View More →" with a sliding arrow */
.arrow-link {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  color: var(--accent);
  font-size: var(--fs-body);
  font-weight: 600;
}
.arrow-link svg { transition: transform 0.25s ease; flex: none; }
.arrow-link:hover svg { transform: translateX(6px); }

@media (prefers-reduced-motion: reduce) {
  .btn, .btn svg, .arrow-link svg { transition: none; }
}

/* ---- components/shape-divider.css ---- */
/* ---- angled section edges ------------------------------------------------

   The seams between DSP's photographic bands are diagonal. On the original
   these are not SVG shape dividers despite the Elementor markup calling them
   that — the `.elementor-shape` divs are empty and the effect is a plain
   skewed block of the *destination* colour laid over the edge of the band.

   Copied verbatim from the live site's inline CSS:

     .section-angled-bottom > .elementor-shape-bottom {
       position:absolute; left:0; width:100%; bottom:0; z-index:8;
       background-color:#fff; padding:7rem 0;
       transform: skewY(4.7deg) translate3d(0,50%,0);
     }
     .section-angled-top > .elementor-shape-top {
       position:absolute; left:0; width:100%; top:0; z-index:8;
       background-color:#fff; padding:3rem 0;
       transform: skewY(-1.7deg) translate3d(0,-50%,0);
     }

   The two angles differ (4.7° vs 1.7°) and so do the band heights (14rem vs
   6rem) — that asymmetry is deliberate in the original, so it is kept.

   The `padding` is doing the work of a height: the element has no content, so
   its box is padding-only. Translating it 50% past the edge means half the
   band hangs outside the section, which is what makes the skew read as a
   single clean diagonal rather than a stripe.
   -------------------------------------------------------------------------- */

.section-angled { position: relative; }

.section-angled > .angle {
  position: absolute;
  left: 0;
  width: 100%;
  z-index: 8;
  background-color: var(--angle-color, #fff);
  pointer-events: none;
}

.section-angled > .angle--bottom {
  bottom: 0;
  padding: 7rem 0;
  transform: skewY(4.7deg) translate3d(0, 50%, 0);
}

.section-angled > .angle--top {
  top: 0;
  padding: 3rem 0;
  transform: skewY(-1.7deg) translate3d(0, -50%, 0);
}

/* A skewed band this tall eats a lot of a short viewport. The original keeps
   the same angle at every width; only the band depth is trimmed so the wedge
   does not swallow the content on a phone. */
@media (max-width: 767px) {
  .section-angled > .angle--bottom { padding: 3.5rem 0; }
  .section-angled > .angle--top { padding: 1.75rem 0; }
}

/* Content must sit above the wedge. */
.section-angled > .container,
.section-angled > .section-inner { position: relative; z-index: 9; }

/* ---- components/topbar.css ---- */
/* ---- top bar -------------------------------------------------------------
   A plain full-width dark band, as on the original: measured rgb(28,31,35)
   edge to edge at x=10, mid-viewport and at the right edge, 58px tall, contact
   details left in the container and the social icon right, everything white.

   This previously carried a skewed parallelogram with a second offset copy
   behind it — an idiom inherited from an earlier client's red topbar, still
   described as "red field"/"orange copy" in the comments it shipped with. It
   left the bar white from roughly halfway across, which went unnoticed only
   because the header beneath it was also white. It is not on the original.
   -------------------------------------------------------------------------- */

.topbar {
  position: relative;
  background: var(--topbar-bg);
  height: var(--topbar-h);
  line-height: var(--topbar-h);
  font-size: 0.875rem;              /* measured 14px */
}
.topbar .container {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.topbar-contact {
  display: flex;
  align-items: center;
  gap: 53px;    /* measured: puts the email at x=341 in a 1440 viewport, as on the original */
}
.topbar-contact a {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  /* WCAG 2.2 target size (2.5.8) is 24x24 minimum. The phone link is this
     business's primary call to action — give it a comfortable thumb target
     rather than the bare minimum. Invisible inside the bar. */
  min-height: 36px;
  color: var(--on-dark);
  font-weight: 600;                 /* measured */
}
.topbar-contact a:hover { color: var(--on-dark-muted); }
.topbar-contact svg { flex: none; }

.topbar-social {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-left: auto;
}
/* The hit area is 40px so it can actually be tapped. */
.topbar-social-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  margin-right: -10px;
  color: var(--on-dark);
}
.topbar-social-link:hover { color: var(--on-dark-muted); }

/* The original's "f" is a 14.4px icon-font glyph: 7x15px of ink, measured on a
   screenshot. This path fills 17.5/24 of its viewBox, so at 21px it draws the
   same 7x15 (measured); the 16px it shipped with drew 6x12 and read as too
   small next to the original. */
.topbar-social-link svg { width: 21px; height: 21px; }

/* The original keeps the bar to one 58px row at every width, and drops the
   email below 576px so phone and icon still fit (measured at 768, 600, 480
   and 390). The email stays one scroll away, in the footer. */
@media (max-width: 575px) {
  .topbar-contact a[href^="mailto:"] { display: none; }
}

/* ---- components/header.css ---- */
/* ---- header & navigation -------------------------------------------------
   The 991px breakpoint here is one of only two structural breakpoints in the
   stylesheet. A horizontal nav becoming a drawer is a genuine change of
   layout, not a resize, so it cannot be expressed fluidly.

   (Media queries can't read CSS custom properties, so --bp-nav in tokens.css
   documents the value; the query below must repeat it literally.)
   -------------------------------------------------------------------------- */

/* The header ground is dark, as on the original (measured #212529 there).
   This is not a style preference: the logo artwork is white on transparency
   (sampled mean of its opaque pixels is rgb(246,246,246)), so on a white
   header it rendered at ~1.08:1 contrast — invisible on all 16 pages, with
   only the grey "PAINTING" half of the wordmark showing. Anything placed in
   here has to read against a near-black ground. */
/* z-index must sit ABOVE .nav-backdrop (1100), not below it. The drawer is a
   child of this element, and `position: sticky` + a z-index makes the header a
   stacking context — so the drawer's own `z-index: 1200` only ranks it against
   its siblings inside the header, and the header's 1000 was what actually
   competed with the backdrop. At 1000 the backdrop painted over the open
   drawer: every menu link hit-tested as `.nav-backdrop`, so a real tap closed
   the menu instead of following the link. Raising the header lifts the drawer
   with it. */
.site-header {
  position: sticky;
  top: 0;
  z-index: 1200;
  background: var(--ink);
  transition: box-shadow 0.25s ease;
}
.site-header.is-stuck { box-shadow: 0 2px 18px rgba(0, 0, 0, 0.35); }

.header-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gap);
  min-height: var(--header-h);
  transition: min-height 0.25s ease;
}
.site-header.is-stuck .header-inner { min-height: var(--header-h-stuck); }

.brand { display: block; flex: none; }
.brand img {
  width: var(--logo-w);
  height: auto;
  transition: width 0.25s ease;
}
.site-header.is-stuck .brand img { width: clamp(112px, 11vw, 150px); }

.nav-toggle {
  display: none;
  width: 44px; height: 44px;
  padding: 0;
  border: 0;
  background: transparent;
  cursor: pointer;
  color: var(--on-dark);
}
.nav-toggle svg { margin: 0 auto; }

.main-nav { margin-left: auto; }
.nav-list {
  display: flex;
  align-items: center;
  gap: clamp(16px, 2.2vw, 30px);
  margin: 0;
  padding: 0;
  list-style: none;
}
.nav-list > li { position: relative; }

.nav-list > li > a {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 8px 0;
  color: var(--on-dark);
  font-size: var(--fs-lead);
  font-weight: 600;
  white-space: nowrap;
}

/* Hover and current-page must differ from rest *visibly*. They previously read
   `color: var(--accent)` against a rest colour of `var(--ink-alt)` — and both
   tokens are #2b2a28, so the rule repainted the colour the link already had:
   no hover feedback and no current-page indicator anywhere in the nav. The
   original signals both with #0169fe; this palette has no chromatic accent, so
   the current page is marked with a rule under the label instead of a hue. */
.nav-list > li > a:hover { color: var(--on-dark-muted); }

.nav-list > li.is-active > a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 2px;
  height: 2px;
  background: currentColor;
}

.has-sub > a::after {
  content: "";
  width: 5px; height: 5px;
  border-right: 1.6px solid currentColor;
  border-bottom: 1.6px solid currentColor;
  transform: rotate(45deg) translateY(-2px);
}

.sub-nav {
  position: absolute;
  top: 100%;
  left: -20px;
  min-width: 218px;
  margin: 0;
  padding: 10px 0;
  list-style: none;
  background: var(--white);
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.13);
  opacity: 0;
  visibility: hidden;
  transform: translateY(10px);
  transition: opacity var(--transition), transform var(--transition), visibility var(--transition);
}
.has-sub:hover > .sub-nav,
.has-sub:focus-within > .sub-nav {
  opacity: 1;
  visibility: visible;
  transform: translateY(0);
}
.sub-nav a {
  display: block;
  padding: 8px 22px;
  color: var(--ink-alt);
  font-size: var(--fs-body);
  font-weight: 500;
}
.sub-nav a:hover { color: var(--accent); background: var(--grey-soft); }

.nav-backdrop {
  position: fixed;
  inset: 0;
  z-index: 1100;
  background: rgba(0, 0, 0, 0.45);
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.3s ease, visibility 0.3s;
}

/* ---- drawer ------------------------------------------------------------ */
@media (max-width: 991px) {
  .nav-toggle { display: inline-flex; align-items: center; justify-content: center; order: 3; }
  .header-inner { min-height: 84px; }

  /* The original pads the logo above and below at these widths — measured
     24px at 768 and 600 (logo at y=82 under the 58px top bar, 227px in all)
     and 22px at 390. Only at rest: the stuck header stays compact. */
  .site-header:not(.is-stuck) .header-inner { padding-block: 24px; }

  .main-nav {
    position: fixed;
    inset: 0 0 0 auto;
    width: min(320px, 86vw);
    z-index: 1200;
    padding: 84px 26px 40px;
    /* Dark, like the original's mobile panel (measured rgb(33,37,41) with
       #dedede links and white for the current page). It also has to be dark
       because the nav links are white for the header they sit in — on the
       white panel this shipped with, they were white on white. */
    background: var(--ink);
    box-shadow: -8px 0 40px rgba(0, 0, 0, 0.2);
    transform: translateX(100%);
    overflow-y: auto;

    /* `visibility` is not cosmetic here: without it the closed drawer's links
       stay in the tab order, so a keyboard user tabbing off the logo lands in
       an invisible menu parked off-screen. Delaying the visibility change by
       the length of the slide keeps the close animation watchable. */
    visibility: hidden;
    transition: transform 0.3s ease, visibility 0s linear 0.3s;
  }
  .main-nav.is-open {
    transform: none;
    visibility: visible;
    transition: transform 0.3s ease, visibility 0s;
  }

  .nav-list { flex-direction: column; align-items: stretch; gap: 0; }
  .nav-list > li { border-bottom: 1px solid rgba(255, 255, 255, 0.14); }
  .nav-list > li > a {
    padding: 14px 0;
    justify-content: space-between;
    width: 100%;
    color: #dedede;               /* measured on the original's mobile panel */
  }
  .nav-list > li > a:hover,
  .nav-list > li.is-active > a { color: var(--on-dark); }
  /* The panel lists one item per row, so the current page reads from its
     brighter colour; the desktop underline would just clutter it. */
  .nav-list > li.is-active > a::after { content: none; }

  .sub-nav {
    position: static;
    opacity: 1;
    visibility: visible;
    transform: none;
    box-shadow: none;
    padding: 0 0 10px;
    display: none;
  }
  .has-sub.is-open > .sub-nav { display: block; }
  .has-sub.is-open > a::after { transform: rotate(-135deg) translateY(-2px); }
  .sub-nav a { padding: 9px 14px; }

  .nav-backdrop.is-open { opacity: 1; visibility: visible; }
}

@media (max-width: 575px) {
  .site-header:not(.is-stuck) .header-inner { padding-block: 22px; }
}

/* ---- components/page-banner.css ---- */
/* ---- inner-page banner ----------------------------------------------------- */

/* Measured on the original: 151px tall at 390, 163px at 768 and above, on
   #f7f7f7. The band was 244px on #ededed — half again as tall, on 14 pages.

   The title is the banner's only content. The breadcrumb that sat under it
   (28px, plus the title's 8px margin) was removed at the client's request on
   2026-09-25; its 36px went back into the padding, 18px above and below, so
   the band is still 151 / 163px and the title stays centred in it. */
.page-banner {
  position: relative;
  padding: calc(clamp(2.633rem, 2.311rem + 1.06vw, 2.821rem) + 18px) 0;   /* → 151px at 390, 163px at 768+ */
  background: var(--grey-soft);
  text-align: center;
}

.page-banner h1 {
  margin: 0;
  font-size: var(--fs-h1-banner);
  font-weight: 700;
  letter-spacing: var(--ls-h1);
  line-height: 1.2;          /* measured: 31.2/26 at 390, 36.96/30.8 at 768+ */
  color: var(--ink);
}

/* ---- components/hero.css ---- */
/* ---- hero ----------------------------------------------------------------
   Full-bleed photograph, near-black ground, a very heavy overlay, and a
   diagonal white wedge along the bottom edge.

   Measured off the original:
     background-color : #2B2A28
     background       : DSP_Painting_Building_Exterior_13.jpg, center/cover
     overlay          : #212529 at opacity 0.9   (.overlay.overlay-op-9.overlay-show:before)
     min-height       : calc(560px + 25vw)
     bottom edge      : .section-angled-bottom (see shape-divider.css)

   The home page (the only page with a hero today) overrides the height and the
   spacing with the positions measured in "home: the header over the hero"
   below; these base rules are what any other hero would get.

   The overlay really is 0.9 — the photograph reads as texture behind an almost
   solid ground rather than as an image. That is the original's choice and it is
   what makes the white type legible, so it is reproduced as measured.
   -------------------------------------------------------------------------- */

.hero {
  position: relative;
  display: grid;
  place-items: center;
  min-height: calc(560px + 25vw);
  padding: var(--section-y) 0 clamp(6rem, 4rem + 8vw, 10rem);
  background-color: var(--accent);
  background-position: center center;
  background-repeat: no-repeat;
  background-size: cover;
  overflow: hidden;
  text-align: center;
}

/* The overlay sits above the photograph and below the content. */
.hero::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  background: #212529;
  opacity: 0.9;
  pointer-events: none;
}

.hero > .container {
  position: relative;
  z-index: 9;
}

/* ---- rotating headline ---------------------------------------------------
   The original stacks two Porto "word rotator" widgets: a static prefix line
   and a rotating one that cycles four phrases on a 2500ms dwell.

   Measured on the live site: the prefix renders at 63px/63px and the rotating
   words at 33.6px/50.4px — the prefix is the larger of the two, which the
   stylesheet alone does not make obvious.

   The original's `font-size: 0.1vw` rule below 767px targets
   `.word-rotate-suffix`, and no suffix element exists on the page, so it
   applies to nothing. The original also jumps the rotator to `7vw` under
   767px, which is *larger* than its desktop size at that breakpoint (53.7px
   at 767 vs 33.6px at 768). That discontinuity is smoothed here into one
   clamp() rather than reproduced.
   -------------------------------------------------------------------------- */

.hero-prefix {
  /* The prefix has to be a <span> because it lives inside the <h1> alongside
     the rotator. An inline box ignores vertical margins, so without this the
     `margin-bottom` below is silently dropped — ~22px of separation at the
     measured 63px size. The rotator being `display: block` still forces the
     line break, which is what makes the loss easy to miss. */
  display: block;
  margin: 0 0 0.35em;
  font-size: var(--fs-hero-prefix);
  font-weight: 400;
  line-height: var(--lh-tight);    /* measured 63px at 63px */
  letter-spacing: var(--ls-display);
  color: var(--on-dark);
}

.word-rotator {
  display: block;
  margin: 0 0 clamp(2rem, 1rem + 4vw, 3.5rem);
  font-size: var(--fs-hero-rotator);
  font-weight: 400;
  line-height: var(--lh-rotator);   /* measured 50.4px at 33.6px */
  letter-spacing: var(--ls-display);
  color: var(--on-dark);
}

/* Every phrase is stacked in the same grid cell so the box is as tall as the
   tallest one and nothing reflows as they swap. Without this the section
   jumps each time a longer phrase comes round. */
.word-rotator__items {
  display: inline-grid;
  vertical-align: top;
}
.word-rotator__item {
  grid-area: 1 / 1;
  padding: 0.1rem 0.5rem;
  opacity: 0;
  transform: translate3d(0, 40%, 0);
  transition: opacity 0.4s ease, transform 0.4s ease;
}
.word-rotator__item.is-active {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

/* With JS off, no item ever gets .is-active — so the first one must show on
   its own. The page must never present an empty headline. */
.no-js .word-rotator__item:first-child,
.word-rotator__items:not(:has(.is-active)) .word-rotator__item:first-child {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  .word-rotator__item { transition: none; }
}

/* ---- home: the header over the hero --------------------------------------
   On the original's home page the top bar and header are transparent and sit
   over the hero photograph, which starts at the very top of the page. The
   rebuild had given the header its own solid ground above the hero, which put
   everything on the home page ~300px lower than the original (client review,
   2026-09-24). Inner pages keep the solid header, as the original does.

   The header stays sticky and in flow; the hero is pulled up underneath it by
   exactly the height of the top bar plus the header at rest (--chrome-h), and
   the header turns solid again once it sticks (.is-stuck, 10-header.js).

   Vertical positions are the original's, measured 2026-09-25, from the top of
   the page:

                       ≥992px   768px   600px   390px
     "We believe in…"    263      189   189.8   182.9
     rotating phrase     370      296   272.8   248.9
     CALL FOR QUOTE      468      394   355.2   314.2
     services box top    585      482     440   387.5
     hero bottom         900      752     710   657.5

   so the gaps below are those differences. Between 768 and 991px the
   original's top padding and the space above the box grow with the viewport
   (at 900: 205.5 / 410.5 / 515 / 785), so those two are lines through its 768
   and 900 values. Under 768px its gaps shrink with its 7vw type; they are the
   straight line through its 600 and 390 values. The rebuild keeps its own smoother type sizes (README), so the
   gaps are applied between the tops of the blocks, not after their bottoms,
   and each block still lands where the original's does. */
.page-home {
  --chrome-h: 299px;         /* 58 top bar + 241 header                     */
  --hero-pad: 263px;         /* top of page → top of "We believe in…"       */
  --hero-gap-1: 107px;       /* top of the prefix → top of the rotator      */
  --hero-gap-2: 98px;        /* top of the rotator → top of the button      */
  --hero-gap-3: 55px;        /* bottom of the 62px button → top of the box  */
  --hero-overlap: 315px;     /* top of the box → bottom of the hero          */
}
@media (max-width: 991px) {
  .page-home {
    --chrome-h: 227px;       /* 58 top bar + 24 + the 121px logo (110 wide) + 24 */
    --hero-pad: calc(93px + 12.5vw);      /* 189 at 768, 205.5 at 900 */
    --hero-gap-3: calc(12.5vw - 70px);    /*  26 at 768,  42.5 at 900 */
    --hero-overlap: 270px;
  }
}
@media (max-width: 767px) {
  .page-home {
    --hero-pad: calc(170.1px + 3.29vw);
    --hero-gap-1: calc(34.4px + 8.1vw);
    --hero-gap-2: calc(33.5px + 8.14vw);
    --hero-gap-3: calc(5.476vw - 10.06px);
  }
}
@media (max-width: 575px) {
  .page-home { --chrome-h: 223px; }   /* 58 + 22 + 121 + 22 */
}

.page-home .topbar {
  z-index: 1000;              /* over the hero pulled up beneath it */
  background: var(--topbar-bg-home);
}
.page-home .site-header:not(.is-stuck) { background: transparent; }

.page-home .hero {
  display: block;
  min-height: 0;
  margin-top: calc(-1 * var(--chrome-h));
  padding: var(--hero-pad) 0 calc(var(--hero-gap-3) + var(--hero-overlap));
}
.page-home .hero h1 { margin: 0; }
/* The prefix's box is 1em tall (line-height 1); the rotator's is its 1.5em
   line plus the items' 0.1rem padding top and bottom. */
.page-home .hero-prefix { margin-bottom: calc(var(--hero-gap-1) - 1em); }
.page-home .word-rotator { margin-bottom: calc(var(--hero-gap-2) - 1.5em - 0.2rem); }
/* Top-aligned, the button's line is exactly the button's 62px. */
.page-home .hero .btn { vertical-align: top; }

/* The original's wedge keeps its depth on phones (measured 104px at 390); the
   services box covers most of it there, so the 3.5rem trim is not needed. */
@media (max-width: 767px) {
  .page-home .hero > .angle--bottom { padding: 104px 0; }
}

/* ---- components/card.css ---- */
/* ---- services box --------------------------------------------------------
   One blue box holding the three services, overlapping the bottom of the home
   hero. Rebuilt 2026-09-25 to match the original after the client's review:
   it had shipped as three separate grey cards on a dark band, starting ~900px
   further down the page, with every icon white.

   Measured on the original (1440 / 1024 / 992 / 768 / 600 / 390):
     box          : linear-gradient(#0169fe → #0088cc), no radius,
                    box-shadow 0 30px 60px -30px rgba(0,0,0,0.45)
     width        : the container minus 55px, left-aligned — at every width
     columns      : 3 at ≥992px, 2 at 768–991px, 1 below
     card         : padding 48px (48px 24px below 768px); icon 75px, then
                    20px, then the title
     title        : 28px / 700 / line-height 1em / #FFFFFF
     description  : 15.4px / 500 / 26px / #FFFFFFCC
     icon colours : Interior white, Exterior black, Commercial red (#ff0000)
     placement    : the box's top is where the hero stops (see hero.css) and
                    "Our Portfolio" starts 79px below the box, at every width

   The original's box is an autoplaying carousel. Below 992px it shows one or
   two cards at a time with arrows; this shows all three, stacked, and nothing
   moves. That is the rebuild's standing decision (README, "Decisions").
   -------------------------------------------------------------------------- */

.home-services {
  position: relative;
  z-index: 10;                                   /* above the hero's wedge (8) */
  margin-top: calc(-1 * var(--hero-overlap));    /* set in hero.css */
}
.home-services + .section { padding-top: 79px; }

.service-box {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  width: calc(100% - 55px);
  background: linear-gradient(var(--blue), var(--blue-deep));
  box-shadow: 0 30px 60px -30px rgba(0, 0, 0, 0.45);
}

.service-card {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 20px;
  padding: 48px;
  text-align: left;
}

.service-card__icon {
  width: 75px;
  height: 75px;
  flex: none;
  color: var(--on-dark);
}
.service-card__icon--black { color: var(--icon-black); }
.service-card__icon--red { color: var(--icon-red); }
.service-card__icon svg { width: 100%; height: 100%; fill: currentColor; }

.service-card h2,
.service-card h3 {
  margin: 0 0 11px;
  font-size: var(--fs-h3);
  font-weight: 700;
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-h3);
  color: var(--on-dark);
}

.service-card p {
  margin: 0;
  font-size: var(--fs-lead);
  font-weight: 500;
  line-height: 1.6883;         /* measured 26px at 15.4px */
  color: var(--on-dark-muted);
}

@media (max-width: 991px) {
  .service-box { grid-template-columns: repeat(2, 1fr); }
}

@media (max-width: 767px) {
  .service-box { grid-template-columns: 1fr; }
  .service-card { padding: 48px 24px; }
}

/* ---- components/counters.css ---- */
/* ---- animated counters ---------------------------------------------------
   Four figures on the "Owner operated" band. The original uses Elementor's
   counter widget: renders 0 in the HTML and animates to `data-to-value` over
   3000ms, with a thousands delimiter (so 1500 → "1,500+").

   Here the finished number is written into the HTML and JS counts *up to* it.
   That inversion matters: Elementor's markup shows "0" to anyone with JS off
   or a failed script, which on a business site means the page claims zero
   years of experience. The static value is the honest fallback.
   -------------------------------------------------------------------------- */

.counters {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(var(--col-min-4), 100%), 1fr));
  gap: var(--gap);
  text-align: center;
}

/* The block that pairs a figure with its label. It is a grid item of
   `.counters`; `min-width: 0` keeps a long label from forcing the track wider
   than its share and pushing the row into horizontal overflow. */
.counter {
  min-width: 0;
}

.counter__value {
  display: block;
  font-size: var(--fs-counter);
  font-weight: 700;
  line-height: var(--lh-tight);
  letter-spacing: normal;        /* measured: the counters are NOT tracked */
  color: var(--on-dark);
  font-variant-numeric: tabular-nums;
}

.counter__label {
  display: block;
  margin-top: 8px;
  font-size: var(--fs-counter-label);
  font-weight: 400;              /* measured w400 */
  line-height: 1.3333;           /* measured 28px at 21px */
  color: var(--muted);           /* measured #969696 */
}

/* ---- components/benefits.css ---- */
/* ---- benefit grids, checklist row, and the pencil-underlined heading -----
   Three related pieces:
     .value-grid    — the "Why Choose Us?" 3×2 of heading + one sentence
     .checklist     — the centred row of check-mark claims on the CTA band
     .highlighted-word — the animated pencil stroke under "FREE ESTIMATE"
   -------------------------------------------------------------------------- */

/* ---- Why Choose Us -------------------------------------------------------
   Six items, no icons, no links. auto-fit gives 3 / 2 / 1 columns on its own. */
.value-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(var(--col-min-3), 100%), 1fr));
  gap: clamp(2rem, 1.25rem + 3vw, 3.25rem);
  text-align: center;
}

.value-grid h3 {
  margin: 0 0 12px;
  font-size: var(--fs-sub);
  font-weight: 700;
  letter-spacing: var(--ls-h3);
  color: var(--ink);
}
.value-grid p { margin: 0; }

/* On the dark band the same grid needs light type. */
.on-dark .value-grid h3 { color: var(--on-dark); }
.on-dark .value-grid p { color: var(--on-dark-muted); }

/* ---- the core-values list ------------------------------------------------
   The original writes these as four <p> elements each starting with a literal
   "•" character, which screen readers announce as prose. A real <ul> carries
   the list semantics; the bullet is drawn by CSS instead of typed. */
.value-list {
  margin: 0;
  padding: 0;
  list-style: none;
}
.value-list li {
  position: relative;
  padding-left: 1.25em;
  margin-bottom: 0.6em;
}
.value-list li::before {
  content: "•";
  position: absolute;
  left: 0;
  color: var(--accent);
}
.value-list b { color: var(--ink); }
.on-dark .value-list b { color: var(--on-dark); }

/* ---- checklist row ------------------------------------------------------- */
.checklist {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: clamp(1rem, 0.5rem + 2vw, 2.5rem);
  margin: 0 0 clamp(2rem, 1.5rem + 2vw, 3rem);
  padding: 0;
  list-style: none;
}

.checklist li {
  display: flex;
  align-items: center;
  gap: 10px;                 /* the original's icon margin-right */
  font-size: var(--fs-label);   /* measured 16.8px */
  font-weight: 500;             /* measured w500, not 700 */
  line-height: 1.2;             /* measured 20.16px at 16.8px */
  letter-spacing: var(--ls-label);
  color: var(--on-dark);
  text-transform: uppercase;
}
.checklist svg { width: 1em; height: 1em; flex: none; fill: currentColor; }

/* ---- pencil-underlined heading -------------------------------------------
   Reproduced from the original's inline CSS:

     .highlighted-word:after {
       right: calc(100% - 74px); bottom: -6px;
       background: <pencil-blue-line.png on the vendor's server>;
       width: 0; height: 15px;
       animation: pencilAnimation 500ms 2s forwards;
       filter: brightness(0) invert(1);
     }
     @keyframes pencilAnimation { 0% { width: 0 } to { width: 74px } }

   74px over 500ms = 0.148 px/ms. The asset is unchanged at 74×15, so the
   original duration is the correct one — no speed conversion needed.

   The source PNG is a blue stroke; `brightness(0) invert(1)` turns it white.
   The original hot-linked it from the Porto theme author's demo server over
   plain HTTP. A byte-identical copy was already hosted on the client's own
   domain, so it is served locally here. (The vendor URL is written out in
   README.md rather than here, so that a grep for vendor domains over the
   built CSS stays clean. For the same reason the quoted background above has
   no url() wrapper: tools/verify.js reads every url() in the built CSS,
   comments included, and reported the quoted one as a missing file.)
   -------------------------------------------------------------------------- */
.highlighted-word { position: relative; }

.highlighted-word::after {
  content: "";
  position: absolute;
  right: calc(100% - 74px);
  bottom: -6px;
  width: 0;
  height: 15px;
  background: url("/assets/img/pencil-blue-line.png") left center / 74px 15px no-repeat;
  filter: brightness(0) invert(1);
  overflow: hidden;
  animation: pencilAnimation 500ms ease 2s forwards;
}

@keyframes pencilAnimation {
  from { width: 0; }
  to   { width: 74px; }
}

/* A stroke that draws itself is decoration; with reduced motion it is simply
   already drawn rather than absent. */
@media (prefers-reduced-motion: reduce) {
  .highlighted-word::after { animation: none; width: 74px; }
}

/* ---- components/estimate-band.css ---- */
/* ---- the "free estimate" band on Home ------------------------------------
   Rebuilt 2026-09-25 to match the original after the client's review, which
   reported its colour and size differ ("los colores y tamaño del área son
   diferentes"). It had shipped as a dark photographic band ~140px shorter.

   Measured on the original at 1440 / 1024 / 992 / 900 / 768 / 600 / 390, as
   offsets from the top of the band:

                          ≥992px    768–991    600      390
     heading top           255.7      168      148     144.6
     heading size           49px      49px    32.2px   29.9px   (line 1.3)
     CALL NOW size          35px      35px     28px     26px    (line 26px)
     FREE ESTIMATE          42px / 800 / line 42px, 4.5px up into the
                            heading's last line, on CALL NOW's baseline
     intro                  25.5 below FREE ESTIMATE (49.5 at 600, 44.6 at 390)
     checklist              51 below the intro (47.5 at 390); a row at ≥992,
                            stacked 30px apart below that
     button                 48 below the checklist; #212529, 20.5px 48px, 65 tall
     band bottom            167.7 below the button (80 below 992px)
     background             #0169fe

   Two things differ on purpose:
   - The original lays a faint Porto demo stock photo (build_bg.jpg) and a
     demo "half transparent circle" PNG over the blue. Both are theme-demo
     files, not DSP's; the operator chose (2026-09-25) to keep them out. The
     circle is redrawn below in CSS from the PNG's own geometry, so the band
     looks the same without shipping it. The photo is not replaced.
   - FREE ESTIMATE is weight 800 on the original. The self-hosted Poppins has
     400–700 (fonts.css), so it renders at 700, a little lighter and narrower.
   The original's third checklist item, FREE LIFETIME UPDATES, stays out — a
   software-licence line from the Porto demo (README, item 6).
   -------------------------------------------------------------------------- */

.estimate-band {
  position: relative;
  padding: 255.7px 0 167.7px;
  background: var(--blue);
  text-align: center;
  overflow: hidden;
}

/* The circle: a ring 3% white, the band between two circles fitted to the
   original PNG's edges (837x594; outer centre 639.7,-55.1 r 643.2 and hole
   centre 679.9,-49.2 r 315.2, both within 5px). As percentages of its box,
   so it scales with the band exactly as the original's image does: full size
   from 900px up, then the band's width at the same aspect. */
.estimate-band::after {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  z-index: 1;
  width: min(837px, 100%);
  aspect-ratio: 837 / 594;
  background: radial-gradient(ellipse 76.85% 108.28% at 76.43% -9.28%,
              rgba(255, 255, 255, 0.031) 99.8%, transparent 100%);
  -webkit-mask-image: radial-gradient(ellipse 37.66% 53.06% at 81.23% -8.28%, transparent 99.8%, #000 100%);
          mask-image: radial-gradient(ellipse 37.66% 53.06% at 81.23% -8.28%, transparent 99.8%, #000 100%);
  pointer-events: none;
}

.estimate-band__title {
  margin: 0;
  font-size: 49px;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: var(--ls-display);
  color: var(--on-dark);
}

.estimate-band__call {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: baseline;
  column-gap: 8px;
  row-gap: 29.7px;
  margin-top: -4.5px;
}
.estimate-band__now {
  margin: 0;
  font-size: 35px;
  font-weight: 700;
  line-height: 26px;
  color: var(--on-dark);
}
.estimate-band__free {
  margin: 0;
  font-size: 42px;
  font-weight: 800;          /* renders 700: see the note at the top */
  line-height: 42px;
  color: var(--on-dark);
}

.estimate-band__intro {
  margin: 25.5px 0 0;
  font-size: var(--fs-label);   /* 16.8px */
  font-weight: 300;
  line-height: 26px;
  color: rgba(230, 230, 230, 0.8);
}

.estimate-band .checklist {
  gap: 30px 70px;              /* measured 70px between items in the row */
  margin: 51px 0 0;
}

.estimate-band__action { margin-top: 48px; }
.estimate-band__btn {
  padding: 20.5px 48px;
  line-height: 24px;
  background: var(--ink);
  color: var(--on-dark);
}
.estimate-band__btn:hover { background: var(--accent-dark); color: var(--on-dark); }

@media (max-width: 991px) {
  .estimate-band { padding: 168px 0 80px; }
  .estimate-band__intro { padding-inline: 20px; }
  .estimate-band .checklist { flex-direction: column; align-items: center; }
}

@media (max-width: 767px) {
  .estimate-band { padding-top: 148px; }
  .estimate-band__title { font-size: 32.2px; }
  .estimate-band__now { font-size: 28px; }
  .estimate-band__intro { margin-top: 49.5px; }
  /* The original's top wedge keeps more depth on phones (measured 44.6px
     padding at 390) than the site-wide 1.75rem trim. */
  .estimate-band > .angle--top { padding: 44.6px 0; }
}

@media (max-width: 575px) {
  .estimate-band { padding-top: 144.6px; }
  .estimate-band__title { font-size: 29.9px; }
  .estimate-band__now { font-size: 26px; }
  /* CALL NOW and FREE ESTIMATE no longer fit one line; the original puts
     CALL NOW 4.9px under the heading and FREE ESTIMATE on the next line. */
  .estimate-band__call { margin-top: 4.9px; }
  .estimate-band__intro { margin-top: 44.6px; }
  .estimate-band .checklist { margin-top: 47.5px; gap: 28px; }
}

/* ---- components/team.css ---- */
/* ---- owner / team --------------------------------------------------------
   DSP is one person, so this is a single centred block rather than a grid of
   cards: heading, name, a wide photograph, then the biography lines.

   The original marks all four biography sentences as <h4> purely to size them.
   They are paragraphs here — they are prose, not headings, and marking them up
   as headings puts four junk entries in every screen reader's heading list.
   The visual size is preserved by .owner__bio.
   -------------------------------------------------------------------------- */

.owner {
  max-width: 900px;
  margin-inline: auto;
  text-align: center;
}

.owner__photo {
  width: 100%;
  margin: clamp(1.5rem, 1rem + 2vw, 2.5rem) 0;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--grey-band);
}
.owner__photo img {
  width: 100%;
  height: auto;
  display: block;
}

.owner__bio p {
  margin: 0 0 1em;
  font-size: var(--fs-h4);
  font-weight: 500;
  line-height: 1.5;
}
.owner__bio p:last-child { margin-bottom: 0; }
.owner__bio em { font-style: italic; }

/* ---- about: tall photo with a rotated caption ----------------------------
   The original rotates the caption -90° over the left-hand photo and hides the
   whole column below the lg breakpoint, so tablet and phone visitors lose the
   image entirely. Here the photo is kept at every width and the caption simply
   stops rotating once there is no tall column to run down.
   -------------------------------------------------------------------------- */
.figure-rotated {
  position: relative;
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--grey-band);
}
.figure-rotated img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.figure-rotated__cap {
  position: absolute;
  left: 0;
  top: 50%;
  transform: rotate(-90deg) translateX(-50%);
  transform-origin: left center;
  margin: 0;
  white-space: nowrap;
  font-size: var(--fs-h4);
  font-weight: 700;
  letter-spacing: var(--ls-h3);

  /* White type with a dark glow assumes a dark photograph behind it. The photo
     this caption actually runs down is a studio portrait on a white backdrop,
     which made the line invisible at every width above the drawer breakpoint.
     Dark type with a white halo is the same treatment the rule below already
     applies once the caption stops rotating, so the two now agree. */
  color: var(--ink);
  text-shadow: 0 0 10px var(--white), 0 0 4px var(--white);
}

@media (max-width: 991px) {
  .figure-rotated__cap {
    position: static;
    transform: none;
    padding: 12px 16px;
    color: var(--ink);
    text-shadow: none;
    white-space: normal;
  }
}

/* ---- components/about.css ---- */
/* ---- About page type scale ------------------------------------------------
   The original does not use one type scale across the site. The homepage sets
   section headings at 38.5px and body at 14px; the About page sets headings at
   30px and body at 16px, rising to 20px for the opening paragraph. Building
   the whole site to the homepage's scale left every line on About materially
   smaller than the original — the lead paragraph at 14px against 20px.

   Every value below was measured on the live page at 390 / 768 / 1100 / 1440.
   The original steps at its breakpoints; these are the same endpoints
   expressed as one clamp, so the two agree at the ends and differ by 1-2px in
   the middle of the range.

   Note on ordering: the page-scoped rules below are (0,2,0), so a single-class
   modifier like `.about-lede` cannot override them. Every modifier here is
   therefore scoped with `.about-page` too and declared after the rule it
   refines — the first cut of this file was not, and the lead paragraph and the
   display heading both silently lost to the generic rules.
   -------------------------------------------------------------------------- */

/* Section headings: measured 32.69px at 390, 30px at 768 and flat above — it
   gets slightly *smaller* as the viewport grows, hence the negative slope. */
.about-page .section-title {
  font-size: clamp(1.875rem, 2.217rem - 0.71vw, 2.043rem);   /* 32.69 → 30 */
  line-height: 1.2;
  letter-spacing: var(--ls-h2);
}

/* The page's opening heading is the one exception: 40.1px at 390 rising to
   60px at 1100, uppercase, and tracked *positive* where every other heading on
   the site is tracked negative. */
.about-page .section-title--display {
  font-size: clamp(2.5rem, 1.813rem + 2.82vw, 3.75rem);      /* 40.1 → 60 */
  line-height: 1.2;
  letter-spacing: 0.01em;
  text-transform: uppercase;
}

/* Body copy on About: 14.86px at 390 → 16px at 768 and above, on a 1.875
   leading rather than the site's default 26/14, and at weight 500. */
.about-page p,
.about-page li {
  font-size: clamp(0.929rem, 0.855rem + 0.30vw, 1rem);       /* 14.86 → 16 */
  font-weight: 500;
  line-height: 1.875;
}

/* Everything inside the owner block drops back to 400 — measured. It sits on
   the deep dark band, where the original uses solid #f6f6f6 for the prose and
   pure white for the quote, not the muted white `.section--dark` hands down. */
.about-page .owner p { font-weight: 400; }
.about-page .owner p,
.about-page .owner cite { color: #f6f6f6; }
.about-page .owner__bio p { color: var(--on-dark); }

/* The lead runs larger still — to 20px — and on a tighter 1.7 leading. */
.about-page .about-lede {
  font-size: clamp(0.929rem, 0.752rem + 0.72vw, 1.25rem);    /* 14.86 → 20 */
  font-weight: 400;
  line-height: 1.7;
}

.about-page .value-grid h3 {
  font-size: clamp(1.2769rem, 1.2404rem + 0.15vw, 1.375rem); /* 20.43 → 22 */
  font-weight: 500;
  letter-spacing: var(--ls-h3);
}

/* Kicker over the rotated photo: small, uppercase, widely tracked (0.1em) —
   the opposite treatment to a heading, which is why it reads as a label. */
.figure-rotated__cap {
  font-size: clamp(0.9375rem, 0.744rem + 0.79vw, 1.125rem);  /* 15 → 18 */
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-alt);
}

/* The owner's pull-quote is the only place on the site set in a serif. */
.about-page .owner__bio p {
  font-family: var(--font-quote);
  font-size: clamp(1.393rem, 1.128rem + 1.09vw, 1.875rem);   /* 22.29 → 30 */
  font-weight: 400;
  line-height: 1.4;
  letter-spacing: 0.01em;
}

.about-page .owner__bio cite {
  font-size: clamp(0.929rem, 0.855rem + 0.30vw, 1rem);       /* 14.86 → 16 */
  font-weight: 500;
  font-style: normal;
  line-height: clamp(1.25rem, 1.1106rem + 0.57vw, 1.625rem);  /* 20 → 26 */
}

/* ---- components/portfolio.css ---- */
/* ---- portfolio grid ------------------------------------------------------
   The original is a Porto "creative grid" driven by the Masonry script, with
   per-item width and height classes:

     .grid-col-1-2 = 50%   .grid-col-1-4 = 25%
     .grid-height-1 = 502px   .grid-height-1-2 = 251px
     ≤767px: .grid-col-md-1-2 = 50%, heights 335px / 167px
     ≤480px: every tile 100% wide
     gutter: 5px each side, 10px below

   Reproduced with CSS grid instead. A 4-column track with a 251px row unit
   expresses exactly the same layout — a half-width tile spans 2 columns, a
   full-height one spans 2 rows — and it needs no script, no reflow on load,
   and no layout jump when an image finishes decoding.

   ⚠️ Tile heights (502/251) are taken from the original's own stylesheet, not
   from a rendered measurement. Confirm in the browser pass.
   -------------------------------------------------------------------------- */

.pf-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: 251px;
  gap: 10px;
}

.pf-item {
  position: relative;
  grid-column: span 1;
  grid-row: span 1;
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--grey-band);
}

.pf-item--wide { grid-column: span 2; }
.pf-item--tall { grid-row: span 2; }

/* The photo is a link to the project, as on the original (a.img-thumbnail over
   the whole tile): a click anywhere on it opens the project. The client
   reported the rebuild opened a project only from its title (2026-09-24).
   It is aria-hidden and out of the tab order, so a screen reader or keyboard
   user meets one link per project — the title — not two. */
.pf-item__img { display: block; width: 100%; height: 100%; }

.pf-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--transition-slow);
}
.pf-item:hover img,
.pf-item:focus-within img { transform: scale(1.1); }   /* measured on the original */

/* ---- caption -------------------------------------------------------------
   The original hides the caption entirely (opacity 0) and slides it up on
   hover. Kept for pointer devices, but with two corrections:

   - `:focus-within` reveals it too, so a keyboard user can see which project
     they are on. Without this the grid is unusable without a mouse.
   - `@media (hover: none)` shows every caption permanently. On a touch device
     there is no hover state at all, so on the original every project title and
     category is simply invisible on a phone.
   -------------------------------------------------------------------------- */
.pf-item__cap {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 2;
  padding: 20px;
  background: linear-gradient(to top, rgba(33, 37, 41, 0.8) 0, transparent 100%);
  opacity: 0;
  transform: translate3d(0, 100%, 0);
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.pf-item:hover .pf-item__cap,
.pf-item:focus-within .pf-item__cap {
  opacity: 1;
  transform: translate3d(0, 0, 0);
}

.pf-item__cap h2,
.pf-item__cap h3 {
  margin: 0;
  font-size: var(--fs-sub);      /* measured 25.5px */
  font-weight: 600;              /* measured w600, not 700 */
  line-height: var(--lh-tight);  /* measured 25.5px at 25.5px */
  letter-spacing: var(--ls-h3);
  color: var(--on-dark);
}
.pf-item__cap h2 a,
.pf-item__cap h3 a { color: inherit; }

/* Measured on the original: 13.6px, weight 300, uppercase, solid white. */
.pf-item__meta {
  display: block;
  margin-top: 4px;
  font-size: 0.85rem;
  font-weight: 300;
  text-transform: uppercase;
  color: var(--on-dark);
}
/* inline-block + vertical padding so the 13px category link still clears the
   24x24 minimum of WCAG 2.2 SC 2.5.8. Inline, it is only 19px tall. */
.pf-item__meta a {
  color: inherit;
  text-decoration: underline;
  display: inline-block;
  padding: 3px 0;
}

/* Stretch the title's link across the caption, so the caption's gradient
   opens the project too. This was meant to cover the whole tile, and never
   did: .pf-item__cap is absolutely positioned (and transformed), so it — not
   the tile — is the box `inset: 0` fills. The photo link above covers the
   rest; the two overlap harmlessly on the caption. */
.pf-item__link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}
/* Keep the category link clickable above that overlay. */
.pf-item__meta { position: relative; z-index: 2; }

@media (hover: none) {
  .pf-item__cap { opacity: 1; transform: none; }
}

/* Two columns on tablets, matching the original's .grid-col-md-1-2. */
@media (max-width: 767px) {
  .pf-grid { grid-template-columns: repeat(2, 1fr); grid-auto-rows: 167px; }
  .pf-item--tall { grid-row: span 2; }
}

/* One column below 480px, as the original does. */
@media (max-width: 479px) {
  .pf-grid { grid-template-columns: 1fr; grid-auto-rows: 220px; }
  .pf-item--wide { grid-column: span 1; }
  .pf-item--tall { grid-row: span 1; }
}

@media (prefers-reduced-motion: reduce) {
  .pf-item img, .pf-item__cap { transition: none; }
}

/* ---- components/testimonials.css ---- */
/* ---- testimonials --------------------------------------------------------
   Two columns: a left rail that sticks while the three cards scroll past it,
   and the cards themselves. The original builds the sticky rail with a plugin
   widget (`alpus-nested-sticky`); `position: sticky` does the same job with no
   script at all.

   Sticky needs the grid item — not the inner element — to be the scroll
   container's child, and it needs `align-self: start`, or the item stretches
   to the row height and has nothing to stick within.
   -------------------------------------------------------------------------- */

.testimonials {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
  gap: clamp(2rem, 1rem + 4vw, 4rem);
  align-items: start;
}

.testimonials__rail {
  position: sticky;
  top: calc(var(--header-h-stuck) + 24px);
  align-self: start;
}

.testimonials__list {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
  margin: 0;
  padding: 0;
  list-style: none;
}

.testimonial {
  padding: clamp(1.75rem, 1.25rem + 2vw, 2.5rem);
  border-radius: var(--radius);
  background: var(--white);
  box-shadow: var(--shadow-card);
}

.testimonial__head {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-bottom: 1.5rem;
}

.testimonial__avatar {
  width: 64px;
  height: 64px;
  flex: none;
  border-radius: 50%;
  object-fit: cover;
  background: var(--accent);
}

.testimonial__name {
  margin: 0;
  font-size: 1rem;                 /* measured 16px */
  font-weight: 700;
  letter-spacing: var(--ls-body);
  color: var(--ink);
}

.testimonial__role {
  margin: 2px 0 0;
  font-size: var(--fs-small);
  color: var(--muted);
}

.testimonial__quote p { margin: 0 0 0.75em; }
.testimonial__quote p:last-child { margin-bottom: 0; }

/* Five stars. The rating is decorative repetition, so the visible stars are
   aria-hidden and the real value goes in a visually-hidden string — otherwise
   a screen reader reads "star star star star star". */
.testimonial__rating {
  display: flex;
  gap: 4px;
  margin-top: 1.25rem;
  color: var(--accent);
}
.testimonial__rating svg { width: 16px; height: 16px; fill: currentColor; }

/* Below the stacking point the rail stops sticking — it would otherwise pin a
   full-width block to the top of a phone screen and cover the cards. */
@media (max-width: 767px) {
  .testimonials__rail { position: static; }
}

/* ---- components/gallery.css ---- */
/* ---- filterable gallery + lightbox --------------------------------------- */

.gallery-filters {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin-bottom: 40px;
  padding: 0;
  list-style: none;
}
.gallery-filters button {
  padding: 8px 20px;
  border: 0;
  background: transparent;
  color: var(--ink-alt);
  font-family: inherit;
  font-size: var(--fs-body);
  font-weight: 600;
  cursor: pointer;
  transition: background var(--transition), color var(--transition);
}
.gallery-filters button:hover { color: var(--accent); }
.gallery-filters button.is-active { background: var(--accent); color: #fff; }

.gallery-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(var(--col-min-gallery), 100%), 1fr));
  gap: clamp(0.875rem, 0.6rem + 1.2vw, 1.375rem);
}

.gallery-item {
  position: relative;
  display: block;
  padding: 0;
  border: 0;
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--grey-band);
  cursor: pointer;
}
.gallery-item img {
  width: 100%; height: 100%;
  object-fit: cover;
  transition: transform var(--transition-slow);
}
.gallery-item:hover img { transform: scale(1.07); }

.gallery-item__cap {
  position: absolute;
  inset: auto 0 0 0;
  padding: 34px 18px 14px;
  background: linear-gradient(180deg, transparent, rgba(34, 37, 41, 0.85));
  color: #fff;
  font-size: var(--fs-body);
  font-weight: 600;
  text-align: left;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity 0.25s ease, transform 0.25s ease;
}
.gallery-item:hover .gallery-item__cap,
.gallery-item:focus-visible .gallery-item__cap { opacity: 1; transform: none; }

/* On touch devices there is no hover, so the caption is always shown. */
@media (hover: none) {
  .gallery-item__cap { opacity: 1; transform: none; }
}

/* ---- lightbox ----------------------------------------------------------- */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 3000;
  display: none;
  align-items: center;
  justify-content: center;
  padding: clamp(1.25rem, 0.5rem + 3vw, 2.5rem) 1.25rem;
  background: rgba(17, 19, 22, 0.94);
}
.lightbox.is-open { display: flex; }
.lightbox img {
  max-width: min(1100px, 92vw);
  max-height: 84vh;
  width: auto;
  object-fit: contain;
}
.lightbox__cap {
  position: absolute;
  bottom: 26px; left: 0; right: 0;
  text-align: center;
  color: rgba(255, 255, 255, 0.86);
  font-size: var(--fs-body);
}
.lightbox__btn {
  position: absolute;
  width: 48px; height: 48px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 0;
  background: rgba(255, 255, 255, 0.1);
  color: #fff;
  cursor: pointer;
  transition: background var(--transition);
}
.lightbox__btn:hover { background: var(--accent); }
.lightbox__close { top: 22px; right: 22px; }
.lightbox__prev { left: 22px; top: 50%; transform: translateY(-50%); }
.lightbox__next { right: 22px; top: 50%; transform: translateY(-50%); }

/* ---- components/footer.css ---- */
/* ---- footer ----------------------------------------------------------------
   Rebuilt 2026-09-25 to match the original after the client's review: the
   rebuild's footer repeated the business name and a "Call For Quote" button,
   carried a large logo, set the contact details with icons on a striped
   #2b2a28 ground, and was 627px tall against the original's 498 — "la
   información es distinta y tiene un contenido adicional".

   Measured on the original (1440 / 768 / 390), from the footer's top edge:
     ground        #212529, no pattern
     Contact Info  +78; 22.68px / 700 / line 1, -0.05em, white (21.06px ≤575)
     labels        ADDRESS / PHONE / EMAIL: 15.12px / 600 / line 1, white,
                   26px under the title, 16px under the previous value
     values        18.9px / 400 / line 24px, #969696, 4px under their label
     Facebook      48px circle, 1px rgba(255,255,255,.1) border, 20px under
                   the email; glyph 12.8px
     rule          1px rgba(247,247,247,.1), 54.7px under the circle
     copyright     28px under the rule, centred, 15.12px / line 26px,
                   rgba(150,150,150,.7) (14.04px ≤575); "DSP Painting ©
                   copyright 2026. All Rights Reserved."
     bottom        32px under the copyright (56px below 992px)
     height        498.4 at ≥992, 522.4 at 768, 542.7 at 390

   Not reproduced: the original's footer is fixed behind the page, which
   scrolls up to reveal it (Porto's "footer reveal"). This one scrolls with
   the page, as before. */

.site-footer {
  position: relative;
  background: var(--ink);
  color: var(--muted);
}

.footer-main { padding-top: 78px; }

.footer-title {
  margin: 0;
  font-size: 22.68px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: var(--ls-display);
  color: var(--on-dark);
}

.footer-contact { margin: 26px 0 0; }
.footer-contact dt {
  margin: 16px 0 0;
  font-size: 15.12px;
  font-weight: 600;
  line-height: 1;
  text-transform: uppercase;
  color: var(--on-dark);
}
.footer-contact dt:first-child { margin-top: 0; }
.footer-contact dd {
  margin: 4px 0 0;
  font-size: 18.9px;
  font-weight: 400;
  line-height: 24px;
  color: var(--muted);
}
.footer-contact a { color: inherit; }
.footer-contact a:hover { color: var(--on-dark); }
/* utilities.css lifts every bare tel: link to 28px for tap size. 24px already
   meets WCAG 2.2's 24x24 minimum, and 28 would push everything under the
   phone number 4px lower than the original. */
/* (0,3,2) — utilities.css's rule is (0,3,1) and loads last, so this must
   outscore it, not tie it. */
.site-footer .footer-contact dd a[href^="tel:"] { min-height: 24px; }

.footer-social { display: flex; gap: 12px; margin-top: 20px; }
.footer-social-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 48px;
  height: 48px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 50%;
  color: var(--on-dark);
}
/* The path fills 17.5/24 of its viewBox: 17.5px draws the original's 12.8px glyph. */
.footer-social-link svg { width: 17.5px; height: 17.5px; }
.footer-social-link:hover { background: rgba(255, 255, 255, 0.1); color: var(--on-dark); }

.footer-bottom {
  margin-top: 54.7px;
  padding: 28px 0 32px;
  border-top: 1px solid rgba(247, 247, 247, 0.1);
  text-align: center;
  font-size: 15.12px;
  line-height: 26px;
  color: rgba(150, 150, 150, 0.7);
}
.footer-bottom p { margin: 0; }
.footer-credit { margin-top: 14px; }
.footer-credit img { width: 96px; margin: 0 auto; opacity: 0.85; }   /* original: 96x96 */

@media (max-width: 991px) {
  .footer-bottom { padding-bottom: 56px; }
}

@media (max-width: 575px) {
  .footer-title { font-size: 21.06px; }
  .footer-bottom { font-size: 14.04px; }
}

/* ---- floating controls -------------------------------------------------- */
/* Call button on touch/small screens, back-to-top on larger ones. */
.call-fab {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 900;
  display: none;
  align-items: center;
  justify-content: center;
  width: 54px; height: 54px;
  border-radius: 50%;
  background: var(--accent);
  color: #fff;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.28);
}
.call-fab:hover { background: var(--accent-dark); color: #fff; }

.to-top {
  position: fixed;
  right: 20px;
  bottom: 20px;
  z-index: 900;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px; height: 40px;
  border: 0;
  background: rgba(34, 37, 41, 0.72);
  color: #fff;
  cursor: pointer;
  opacity: 0;
  visibility: hidden;
  transition: opacity 0.25s ease, visibility 0.25s, background var(--transition);
}
.to-top.is-visible { opacity: 1; visibility: visible; }
.to-top:hover { background: var(--accent); }

@media (max-width: 991px) {
  .call-fab { display: inline-flex; }
  .to-top { display: none; }
}

/* ---- components/dark-band.css ---- */
/* ---- dark bands ----------------------------------------------------------
   Most of this site's sections are dark. `.section--dark` sets the ground;
   `.on-dark` flips the text colours for anything inside it, since components
   otherwise inherit the light-background palette and would be unreadable.

   `.section--photo` is the same thing with a photograph behind it and the
   original's heavy overlay on top: #212529 at 0.9. Set the image with an
   inline `background-image` on the section — it is per-page content, not a
   design token, so it does not belong in the stylesheet.
   -------------------------------------------------------------------------- */

.section--photo,
.section--dark,
.on-dark {
  color: var(--on-dark-muted);
  isolation: isolate;
}

.section--dark { background: var(--accent); }

/* The original does not use one dark. Its counter band measures #2b2a28 (the
   value of --accent above), but the About page's closing band is a shade
   deeper at #1f1f1f. Applied alongside .section--dark so the text-colour flips
   below still apply. */
.section--dark-deep { background: var(--ink-deep); }

/* `.section--photo` is always a dark photographic band, so it flips its own
   text colours. Relying on the author to also add `.on-dark` means one
   forgotten class renders near-black headings on a near-black photo. */
.section--photo .section-title,
.section--photo h2,
.section--photo h3,
.section--photo h4,
.section--dark .section-title,
.section--dark h2,
.section--dark h3,
.section--dark h4,
.on-dark .section-title,
.on-dark h2,
.on-dark h3,
.on-dark h4 { color: var(--on-dark); }

.section--photo .section-intro,
.section--dark .section-intro,
.on-dark .section-intro { color: var(--on-dark-muted); }

.section--dark a:not(.btn),
.on-dark a:not(.btn) { color: var(--on-dark); }

/* ---- photographic band --------------------------------------------------- */
.section--photo {
  position: relative;
  background-color: var(--accent);
  background-position: center center;
  background-repeat: no-repeat;
  background-size: cover;
}

.section--photo::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  background: #212529;
  opacity: 0.9;
  pointer-events: none;
}

.section--photo > .container { position: relative; z-index: 9; }

/* Readable measure for a block of copy inside a full-width band. */
.narrow {
  max-width: 720px;
  margin-inline: auto;
}

/* ---- utilities.css ---- */
/* ---- utilities, motion, print ------------------------------------------- */

/* A block-level image centred in its column. `img` is `display: block` in the
   reset, so `text-align: center` on the parent does nothing to it and auto
   margins are the only way. Note that `.narrow` is not a substitute: its
   `max-width: 720px` outranks the reset's `max-width: 100%`, so an image
   narrower than 720px would keep its intrinsic width and overflow a phone. */
.img-center { margin-inline: auto; }

/* A centred row of buttons closing a section. `.section-head--center` was being
   used for this, but it only sets text-align and carries no top margin, so a
   trailing button sat flush against the grid above it. */
.actions {
  margin-top: clamp(2rem, 1.5rem + 2vw, 3.125rem);
  text-align: center;
}
.actions .btn + .btn { margin-left: 10px; }

/* Any phone link anywhere is a tap target, including one written directly into
   page content. Enforce the WCAG 2.2 minimum (24x24) globally so a bare
   `tel:` link can never ship too small to hit on a phone — which would be a
   particularly bad failure on a business whose phone number is the product.

   `:not(.btn)` and `:not(.call-fab)` are load-bearing. An attribute selector
   scores (0,1,1) — higher than a plain class — and this file is last in the
   manifest, so without the exclusions this rule beats any single-class
   `display: none` on a `tel:` link. It did exactly that twice: to a header
   "Call For Quote" button (since removed), which kept rendering at 207px on
   phones and pushed the menu button off the viewport; and to the round
   `.call-fab`, which is meant for ≤991px only (footer.css) but showed at every
   width, sitting over the back-to-top button on desktop (fixed 2026-09-25).
   Buttons guarantee their own hit area; this rule is only for bare links in
   copy. */
a[href^="tel:"]:not(.btn):not(.call-fab) {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
}

/* Scroll reveal. Hidden only when JS is present — with JS off, or if the
   observer never fires, content stays visible. main.js also has a timed
   failsafe. An unreadable page is never an acceptable failure mode. */
.js .reveal {
  opacity: 0;
  transform: translateY(24px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}
.js .reveal.is-visible { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .js .reveal { opacity: 1; transform: none; }
}

@media print {
  .topbar,
  .site-header,
  .site-footer,
  .call-fab,
  .to-top,
  .gallery-filters,
  .skip-link { display: none; }

  body { color: #000; background: #fff; }
  .section { padding: 1rem 0; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 0.75em; }
}
