/* ==========================================================================
   City Commercial Drywall & Firestopping Ltd. — interior construction systems, Surrey BC
   Hand-written CSS. No frameworks, no libraries.
   Order: tokens → reset → type → layout → header → footer → components →
          page blocks → motion → responsive → reduced motion / print
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. Tokens
   -------------------------------------------------------------------------- */
:root {
  /* Surfaces — warm-grey site family, not neutral gray */
  --ink:            #101112;
  --ink-2:          #191b1d;
  --ink-3:          #232629;
  --surface:        #ffffff;
  --surface-2:      #f4f4f1;
  --surface-3:      #e9e8e3;
  --line:           #d8d7d1;
  --line-strong:    #b9b8b1;
  --line-dark:      #33363a;

  /* Text — secondary tinted from the surface hue */
  --text:           #16181a;
  --text-muted:     #55585c;
  --text-on-ink:    #ffffff;
  --text-muted-ink: #a9adb2;

  /* Accent — CCDF signal red, sampled from the logo. It is a surface colour:
     rules, tabs, buttons and the header bar. Text sitting on it uses
     --accent-text (white, 4.6:1); red text on the dark ink surfaces uses
     --accent-bright, because the brand red itself only reaches 4.1:1 there. */
  --accent:         #e81620;
  --accent-press:   #b8101a;
  --accent-soft:    #fdeaec;
  --accent-text:    #ffffff;
  --accent-bright:  #ff4b52;

  /* Feedback — a deeper, browner red than the brand accent, so an error
     state never reads as branding. */
  --danger:         #a3121b;
  --danger-soft:    #fdeceb;
  --success:        #17603a;
  --success-soft:   #e8f4ed;

  /* Motion — one shared curve, three speeds */
  --ease:           cubic-bezier(.215, .61, .355, 1);
  --dur-quick:      160ms;
  --dur-base:       240ms;
  --dur-slow:       340ms;

  /* Depth — real offset, soft blur */
  --shadow-1: 0 1px 2px rgba(16,17,18,.06), 0 8px 24px -14px rgba(16,17,18,.30);
  --shadow-2: 0 2px 4px rgba(16,17,18,.07), 0 18px 40px -18px rgba(16,17,18,.36);

  /* Structure */
  --wrap:        76rem;
  --wrap-narrow: 46rem;
  --gutter:      clamp(1.25rem, 4vw, 2.5rem);
  --section-y:   clamp(3.75rem, 8vw, 8rem);
  --radius:      3px;
  --header-h:    4.5rem;
  --header-gap:  clamp(.625rem, 2vw, 1.25rem);
  --header-total: calc(var(--header-h) + var(--header-gap) * 2);

  /* Type */
  --font-display: "Archivo", "Helvetica Neue", Arial, sans-serif;
  --font-body:    "Inter", "Helvetica Neue", Arial, sans-serif;

  --fs-display: clamp(2.75rem, 7.4vw, 5.25rem);
  /* Home hero headline. Its own step because the headline is two sentences,
     not the three words --fs-display was drawn for. */
  --fs-hero-title: clamp(1.875rem, 5.4vw, 3.875rem);
  --fs-h1:      clamp(2.15rem, 5vw, 3.5rem);
  --fs-h2:      clamp(1.75rem, 3.4vw, 2.625rem);
  --fs-h3:      clamp(1.1875rem, 1.6vw, 1.5rem);
  --fs-h4:      clamp(1.0625rem, 1vw, 1.1875rem);
  --fs-lead:    clamp(1.1875rem, 1.1vw + .9rem, 1.4375rem);
  --fs-body:    clamp(1.0625rem, .3vw + 1rem, 1.125rem);
  --fs-small:   .9375rem;
  --fs-meta:    .78125rem;
}

/* --------------------------------------------------------------------------
   2. Reset & browser surfaces
   -------------------------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
  /* Deliberately no `scroll-behavior: smooth` here. It applied to every
     scroll the browser makes — including its own jump to a #hash when a
     page is opened at one — so arriving on the home page from Services in
     another page's nav started at the top and slid the reader down, over
     the top of the arrival transition. Instant here means a hash arrival
     lands on the section before the first paint; the glide for a click on
     an in-page link is asked for explicitly in script.js (5c). */
  scroll-padding-top: calc(var(--header-total) + 1.5rem);
  /* Themed scrollbar rather than the platform default */
  scrollbar-color: var(--line-strong) var(--surface-2);
  scrollbar-width: thin;
}

body {
  margin: 0;
  /* No background here on purpose — the root carries it and each block paints
     its own, so page transitions can fade the page off cleanly. See 20b. */
  color: var(--text);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: 1.65;
  letter-spacing: -.011em;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-wrap: break-word;
}

body.is-locked { overflow: hidden; }

::selection { background: var(--accent); color: var(--accent-text); text-shadow: none; }

::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track { background: var(--surface-2); }
::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border: 3px solid var(--surface-2);
  border-radius: 8px;
}
::-webkit-scrollbar-thumb:hover { background: #8d8c85; }

img, svg, video { display: block; max-width: 100%; }
img { height: auto; background: var(--surface-3); }

/* Author display rules must never win over the hidden attribute. */
[hidden] { display: none !important; }

hr {
  border: 0;
  border-top: 1px solid var(--line);
  margin: clamp(2.5rem, 5vw, 4rem) 0;
}

/* Focus — visible everywhere, keyboard only */
:where(a, button, input, textarea, select, summary, [tabindex]):focus-visible {
  outline: 3px solid var(--ink);
  outline-offset: 3px;
  border-radius: 1px;
}
.section--ink :where(a, button, input, textarea, select, summary, [tabindex]):focus-visible,
.site-footer :where(a, button, input, textarea, select, summary, [tabindex]):focus-visible,
.hero :where(a, button, summary, [tabindex]):focus-visible {
  outline-color: var(--accent);
}

/* --------------------------------------------------------------------------
   3. Typography
   -------------------------------------------------------------------------- */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 1.06;
  letter-spacing: -.032em;
  margin: 0 0 .6em;
  text-wrap: balance;
}
h1 { font-size: var(--fs-h1); font-weight: 800; }
h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); line-height: 1.18; letter-spacing: -.024em; }
h4 { font-size: var(--fs-h4); line-height: 1.3;  letter-spacing: -.018em; }

/* Generous space above a heading, tight below */
* + h2 { margin-top: clamp(2.5rem, 5vw, 4rem); }
* + h3 { margin-top: clamp(1.75rem, 3vw, 2.5rem); }
* + h4 { margin-top: 1.5rem; }

p { margin: 0 0 1.15em; max-width: 68ch; }
p:last-child { margin-bottom: 0; }

.lead {
  font-size: var(--fs-lead);
  line-height: 1.45;
  letter-spacing: -.019em;
  color: var(--text);
  max-width: 40ch;
}
.muted { color: var(--text-muted); }

strong, b { font-weight: 600; }

.meta {
  font-family: var(--font-display);
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 0 0 1.25rem;
  line-height: 1.4;
}
.section--ink .meta { color: var(--text-muted-ink); }

/* Numerals in specs and tables line up */
.tabular, table, .spec-list dd, .fact-value { font-variant-numeric: tabular-nums; }

/* Links */
a { color: inherit; text-decoration-thickness: 1px; text-underline-offset: .22em; }

.link {
  color: var(--text);
  text-decoration: none;
  position: relative;
  font-weight: 600;
  padding-bottom: 2px;
  display: inline-flex;
  align-items: center;
  gap: .5rem;
}
.link::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-base) var(--ease);
}
.link:hover::after, .link:focus-visible::after { transform: scaleX(1); }
.link .icon { transition: transform var(--dur-base) var(--ease); }
.link:hover .icon { transform: translateX(4px); }
.section--ink .link { color: var(--text-on-ink); }

/* A .link that has to read as a button rather than as a word: the rule is
   drawn at full width at rest in the text colour and only changes to accent
   on hover, while the arrow slides right. The selector doubles up on .link
   so it outranks the 44px tap-target block further down, and the ::after
   transform is pinned to scaleX(1) in both states so the shared
   .link:hover::after wipe never fires on it. */
.link.link--rule {
  gap: .6rem;
  padding-bottom: .55rem;
}
.link.link--rule::after {
  bottom: 0;
  transform: scaleX(1);
  background: currentColor;
  transition: background-color var(--dur-base) var(--ease);
}
.link.link--rule:hover::after,
.link.link--rule:focus-visible::after { background: var(--accent); }

/* Inline prose links keep a real underline */
.prose a:not(.link):not(.btn) {
  color: var(--text);
  text-decoration: underline;
  text-decoration-color: var(--line-strong);
  text-decoration-thickness: 2px;
  transition: text-decoration-color var(--dur-quick) var(--ease);
}
.prose a:not(.link):not(.btn):hover { text-decoration-color: var(--accent); }

ul, ol { margin: 0 0 1.15em; padding-left: 1.3em; }
li { margin-bottom: .5em; max-width: 66ch; }
li:last-child { margin-bottom: 0; }
li::marker { color: var(--text-muted); }
.section--ink li::marker { color: var(--accent-bright); }

/* --------------------------------------------------------------------------
   4. Layout
   -------------------------------------------------------------------------- */
.wrap {
  width: 100%;
  max-width: var(--wrap);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.wrap--narrow { max-width: calc(var(--wrap-narrow) + var(--gutter) * 2); }

/* Every top-level block paints its own background rather than borrowing
   body's. At rest this changes nothing — it is the same white body carried
   before — but it means a page transition (20b) fades the page's background
   out along with the content sitting on it, instead of leaving a sheet of
   white behind that the content has to disappear against. */
.section { padding-block: var(--section-y); background: var(--surface); }
.section--tight { padding-block: clamp(2.5rem, 5vw, 4.5rem); }
/* Two untinted sections in a row stack both paddings into one gap with no
   change of background to justify it, which reads as a hole rather than a
   break. The previous section's padding-bottom already separates them. Where
   the background does change (--alt, --ink, .cta) the full pair still earns
   its space, so those are excluded on both sides. */
:is(.section:not(.section--alt, .section--ink, .cta))
+ :is(.section:not(.section--alt, .section--ink, .cta)) { padding-top: 0; }
.section--alt  { background: var(--surface-2); }
.section--ink  { background: var(--ink); color: var(--text-on-ink); }
.section--ink h1, .section--ink h2, .section--ink h3, .section--ink h4 { color: var(--text-on-ink); }
.section--ink .muted { color: var(--text-muted-ink); }
.section--ink p { color: #e4e6e8; }
.section--ink .lead { color: var(--text-on-ink); }

.grid { display: grid; gap: clamp(1.5rem, 3vw, 2.5rem); }
.grid--2 { grid-template-columns: repeat(auto-fit, minmax(min(100%, 22rem), 1fr)); }

/* Section head: heading left, supporting copy right */
.section-head {
  display: grid;
  gap: clamp(1rem, 3vw, 3.5rem);
  align-items: end;
  margin-bottom: clamp(2.25rem, 4vw, 3.5rem);
}
@media (min-width: 56rem) {
  .section-head { grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); }
  .section-head > :last-child { padding-bottom: .4rem; }
  .section-head--qa > div > p { padding-bottom: .4rem; }
}
/* Question heads: the answer paragraph has to follow its heading directly in
   the markup, so the two wrappers stop forming boxes and the heading and the
   answer become the head's own two columns. */
.section-head--qa > div { display: contents; }
.section-head h2 { margin-bottom: 0; }
.section-head p { color: var(--text-muted); margin-bottom: 0; }
.section--ink .section-head p { color: var(--text-muted-ink); }

.actions {
  display: flex;
  flex-wrap: wrap;
  gap: .875rem;
  align-items: center;
  margin-top: clamp(1.75rem, 3vw, 2.5rem);
}

.skip-link {
  position: absolute;
  left: 1rem;
  top: -100%;
  z-index: 200;
  background: var(--accent);
  color: var(--accent-text);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: var(--fs-meta);
  letter-spacing: .1em;
  text-transform: uppercase;
  padding: .875rem 1.25rem;
  text-decoration: none;
  border-radius: var(--radius);
  transition: top var(--dur-base) var(--ease);
}
.skip-link:focus { top: .75rem; }

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

/* --------------------------------------------------------------------------
   5. Header & navigation
   -------------------------------------------------------------------------- */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: transparent;
  padding-block: var(--header-gap);
  transition: transform var(--dur-slow) var(--ease), padding-top var(--dur-base) var(--ease);
}
.site-header.is-stuck { padding-top: calc(var(--header-gap) * .45); }
.site-header.is-hidden { transform: translateY(calc(-100% - 1.5rem)); }

.site-header__inner {
  min-height: var(--header-h);
  display: flex;
  align-items: center;
  gap: clamp(1rem, 3vw, 2.5rem);
  /* Ink rather than the accent: the CCDF mark is itself brand red, so a red
     bar would swallow it. Red stays on the bar as the CTA and the hover rule. */
  background: var(--ink);
  color: var(--text-on-ink);
  padding-inline: clamp(1rem, 2.5vw, 1.5rem);
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: .75rem;
  text-decoration: none;
  flex: 0 0 auto;
  padding: .35rem 0;
}
/* The mark is a transparent PNG now, so it needs no blend mode to sit on the bar.
   The art is 640x128, so the box is pinned to that exact 5:1 ratio — a 116x24
   box is 4.83:1 and `contain` was letterboxing the mark inside it. */
.brand__mark { width: 116px; height: auto; aspect-ratio: 5 / 1; object-fit: contain; background: none; }
.brand__text {
  display: none;
  font-family: var(--font-display);
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .17em;
  text-transform: uppercase;
  color: var(--text-muted-ink);
  line-height: 1.35;
  max-width: 9rem;
  border-left: 1px solid var(--line-dark);
  padding-left: .75rem;
}
@media (min-width: 30rem) { .brand__text { display: block; } }

.nav { display: none; flex: 1 1 auto; }
.nav__list {
  display: flex;
  align-items: center;
  justify-content: space-evenly;
  gap: clamp(1.25rem, 2.5vw, 2.25rem);
  list-style: none;
  margin: 0;
  padding: 0;
}
.nav__list li { margin: 0; }
.nav__link {
  position: relative;
  display: block;
  padding: .5rem .25rem;
  font-family: var(--font-display);
  font-size: .8125rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-on-ink);
  text-decoration: none;
  transition: color var(--dur-quick) var(--ease);
}
.nav__link::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: .15rem;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-base) var(--ease);
}
.nav__link:hover::after, .nav__link:focus-visible::after { transform: scaleX(1); }
.nav__link[aria-current="page"] { color: var(--text-on-ink); }
.nav__link[aria-current="page"]::after { transform: scaleX(1); }

.header__actions { display: none; align-items: center; gap: 1.25rem; flex: 0 0 auto; margin-left: auto; }
/* Hidden until 84rem, where the bar has room for brand + nav + phone + CTA.
   Below that the phone would push the CTA off the right edge, so it drops and
   the number is carried by the mobile menu, the footer and the contact page.
   (A second `display: inline-flex` used to sit in this block and beat the
   `none`, which is what put the number back in the bar and overflowed the
   header between 70rem and 84rem.) */
.header__phone {
  display: none;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: .875rem;
  color: var(--text-on-ink);
  text-decoration: none;
  white-space: nowrap;
  align-items: center;
  gap: .45rem;
  padding: .5rem .875rem;
  position: relative;
}
.header__phone::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: .2rem;
  height: 2px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur-base) var(--ease);
}
.header__phone:hover::after, .header__phone:focus-visible::after { transform: scaleX(1); }

@media (min-width: 70rem) {
  .nav { display: block; }
  /* Dropping "Contact" left one link's worth of slack in a nav that still
     stretches the full bar, so space-evenly spread the remaining four apart and
     opened a hole in front of the CTA. This start padding parks that slack
     beside the brand instead: the four links keep their old rhythm and the
     group sits back up against the button. It ramps in from 0 at 70rem — where
     the bar is measured full — to 6rem by 81rem, past which the bar is capped
     at --wrap and the nav stops growing, so the padding stops too. */
  .nav { padding-inline-start: clamp(0rem, 54.5vw - 610.9px, 6rem); }
  /* The nav's trailing slack plus the header's own gap already separate the
     last link from the CTA, so this gap only ever spaced the phone (now hidden
     at every width) from the button. */
  .header__actions { display: flex; gap: clamp(2.5rem, 13.33vw - 6.83rem, 3.5rem); }
}
/* The phone no longer appears in the bar at any width, and raising the old
   88rem threshold would not bring it back: the bar is capped at --wrap
   (76rem), so its inner width stops growing at about 1296px of viewport and
   the fit is the same on a 1440 and a 2560 screen. Measured at that cap:
   brand 272 + nav 516 (already at its minimum, links touching their 36px
   gaps) + CTA 199 + padding and gaps 128 = 1216, exactly full. Adding the
   141px number overflowed it by 72px and hung the CTA off the right edge.
   The number is carried by the mobile menu, by every page's footer and by
   the contact page, so nothing is lost but the duplicate. If it has to come
   back into the bar, something else has to leave it — a nav link, the brand
   tagline, or the CTA. */

/* Tap-to-call, phone and tablet only. It shares the ghost-outline idiom of the
   MENU button beside it so the two read as a pair at the end of the bar, and it
   puts the number one tap away on every page without touching the desktop bar,
   which is measured exactly full at 70rem. */
.header__call {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .45rem;
  min-height: 2.75rem;
  margin-left: auto;
  padding: .5rem .85rem;
  border: 1px solid rgba(255,255,255,.42);
  border-radius: var(--radius);
  color: var(--text-on-ink);
  font-family: var(--font-display);
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  text-decoration: none;
  flex: 0 0 auto;
  transition: background var(--dur-quick) var(--ease), color var(--dur-quick) var(--ease),
              border-color var(--dur-quick) var(--ease);
}
.header__call:hover { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }
.header__call:active { transform: scale(.98); }
@media (min-width: 70rem) { .header__call { display: none; } }

/* Mobile menu */
.nav-toggle {
  display: inline-flex;
  align-items: center;
  /* 44px tall: the only way into the navigation on a phone, so it has to be a
     full-size target rather than the 38px the padding alone gave it. */
  min-height: 2.75rem;
  gap: .6rem;
  /* Below the nav breakpoint the nav and the header actions are both out of
     flow, so without this the button lands beside the brand instead of at the
     end of the bar. */
  margin-left: auto;
  background: none;
  border: 1px solid rgba(255,255,255,.42);
  border-radius: var(--radius);
  color: var(--text-on-ink);
  font-family: var(--font-display);
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  padding: .7rem .9rem;
  cursor: pointer;
  transition: background var(--dur-quick) var(--ease), color var(--dur-quick) var(--ease),
              border-color var(--dur-quick) var(--ease);
}
.nav-toggle:hover { background: var(--surface); color: var(--ink); border-color: var(--surface); }
.nav-toggle:active { transform: scale(.98); }
.nav-toggle__bars { display: block; width: 18px; height: 12px; position: relative; }
.nav-toggle__bars::before,
.nav-toggle__bars::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  height: 2px;
  background: currentColor;
  transition: transform var(--dur-base) var(--ease);
}
.nav-toggle__bars::before { top: 0; }
.nav-toggle__bars::after { top: 10px; }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::before { transform: translateY(5px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle__bars::after { transform: translateY(-5px) rotate(-45deg); }

@keyframes nav-toggle-bounce {
  0% { transform: translateX(0) scale(1); }
  40% { transform: translateX(6px) scale(1.16); }
  70% { transform: translateX(-2px) scale(.95); }
  100% { transform: translateX(0) scale(1); }
}
.nav-toggle.is-bouncing { animation: nav-toggle-bounce .45s var(--ease); }

/* This must come after the base .nav-toggle rule above so it wins the
   cascade tie at the nav breakpoint regardless of source order elsewhere. */
@media (min-width: 70rem) {
  .nav-toggle { display: none; }
}

/* Below the nav breakpoint the toggle is the only way into the navigation, so
   the bar must not hide on scroll-down the way the desktop header does — it
   stays stuck to the top the whole way down the page, compacted, with the
   button held hard against the right gutter. */
@media (max-width: 69.99rem) {
  .site-header.is-hidden { transform: none; }
  /* Flush to the top once stuck, so no strip of page shows above the bar. */
  .site-header.is-stuck { padding-top: 0; padding-bottom: calc(var(--header-gap) * .45); }
  .site-header.is-stuck .site-header__inner { min-height: calc(var(--header-h) * .86); }
  /* The call link now carries the `margin-left: auto` that pushes the pair to
     the end of the bar, so the toggle only needs the gap between them. */
  .nav-toggle { margin-left: .5rem; margin-right: 0; }
  /* Locking the body kills `position: sticky`, which dropped the bar — and the
     close button with it — back to the top of the document while the overlay
     was open. Fixed for the duration of the lock keeps it on screen. */
  body.is-locked .site-header { position: fixed; top: 0; left: 0; right: 0; }
}

.mobile-nav {
  position: fixed;
  /* Full-bleed rather than inset below the bar: the bar compacts as you scroll,
     so an inset panel left a strip of the page showing between the two. The
     header sits above this on z-index and the offset moves into the padding. */
  inset: 0;
  z-index: 99;
  background: var(--ink);
  color: var(--text-on-ink);
  padding: calc(var(--header-total) + clamp(1.5rem, 6vw, 3rem)) var(--gutter) 3rem;
  overflow-y: auto;
  display: none;
}
.mobile-nav.is-open { display: block; }
@media (min-width: 70rem) { .mobile-nav, .mobile-nav.is-open { display: none; } }

.mobile-nav__list { list-style: none; margin: 0 0 2rem; padding: 0; }
.mobile-nav__list li { margin: 0; border-bottom: 1px solid var(--line-dark); max-width: none; }
.mobile-nav__link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1.05rem 0;
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 700;
  letter-spacing: -.028em;
  color: var(--text-on-ink);
  text-decoration: none;
  transition: color var(--dur-quick) var(--ease), padding-left var(--dur-base) var(--ease);
}
.mobile-nav__link:hover { color: var(--accent-bright); padding-left: .5rem; }
.mobile-nav__link[aria-current="page"] { color: var(--accent-bright); }
.mobile-nav__contact { display: grid; gap: .75rem; }
.mobile-nav__contact a { color: var(--text-on-ink); }

/* --------------------------------------------------------------------------
   6. Buttons
   -------------------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .6rem;
  min-height: 3rem;
  padding: .8rem 1.6rem;
  border: 1px solid transparent;
  border-radius: var(--radius);
  background: none;
  font-family: var(--font-display);
  font-size: .8125rem;
  font-weight: 700;
  letter-spacing: .1em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition: background-color var(--dur-base) var(--ease),
              color var(--dur-base) var(--ease),
              border-color var(--dur-base) var(--ease),
              transform var(--dur-quick) var(--ease);
}
.btn .icon { transition: transform var(--dur-base) var(--ease); flex: 0 0 auto; }
.btn:hover .icon { transform: translateX(4px); }
.btn:active { transform: scale(.975); }

.btn--primary { background: var(--ink); color: var(--surface); border-color: var(--ink); }
.btn--primary:hover { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }

.btn--accent { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }
.btn--accent:hover { background: var(--surface); color: var(--ink); border-color: var(--surface); }

/* Inside the ink header bar an ink-filled button disappears, so the header
   CTA carries the accent instead. */
.site-header .btn--primary { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }
.site-header .btn--primary:hover { background: var(--surface); color: var(--ink); border-color: var(--surface); }

.btn--ghost { color: var(--text); border-color: var(--line-strong); }

.header-cta { min-height: 2.5rem; padding: .5rem 1.2rem; font-size: .75rem; }
.btn--ghost:hover { background: var(--ink); color: var(--surface); border-color: var(--ink); }

.btn--on-ink { color: var(--text-on-ink); border-color: rgba(255,255,255,.45); }
.btn--on-ink:hover { background: var(--surface); color: var(--ink); border-color: var(--surface); }

.btn[disabled], .btn[aria-disabled="true"] {
  opacity: .5;
  cursor: not-allowed;
  pointer-events: none;
}
.btn.is-loading { pointer-events: none; opacity: .8; }
.btn.is-loading .btn__label::after {
  content: "";
  display: inline-block;
  width: 1em;
  text-align: left;
  animation: dots 1.2s steps(4, end) infinite;
}
@keyframes dots { 0% { content: ""; } 25% { content: "."; } 50% { content: ".."; } 75% { content: "..."; } }

.icon { width: 1em; height: 1em; stroke: currentColor; fill: none; stroke-width: 1.75; stroke-linecap: square; stroke-linejoin: miter; }
.icon--lg { width: 1.75rem; height: 1.75rem; stroke-width: 1.5; }

/* --------------------------------------------------------------------------
   7. Hero
   -------------------------------------------------------------------------- */
.hero {
  position: relative;
  background: var(--ink);
  color: var(--text-on-ink);
  overflow: hidden;
  isolation: isolate;
  margin-top: calc(var(--header-total) * -1);
  padding-top: var(--header-total);
  /* Fill the first screen. svh keeps phones honest when the browser chrome is
     showing; the vh line above it is the fallback for anything without svh.
     min-height only, so the hero still grows if the copy needs more room. */
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.hero__media { position: absolute; inset: 0; z-index: -2; }
.hero__media img,
.hero__slide {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center 42%;
  -webkit-mask-image: linear-gradient(to right, rgba(0,0,0,.75) 0%, rgba(0,0,0,.75) 42%, rgba(0,0,0,.95) 58%, rgba(0,0,0,.95) 100%);
  mask-image: linear-gradient(to right, rgba(0,0,0,.75) 0%, rgba(0,0,0,.75) 42%, rgba(0,0,0,.95) 58%, rgba(0,0,0,.95) 100%);
}
/* Four slides, 5s of screen time each, on one shared 20s clock — each slide
   holds for a quarter of the cycle and the delays step it round. The keyframe
   percentages are the old two-slide shape halved, so the fade and the slow
   push-in read exactly as they did before. */
.hero__slide {
  position: absolute;
  inset: 0;
  opacity: 0;
  animation: heroSlideshow 20s ease-in-out infinite;
}
.hero__slide:nth-of-type(1) { animation-delay: 0s; }
.hero__slide:nth-of-type(2) { animation-delay: 5s; }
.hero__slide:nth-of-type(3) { animation-delay: 10s; }
.hero__slide:nth-of-type(4) { animation-delay: 15s; }
@keyframes heroSlideshow {
  0%     { opacity: 0; transform: scale(1); }
  2.5%   { opacity: 1; }
  22.5%  { opacity: 1; transform: scale(1.1); }
  25%    { opacity: 0; transform: scale(1.1); }
  100%   { opacity: 0; }
}
.hero::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(to right, rgba(16,17,18,.75) 0%, rgba(16,17,18,.58) 46%, rgba(16,17,18,.34) 100%),
    linear-gradient(to top, rgba(16,17,18,.68) 0%, rgba(16,17,18,0) 55%);
}
/* Below the desktop breakpoint the hero copy spans the full bar instead of
   sitting in the left column, so its right-hand end lands on the weak .34 end
   of the scrim. Measured over the steel-stud slide at 390px, the lead fell to
   3.2:1 against the hi-vis vest behind it. Flattening the ramp holds every
   line of the lead above 4.5:1 on both slides without darkening the left side
   any further. */
@media (max-width: 69.99rem) {
  .hero::after {
    background:
      linear-gradient(to right, rgba(16,17,18,.78) 0%, rgba(16,17,18,.72) 46%, rgba(16,17,18,.66) 100%),
      linear-gradient(to top, rgba(16,17,18,.68) 0%, rgba(16,17,18,0) 55%);
  }
}
@keyframes heroSettle { from { transform: scale(1.09); opacity: 0; } to { transform: scale(1); opacity: 1; } }

/* Symmetric block padding + the centring above gives the same air under the
   header as above the fold line, and lifts the h1 clear of the nav. */
.hero__inner {
  padding-block: clamp(1.75rem, 4vw, 2.75rem);
  max-width: 54rem;
}
/* Eyebrow: names the offer before the h1 states the promise. Same uppercase
   slug idiom as the credential label, with the accent dash tying it to the
   rule under the headline. */
.hero__eyebrow {
  display: flex;
  align-items: center;
  gap: .7rem;
  margin: 0 0 clamp(.85rem, 1.6vw, 1.25rem);
  font-family: var(--font-display);
  font-size: clamp(.75rem, .3vw + .68rem, .875rem);
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  line-height: 1.3;
  color: #e8eaec;
  animation: heroRise 800ms var(--ease) 40ms both;
}
.hero__eyebrow::before {
  content: "";
  flex: 0 0 auto;
  width: 1.75rem;
  height: 3px;
  background: var(--accent);
}

.hero h1 {
  font-size: var(--fs-display);
  letter-spacing: -.038em;
  line-height: .98;
  margin-bottom: 0;
  color: var(--text-on-ink);
}
/* Kept for any page or override still using the two-line headline markup. */
.hero__title-line { display: block; }

/* The flipping headline: one grid cell holds all five words stacked, so the
   line never reflows as they swap. Each word turns in on the x-axis, holds,
   and turns back out on a shared 25s clock — five words, 5s apart. */
.hero__flip {
  display: inline-grid;
  grid-template-columns: 1fr;
  vertical-align: top;
  perspective: 500px;
}
.hero__flip-word {
  grid-row: 1;
  grid-column: 1;
  opacity: 0;
  white-space: nowrap;
  transform: rotateX(90deg);
  backface-visibility: hidden;
  animation: heroFlipWord 25s ease-in-out infinite;
}
.hero__flip-word:nth-of-type(1) { animation-delay: 0s; }
.hero__flip-word:nth-of-type(2) { animation-delay: 5s; }
.hero__flip-word:nth-of-type(3) { animation-delay: 10s; }
.hero__flip-word:nth-of-type(4) { animation-delay: 15s; }
.hero__flip-word:nth-of-type(5) { animation-delay: 20s; }
@keyframes heroFlipWord {
  0%    { opacity: 0; transform: rotateX(90deg); }
  2%    { opacity: 1; transform: rotateX(0deg); }
  18%   { opacity: 1; transform: rotateX(0deg); }
  20%   { opacity: 0; transform: rotateX(-90deg); }
  100%  { opacity: 0; transform: rotateX(90deg); }
}

/* The article has to agree with the word it introduces: "a Commercial" but
   "an Institutional". Rather than freeze on one wrong form, the "n" is its own
   flip — it turns in with Institutional (word 2, 5s) and back out with
   Industrial (word 3, 15s), on the same 25s clock as the words above.

   Collapsing it to zero width is what keeps the spacing honest: the grid
   column goes 0fr → 1fr so "Got a" closes right up when the n is away,
   instead of leaving a double space in front of the word. */
.hero__flip-n {
  display: inline-grid;
  grid-template-columns: 0fr;
  vertical-align: top;
  perspective: 500px;
  animation: heroFlipArticleN 25s ease-in-out infinite;
}
/* Clipped on the inline axis only — `hidden` would force the block axis too
   and crop the glyph, because the h1 runs a line-height below 1. */
.hero__flip-n-inner { min-width: 0; overflow-x: clip; backface-visibility: hidden; }
@keyframes heroFlipArticleN {
  0%,  20%  { grid-template-columns: 0fr; opacity: 0; transform: rotateX(90deg); }
  22%, 58%  { grid-template-columns: 1fr; opacity: 1; transform: rotateX(0deg); }
  60%, 100% { grid-template-columns: 0fr; opacity: 0; transform: rotateX(-90deg); }
}
.hero__rule {
  height: 4px;
  background: var(--accent);
  width: min(9rem, 40%);
  margin: clamp(1.5rem, 3vw, 2.25rem) 0 clamp(1.25rem, 2.5vw, 1.75rem);
  transform-origin: left;
  animation: ruleDraw 700ms var(--ease) 400ms both;
}
@keyframes ruleDraw { from { transform: scaleX(0); } to { transform: scaleX(1); } }

.hero__lead {
  font-size: var(--fs-lead);
  line-height: 1.45;
  letter-spacing: -.017em;
  color: #e8eaec;
  max-width: 44ch;
  animation: heroRise 800ms var(--ease) 240ms both;
}
.hero h1 { animation: heroRise 800ms var(--ease) 80ms both; }
.hero .actions { animation: heroRise 800ms var(--ease) 400ms both; }
.hero__trust { animation: heroRise 800ms var(--ease) 540ms both; }
@keyframes heroRise { from { opacity: 0; transform: translateY(22px); } to { opacity: 1; transform: none; } }

.hero__trust {
  margin-top: clamp(2rem, 4vw, 3rem);
  padding-top: 1.5rem;
  border-top: 1px solid rgba(255,255,255,.18);
  font-size: var(--fs-small);
  color: var(--text-muted-ink);
  max-width: none;
  width: 100vw;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  padding-inline: var(--gutter);
  box-sizing: border-box;
  /* Label on its own row, then the credential chips with the Hilti mark
     pushed out to the right edge of the same row. */
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .85rem clamp(1rem, 2vw, 1.75rem);
}
.hero__trust strong { color: var(--text-on-ink); font-weight: 600; }

.hero__trust-label {
  order: 0;
  font-size: var(--fs-small);
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--text-muted-ink);
}

/* Credentials set — a static row of set-out chips, no longer a rolling loop. */
.trust-set {
  display: flex;
  order: 2;
  flex: 0 0 100%;
  min-width: 0;
}
/* Equal columns rather than a flex row, so every pill is exactly the same size
   whatever its label runs to. A label too long for its column wraps inside the
   pill; the row then sizes to the tallest, and the rest match it. */
.trust-set__list {
  display: grid;
  /* Three and three. Six across would fit the short scopes but force
     "Acoustic / T-Bar Ceilings" onto two lines at every desktop width below
     about 1700px, which is the uneven look the set exists to avoid. */
  grid-template-columns: repeat(3, minmax(0, 1fr));
  grid-auto-rows: 1fr;
  gap: .5rem .5rem;
  /* Take the whole strip so the columns divide the full width. */
  flex: 1 1 100%;
  min-width: 0;
}
.trust-set__item {
  position: relative;
  display: flex;
  align-items: stretch;
  justify-content: center;
  min-width: 0;
}
/* Spare copies of the chip set: kept in the DOM for the editor, never shown. */
.trust-set .trust-set__item--spare { display: none; }

/* Text chip: a bordered slug carrying one trade. Fills its column edge to edge
   and top to bottom so the pills read as one set of identical bubbles. */
.trust-set__text {
  display: flex;
  width: 100%;
  justify-content: center;
  align-items: center;
  text-align: center;
  gap: .45rem;
  padding: .5rem .7rem;
  border: 1px solid rgba(255,255,255,.28);
  border-radius: 100px;
  font-family: var(--font-display);
  font-size: clamp(.6875rem, .42vw + .4rem, .8125rem);
  font-weight: 700;
  letter-spacing: .045em;
  text-transform: uppercase;
  color: var(--text-on-ink);
  white-space: normal;
  transition: border-color var(--dur-base) var(--ease), background-color var(--dur-base) var(--ease);
}
.trust-set__text::before {
  content: "";
  flex: 0 0 auto;
  width: .5rem;
  height: .5rem;
  background: var(--accent);
}
.trust-set__item:hover .trust-set__text { border-color: var(--accent); background: rgba(232,22,32,.14); }

/* Hilti mark, held hard right on the label's row at every width — the crew is
   Hilti-certified and factory-trained, so the mark earns its place. The chips
   then get the full strip width to themselves on the row beneath. */
.trust-hilti {
  display: inline-flex;
  flex: 0 0 auto;
  align-items: center;
  order: 1;
  margin-left: auto;
}
.trust-hilti__mark {
  display: block;
  width: auto;
  height: clamp(1.375rem, 1.9vw, 1.75rem);
}

/* Phone: six pills stacked six deep would swamp the hero, so the scopes drop
   to a two-column bulleted list — same reading size, three rows deep. */
@media (max-width: 47.99rem) {
  .trust-set__list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: .4rem .9rem;
  }
  .trust-set .trust-set__item { justify-content: flex-start; align-items: center; }
  .trust-set__text {
    justify-content: flex-start;
    text-align: left;
    padding: 0;
    border: 0;
    border-radius: 0;
    font-size: .75rem;
    letter-spacing: .04em;
    line-height: 1.3;
    white-space: normal;
  }
  .trust-set__item:hover .trust-set__text { background: none; border-color: transparent; }

  /* The headline is two sentences and the lead names six scopes, so the whole
     stack is tightened — otherwise the buttons and the scope list fall under
     the fold on a 664px screen and the hero stops selling. */
  .hero__inner { padding-block: 1.25rem; }
  .hero__eyebrow {
    gap: .55rem;
    margin-bottom: .7rem;
    font-size: .6875rem;
    /* Tracked just tight enough to hold the eyebrow on one line at 390px. */
    letter-spacing: .055em;
  }
  .hero__eyebrow::before { width: 1.25rem; }
  .hero__rule { margin: 1rem 0 .9rem; }
  .hero__lead { font-size: 1.0625rem; line-height: 1.42; }
  .hero .actions { margin-top: 1.4rem; gap: .625rem; }
  .hero__trust { margin-top: 1.4rem; padding-top: 1rem; }
}

/* Compact page hero for inner pages */
.page-hero {
  background: var(--ink);
  color: var(--text-on-ink);
  position: relative;
  overflow: hidden;
  isolation: isolate;
  margin-top: calc(var(--header-total) * -1);
  padding-top: var(--header-total);
}
.page-hero__media { position: absolute; inset: 0; z-index: -2; }
.page-hero__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: .38;
  animation: heroSettle 1200ms var(--ease) both;
}
.page-hero::after {
  content: "";
  position: absolute; inset: 0; z-index: -1;
  background: linear-gradient(to right, rgba(16,17,18,.92), rgba(16,17,18,.6));
}

/* Hero photos carry the light --surface-3 placeholder from the base img rule,
   which on a cold cache paints a pale panel inside a dark hero while the
   photograph is still arriving — a bright flash right after a page loads, and
   the most visible one when moving between two dark-topped pages. Ink is the
   colour the hero is already painted, so an undecoded photo is simply
   invisible instead. */
.hero__media img,
.hero__slide,
.page-hero__media img { background: var(--ink); }
.page-hero__inner { padding-block: clamp(3.5rem, 9vw, 6.5rem); max-width: 50rem; }
.page-hero h1 { color: var(--text-on-ink); margin-bottom: 0; animation: heroRise 700ms var(--ease) 60ms both; }
.page-hero__rule {
  height: 4px; background: var(--accent);
  width: min(7rem, 34%);
  margin: 1.5rem 0 1.25rem;
  transform-origin: left;
  animation: ruleDraw 620ms var(--ease) 320ms both;
}
.page-hero p {
  color: #e4e6e8;
  font-size: var(--fs-lead);
  line-height: 1.45;
  letter-spacing: -.016em;
  max-width: 46ch;
  margin-bottom: 0;
  animation: heroRise 700ms var(--ease) 200ms both;
}

/* Breadcrumbs */
.breadcrumbs { padding-top: 1.25rem; font-size: var(--fs-small); }
.breadcrumbs ol { list-style: none; display: flex; flex-wrap: wrap; gap: .5rem; margin: 0; padding: 0; }
.breadcrumbs li { margin: 0; display: flex; align-items: center; gap: .5rem; color: var(--text-muted-ink); }
.breadcrumbs li + li::before { content: "/"; color: rgba(255,255,255,.35); }
.breadcrumbs a { color: var(--text-muted-ink); text-decoration: none; }
.breadcrumbs a:hover { color: var(--accent-bright); text-decoration: underline; }
.breadcrumbs [aria-current="page"] { color: var(--text-on-ink); }

/* --------------------------------------------------------------------------
   8. Editorial rows (alternating image / copy)
   -------------------------------------------------------------------------- */
.row {
  display: grid;
  gap: clamp(1.75rem, 4vw, 4rem);
  align-items: center;
  padding-block: clamp(2.25rem, 4vw, 3.5rem);
  border-top: 1px solid var(--line);
}
.section--ink .row { border-top-color: var(--line-dark); }
.row:first-of-type { border-top: 0; padding-top: 0; }
@media (min-width: 56rem) {
  .row { grid-template-columns: minmax(0, 1fr) minmax(0, 1.1fr); }
  .row--flip .row__media { order: 2; }
  /* One row per section, so the row's own padding would
     double up on the section padding that already surrounds it. The media
     column is the narrow one either way round, so a flipped row renders the
     same size as an unflipped one. */
  .svc-row { padding-block: 0; grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); }
  .svc-row.row--flip { grid-template-columns: minmax(0, 1.4fr) minmax(0, 1fr); }
  .svc-row .row__body > ul { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
/* Once the copy column is wide enough that the spec lists stop wrapping, every
   service section can hold one shared height. */
@media (min-width: 75rem) {
  .svc-row, .svc-intro { min-height: 22.25rem; }
}
.svc-row .row__body > ul {
  display: grid;
  gap: 0 2rem;
  margin-bottom: 0;
}
.svc-row .row__body > ul li { margin-bottom: 0; max-width: none; }
.svc-row .row__body h2 { margin-bottom: .75rem; }
.svc-row .row__body p { margin-bottom: .75rem; }
.svc-intro .prose h2 { margin-bottom: .6rem; }
.svc-intro .prose p { margin-bottom: .6rem; }
.row__media { position: relative; overflow: hidden; border-radius: var(--radius); }
.row__media img {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease);
}
.row:hover .row__media img { transform: scale(1.03); }
.row__body h3 { margin-bottom: .5rem; }
.row__body p { color: var(--text-muted); }
.section--ink .row__body p { color: var(--text-muted-ink); }
.row__body .link { margin-top: 1rem; }

.svc-groups {
  display: grid;
  gap: 1.5rem;
  margin-top: .3rem;
  padding-top: .3rem;
  border-top: 1px solid var(--line);
}
.section--ink .svc-groups { border-top-color: var(--line-dark); }
@media (min-width: 56rem) {
  .svc-groups {
    grid-template-columns: repeat(2, 1fr);
    grid-template-rows: auto repeat(4, auto);
    gap: 0 2rem;
  }
  .svc-group {
    display: grid;
    grid-row: 1 / -1;
    grid-template-rows: subgrid;
  }
  .svc-group__label { grid-row: 1; margin-bottom: 0; align-self: end; }
  .svc-group ul {
    display: grid;
    grid-row: 2 / -1;
    grid-template-rows: subgrid;
  }
  .svc-group li { margin-bottom: 0; align-self: start; }
}
.svc-group__label {
  font-family: var(--font-display);
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--text-muted);
  margin: 0 0 .4rem;
}
.section--ink .svc-group__label { color: var(--text-muted-ink); }
.svc-group ul { margin: 0; }
.svc-group li { max-width: none; }

/* --------------------------------------------------------------------------
   8a. Centre line — a marker dot at each service row
   A vertical rule runs down the gutter between the two row columns, with a
   dot at the centre of every row, echoing a step-by-step progression.
   Desktop only (56rem+): below that the row stacks into one column and a
   line down the middle would cut across the text instead of sitting in the
   gutter between two columns.
   -------------------------------------------------------------------------- */
.services-rows__line,
.row__dot { display: none; }
@media (min-width: 56rem) {
  [data-mb="services-rows"] { position: relative; }
  .services-rows__line {
    display: block;
    position: absolute;
    top: 0;
    bottom: 0;
    left: 47.5%;
    width: 1px;
    background: var(--line-strong);
    transform: translateX(-50%);
  }
  .section--ink .services-rows__line { background: var(--line-dark); }
  .row { position: relative; }
  .row__dot {
    display: block;
    position: absolute;
    top: 50%;
    left: 47.5%;
    width: .5rem;
    height: .5rem;
    /* Centred with margin, not transform: [data-reveal].is-revealed sets
       transform: none at higher specificity, which would otherwise wipe out
       a translate(-50%, -50%) once the reveal animation finishes. */
    margin: -.25rem 0 0 -.25rem;
    border-radius: 50%;
    background: var(--ink);
    z-index: 1;
  }
}

/* --------------------------------------------------------------------------
   8a. Portrait frame — a photo set into a dark panel that fills its column and
   drifts against the scroll. The photo is cut taller than the frame so it has
   somewhere to travel; script.js writes --px-y (-1 at the bottom of the
   viewport, 1 at the top) on [data-parallax]. Without JS, or with reduced
   motion, --px-y stays 0 and the photo simply sits centred in the panel.
   -------------------------------------------------------------------------- */
.portrait-frame {
  position: relative;
  margin: 0;
  /* Stacked: the frame sets its own shape. Beside the copy: it stretches to
     whatever the copy column measures. */
  aspect-ratio: 3 / 2;
  overflow: hidden;
  border-radius: var(--radius);
  /* The background: an ink-dark panel, so the frame still reads as a
     deliberate block while the photo decodes or is mid-travel. */
  background: linear-gradient(180deg, var(--ink-3), var(--ink));
  box-shadow: var(--shadow-2);
}
@media (min-width: 56rem) {
  .portrait-frame { aspect-ratio: auto; height: 100%; min-height: 22rem; }
}
.portrait-frame img {
  position: absolute;
  left: 0;
  top: -8%;
  width: 100%;
  height: 116%;
  object-fit: cover;
  object-position: 50% 38%;
  /* 6% of the photo's own height each way — comfortably inside the 8%
     overshoot, so the panel never shows through at either end of the travel. */
  transform: translate3d(0, calc(var(--px-y, 0) * 6%), 0);
}
/* Brand-red tab along the bottom edge, the same accent the buttons use. */
.portrait-frame::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: clamp(3rem, 20%, 6.5rem);
  height: 5px;
  background: var(--accent);
  z-index: 1;
}

/* The visual editor pinned this photo to a fixed 521×280 box back when it was
   a loose image; its card owns the sizing now. Cancelled here rather than in
   mb-overrides.css (see section 22). */
body [data-mb="intro-team-image"] {
  width: 100% !important;
  max-width: none !important;
  height: auto !important;
  translate: 0 0 !important;
}

/* --------------------------------------------------------------------------
   8b. Service cards (home intro)
   The five trades, one card each: photo, name, what it covers. This is the
   site's services section (#services), linked from the nav and footer. Three
   across on desktop, the last two widened to fill the row rather than leaving
   a gap.
   -------------------------------------------------------------------------- */
.svc-cards { grid-template-columns: repeat(auto-fit, minmax(min(100%, 17rem), 1fr)); }
/* The nav links to #services on the section head, so the heading and its lead
   paragraph land in full under the sticky header rather than being clipped by
   it. The page-wide scroll-padding (header + 1.5rem) supplies the gap. */
.svc-card {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  transition: border-color var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease),
              transform var(--dur-base) var(--ease);
}
.svc-card:hover { border-color: var(--line-strong); box-shadow: var(--shadow-2); transform: translateY(-3px); }
.svc-card__media { margin: 0; overflow: hidden; background: var(--surface-3); }
.svc-card__media img {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease);
}
.svc-card:hover .svc-card__media img { transform: scale(1.045); }
.svc-card__body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: .6rem;
  flex: 1;
  padding: clamp(1.15rem, 1.8vw, 1.6rem);
}
.svc-card__title { font-size: var(--fs-h4); margin: 0; }
.svc-card__body p { color: var(--text-muted); font-size: var(--fs-small); margin: 0; max-width: 54ch; }
.svc-card__body .link { margin-top: auto; padding-top: .9rem; font-size: var(--fs-small); }
.svc-cards__more { margin: clamp(1.75rem, 3vw, 2.5rem) 0 0; }
@media (min-width: 64rem) {
  /* Six columns so the six service cards sit three-up across two even rows.
     The --wide modifier is kept for any card that needs half a row to itself;
     it turns horizontal there, or its photo would tower over the row above. */
  .svc-cards { grid-template-columns: repeat(6, minmax(0, 1fr)); }
  .svc-card { grid-column: span 2; }
  .svc-card--wide { grid-column: span 3; flex-direction: row; }
  .svc-card--wide .svc-card__media { flex: 0 0 44%; }
  .svc-card--wide .svc-card__media img { height: 100%; aspect-ratio: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .svc-card:hover { transform: none; }
  .svc-card:hover .svc-card__media img { transform: none; }
}

/* --------------------------------------------------------------------------
   9. Project cards
   -------------------------------------------------------------------------- */
.projects-grid { display: grid; gap: clamp(1.5rem, 2.5vw, 2rem); list-style: none; margin: 0; padding: 0; }
@media (min-width: 40rem) { .projects-grid { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 66rem) { .projects-grid { grid-template-columns: repeat(3, 1fr); } }
.projects-grid > li { margin: 0; max-width: none; }

.project {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  transition: border-color var(--dur-base) var(--ease), box-shadow var(--dur-base) var(--ease),
              transform var(--dur-base) var(--ease);
}
.project:hover { border-color: var(--line-strong); box-shadow: var(--shadow-2); transform: translateY(-3px); }
.project__media { overflow: hidden; background: var(--surface-3); }
.project__media img {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease);
}
.project:hover .project__media img { transform: scale(1.045); }
.project__body { padding: 1.25rem 1.25rem 1.4rem; display: flex; flex-direction: column; gap: .5rem; flex: 1; }
.project__tag {
  font-family: var(--font-display);
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.project__title { font-size: 1.125rem; line-height: 1.25; margin: 0; letter-spacing: -.022em; }
.project__note { font-size: var(--fs-small); color: var(--text-muted); margin: 0; }
.project__marker {
  margin-top: auto;
  padding-top: .9rem;
  height: 3px;
  width: 2.5rem;
  background: var(--line);
  transition: width var(--dur-slow) var(--ease), background-color var(--dur-base) var(--ease);
  border-radius: 2px;
  box-sizing: content-box;
  background-clip: content-box;
}
.project:hover .project__marker { width: 5rem; background-color: var(--accent); }

/* The main photo is a link into the lightbox, so it needs the anchor reset and
   somewhere to hang the zoom badge. */
a.project__media { position: relative; display: block; text-decoration: none; color: inherit; }
.project__zoom {
  position: absolute;
  top: .75rem; right: .75rem;
  width: 2rem; height: 2rem;
  display: grid;
  place-items: center;
  background: var(--accent);
  color: var(--accent-text);
  border-radius: 50%;
  opacity: 0;
  transform: scale(.7);
  transition: opacity var(--dur-base) var(--ease), transform var(--dur-base) var(--ease);
}
.project:hover .project__zoom,
a.project__media:focus-visible .project__zoom { opacity: 1; transform: none; }
/* Touch has no hover state to reveal it, so the badge just stays put. */
@media (hover: none) { .project__zoom { opacity: 1; transform: none; } }

/* Scope of work: labels, not controls — the filter bar does the filtering. */
.scope-tags { display: flex; flex-wrap: wrap; gap: .375rem; list-style: none; margin: .5rem 0 0; padding: 0; }
.scope-tag {
  margin: 0;
  max-width: none;
  font-family: var(--font-display);
  font-size: .6875rem;
  font-weight: 700;
  letter-spacing: .07em;
  text-transform: uppercase;
  line-height: 1.2;
  padding: .32rem .55rem;
  border: 1px solid var(--line);
  border-radius: 100px;
  background: var(--surface-2);
  color: var(--text-muted);
}
.section--ink .scope-tag { border-color: var(--line-dark); background: var(--ink-3); color: var(--text-muted-ink); }

/* Optional gallery strip. Each thumb is a plain link to the full photo, so it
   still opens without JS; the lightbox only intercepts the click. */
.project__thumbs { display: flex; gap: .4rem; list-style: none; margin: .9rem 0 0; padding: 0; }
.project__thumbs > li { margin: 0; max-width: none; flex: 1 1 0; min-width: 0; }
.project__thumb {
  display: block;
  overflow: hidden;
  border-radius: 2px;
  background: var(--surface-3);
  text-decoration: none;
}
.project__thumb img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  transition: opacity var(--dur-base) var(--ease), transform var(--dur-slow) var(--ease);
}
.project__thumb:hover img, .project__thumb:focus-visible img { opacity: .72; transform: scale(1.06); }

/* A dashed banner for build-time notes (placeholder data, missing photos). */
.notice {
  border: 1px dashed var(--line-strong);
  border-left: 3px solid var(--accent);
  border-radius: var(--radius);
  background: var(--surface-2);
  padding: 1rem 1.25rem;
  margin-bottom: clamp(1.5rem, 3vw, 2.25rem);
}
.notice p { margin: 0; color: var(--text-muted); font-size: var(--fs-small); max-width: 72ch; }

/* Filter bar */
.filters { margin-bottom: clamp(1.75rem, 3vw, 2.5rem); }
.filters__label { margin-bottom: .85rem; }
.filters__row { display: flex; flex-wrap: wrap; gap: .5rem; }
.filter-btn {
  font-family: var(--font-display);
  font-size: .8125rem;
  font-weight: 700;
  letter-spacing: .06em;
  text-transform: uppercase;
  padding: .6rem 1.05rem;
  min-height: 2.75rem;
  border: 1px solid var(--line-strong);
  border-radius: 100px;
  background: var(--surface);
  color: var(--text);
  cursor: pointer;
  transition: background-color var(--dur-quick) var(--ease), color var(--dur-quick) var(--ease),
              border-color var(--dur-quick) var(--ease), transform var(--dur-quick) var(--ease);
}
.filter-btn:hover { border-color: var(--ink); background: var(--surface-2); }
.filter-btn:active { transform: scale(.97); }
.filter-btn[aria-pressed="true"] { background: var(--ink); color: var(--surface); border-color: var(--ink); }
.filter-count { color: var(--text-muted); font-size: var(--fs-small); margin: 0 0 1.5rem; }

.empty-state {
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius);
  padding: clamp(2rem, 5vw, 3.5rem);
  text-align: center;
  background: var(--surface-2);
}
.empty-state h3 { margin-bottom: .5rem; }
.empty-state p { color: var(--text-muted); margin-inline: auto; }

/* --------------------------------------------------------------------------
   10. Feature project block
   -------------------------------------------------------------------------- */
.feature { display: grid; gap: clamp(2rem, 4vw, 3.5rem); align-items: start; }
@media (min-width: 60rem) { .feature { grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr); } }
.feature__media { border-radius: var(--radius); overflow: hidden; }
.feature__media img { width: 100%; aspect-ratio: 3 / 2; object-fit: cover; }
.feature__caption { font-size: var(--fs-small); color: var(--text-muted-ink); margin-top: .85rem; }

.spec-list { margin: 0; display: grid; gap: 0; }
.spec-list div {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: .5rem 1.5rem;
  padding: .85rem 0;
  border-bottom: 1px solid var(--line);
}
.section--ink .spec-list div { border-bottom-color: var(--line-dark); }
.spec-list div:first-child { border-top: 1px solid var(--line); }
.section--ink .spec-list div:first-child { border-top-color: var(--line-dark); }
.spec-list dt {
  font-family: var(--font-display);
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.section--ink .spec-list dt { color: var(--text-muted-ink); }
.spec-list dd { margin: 0; font-weight: 600; text-align: right; }

/* --------------------------------------------------------------------------
   11. Quotes & testimonials
   -------------------------------------------------------------------------- */
.quote { margin: 0; max-width: 44rem; }
.quote__mark {
  display: block;
  width: 2.25rem;
  height: 4px;
  background: var(--accent);
  margin-bottom: clamp(1.5rem, 3vw, 2rem);
}
.quote p {
  font-family: var(--font-display);
  font-size: clamp(1.3rem, 2.2vw, 1.875rem);
  font-weight: 700;
  line-height: 1.28;
  letter-spacing: -.028em;
  max-width: 30ch;
}
.quote--sm p { font-size: clamp(1.125rem, 1.4vw, 1.3125rem); line-height: 1.42; max-width: 44ch; }
.quote__cite {
  display: block;
  margin-top: 1.5rem;
  font-family: var(--font-body);
  font-size: var(--fs-small);
  font-style: normal;
  font-weight: 600;
  letter-spacing: 0;
  line-height: 1.5;
}
.quote__cite span { display: block; font-weight: 400; color: var(--text-muted); }
.section--ink .quote__cite span { color: var(--text-muted-ink); }

.testimonial {
  padding-block: clamp(2.25rem, 5vw, 3.5rem);
  border-top: 1px solid var(--line);
}
.testimonial:first-of-type { border-top: 0; padding-top: 0; }
.testimonial-grid { display: grid; gap: clamp(1.25rem, 3vw, 3rem); }
@media (min-width: 56rem) {
  .testimonial-grid { grid-template-columns: minmax(0, 16rem) minmax(0, 1fr); align-items: start; }
}
.testimonial__who h3 { font-size: 1.125rem; margin-bottom: .25rem; letter-spacing: -.02em; }
.testimonial__who p { font-size: var(--fs-small); color: var(--text-muted); margin: 0; }
.testimonial__body p { max-width: 62ch; }
.testimonial__body p:first-child::before { content: "\201C"; }
.testimonial__body p:last-of-type::after { content: "\201D"; }
/* The references page states what CCDF can evidence rather than quoting
   anyone, so those cards drop the quotation marks. */
.testimonial__body--plain p:first-child::before,
.testimonial__body--plain p:last-of-type::after { content: none; }

/* --------------------------------------------------------------------------
   12. Numbered process list
   -------------------------------------------------------------------------- */
.steps { list-style: none; counter-reset: step; margin: 0; padding: 0; display: grid; gap: 0; }
.steps > li {
  counter-increment: step;
  display: grid;
  gap: .35rem 1.5rem;
  grid-template-columns: 3rem minmax(0, 1fr);
  padding: 1.4rem 0;
  border-top: 1px solid var(--line);
  max-width: none;
  margin: 0;
}
.section--ink .steps > li { border-top-color: var(--line-dark); }
.steps > li::before {
  content: counter(step, decimal-leading-zero);
  grid-row: 1 / span 2;
  font-family: var(--font-display);
  font-size: 1.125rem;
  font-weight: 800;
  letter-spacing: -.01em;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
  padding-top: .12em;
}
.section--ink .steps > li::before { color: var(--accent-bright); }
.steps h3 { font-size: 1.125rem; margin: 0; letter-spacing: -.02em; }
.steps p { margin: 0; color: var(--text-muted); }
.section--ink .steps p { color: var(--text-muted-ink); }

/* --------------------------------------------------------------------------
   13. Capability table
   -------------------------------------------------------------------------- */
.table-wrap { overflow-x: auto; margin-block: 1.5rem; }
table { width: 100%; border-collapse: collapse; font-size: var(--fs-small); min-width: 34rem; }
caption { text-align: left; color: var(--text-muted); font-size: var(--fs-small); margin-bottom: .85rem; }
th, td { text-align: left; padding: .85rem 1rem .85rem 0; border-bottom: 1px solid var(--line); vertical-align: top; }
thead th {
  font-family: var(--font-display);
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--text-muted);
  border-bottom-color: var(--line-strong);
}
tbody th { font-weight: 600; }
tbody tr:hover { background: var(--surface-2); }

/* --------------------------------------------------------------------------
   14. FAQ
   -------------------------------------------------------------------------- */
.faq { max-width: 52rem; }
.faq details {
  border-top: 1px solid var(--line);
  padding: 0;
}
.faq details:last-of-type { border-bottom: 1px solid var(--line); }
.section--ink .faq details { border-top-color: var(--line-dark); }
.section--ink .faq details:last-of-type { border-bottom-color: var(--line-dark); }
.faq summary {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1.25rem;
  padding: 1.25rem 0;
  cursor: pointer;
  list-style: none;
  font-family: var(--font-display);
  font-size: clamp(1.0625rem, 1.2vw, 1.1875rem);
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: 1.35;
  transition: color var(--dur-quick) var(--ease);
}
.faq summary::-webkit-details-marker { display: none; }
.faq summary:hover { color: var(--text-muted); }
.section--ink .faq summary:hover { color: var(--accent-bright); }
/* The mark is a 14px square rotated 45deg, so it paints across its ~19.8px
   diagonal and hangs ~2.9px outside its own 14px box — by a different amount at
   every angle of the spin. Sat flush against the summary's right edge that made
   the arm shave off and come back mid-rotation. The right/bottom margins reserve
   the area the mark sweeps through, so it stays inside its own box throughout. */
.faq summary::after {
  content: "";
  flex: 0 0 auto;
  width: 14px; height: 14px;
  margin: .35em 4px 8px 0;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  transform: rotate(45deg) translate(-2px, -2px);
  transition: transform var(--dur-base) var(--ease);
}
.faq details[open] summary::after { transform: rotate(-135deg) translate(-3px, -3px); }
.faq details > div { padding-bottom: 1.4rem; }
.faq details p { color: var(--text-muted); max-width: 64ch; }
.section--ink .faq details p { color: var(--text-muted-ink); }

/* --------------------------------------------------------------------------
   15. Gallery
   -------------------------------------------------------------------------- */
.gallery { display: grid; gap: clamp(.75rem, 1.5vw, 1.25rem); list-style: none; margin: 0; padding: 0; }
@media (min-width: 34rem) { .gallery { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 54rem) { .gallery { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 78rem) { .gallery { grid-template-columns: repeat(4, 1fr); } }
.gallery > li { margin: 0; max-width: none; }

.shot {
  display: block;
  position: relative;
  overflow: hidden;
  border-radius: var(--radius);
  background: var(--ink);
  text-decoration: none;
  color: var(--text-on-ink);
}
.shot img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease), opacity var(--dur-base) var(--ease);
}
.shot:hover img, .shot:focus-visible img { transform: scale(1.05); opacity: .55; }
.shot__caption {
  position: absolute;
  inset: auto 0 0 0;
  padding: 2.5rem .9rem .9rem;
  font-size: var(--fs-small);
  line-height: 1.4;
  background: linear-gradient(to top, rgba(16,17,18,.9), rgba(16,17,18,0));
  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--dur-base) var(--ease), transform var(--dur-base) var(--ease);
}
.shot:hover .shot__caption, .shot:focus-visible .shot__caption { opacity: 1; transform: none; }
.shot__zoom {
  position: absolute;
  top: .75rem; right: .75rem;
  width: 2rem; height: 2rem;
  display: grid;
  place-items: center;
  background: var(--accent);
  color: var(--accent-text);
  border-radius: 50%;
  opacity: 0;
  transform: scale(.7);
  transition: opacity var(--dur-base) var(--ease), transform var(--dur-base) var(--ease);
}
.shot:hover .shot__zoom, .shot:focus-visible .shot__zoom { opacity: 1; transform: none; }

/* --------------------------------------------------------------------------
   15b. Lightbox

   A single <dialog> that script.js builds once and reuses for every gallery
   on the page. The thumbnails underneath are ordinary links to the full-size
   photo, so with JS off the browser simply opens the image — nothing here is
   required for a visitor to see a project's photos.
   -------------------------------------------------------------------------- */
.lightbox {
  position: fixed;
  inset: 0;
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  /* The dim is painted on the dialog itself, not only on ::backdrop: the
     dialog already fills the viewport, and this way the shade is there in
     any engine whose ::backdrop support is patchy. A click that lands on
     this box rather than on its contents closes the viewer (script.js 4b). */
  background: rgba(10, 11, 12, .93);
  color: var(--text-on-ink);
  overscroll-behavior: contain;
}
.lightbox[open] { display: grid; grid-template-rows: 1fr auto; place-items: center; }
.lightbox::backdrop { background: rgba(10, 11, 12, .93); }

.lightbox__figure {
  margin: 0;
  display: grid;
  gap: .9rem;
  justify-items: center;
  align-content: center;
  padding: clamp(3.5rem, 7vw, 4.5rem) clamp(1rem, 4vw, 2.5rem) .5rem;
  max-width: min(72rem, 100%);
  min-height: 0;
}
.lightbox__img {
  max-width: 100%;
  max-height: min(68vh, 68dvh);
  width: auto;
  height: auto;
  object-fit: contain;
  background: var(--ink-2);
  border-radius: var(--radius);
}
.lightbox__caption {
  margin: 0;
  font-size: var(--fs-small);
  line-height: 1.45;
  color: var(--text-muted-ink);
  text-align: center;
  max-width: 60ch;
}

.lightbox__btn {
  display: inline-grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  border: 1px solid rgba(255, 255, 255, .34);
  border-radius: 50%;
  background: rgba(16, 17, 18, .6);
  color: var(--text-on-ink);
  cursor: pointer;
  transition: background-color var(--dur-quick) var(--ease), border-color var(--dur-quick) var(--ease),
              transform var(--dur-quick) var(--ease);
}
.lightbox__btn:hover { background: var(--accent); border-color: var(--accent); color: var(--accent-text); }
.lightbox__btn:active { transform: scale(.94); }
.lightbox__btn[disabled] { opacity: .35; cursor: default; }
.lightbox__btn[disabled]:hover { background: rgba(16, 17, 18, .6); border-color: rgba(255, 255, 255, .34); color: var(--text-on-ink); }
.lightbox__btn .icon { width: 18px; height: 18px; }

.lightbox__close { position: absolute; top: clamp(.75rem, 2vw, 1.25rem); right: clamp(.75rem, 2vw, 1.25rem); }

/* Prev / next sit in a bar under the photo rather than over its edges: at
   390px wide, side arrows cover the picture they are meant to help you read. */
.lightbox__bar {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 1rem;
  padding: .75rem clamp(1rem, 4vw, 2.5rem) clamp(1rem, 3vw, 1.75rem);
}
.lightbox__count {
  margin: 0;
  font-family: var(--font-display);
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: .11em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  color: var(--text-muted-ink);
  min-width: 5.5rem;
  text-align: center;
}
.lightbox :where(button):focus-visible { outline-color: var(--accent); }

@media (prefers-reduced-motion: reduce) {
  .lightbox__btn, .lightbox__img { transition: none; }
}

.social-links { display: flex; flex-wrap: wrap; gap: .875rem; }
.social-link {
  display: inline-flex;
  align-items: center;
  gap: .6rem;
  padding: .8rem 1.25rem;
  border: 1px solid var(--line-strong);
  border-radius: 100px;
  text-decoration: none;
  font-family: var(--font-display);
  font-size: .875rem;
  font-weight: 700;
  transition: background-color var(--dur-base) var(--ease), color var(--dur-base) var(--ease),
              border-color var(--dur-base) var(--ease), transform var(--dur-quick) var(--ease);
}
.social-link:hover { background: var(--ink); color: var(--surface); border-color: var(--ink); }
.social-link:active { transform: scale(.98); }
.section--ink .social-link { border-color: rgba(255,255,255,.4); color: var(--text-on-ink); }
.section--ink .social-link:hover { background: var(--accent); color: var(--accent-text); border-color: var(--accent); }

/* --------------------------------------------------------------------------
   16. Facts strip (inline, not a metric hero)
   -------------------------------------------------------------------------- */
.facts { list-style: none; margin: 0; padding: 0; display: grid; gap: 0; }
.facts > li {
  margin: 0;
  max-width: none;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .5rem 1rem;
  justify-content: space-between;
  padding: .8rem 0;
  border-bottom: 1px solid var(--line);
}
.section--ink .facts > li { border-bottom-color: var(--line-dark); }
.fact-label { color: var(--text-muted); font-size: var(--fs-small); }
.section--ink .fact-label { color: var(--text-muted-ink); }
.fact-value { font-family: var(--font-display); font-weight: 700; letter-spacing: -.02em; }

/* --------------------------------------------------------------------------
   17. Forms
   -------------------------------------------------------------------------- */
.form { max-width: 38rem; }
.field { margin-bottom: 1.4rem; }
.field > label {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: .11em;
  text-transform: uppercase;
  margin-bottom: .5rem;
}
.field__hint { display: block; font-family: var(--font-body); font-size: var(--fs-small); font-weight: 400; letter-spacing: 0; text-transform: none; color: var(--text-muted); margin-top: .3rem; }

input[type="text"], input[type="email"], input[type="tel"], select, textarea {
  width: 100%;
  font-family: inherit;
  font-size: 1rem;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  padding: .85rem 1rem;
  min-height: 3rem;
  caret-color: var(--ink);
  transition: border-color var(--dur-quick) var(--ease), box-shadow var(--dur-quick) var(--ease),
              background-color var(--dur-quick) var(--ease);
}
textarea { min-height: 9.5rem; resize: vertical; line-height: 1.6; }
select { appearance: none; background-image: linear-gradient(45deg, transparent 50%, var(--text) 50%), linear-gradient(135deg, var(--text) 50%, transparent 50%); background-position: calc(100% - 20px) 1.35rem, calc(100% - 14px) 1.35rem; background-size: 6px 6px, 6px 6px; background-repeat: no-repeat; padding-right: 2.75rem; }
::placeholder { color: #6b6e73; opacity: 1; }
input:hover, textarea:hover, select:hover { border-color: var(--ink); }
input:focus, textarea:focus, select:focus {
  border-color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}
.field.has-error input, .field.has-error textarea, .field.has-error select {
  border-color: var(--danger);
  background: var(--danger-soft);
}
.field__error {
  display: none;
  margin-top: .5rem;
  font-size: var(--fs-small);
  color: var(--danger);
  font-weight: 600;
}
.field.has-error .field__error { display: flex; gap: .4rem; align-items: flex-start; }
.hp-field { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

.form-note { font-size: var(--fs-small); color: var(--text-muted); margin-top: 1rem; max-width: 46ch; }
.form-status {
  display: none;
  margin-bottom: 1.5rem;
  padding: 1rem 1.15rem;
  border: 1px solid var(--danger);
  background: var(--danger-soft);
  border-radius: var(--radius);
  font-size: var(--fs-small);
  color: var(--danger);
  font-weight: 600;
}
.form-status.is-visible { display: block; }

/* --------------------------------------------------------------------------
   17b. Request a Quote band

   The one block on a page where the form is the point, so .form's reading
   width cap is lifted and the short fields pair up into two columns once
   there is room for two readable ones. Below that they stack, which is also
   what every field does on a phone.
   -------------------------------------------------------------------------- */
.quote-layout { display: grid; gap: clamp(2rem, 4vw, 3rem); }
@media (min-width: 64rem) {
  .quote-layout { grid-template-columns: minmax(0, .82fr) minmax(0, 1.18fr); gap: clamp(2.5rem, 5vw, 4.5rem); }
  /* Contact leads with the form and keeps the direct details alongside it. */
  .quote-layout--flip { grid-template-columns: minmax(0, 1.18fr) minmax(0, .82fr); }
}
.quote-intro > :last-child { margin-bottom: 0; }
.quote-intro h2 { margin-bottom: .75rem; }
.quote-intro__rule {
  width: 4.5rem;
  height: 4px;
  background: var(--accent);
  margin-bottom: 1.4rem;
}
.quote-lead { font-size: var(--fs-lead); line-height: 1.45; max-width: 34ch; }
.quote-points { list-style: none; padding: 0; margin: 1.75rem 0 1.5rem; }
.quote-points > li {
  padding: .8rem 0;
  border-top: 1px solid var(--line);
  font-size: var(--fs-small);
  color: var(--text-muted);
}
.quote-points > li:last-child { border-bottom: 1px solid var(--line); }
.quote-direct { font-size: var(--fs-small); }

.form--quote { max-width: none; }
/* Row gap stays 0: .field already carries its own margin-bottom. */
.field-row { display: grid; gap: 0 1.25rem; }
@media (min-width: 34rem) {
  .field-row { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* Scope checkboxes — a fieldset dressed as one more labelled field. */
.scopes-field { border: 0; padding: 0; margin: 0 0 1.4rem; min-width: 0; }
.scopes-field > legend {
  display: block;
  padding: 0;
  font-family: var(--font-display);
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: .11em;
  text-transform: uppercase;
  margin-bottom: .5rem;
}
.checks { display: grid; gap: .5rem; }
@media (min-width: 34rem) {
  .checks { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
.check {
  display: flex;
  align-items: center;
  gap: .65rem;
  padding: .7rem .85rem;
  min-height: 3rem;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius);
  background: var(--surface);
  font-size: var(--fs-small);
  line-height: 1.3;
  cursor: pointer;
  transition: border-color var(--dur-quick) var(--ease), background-color var(--dur-quick) var(--ease);
}
.check:hover { border-color: var(--ink); }
.check input[type="checkbox"] {
  flex: 0 0 auto;
  width: 1.15rem;
  height: 1.15rem;
  margin: 0;
  accent-color: var(--accent);
}
.check:has(input:checked) { border-color: var(--accent); background: var(--accent-soft); }

/* File input — the native control, given the same box as a text field, with
   a dashed edge so it reads as a drop target rather than another text line. */
.field input[type="file"] {
  width: 100%;
  font-family: inherit;
  font-size: var(--fs-small);
  color: var(--text);
  background: var(--surface);
  border: 1px dashed var(--line-strong);
  border-radius: var(--radius);
  padding: .8rem 1rem;
  min-height: 3rem;
  cursor: pointer;
}
.field input[type="file"]:hover { border-color: var(--ink); }
.field input[type="file"]::file-selector-button {
  font: inherit;
  font-weight: 600;
  margin-right: .85rem;
  padding: .4rem .8rem;
  cursor: pointer;
  color: var(--text-on-ink);
  background: var(--ink);
  border: 0;
  border-radius: var(--radius);
}

.contact-cards { margin: 0; }
.contact-line {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: .3rem;
  padding: 1.15rem 0;
  border-top: 1px solid var(--line);
}
.contact-line:last-child { border-bottom: 1px solid var(--line); }
.contact-line dt {
  display: flex;
  align-items: center;
  gap: .6rem;
  font-family: var(--font-display);
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: .11em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.contact-line dt .icon { flex: 0 0 auto; width: 1.25rem; height: 1.25rem; }
.contact-line dd { margin: 0; font-size: 1.0625rem; padding-left: 1.85rem; }
.contact-line a { font-weight: 600; }

/* --------------------------------------------------------------------------
   18. CTA band
   -------------------------------------------------------------------------- */
.cta {
  background: var(--ink);
  color: var(--text-on-ink);
  position: relative;
  overflow: hidden;
}
.cta::before {
  content: "";
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 4px;
  background: var(--accent);
}
.cta__inner { display: grid; gap: clamp(1.5rem, 4vw, 3.5rem); align-items: center; }
@media (min-width: 56rem) { .cta__inner { grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr); } }
@media (min-width: 56rem) { [data-mb="about-cta-inner"] { grid-template-columns: minmax(0, 1fr) minmax(0, 1.5fr); } }
.cta h2 { color: var(--text-on-ink); margin-bottom: .75rem; }
.cta p { color: var(--text-muted-ink); margin-bottom: 0; }
.cta .actions { margin-top: 0; }
@media (min-width: 56rem) { .cta .actions { justify-content: flex-end; } }

/* --------------------------------------------------------------------------
   19. Footer
   -------------------------------------------------------------------------- */
.site-footer {
  background: var(--ink-2);
  color: var(--text-muted-ink);
  padding-block: clamp(3rem, 6vw, 4.5rem) 2rem;
  font-size: var(--fs-small);
}
.site-footer a { color: var(--text-on-ink); text-decoration: none; }
.site-footer a:hover { color: var(--accent-bright); text-decoration: underline; text-underline-offset: .25em; }
.footer-grid { display: grid; gap: clamp(2rem, 4vw, 3rem); }
@media (min-width: 46rem) { .footer-grid { grid-template-columns: minmax(0, 1.4fr) repeat(2, minmax(0, 1fr)); } }
@media (min-width: 68rem) { .footer-grid { grid-template-columns: minmax(0, 1.6fr) repeat(3, minmax(0, 1fr)); } }

/* Transparent PNG, so it needs no inversion to sit on the dark footer.
   Same exact 5:1 box as the header mark, for the same reason. */
.footer-brand__mark {
  width: 148px; height: auto; aspect-ratio: 5 / 1;
  object-fit: contain;
  background: none;
  margin-bottom: 1.25rem;
}
.footer-brand p { max-width: 34ch; margin-bottom: 1.25rem; }

.footer-col h2 {
  font-size: var(--fs-meta);
  font-weight: 700;
  letter-spacing: .13em;
  text-transform: uppercase;
  color: var(--text-on-ink);
  margin: 0 0 1rem;
  line-height: 1.4;
}
.footer-col ul { list-style: none; margin: 0; padding: 0; }
.footer-col li { margin-bottom: .6rem; max-width: none; }

.footer-bottom {
  margin-top: clamp(2.5rem, 5vw, 3.5rem);
  padding-top: 1.5rem;
  border-top: 1px solid var(--line-dark);
  display: flex;
  flex-wrap: wrap;
  gap: .75rem 1.5rem;
  justify-content: space-between;
  align-items: center;
  font-size: .8125rem;
}
.footer-bottom p { margin: 0; max-width: none; }

/* --------------------------------------------------------------------------
   20. Scroll reveal — visible by default, only hidden once JS opts in
   -------------------------------------------------------------------------- */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(20px);
  transition: opacity 600ms var(--ease), transform 600ms var(--ease);
  transition-delay: calc(var(--reveal-i, 0) * 70ms);
}
.js [data-reveal].is-revealed { opacity: 1; transform: none; }

/* The service cards glide in from the side instead of rising, staggered across
   the row so it assembles left to right. The offset is negative, and in LTR a
   box hanging off the left edge never opens a horizontal scrollbar, so the
   cards can start outside the wrap without anything to clip them. */
.js .svc-cards > [data-reveal] {
  opacity: 0;
  transform: translate3d(-4.5rem, 0, 0);
  transition: opacity 820ms cubic-bezier(.16, .84, .32, 1),
              transform 820ms cubic-bezier(.16, .84, .32, 1);
  transition-delay: calc(var(--reveal-i, 0) * 100ms);
}
.js .svc-cards > [data-reveal].is-revealed { opacity: 1; transform: translate3d(0, 0, 0); }

/* --------------------------------------------------------------------------
   20a. Signature reveal — service photos build out of a wireframe as you scroll
   The three service photos share one scroll-scrubbed treatment: the accent
   line sketch wipes in from the bottom edge upward as the row travels up the
   viewport, then the photograph itself wipes up over it while the sketch
   fades away. script.js measures the scroll and writes the eased progress on
   .row__media as --wf-line / --wf-img / --wf-out, and only adds .wf-scrub to
   <html> once it knows it can drive them — without JS, or with reduced
   motion, the photo is simply there and the sketch stays hidden.
   -------------------------------------------------------------------------- */
.row__media .wf {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  pointer-events: none;
  opacity: 0;
}
.row__media .wf-line {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.25;
  vector-effect: non-scaling-stroke;
}
.row__media .wf-dot { fill: var(--accent); }

/* Soft-edged wipes: the mask edge travels from just below the bottom of the
   box (-26%) to just past the top, so both ends of the scrub are clean. */
.wf-scrub .row__media[data-wf-scrub] .wf {
  --wf-edge: calc(var(--wf-line, 0) * 126% - 26%);
  opacity: calc(1 - var(--wf-out, 0));
  -webkit-mask-image: linear-gradient(to top, #000 var(--wf-edge), transparent calc(var(--wf-edge) + 15%));
  mask-image: linear-gradient(to top, #000 var(--wf-edge), transparent calc(var(--wf-edge) + 15%));
}
.wf-scrub .row__media[data-wf-scrub] img {
  --wf-edge: calc(var(--wf-img, 0) * 116% - 16%);
  -webkit-mask-image: linear-gradient(to top, #000 var(--wf-edge), transparent calc(var(--wf-edge) + 13%));
  mask-image: linear-gradient(to top, #000 var(--wf-edge), transparent calc(var(--wf-edge) + 13%));
}

/* Traced variant (.wf--trace): instead of the bottom-up wipe, each closed
   outline draws itself around its column — stroke-dashoffset runs 1 -> 0 as
   --wf-line passes through that shape's window (--d start, --span length).
   The photo wipe and the sketch fade-out above are shared and unchanged. */
.wf-scrub .row__media[data-wf-scrub] .wf--trace {
  -webkit-mask-image: none;
  mask-image: none;
}
.row__media .wf-trace {
  fill: none;
  stroke: var(--accent);
  stroke-width: 1.5;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
}
.wf-scrub .row__media[data-wf-scrub] .wf--trace .wf-trace {
  stroke-dashoffset: clamp(0, calc(1 - (var(--wf-line, 0) - var(--d, 0)) / var(--span, 0.56)), 1);
}
/* The photo grows slightly on row hover; keep the traced overlay glued to it. */
.row:hover .row__media .wf--trace { transform: scale(1.03); }
.row__media .wf--trace { transition: transform var(--dur-slow) var(--ease); }

/* --------------------------------------------------------------------------
   20b. Page transitions — one effect for the whole site

   Leaving a page, the header, main and footer slide left and fade off;
   arriving, the new page's header, main and footer slide in from the right
   and fade up, staggered top to bottom so the page assembles rather than
   arriving as one slab.

   The arrival half is pure CSS: it runs off the page load with no script at
   all, and `both` fill means the end state is the normal page, so a browser
   that never ran the animation still shows everything. The departure half is
   the only part that needs script (it has to hold the navigation back until
   the slide is done) and script.js simply navigates straight away if it
   can't run it.

   Movement uses the `translate` property, not `transform`: the header
   already animates `transform` for its hide-on-scroll, and the two compose
   instead of overwriting each other.
   -------------------------------------------------------------------------- */
:root {
  --pt-shift: clamp(1.25rem, 4vw, 3.5rem);
  --pt-in-dur: 480ms;
  --pt-out-dur: 240ms;
  /* The fade is shared by every block and deliberately shorter than the
     slide: the page is solid again early and then simply keeps gliding. */
  --pt-fade-in-dur: 300ms;
  --pt-fade-out-dur: 220ms;
  --pt-in-ease: cubic-bezier(.16, .84, .32, 1);
  /* Gathers pace on the way out, but not so hard that the first half of the
     exit looks like nothing is happening and the rest snaps. */
  --pt-out-ease: cubic-bezier(.55, .085, .68, .53);
}

/* The incoming page starts a shift to the right of where it lands. In LTR
   that is the one direction that can open a horizontal scrollbar, so clip it
   at the root — `clip` rather than `hidden` because it doesn't turn the root
   into a scroll container, which would break every sticky element.

   The ink on the root is what kills the white flash. --surface is pure white
   and it is body that carries it, so body's white was propagating to the
   canvas and every fade bottomed out on white. With the root painted ink,
   white belongs to body's own box and can be faded off it, and whatever is
   behind the page at any moment — mid-fade, or in the browser's own gap
   between two documents — is the brand's dark instead. min-height keeps
   body's white covering the screen on a short page so the ink only ever
   shows during a transition. */
html { overflow-x: clip; background: var(--ink); }
body { min-height: 100vh; }

/* There is deliberately no backdrop layer and no background animation here.
   Both earlier attempts — a fixed ink panel, then fading body's own
   background — were a second layer running on its own clock, and whenever it
   drifted out of step with the content the white page behind them both
   showed through the gap. Instead every block paints its own background
   (section 4), body paints none, and the root is ink: fading the blocks
   takes their backgrounds with them and uncovers the same ink everywhere at
   once, so there is nothing left that can fall out of step. */
body { background: transparent; }

/* Movement, with the page split into lanes down the screen so it does not
   travel as one rigid slab: the top leads on a short, quick throw and each
   lane below it starts later, travels further and takes longer, so the page
   peels away and gathers back together rather than sliding. --pt-lane is
   0 at the top of the screen and 1 at the bottom.

   The blur is the motion blur. It is heaviest at the start of an arrival and
   the end of a departure — the fast part of each move — and resolves to zero
   as the page settles, so it reads as speed rather than as a soft page. */
/* Movement and opacity run on separate clocks, and only movement is laned.

   When the fade was laned too, each block announced its own arrival — and a
   white block announces it far louder than a dark one, because white at half
   opacity over ink is mid grey while a dark hero at half opacity is still
   just dark. The result was a pale strip sitting at the bottom of the screen
   for a few hundred milliseconds while the hero above it was still dark,
   which is exactly the "bit of a white flash" that was left. One short fade
   shared by every block means the whole page arrives as one piece; the lanes
   then do their work purely as speed. */
@keyframes pt-slide-in  { from { translate: var(--pt-dist) 0; } to { translate: 0 0; } }
@keyframes pt-slide-out { from { translate: 0 0; } to { translate: calc(var(--pt-dist) * -1.15) 0; } }
@keyframes pt-fade-in   { from { opacity: 0; } to { opacity: 1; } }
@keyframes pt-fade-out  { from { opacity: 1; } to { opacity: 0; } }

/* The blur runs on a linear clock, not the eased one the movement uses. On
   the movement's own ease-out curve it collapsed almost immediately — 6px
   down to 0.3px within 150ms, measured — so it never actually read as motion
   blur. Decaying evenly keeps it on screen for the part of the move that is
   genuinely fast. */
@keyframes pt-blur-in  { from { filter: blur(var(--pt-blur)); } 65% { filter: blur(0); } to { filter: blur(0); } }
@keyframes pt-blur-out { from { filter: blur(0); } to { filter: blur(var(--pt-blur)); } }

/* The blur rides on the blocks that move, not on #main as a whole: one filter
   over the whole of main hands the compositor up to 5000px of page to raster
   for an 800px window, where per-block it can skip the ones off screen. */
.site-header,
#main > *,
.site-footer {
  --pt-lane: .5;
  --pt-blur: 6px;
  --pt-dist: calc(var(--pt-shift) * (1 + var(--pt-lane) * 1.15));
  /* Three clocks: the slide is eased and laned, the fade is short and shared,
     the blur is linear. `backwards` on the blur so it reverts to no filter
     once it lands rather than leaving every block on a permanent blur(0)
     compositing layer. */
  animation: pt-slide-in calc(var(--pt-in-dur) * (1 + var(--pt-lane) * .4)) var(--pt-in-ease) calc(var(--pt-lane) * 110ms) both,
             pt-fade-in var(--pt-fade-in-dur) var(--pt-in-ease) both,
             pt-blur-in calc(var(--pt-in-dur) * 1.1) linear backwards;
}

/* On an arrival the page is always at the top, so document order down the
   page is the same thing as position down the screen and the lanes can be
   assigned in plain CSS. On a departure the reader can be anywhere, so
   script.js measures each block against the viewport and overwrites these. */
.site-header          { --pt-lane: 0; }
#main > :nth-child(1) { --pt-lane: .2; }
#main > :nth-child(2) { --pt-lane: .45; }
#main > :nth-child(3) { --pt-lane: .7; }
#main > :nth-child(n+4) { --pt-lane: .85; }
.site-footer          { --pt-lane: 1; }

/* Departure: same lane split, compressed into the shorter exit. */
html.is-leaving .site-header,
html.is-leaving #main > *,
html.is-leaving .site-footer {
  animation: pt-slide-out calc(var(--pt-out-dur) * (1 + var(--pt-lane) * .3)) var(--pt-out-ease) calc(var(--pt-lane) * 55ms) both,
             pt-fade-out var(--pt-fade-out-dur) var(--pt-out-ease) both,
             pt-blur-out var(--pt-out-dur) linear both;
}

/* Nothing is clickable once the page has started to leave. */
html.is-leaving body { pointer-events: none; }

/* --------------------------------------------------------------------------
   21. Utilities
   -------------------------------------------------------------------------- */
.u-mt-0 { margin-top: 0; }
.u-measure { max-width: 62ch; }

/* --------------------------------------------------------------------------
   22. Mobile guards for visual-editor overrides
   mb-overrides.css carries fixed-pixel translate/width values captured at a
   wide desktop canvas. Below the header's own nav breakpoint those values
   push content off-screen, so we cancel them here (higher specificity,
   scoped to mobile) rather than touching mb-overrides.css itself.
   -------------------------------------------------------------------------- */
@media (max-width: 69.99rem) {
  body [data-mb="site-header-inner"] {
    width: 100% !important;
    max-width: 100% !important;
  }
  body [data-mb="hero-title"] {
    font-size: var(--fs-display) !important;
    translate: 0 0 !important;
  }
  body [data-mb="hero-lead"],
  body [data-mb="hero-rule"],
  body [data-mb="hero-cta-primary"],
  body [data-mb="hero-cta-secondary"],
  body [data-mb="hero-trust"],
  body [data-mb="p-j3p6"],
  body [data-mb="services-title"] {
    translate: 0 0 !important;
  }
  body [data-mb="intro-team-image"] {
    width: 100% !important;
    max-width: 100% !important;
    height: 116% !important;
    translate: 0 0 !important;
  }
}

/* --------------------------------------------------------------------------
   23. Reduced motion
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .001ms !important;
    scroll-behavior: auto !important;
  }
  .js [data-reveal] { opacity: 1; transform: none; }
  .js .svc-cards > [data-reveal] { opacity: 1; transform: none; }
  /* Wireframe scrub (20a): skip the sketch, show the photos whole. */
  .row__media[data-wf-scrub] img { -webkit-mask-image: none; mask-image: none; }
  .row__media .wf { opacity: 0; }
  .hero__media img:not(.hero__slide) { transform: none; opacity: 1; }
  .hero__slide { transform: none; opacity: 0; }
  .hero__slide:first-of-type { opacity: 1; }
  .hero__eyebrow { animation: none; }
  .hero__flip-word { transform: none; opacity: 0; }
  .hero__flip-word:first-of-type { opacity: 1; }
  /* Only "Commercial" is shown, so the article settles on "a" and stays there. */
  .hero__flip-n { animation: none; grid-template-columns: 0fr; opacity: 0; transform: none; }
  .site-header.is-hidden { transform: none; }
  .shot__caption { opacity: 1; transform: none; }
  /* Page transitions (20b): no slide, no blur — pages just swap. Every block
     keeps its own opaque background, so the ink root stays covered. */
  .site-header, #main, #main > *, .site-footer,
  html.is-leaving .site-header, html.is-leaving #main,
  html.is-leaving #main > *, html.is-leaving .site-footer {
    animation: none !important;
    opacity: 1;
    translate: none;
    filter: none;
  }
}

/* --------------------------------------------------------------------------
   24. Print
   -------------------------------------------------------------------------- */
@media print {
  /* Never print a half-slid page, or a sheet of ink behind it (20b). */
  html, body { background: #fff !important; }
  #main, #main > *, .site-footer {
    animation: none !important; opacity: 1; translate: none; filter: none;
    background: #fff !important;
  }
  .site-header, .mobile-nav, .cta, .nav-toggle, .filters { display: none !important; }
  body { color: #000; background: #fff; font-size: 11pt; }
  .hero, .page-hero, .section--ink { background: #fff !important; color: #000 !important; }
  /* No full-screen hero on paper — it would print as a page of white. */
  .hero { min-height: 0; display: block; }
  .hero__media, .page-hero__media, .hero::after, .page-hero::after { display: none; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 9pt; }
}


/* --------------------------------------------------------------------------
   25. Replaceable photo slots
   Every photograph on this site is a slot the client will swap for one of
   their own project photos. Two small primitives keep that swap from
   disturbing the layout:

   .photo-slot  the image fills its box by cropping, never by stretching, so a
                replacement file whose aspect ratio differs from the one it
                replaces still sits correctly in the slot. (Most slots already
                get this from their component rule; .photo-slot is for the
                loose images that do not belong to one.)

   .focal-*     names the part of the photograph that must survive that crop.
                Swap the class on the <img>, not the rule, when a new photo
                needs a different anchor. Declared after the component rules
                and matched as img.focal-* so it outranks the component's own
                object-position at equal specificity.
   -------------------------------------------------------------------------- */
img.photo-slot  { object-fit: cover; }
img.focal-top    { object-position: 50% 15%; }
img.focal-upper  { object-position: 50% 32%; }
img.focal-center { object-position: 50% 50%; }
img.focal-lower  { object-position: 50% 68%; }
img.focal-bottom { object-position: 50% 85%; }
img.focal-left   { object-position: 25% 50%; }
img.focal-right  { object-position: 75% 50%; }

/* --------------------------------------------------------------------------
   26. Phone and tablet: reading size and touch targets
   Last in the file so these win the cascade tie against the component rules
   they lift, without having to raise any selector's specificity.

   Two things only, both measured at 390x664 and 820x1180:
   - secondary copy comes up to 16px, the floor for comfortable reading at
     arm's length. --fs-small (15px) is what the whole site uses for second-rank
     body text — card copy, hints, captions, the footer — so raising the token
     once lifts all of it in step rather than patching a dozen components.
   - anything you tap gets a 44px box. Most of the site already had one; the
     exceptions were links sitting as bare inline text (footer lists,
     breadcrumbs, the contact page's phone and email), which measured 16-25px
     tall. They get block padding rather than a larger font, so the type size
     and the design hold and only the target grows.
   -------------------------------------------------------------------------- */
@media (max-width: 69.99rem) {
  :root { --fs-small: 1rem; }

  /* Uppercase micro-labels are tracked and bold, so they read a step smaller
     than body copy — but 11px is below the floor even for those. */
  .project__tag,
  .scope-tag { font-size: .75rem; }

  /* Footer link lists: the type stays at --fs-small, the row grows to 44px.
     The li margin goes into the link's own padding so the rhythm is unchanged. */
  .footer-col li { margin-bottom: 0; }
  /* Rows that are plain text rather than links (the address, the service area)
     get no padding from the rule below, so they keep their own spacing and the
     column's rhythm stays even. */
  .footer-col li:not(:has(a)) { margin-block: .6rem; }
  .footer-col li a {
    display: inline-flex;
    align-items: center;
    min-height: 2.75rem;
    /* Keep the underline hugging the text rather than the padded box. */
    text-decoration-thickness: from-font;
  }
  .footer-bottom { font-size: .9375rem; }
  .footer-bottom a { display: inline-flex; align-items: center; min-height: 2.75rem; }
  .social-link { font-size: .9375rem; }

  /* The logo is the home link; at 23px of artwork plus its own padding it was
     a 34px target. */
  .brand { min-height: 2.75rem; }

  /* Standalone .link calls-to-action sat at ~30px. The growth goes on as
     padding rather than min-height so the hover underline, which is anchored
     to the bottom of the box, stays on the text instead of floating below it. */
  .link {
    padding-top: calc((2.75rem - 1.5em - 2px) / 2);
    padding-bottom: calc((2.75rem - 1.5em) / 2);
  }
  .link::after { bottom: calc((2.75rem - 1.5em) / 2 - 2px); }

  /* Breadcrumbs: the separator is on the li, so the padding goes on the link. */
  .breadcrumbs li { align-items: stretch; }
  .breadcrumbs a,
  .breadcrumbs [aria-current="page"] {
    display: inline-flex;
    align-items: center;
    min-height: 2.75rem;
  }
  .breadcrumbs li + li::before { align-self: center; }

  /* Contact page: the phone and email are the point of the page on a phone, so
     they are the last links that should be a 20px target. */
  .contact-line dd a {
    display: inline-flex;
    align-items: center;
    min-height: 2.75rem;
  }
}
