/* =============================================================================
   7 Arc Build — one stylesheet, no build step.

   Consolidated from the previous Astro build's tokens.css + global.css + the
   per-component scoped styles. Tailwind has been removed: it was imported but
   never used — there was not a single utility class in the markup.

   ORDER MATTERS. There are no cascade layers here (they existed only to keep
   Tailwind's utilities in the right place). Everything resolves by source order
   and specificity, so add new rules to the section they belong to rather than
   appending to the end.

     1  Fonts
     2  Tokens          — the single source of truth for every design value
     3  Base
     4  Layout
     5  Components
     6  Site chrome     — nav, footer, mobile action bar
     7  Hero            — the scroll-scrubbed build sequence
     8  Page sections
     9  Utilities
    10  No JavaScript
   ============================================================================= */

/* =============================================================================
   1  FONTS — self-hosted, variable weight axis, Latin subset.
   Licences sit beside the files in /assets/fonts/ as the SIL OFL requires.
   ============================================================================= */

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

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

/* =============================================================================
   2  TOKENS

   Contrast ratios below are computed, not estimated. Body text clears 4.5:1;
   large text and UI edges clear 3:1. Recompute if a value changes.
   ============================================================================= */

:root {
  /* -- Colour: the four brand colours, exactly as the client supplied them -- */
  --colour-orange: #ea7528;
  --colour-silver: #c5c6c5;
  --colour-slate: #464e53;
  --colour-steel: #828e92;

  /* The neutral ramp, built from the brand greys. Where a step is a brand
     colour it points at it; the rest are the steel thinned with white, and
     the slate at half brightness for the darkest step. */
  --colour-stone-0: #ffffff;
  --colour-stone-50: #f5f6f6; /* steel at 8% */
  --colour-stone-100: #eceeef; /* steel at 15% */
  --colour-concrete: var(--colour-silver); /* 1.71:1 on white — borders and fills only */
  --colour-stone-300: var(--colour-silver); /* 4.95:1 on slate, 8.79:1 on stone-900 */
  --colour-stone-500: var(--colour-steel); /* 3.37:1 on white — FAILS AA. Hairlines and edges only. */
  --colour-stone-700: var(--colour-slate); /* 8.48:1 on white */
  --colour-stone-900: #23272a; /* 15.05:1 on white */

  /* Accent — the brand orange. Carries every action, line and dot. It is too
     light for white text on it (2.97:1) or for text on white, so text on an
     orange fill is stone-900, and orange text uses the deep shade. */
  --colour-marker: var(--colour-orange); /* stone-900 on it: 5.07:1 */
  --colour-marker-hover: #a8541d; /* white on it: 5.32:1; it on stone-100: 4.57:1 */
  --colour-marker-tint: #fdf1ea;

  /* Semantic — form feedback only. Never decorative. */
  --colour-error: #b3261e;
  --colour-error-tint: #fdf0ef;
  --colour-success: #2f6b4f;
  --colour-success-tint: #eef5f1;

  /* Roles — components reference these, so a retheme is one edit. */
  --colour-bg: var(--colour-stone-0);
  --colour-bg-raised: var(--colour-stone-50);
  --colour-bg-inverse: var(--colour-slate);
  --colour-text: var(--colour-stone-900);
  --colour-text-muted: var(--colour-stone-700);
  --colour-text-inverse: var(--colour-stone-0);
  --colour-text-inverse-muted: var(--colour-stone-300);
  --colour-border: var(--colour-concrete);
  --colour-border-strong: var(--colour-stone-500);
  --colour-action: var(--colour-marker);
  --colour-action-hover: var(--colour-marker-hover);
  --colour-action-text: var(--colour-marker-hover); /* orange text on a light ground */
  --colour-on-action: var(--colour-stone-900); /* text and icons on an orange fill */
  --colour-focus: var(--colour-marker-hover);

  /* -- Type ----------------------------------------------------------------- */
  --font-display: 'Archivo', ui-sans-serif, system-ui, sans-serif;
  --font-body: 'Inter', ui-sans-serif, system-ui, sans-serif;

  /* Modular scale, ratio 1.25 from a 16px base. Never pick a size off-scale. */
  --text-xs: 0.75rem;
  --text-sm: 0.875rem;
  --text-base: 1rem;
  --text-lg: 1.25rem;
  --text-xl: 1.5625rem;
  --text-2xl: 1.9375rem;
  --text-3xl: 2.4375rem;
  --text-4xl: 3.0625rem;

  --weight-regular: 400;
  --weight-medium: 500;
  --weight-semibold: 600;
  --weight-bold: 700;

  --leading-tight: 1.08;
  --leading-snug: 1.25;
  --leading-body: 1.6;

  --tracking-tight: -0.02em;
  --tracking-normal: 0em;
  --tracking-wide: 0.08em;

  /* -- Space: 4px base ------------------------------------------------------ */
  --space-1: 0.25rem;
  --space-2: 0.5rem;
  --space-3: 0.75rem;
  --space-4: 1rem;
  --space-6: 1.5rem;
  --space-8: 2rem;
  --space-12: 3rem;
  --space-16: 4rem;
  --space-24: 6rem;
  --space-32: 8rem;

  /* -- Structure ------------------------------------------------------------ */
  --measure: 68ch;
  --container: 78rem;
  --container-narrow: 46rem;
  --gutter: var(--space-6);

  /* Radius is 0 everywhere. The arch is the only curve, and it is a mask. */
  --radius-none: 0;
  --radius-full: 9999px;

  --rule: 1px;
  --touch-min: 2.75rem;
  --nav-h: 4.5rem;

  /* The segmental arch. Rise is the arc height as a share of the width — under
     50% is what makes it segmental rather than a semicircle. */
  --arch-rise: 18%;


  /* -- Motion --------------------------------------------------------------- */
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --duration-fast: 120ms;
  --duration-base: 240ms;
  --duration-slow: 560ms;

  --focus-ring: 3px;
  --focus-offset: 2px;
}

/* =============================================================================
   3  BASE
   ============================================================================= */

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

html {
  -webkit-text-size-adjust: 100%;
  scroll-behavior: smooth;
  /* Anchor targets must clear the fixed bar. */
  scroll-padding-top: calc(var(--nav-h) + var(--space-4));
}

body {
  margin: 0;
  background-color: var(--colour-bg);
  color: var(--colour-text);
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-body);
  font-weight: var(--weight-regular);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1,
h2,
h3,
h4 {
  font-family: var(--font-display);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
  text-wrap: balance;
  margin: 0;
}

p,
li {
  text-wrap: pretty;
}

p {
  margin: 0;
}

img {
  max-width: 100%;
}

a {
  color: var(--colour-action-text);
  text-underline-offset: 0.25em;
}

/* One visible focus treatment for the whole site. Never removed. */
:focus-visible {
  outline: var(--focus-ring) solid var(--colour-focus);
  outline-offset: var(--focus-offset);
}

/* On the dark grounds the ring turns white: the orange is too close in
   brightness to the slate to show there. */
.on-inverse,
.foot,
.actionbar,
.hero,
.rhero,
.nav:not(.nav--solid):not(.is-solid) {
  --colour-focus: var(--colour-stone-0);
}

::selection {
  background-color: var(--colour-marker);
  color: var(--colour-on-action);
}

/* Skip link — first tab stop on every page. */
.skip-link {
  position: absolute;
  left: var(--space-4);
  top: var(--space-4);
  z-index: 100;
  padding: var(--space-3) var(--space-4);
  background-color: var(--colour-stone-900);
  color: var(--colour-text-inverse);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  text-decoration: none;
  transform: translateY(-200%);
}

.skip-link:focus {
  transform: translateY(0);
}

/* =============================================================================
   4  LAYOUT
   ============================================================================= */

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

.measure {
  max-width: var(--measure);
}

/* Running text keeps to a readable line length wherever it sits, including
   where a layout stacks to full width on a tablet. */
.steps__note,
.step__detail,
.contact__inner p {
  max-width: var(--measure);
}

/* Vertical rhythm for the whole site. */
.band {
  padding-block: var(--space-24);
  background-color: var(--colour-bg);
}

@media (min-width: 48rem) {
  .band {
    padding-block: var(--space-32);
  }
}

.band--raised {
  background-color: var(--colour-bg-raised);
}

.band--inverse {
  background-color: var(--colour-bg-inverse);
  color: var(--colour-text-inverse);
}

.h2 {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  margin: var(--space-4) 0 0;
  max-width: 22ch;
}

@media (min-width: 48rem) {
  .h2 {
    font-size: var(--text-3xl);
  }
}

/* A quieter headline, for a section whose content carries the weight. */
.h2--sm {
  font-size: var(--text-xl);
}

@media (min-width: 48rem) {
  .h2--sm {
    font-size: var(--text-2xl);
  }
}

.sechead {
  margin-bottom: var(--space-16);
}

.sechead--split {
  display: grid;
  gap: var(--space-6);
}

@media (min-width: 48rem) {
  .sechead--split {
    grid-template-columns: 1fr auto;
    align-items: end;
  }
}

.sechead__aside {
  color: var(--colour-text-muted);
  font-size: var(--text-sm);
  max-width: 24ch;
}

.lede {
  font-size: var(--text-lg);
  color: var(--colour-text-muted);
  max-width: var(--measure);
}

.stack {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
}

/* =============================================================================
   5  COMPONENTS
   ============================================================================= */

/* -- Eyebrow: a dimension line, not a label with a trailing rule ------------- */
/* A label with a line running off to the right is one of the most recognisable
   template tells. A dimension line has END TICKS — two pseudo-elements'
   difference, and it stops being decoration and becomes a drawing convention. */
.eyebrow {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--colour-text);
  margin: 0;
}

.eyebrow::before {
  content: '';
  width: var(--rule);
  height: var(--space-3);
  background-color: var(--colour-marker);
  flex-shrink: 0;
}

.eyebrow::after {
  content: '';
  flex: 1;
  height: var(--space-3);
  border-right: var(--rule) solid var(--colour-marker);
  background-image: linear-gradient(var(--colour-marker), var(--colour-marker));
  background-size: 100% var(--rule);
  background-position: center;
  background-repeat: no-repeat;
}

/* A section's own title, and the largest type in the section: the line under
   it (the .h2) is set a step smaller, below. The plain .eyebrow stays small
   for labels inside components, such as a project panel's category. */
.eyebrow--section {
  gap: var(--space-4);
  font-size: var(--text-xl);
  font-weight: var(--weight-bold);
}

.eyebrow--section::before,
.eyebrow--section::after {
  height: var(--space-4);
}

/* The heading under a section title supports it, so it sits a step below.
   A class, not a sibling selector: the heading is not always the title's
   next element (the intro puts it in a grid beside its text). An .h2 without
   it, such as the quote band's, keeps its full size. No width limit, so the
   line stays on one row wherever the page has room for it. On a phone the size
   follows the screen: the longest of the short lines ("How a job runs, from
   first call to handover.") is about 19 times its font size wide, so dividing
   the space by 20 keeps it on one row. */
.h2--under {
  font-size: clamp(var(--text-sm), calc((100vw - 2 * var(--gutter)) / 20), var(--text-lg));
  max-width: none;
}

@media (min-width: 48rem) {
  .eyebrow--section {
    font-size: var(--text-3xl);
  }

  .eyebrow--section::before,
  .eyebrow--section::after {
    height: var(--space-6);
  }

  .h2--under {
    font-size: var(--text-lg);
  }
}

/* Every section title has a full-width row of its own, so its line always
   measures against the page. Up to 72rem the line runs the full width; from
   72rem it stops at the middle of the page. (Below 72rem, half the page is
   narrower than the longest title, "Recent projects".) min-width keeps a
   title on one line with at least a short line after it. */
@media (min-width: 72rem) {
  .eyebrow--section {
    width: 50%;
    min-width: fit-content;
  }

  .eyebrow--section::after {
    min-width: var(--space-12);
  }

  /* The contact page's title sits in the right-hand column, which starts at
     the middle of the page, so its line spans that column instead. */
  .contact__inner .eyebrow--section {
    width: auto;
    min-width: 0;
  }
}

/* On a dark ground the eyebrow inherits, so only the text colour changes. */
.eyebrow--inverse {
  color: var(--colour-text-inverse);
}

/* -- Buttons ---------------------------------------------------------------- */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: var(--touch-min);
  padding: var(--space-3) var(--space-6);
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
  text-decoration: none;
  border: var(--rule) solid transparent;
  border-radius: var(--radius-none);
  cursor: pointer;
  transition:
    background-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out);
}

.btn-primary {
  background-color: var(--colour-action);
  color: var(--colour-on-action);
  border-color: var(--colour-action);
}

.btn-primary:hover {
  background-color: var(--colour-action-hover);
  color: var(--colour-stone-0);
  border-color: var(--colour-action-hover);
}

.btn-primary:active {
  transform: translateY(1px);
}

.btn-secondary {
  background-color: transparent;
  color: var(--colour-text);
  border-color: var(--colour-stone-900);
}

.btn-secondary:hover {
  background-color: var(--colour-stone-900);
  color: var(--colour-text-inverse);
}

/* On a dark band the secondary button inverts to stay legible. */
.on-inverse .btn-secondary {
  color: var(--colour-text-inverse);
  border-color: var(--colour-stone-300);
}

.on-inverse .btn-secondary:hover {
  background-color: var(--colour-stone-0);
  color: var(--colour-stone-900);
  border-color: var(--colour-stone-0);
}

.btn:disabled,
.btn[aria-disabled='true'] {
  background-color: var(--colour-stone-100);
  color: var(--colour-stone-500);
  border-color: var(--colour-border);
  cursor: not-allowed;
}

.inline-link {
  font-family: var(--font-display);
  font-weight: var(--weight-semibold);
  color: var(--colour-action-text);
  text-underline-offset: 0.3em;
}

/* -- Icons ------------------------------------------------------------------ */
/* One stroked set at one weight, shared by the nav, the contact page and the
   footer. Always paired with a text label, never carrying meaning alone. */
.icon {
  width: 1.15em;
  height: 1.15em;
  flex-shrink: 0;
  /* Optical alignment against a cap-height label. */
  vertical-align: -0.2em;
}

/* -- The segmental arch: the signature element ------------------------------ */
/* Used on ONE feature image per page and nowhere else. Reserving it is what
   makes it a signature rather than a motif — and it clips anything in the top
   corners, which is why the placeholder badge must never sit under it. */
.arch {
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M0,100 L0,18 Q50,-18 100,18 L100,100 Z' fill='%23000'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100' preserveAspectRatio='none'%3E%3Cpath d='M0,100 L0,18 Q50,-18 100,18 L100,100 Z' fill='%23000'/%3E%3C/svg%3E");
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
}

/* -- Photographs ------------------------------------------------------------ */
.photo-frame {
  position: relative;
  display: block;
  overflow: hidden;
  background-color: var(--colour-concrete);
}

.photo {
  display: block;
  width: 100%;
  height: auto;
  object-fit: cover;
}

/* Unmissable on purpose. This badge is the difference between "we are
   evaluating a layout" and "this builder is claiming another firm's work". */
.photo__badge {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  padding: var(--space-1) var(--space-3);
  background-color: var(--colour-stone-900);
  color: var(--colour-stone-0);
  font-family: var(--font-display);
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

/* The arch cuts the top corners, so a badge anchored top-left is sliced in half
   and reads as "…DER". It is a truthfulness guard, not decoration — under an
   arch it moves to the bottom edge, which the mask leaves square. */
.photo-frame.arch .photo__badge {
  top: auto;
  bottom: 0;
}

/* -- Project card ----------------------------------------------------------- */
.card {
  margin: 0;
}

.card__cap {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-2);
  padding-top: var(--space-4);
  margin-top: var(--space-4);
  border-top: var(--rule) solid var(--colour-border);
}

.card__title {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
}

.card__meta {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

/* -- [VERIFY] marker -------------------------------------------------------- */
/* An unconfirmed fact. Visible on the page on purpose — the client has to see
   the hole to fill it. Never ship to production with these present. */
.verify {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  padding: 0 var(--space-2);
  background-color: var(--colour-marker-tint);
  border-bottom: 2px solid var(--colour-marker);
  color: var(--colour-marker-hover);
  font-family: var(--font-display);
  font-size: 0.9em;
  font-weight: var(--weight-semibold);
  font-style: normal;
}

.verify abbr {
  text-decoration: none;
}

.verify__item {
  font-weight: var(--weight-regular);
}

/* -- Form fields ------------------------------------------------------------ */
.field {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.field__label {
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
}

.field__req {
  font-weight: var(--weight-regular);
  color: var(--colour-text-muted);
}

.field__hint {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

.field__control {
  width: 100%;
  min-height: var(--touch-min);
  padding: var(--space-3);
  background-color: var(--colour-stone-0);
  border: var(--rule) solid var(--colour-border-strong);
  border-radius: var(--radius-none);
  font-family: var(--font-body);
  font-size: var(--text-base);
  color: var(--colour-text);
  transition: border-color var(--duration-fast) var(--ease-out);
}

.field__control:hover {
  border-color: var(--colour-stone-700);
}

.field__control:user-invalid {
  border-color: var(--colour-error);
  background-color: var(--colour-error-tint);
}

.field__error {
  display: none;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  color: var(--colour-error);
}

.field__control:user-invalid ~ .field__error {
  display: flex;
}

/* Honeypot. Off-screen rather than display:none — a hidden input is the first
   thing a bot skips. Screen readers get the aria-hidden + tabindex instead. */
.honeypot {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

.form__grid {
  display: grid;
  gap: var(--space-6);
}

@media (min-width: 40rem) {
  .form__grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .field--wide {
    grid-column: 1 / -1;
  }
}

.form__submit {
  margin-top: var(--space-8);
}

.form__privacy {
  margin-top: var(--space-4);
  font-size: var(--text-xs);
  color: var(--colour-text-muted);
}

/* =============================================================================
   6  SITE CHROME
   ============================================================================= */

/* -- Fixed navigation ------------------------------------------------------- */
/* Transparent while it sits over the home page's hero, solid everywhere else.
   Pages without a hero carry .nav--solid in the markup so there is no flash of
   white-on-white before any script runs. */
.nav {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 40;
  color: var(--colour-text-inverse);
  border-bottom: var(--rule) solid transparent;
  transition:
    background-color var(--duration-base) var(--ease-out),
    color var(--duration-base) var(--ease-out),
    border-color var(--duration-base) var(--ease-out);
}

.nav--solid,
.nav.is-solid {
  background-color: var(--colour-bg);
  color: var(--colour-text);
  border-bottom-color: var(--colour-border);
}

/* Residential page: transparent over its hero, then dark blurred glass once
   the page has scrolled 50px (residential-hero.js adds .is-glass). Stays dark
   below the hero, so the white type never needs to flip. */
.nav.is-glass {
  background-color: rgb(35 39 42 / 0.55);
  border-bottom-color: rgb(255 255 255 / 0.08);
  -webkit-backdrop-filter: blur(16px) saturate(140%);
  backdrop-filter: blur(16px) saturate(140%);
}

@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .nav.is-glass {
    background-color: rgb(35 39 42 / 0.92);
  }
}

/* No script means no .is-glass, and white type over the white page below the
   hero. Solid dark from the start instead. */
@media (scripting: none) {
  .nav--glassy {
    background-color: var(--colour-stone-900);
  }
}

.nav__inner {
  display: flex;
  align-items: center;
  gap: var(--space-6);
  min-height: var(--nav-h);
}

.nav__mark {
  display: inline-flex;
  align-items: center;
  gap: var(--space-3);
  color: inherit;
  text-decoration: none;
  margin-right: auto;
}

/* The client's logo. The light version shows while the bar floats over the
   dark hero (or as tinted glass); the colour version once the bar is solid. */
.nav__logo img {
  display: block;
  width: auto;
  height: 2.75rem;
}

.nav__logo--colour,
.nav--solid .nav__logo--light,
.nav.is-solid .nav__logo--light {
  display: none;
}

.nav--solid .nav__logo--colour,
.nav.is-solid .nav__logo--colour {
  display: block;
}

/* Desktop links */
.nav__desktop {
  display: none;
}

@media (min-width: 64rem) {
  .nav__desktop {
    display: block;
  }
}

.nav__list {
  display: flex;
  align-items: center;
  gap: var(--space-8);
  list-style: none;
  margin: 0;
  padding: 0;
}

.nav__link {
  display: inline-block;
  padding-block: var(--space-2);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: inherit;
  text-decoration: none;
  /* Drawn, not toggled — a border would shift the baseline on the active item. */
  border-bottom: 2px solid transparent;
  opacity: 0.78;
  transition:
    border-color var(--duration-fast) var(--ease-out),
    opacity var(--duration-fast) var(--ease-out);
}

.nav__link:hover,
.nav__link[aria-current='page'] {
  opacity: 1;
  border-bottom-color: var(--colour-marker);
}

.nav__actions {
  display: flex;
  align-items: center;
  gap: var(--space-4);
}

.nav__phone {
  display: none;
  align-items: center;
  gap: var(--space-2);
  color: inherit;
  text-decoration: none;
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  white-space: nowrap;
}

.nav__phone svg {
  width: 1rem;
  height: 1rem;
}

.nav__phone:hover {
  color: var(--colour-marker);
}

.nav--solid .nav__phone:hover,
.nav.is-solid .nav__phone:hover {
  color: var(--colour-action-text);
}

@media (min-width: 48rem) {
  .nav__phone {
    display: inline-flex;
  }
}

.nav__cta {
  display: none;
}

@media (min-width: 64rem) {
  .nav__cta {
    display: inline-flex;
  }
}

/* -- Mobile menu: a native <details>, so it opens with no JavaScript --------- */
/* The previous build refused a hamburger on the grounds that "a hamburger
   cannot promise reachability without script". <details>/<summary> is plain
   HTML — it opens with scripting disabled, it is keyboard operable, and screen
   readers announce its expanded state. It keeps the promise. */
.nav__disc {
  position: relative;
}

@media (min-width: 64rem) {
  .nav__disc {
    display: none;
  }
}

.nav__toggle {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--touch-min);
  min-width: var(--touch-min);
  padding: var(--space-2) var(--space-3);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: inherit;
  cursor: pointer;
  border: var(--rule) solid currentColor;
  list-style: none;
}

/* Remove the default disclosure triangle in every engine. */
.nav__toggle::-webkit-details-marker {
  display: none;
}

.nav__toggle::marker {
  content: '';
}

.nav__bars {
  width: 1rem;
  height: 0.75rem;
  flex-shrink: 0;
}

.nav__panel {
  position: absolute;
  top: calc(100% + var(--space-3));
  right: 0;
  min-width: 14rem;
  background-color: var(--colour-bg);
  color: var(--colour-text);
  border: var(--rule) solid var(--colour-border);
  box-shadow: 0 12px 32px rgb(35 39 42 / 0.16);
}

.nav__panel ul {
  list-style: none;
  margin: 0;
  padding: 0;
}

.nav__panel a {
  display: block;
  padding: var(--space-3) var(--space-4);
  min-height: var(--touch-min);
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
  color: var(--colour-text);
  text-decoration: none;
  border-bottom: var(--rule) solid var(--colour-border);
}

.nav__panel li:last-child a {
  border-bottom: 0;
}

.nav__panel a:hover,
.nav__panel a[aria-current='page'] {
  background-color: var(--colour-stone-100);
  color: var(--colour-action-hover);
}

/* -- Footer ----------------------------------------------------------------- */
.foot {
  background-color: var(--colour-bg-inverse);
  color: var(--colour-text-inverse-muted);
  padding-block: var(--space-16);
  /* The mobile action bar covers the last of the page. */
  padding-bottom: calc(var(--space-16) + var(--touch-min));
}

@media (min-width: 48rem) {
  .foot {
    padding-bottom: var(--space-16);
  }
}

.foot__inner {
  display: grid;
  gap: var(--space-8);
}

@media (min-width: 48rem) {
  .foot__inner {
    grid-template-columns: 1.2fr 1fr 1fr;
  }
}

.foot__mark {
  margin: 0;
}

.foot__logo img {
  display: block;
  width: auto;
  height: 3.5rem;
}

.foot__line {
  margin-top: var(--space-3);
  font-size: var(--text-sm);
  max-width: 34ch;
}

.foot h2 {
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--colour-stone-300);
  margin-bottom: var(--space-4);
}

.foot ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  font-size: var(--text-sm);
}

.foot a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--colour-text-inverse-muted);
  text-decoration: none;
}

.foot a:hover {
  color: var(--colour-text-inverse);
  text-decoration: underline;
}

/* The bottom row: the legal line, and the web developer's credit at the end. */
.foot__base {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-2) var(--space-6);
  margin-top: var(--space-12);
  padding-top: var(--space-6);
  border-top: var(--rule) solid rgb(255 255 255 / 0.14);
  font-size: var(--text-xs);
}

/* -- Mobile action bar ------------------------------------------------------ */
/* Two of the three user types decide from a phone, and the cafe owner uses
   WhatsApp rather than a form. On a long page a quote button that only exists
   at the bottom is one most phone visitors never reach. */
.actionbar {
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 45;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  background-color: var(--colour-stone-900);
  border-top: var(--rule) solid rgb(255 255 255 / 0.16);
  padding-bottom: env(safe-area-inset-bottom, 0);
}

@media (min-width: 48rem) {
  .actionbar {
    display: none;
  }
}

.actionbar a {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: var(--touch-min);
  padding: var(--space-3);
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--colour-text-inverse);
  text-decoration: none;
}

.actionbar a + a {
  border-left: var(--rule) solid rgb(255 255 255 / 0.16);
}

.actionbar .actionbar__go {
  background-color: var(--colour-action);
  color: var(--colour-on-action);
}

/* =============================================================================
   7  HERO — the scroll-scrubbed build sequence (home page only)

   The track height IS the scrub length: the sticky stage stays put while this
   element passes the viewport, so (height - 100svh) of scroll maps to the
   whole frame set. Shorter on a phone — the same sequence should not cost four extra
   thumb-swipes there than it does on a desktop wheel.
   ============================================================================= */

/* --hero-scroll is a site's multiplier on the track (from hero.frames.scroll
   in its config): longer footage gets more scroll, not faster frames. */
.hero {
  --hero-track: 300svh;
  position: relative;
  height: calc(var(--hero-track) * var(--hero-scroll, 1));
  background-color: var(--colour-stone-900);
}

@media (min-width: 48rem) {
  .hero {
    --hero-track: 420svh;
  }
}

.hero__stage {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
  isolation: isolate;
}

.hero__still,
.hero__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

.hero__still {
  object-fit: cover;
}

.hero__still--end {
  display: none;
}

/* Transparent until the first draw lands, so the server-rendered still shows
   through rather than an opaque black rectangle. */
.hero__canvas {
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease-out);
}

.hero.is-ready .hero__canvas {
  opacity: 1;
}

.hero__veil {
  position: absolute;
  inset: 0;
  background:
    linear-gradient(
      to bottom,
      rgb(35 39 42 / 0.66) 0%,
      rgb(35 39 42 / 0.16) 26%,
      rgb(35 39 42 / 0) 46%
    ),
    linear-gradient(
      to top,
      rgb(35 39 42 / 0.82) 0%,
      rgb(35 39 42 / 0.34) 32%,
      rgb(35 39 42 / 0) 62%
    );
}

.hero__inner {
  position: absolute;
  inset: 0;
  display: grid;
  align-content: end;
  padding-bottom: var(--space-16);
  color: var(--colour-text-inverse);
}

@media (min-width: 48rem) {
  .hero__inner {
    padding-bottom: var(--space-24);
  }
}

/* The acts stack in one grid cell and cross-fade. Only opacity and transform
   are animated — both compositor properties, so the scrub never lays out. */
.hero__act {
  grid-area: 1 / 1;
}

.hero__act--intro {
  max-width: 46rem;
}

/* -- Split: the logo left, the intro right -------------------------------- */
/* The split and the outro share .hero__inner's one cell and cross-fade.
   Stacked below 64rem, logo first. */
.hero__split {
  grid-area: 1 / 1;
  display: grid;
  gap: var(--space-8);
  align-items: end;
}

@media (min-width: 64rem) {
  .hero__split {
    grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
    gap: var(--space-16);
    align-items: center;
  }
}

.hero__right {
  display: grid;
  min-width: 0;
}

.hero__logo {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-4);
}

/* The light version of the logo, large, over the dark footage. */
.hero__logoimg img {
  display: block;
  width: 11rem;
  height: auto;
}

@media (min-width: 64rem) {
  .hero__logoimg img {
    width: min(100%, 26rem);
  }
}

.hero__eyebrow {
  color: var(--colour-text-inverse);
  margin-bottom: var(--space-6);
}

.hero__title {
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  margin: 0 0 var(--space-6);
}

@media (min-width: 48rem) {
  .hero__title {
    font-size: var(--text-4xl);
  }
}

.hero__lead {
  font-size: var(--text-lg);
  line-height: var(--leading-body);
  color: var(--colour-text-inverse);
  max-width: 34rem;
  margin: 0 0 var(--space-8);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
}

/* Footage that opens on something pale (hero.brightStart): a soft dark backing
   behind the intro keeps its white text clear. It sits inside the intro, so it
   fades with it and leaves the rest of the footage undimmed. */
.hero--bright .hero__act--intro {
  position: relative;
  isolation: isolate;
}

.hero--bright .hero__act--intro::before {
  content: '';
  position: absolute;
  inset: calc(-1 * var(--space-8));
  z-index: -1;
  background-color: rgb(35 39 42 / 0.72);
  filter: blur(var(--space-8));
}

/* Stacked over the slab below 64rem, the large light logo washes out and crowds
   the intro. The nav's logo is on show there anyway. */
.hero--bright .hero__logo {
  display: none;
}

@media (min-width: 64rem) {
  .hero--bright .hero__logo {
    display: flex;
  }
}

/* -- Stage steps: a full-width band, one step at a time -------------------- */
/* Hidden until hero.js shows it, so no JavaScript and Data Saver never see an
   empty band. pointer-events: none — it sits over the outro's buttons while
   the two cross-fade, and carries no links of its own.

   The band spans the whole screen at any width; the text inside it lines up
   with the page through .container-page. */
.hero__steps {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--space-16);
  opacity: 0;
  pointer-events: none;
  color: var(--colour-text-inverse);
}

/* Transparent ochre, strongest in the middle. It sits low in the stage, over
   the dark foot of .hero__veil, which keeps white text clear on top of it.
   Its own layer so hero.js can sweep it in (scaleX) without stretching the
   text. */
.hero__band {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    color-mix(in srgb, var(--colour-marker) 18%, transparent) 0%,
    color-mix(in srgb, var(--colour-marker) 45%, transparent) 50%,
    color-mix(in srgb, var(--colour-marker) 18%, transparent) 100%
  );
  border-block: 1px solid color-mix(in srgb, var(--colour-marker) 60%, transparent);
  box-shadow: 0 12px 32px rgb(0 0 0 / 0.25);
  transform: scaleX(0);
  transform-origin: left center;
}

/* The timeline, in the style of "How it runs": a track, a fill that grows
   along it, a square dot per step and the step's text beside it. hero.js sets
   --fill (0 to 1) and .is-reached; it fades each text block in itself.

   Geometry: dots sit at the start of n equal rows (phone) or columns (from
   48rem), one per stage. The track is one gap longer than the points, which
   puts dot k at exactly k/n of its length — the same fraction hero.js fills
   to at the moment dot k is reached. */
.hero__timeline {
  --dot: 14px;
  --pad: var(--space-6);
  --gap: var(--space-4);
  --line: color-mix(in srgb, var(--colour-marker) 60%, white);
  position: relative;
  padding-block: var(--pad);
  opacity: 0;
}

.hero__track,
.hero__fill {
  position: absolute;
  top: calc(var(--pad) + var(--dot) / 2);
  left: calc(var(--dot) / 2 - 1px);
  width: 2px;
  height: calc(100% - 2 * var(--pad) + var(--gap));
}

.hero__track {
  background-color: rgb(255 255 255 / 0.35);
}

.hero__fill {
  background-color: var(--line);
  transform: scaleY(var(--fill, 0));
  transform-origin: top;
}

.hero__points {
  position: relative;
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-auto-rows: 1fr;
  gap: var(--gap);
}

.hero__point {
  display: grid;
  grid-template-columns: var(--dot) minmax(0, 1fr);
  column-gap: var(--space-4);
  align-items: start;
}

.hero__dot {
  width: var(--dot);
  height: var(--dot);
  border: 2px solid var(--line);
  background-color: transparent;
  transition:
    background-color var(--duration-base) var(--ease-out),
    box-shadow var(--duration-base) var(--ease-out);
}

.hero__point.is-reached .hero__dot {
  background-color: var(--line);
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--colour-marker) 35%, transparent);
}

.hero__pointtext {
  display: grid;
  row-gap: var(--space-1);
  margin-top: calc(var(--dot) / 2 - 0.6em);
  opacity: 0;
  text-shadow:
    0 1px 2px rgb(0 0 0 / 0.45),
    0 2px 12px rgb(0 0 0 / 0.35);
}

.hero__stagen {
  font-family: var(--font-display);
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  color: var(--line);
}

.hero__stagename {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
}

.hero__stagenote {
  margin: 0;
  font-size: var(--text-base);
  line-height: var(--leading-snug);
}

/* From 48rem: across the band, text under each dot. */
@media (min-width: 48rem) {
  .hero__timeline {
    --pad: var(--space-8);
    --gap: var(--space-6);
  }

  .hero__track,
  .hero__fill {
    top: calc(var(--pad) + var(--dot) / 2 - 1px);
    left: 0;
    width: calc(100% + var(--gap));
    height: 2px;
  }

  .hero__fill {
    transform: scaleX(var(--fill, 0));
    transform-origin: left;
  }

  .hero__points {
    grid-auto-rows: auto;
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
  }

  .hero__point {
    grid-template-columns: minmax(0, 1fr);
    row-gap: var(--space-4);
  }

  .hero__pointtext {
    margin-top: 0;
  }

  .hero__stagename {
    font-size: var(--text-xl);
  }
}

/* Long notes (hero.longNotes) sit in their own block under the timeline, so
   the steps still fit the one screen the stage is pinned to.

   Below 64rem there is no room for every note at once: the steps tighten into
   a compact list, and the notes share one grid cell, which sizes itself to the
   longest, with only the current one showing. From 64rem each note has its own
   column under its dot and stays once reached. hero.js sets .is-reached and
   .is-current. */
.hero__steps--long .hero__timeline {
  --pad: var(--space-4);
  --gap: var(--space-2);
}

.hero__steps--long .hero__pointtext {
  grid-auto-flow: column;
  justify-content: start;
  align-items: baseline;
  column-gap: var(--space-2);
}

.hero__steps--long .hero__stagename {
  font-size: var(--text-base);
}

/* A two-line name elsewhere in the row must not stretch this point's rows and
   drop its number out of line. */
.hero__steps--long .hero__point {
  align-content: start;
}

.hero__notes {
  position: relative;
  display: grid;
  /* Lined up under the step names: a dot's width and its gap. */
  padding-left: calc(14px + var(--space-4));
  padding-bottom: var(--space-4);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  text-shadow:
    0 1px 2px rgb(0 0 0 / 0.45),
    0 2px 12px rgb(0 0 0 / 0.35);
}

.hero__note {
  grid-area: 1 / 1;
  margin: 0;
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease-out);
}

.hero__note.is-current {
  opacity: 1;
}

@media (min-width: 48rem) {
  .hero__steps--long .hero__pointtext {
    grid-auto-flow: row;
    align-items: normal;
  }

  .hero__steps--long .hero__point {
    row-gap: var(--space-2);
  }

  .hero__notes {
    padding-left: 0;
  }
}

@media (min-width: 64rem) {
  .hero__steps--long .hero__timeline {
    --pad: var(--space-8);
    --gap: var(--space-6);
  }

  .hero__steps--long .hero__point {
    row-gap: var(--space-4);
  }

  .hero__steps--long .hero__stagename {
    font-size: var(--text-xl);
  }

  .hero__notes {
    grid-auto-flow: column;
    grid-auto-columns: minmax(0, 1fr);
    column-gap: var(--space-6);
    margin-top: calc(-1 * var(--space-4));
    padding-bottom: var(--space-8);
  }

  .hero__note {
    grid-area: auto;
  }

  .hero__note.is-reached {
    opacity: 1;
  }
}

.hero__act--outro {
  max-width: 40rem;
  opacity: 0;
  align-self: end;
}

.hero__outrolead {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  margin: 0 0 var(--space-4);
}

@media (min-width: 48rem) {
  .hero__outrolead {
    font-size: var(--text-3xl);
  }
}

.hero__outrosub {
  font-size: var(--text-base);
  line-height: var(--leading-body);
  color: var(--colour-text-inverse-muted);
  margin: 0 0 var(--space-8);
  max-width: 30rem;
}

/* Honesty marker. Mirrors the "Placeholder" badge on every photograph. */
.hero__mark {
  position: absolute;
  right: var(--space-4);
  bottom: var(--space-4);
  padding: var(--space-1) var(--space-3);
  background-color: rgb(35 39 42 / 0.7);
  color: var(--colour-stone-300);
  font-family: var(--font-display);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

.hero__cue {
  position: absolute;
  left: 50%;
  bottom: var(--space-6);
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-2);
  color: var(--colour-stone-300);
  font-family: var(--font-display);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  transition: opacity var(--duration-base) var(--ease-out);
}

@media (max-width: 47.99rem) {
  .hero__cue {
    display: none;
  }
}

.hero__cueline {
  width: var(--rule);
  height: var(--space-8);
  background: linear-gradient(to bottom, var(--colour-stone-300), transparent);
}

.hero__progress {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  background-color: rgb(255 255 255 / 0.12);
  transition: opacity var(--duration-slow) var(--ease-out);
}

.hero.is-loaded .hero__progress {
  opacity: 0;
}

.hero__progressbar {
  display: block;
  height: 100%;
  width: 0%;
  background-color: var(--colour-marker);
}

/* =============================================================================
   7b  RESIDENTIAL HERO — scroll-scrubbed frame sequence
   Driven by residential-hero.js (GSAP ScrollTrigger). A 400vh track with a
   sticky 100vh stage; three beats of copy cross-fade over a canvas.

   Two layouts:
     scrub    the default while scripting is on, so the first paint is already
              the final layout (no shift when JS lands). The device's
              reduce-motion setting is deliberately ignored (client decision).
     static   no JavaScript, or .is-static (Data Saver, GSAP missing): one
              screen, last frame as a still, all copy visible
   ============================================================================= */

.rhero {
  position: relative;
  height: 400vh;
  background-color: var(--colour-stone-900);
  color: var(--colour-text-inverse);
  font-family: var(--font-body);
}

.rhero__stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  overflow: hidden;
  isolation: isolate;
}

.rhero__still,
.rhero__still img,
.rhero__canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
}

.rhero__still--end {
  display: none;
}

/* Transparent until the first frame is drawn, so the server-rendered still
   shows through rather than an empty black canvas. */
.rhero__canvas {
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease-out);
}

.rhero.is-ready .rhero__canvas {
  opacity: 1;
}

/* Bottom 40% carries the subhead and the call to action. The light overall
   wash is for the opening frames, whose sky is pale blue behind a white
   headline. */
.rhero__veil {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to top, rgb(35 39 42 / 0.8) 0%, rgb(35 39 42 / 0.4) 22%, rgb(35 39 42 / 0) 40%),
    linear-gradient(to bottom, rgb(35 39 42 / 0.5) 0%, rgb(35 39 42 / 0) 18%),
    rgb(35 39 42 / 0.22);
}

/* Raised by the scrub behind the call to action. In the static layout it is
   always up, since the copy sits over the lit final frame from the start. */
.rhero__dim {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-color: rgb(35 39 42 / 0.4);
  opacity: 0;
}

/* -- Beats ------------------------------------------------------------------ */
.rhero__beats {
  position: absolute;
  inset: 0;
  display: grid;
  padding-top: var(--nav-h);
  /* Clears the fixed Call / WhatsApp / Free quote bar on phones. */
  padding-bottom: var(--space-24);
}

@media (min-width: 48rem) {
  .rhero__beats {
    padding-bottom: var(--space-16);
  }
}

.rhero__beat {
  grid-area: 1 / 1;
  font-weight: var(--weight-semibold);
  text-shadow: 0 1px 24px rgb(0 0 0 / 0.25);
  will-change: opacity, transform;
}

.rhero__beat--head {
  align-self: center;
  justify-self: center;
  text-align: center;
}

.rhero__beat--sub {
  align-self: end;
  justify-self: start;
  text-align: left;
  max-width: 36rem;
  opacity: 0;
}

.rhero__beat--cta {
  align-self: center;
  justify-self: center;
  text-align: center;
  opacity: 0;
  pointer-events: none;
}

/* Clickable only once it is actually visible; residential-hero.js sets this. */
.rhero__beat--cta.is-live {
  pointer-events: auto;
}

/* A keyboard user tabbing onto a faded button sees it. Overrides the inline
   opacity GSAP writes. */
.rhero__beat:focus-within {
  opacity: 1 !important;
}

.rhero__eyebrow {
  margin: 0 0 var(--space-4);
  font-size: var(--text-sm);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--colour-stone-300);
}

.rhero__title {
  max-width: 11ch;
  margin: 0 auto;
  text-wrap: balance;
  font-family: var(--font-body);
  font-size: clamp(2.5rem, 7vw, 6rem);
  font-weight: var(--weight-semibold);
  line-height: 1.02;
  letter-spacing: -0.03em;
}

.rhero__sub {
  margin: 0;
  font-size: clamp(1.5rem, 3.4vw, 2.75rem);
  line-height: 1.15;
  letter-spacing: -0.03em;
}

.rhero__ctatext {
  margin: 0 0 var(--space-8);
  font-size: clamp(2rem, 5vw, 4rem);
  line-height: 1.05;
  letter-spacing: -0.03em;
}

.rhero__alt {
  margin: var(--space-4) 0 0;
  font-size: var(--text-sm);
  font-weight: var(--weight-regular);
  color: var(--colour-text-inverse);
}

.rhero__alt a {
  color: var(--colour-text-inverse);
  text-underline-offset: 0.2em;
}

/* Honesty marker. Mirrors the "Placeholder" badge on every photograph. */
.rhero__mark {
  position: absolute;
  right: var(--space-4);
  bottom: var(--space-4);
  padding: var(--space-1) var(--space-3);
  background-color: rgb(35 39 42 / 0.7);
  color: var(--colour-stone-300);
  font-family: var(--font-display);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

/* -- Scroll cue ------------------------------------------------------------- */
.rhero__cue {
  position: absolute;
  left: 50%;
  bottom: var(--space-6);
  transform: translateX(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
  color: var(--colour-stone-300);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  transition: opacity var(--duration-base) var(--ease-out);
}

/* Above the fixed action bar on phones. */
@media (max-width: 47.99rem) {
  .rhero__cue {
    bottom: calc(var(--space-16) + var(--space-2));
  }
}

.rhero__cue.is-gone {
  opacity: 0;
}

.rhero__chevron {
  width: 1.25rem;
  height: 1.25rem;
  animation: rhero-bob 1.6s var(--ease-in-out) infinite;
}

@keyframes rhero-bob {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(6px);
  }
}

/* -- Load progress ---------------------------------------------------------- */
.rhero__progress {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 2px;
  z-index: 1;
  background-color: rgb(255 255 255 / 0.12);
  transition: opacity var(--duration-slow) var(--ease-out);
}

.rhero.is-loaded .rhero__progress {
  opacity: 0;
}

.rhero__progressbar {
  display: block;
  height: 100%;
  background-color: var(--colour-marker);
  transform: scaleX(0);
  transform-origin: left center;
}

/* -- Static layout ------------------------------------------------------------
   The same block twice: once for the CSS-only cases, once for the runtime
   ones. Keep them identical. */
@media (scripting: none) {
  .rhero {
    height: auto;
  }

  .rhero__stage {
    position: relative;
    height: auto;
    min-height: 100svh;
    display: grid;
  }

  .rhero__still--start,
  .rhero__canvas,
  .rhero__cue,
  .rhero__progress {
    display: none;
  }

  .rhero__still--end {
    display: block;
  }

  .rhero__dim {
    opacity: 1;
  }

  .rhero__beats {
    position: relative;
    align-content: center;
    justify-items: center;
    gap: var(--space-8);
    padding-top: calc(var(--nav-h) + var(--space-12));
    padding-bottom: var(--space-16);
    text-align: center;
  }

  .rhero__beat {
    grid-area: auto;
    align-self: auto;
    justify-self: center;
    text-align: center;
    opacity: 1;
    pointer-events: auto;
    will-change: auto;
  }
}

.rhero.is-static {
  height: auto;
}

.rhero.is-static .rhero__stage {
  position: relative;
  height: auto;
  min-height: 100svh;
  display: grid;
}

.rhero.is-static .rhero__still--start,
.rhero.is-static .rhero__canvas,
.rhero.is-static .rhero__cue,
.rhero.is-static .rhero__progress {
  display: none;
}

.rhero.is-static .rhero__still--end {
  display: block;
}

.rhero.is-static .rhero__dim {
  opacity: 1;
}

.rhero.is-static .rhero__beats {
  position: relative;
  align-content: center;
  justify-items: center;
  gap: var(--space-8);
  padding-top: calc(var(--nav-h) + var(--space-12));
  padding-bottom: var(--space-16);
  text-align: center;
}

.rhero.is-static .rhero__beat {
  grid-area: auto;
  align-self: auto;
  justify-self: center;
  text-align: center;
  opacity: 1;
  pointer-events: auto;
  will-change: auto;
}

/* =============================================================================
   8  PAGE SECTIONS
   ============================================================================= */

/* -- Page head (every page except home) ------------------------------------- */
.page-head {
  padding-top: calc(var(--nav-h) + var(--space-16));
  padding-bottom: var(--space-16);
  background-color: var(--colour-bg-raised);
  border-bottom: var(--rule) solid var(--colour-border);
}

/* Below a hero, the nav no longer sits over this block. */
.page-head--after-hero {
  padding-top: var(--space-16);
}

/* 32ch sets the main site's "Expert construction management services in
   Melbourne" on two lines where the page has room, breaking after
   "management". The other page headings are shorter and stay on one. */
.page-head__heading {
  font-family: var(--font-display);
  font-size: var(--text-3xl);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  max-width: 32ch;
}

@media (min-width: 48rem) {
  .page-head__heading {
    font-size: var(--text-4xl);
  }
}

.page-head__lead {
  margin-top: var(--space-6);
  font-size: var(--text-lg);
  color: var(--colour-text-muted);
  max-width: var(--measure);
}

/* -- Intro + credentials ---------------------------------------------------- */
/* Heading, then its text directly underneath — one column at every width, so
   the text never drifts away from the heading it belongs to. The text keeps
   to a readable line length (.measure). */
.intro {
  display: grid;
  gap: var(--space-6);
  margin-top: var(--space-4);
}

.intro > .h2 {
  margin-top: 0;
  max-width: none;
}

.intro__body p + p {
  margin-top: var(--space-4);
}

/* -- About: a full-width photograph, the sections, then the team ---------- */
/* The photograph spans the container at its own shape, uncropped: the client's
   About photo is two photographs side by side, and any crop would cut into
   both and centre the join. */
.about__media .photo {
  aspect-ratio: auto;
  height: auto;
}

/* The About text: its label across the page, then the statement on the left and
   the paragraphs on the right. */
.approach {
  margin-top: var(--space-16);
}

.approach__split {
  display: grid;
  gap: var(--space-6);
  margin-top: var(--space-8);
}

.approach__split > .h2,
.approach__part > .h2 {
  margin-top: 0;
}

.approach__part > .intro__body {
  margin-top: var(--space-3);
}

/* A section without its own heading: the text spans both columns. */
.approach__full {
  grid-column: 1 / -1;
}

@media (min-width: 64rem) {
  .approach__split {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-16);
  }
}

/* Four people: one column on a phone, two from 40rem, four from 64rem. */
.team {
  list-style: none;
  margin: var(--space-12) 0 0;
  padding: 0;
  display: grid;
  gap: var(--space-8);
}

@media (min-width: 40rem) {
  .team {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 64rem) {
  .team {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

.team__name {
  margin: var(--space-4) 0 0;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
}

.team__desc {
  margin: var(--space-2) 0 0;
  font-size: var(--text-base);
  line-height: var(--leading-body);
  color: var(--colour-text-muted);
}

/* -- Services: three stages, each a split of photographs and copy ---------- */
.svcsplits {
  display: grid;
  gap: var(--space-8);
}

@media (min-width: 64rem) {
  .svcsplits {
    gap: var(--space-12);
  }
}

/* Stacked on a phone, a service's photo and text sit closer together than two
   services do, so each photo still reads as belonging to its own text. */
.svcsplit {
  display: grid;
  gap: var(--space-4);
  align-items: center;
}

@media (min-width: 64rem) {
  .svcsplit {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-16);
  }

  /* Every second stage puts its photographs on the right. Order, not source
     order, so a screen reader meets each stage the same way round. */
  .svcsplit--flip .svcsplit__media {
    order: 2;
  }
}

.svcsplit__n {
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  color: var(--colour-action-text);
}

.svcsplit__name {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  margin: var(--space-2) 0 var(--space-6);
}

@media (min-width: 48rem) {
  .svcsplit__name {
    font-size: var(--text-3xl);
  }
}

.svcsplit__text {
  color: var(--colour-text-muted);
  max-width: var(--measure);
}

.svcsplit__text + .svcsplit__text {
  margin-top: var(--space-4);
}

.svcsplit__verify {
  margin-top: var(--space-6);
  font-size: var(--text-sm);
}

.svc__scope {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-6) 0 0;
  padding: 0;
}

.svc__scopeitem {
  padding: var(--space-1) var(--space-3);
  border: var(--rule) solid var(--colour-border);
  font-family: var(--font-display);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--colour-text-muted);
}

/* -- Why choose: numbered reasons on the dark band -------------------------- */
/* The dark band keeps the light and raised bands either side of it apart. */
.why .eyebrow {
  color: inherit;
}

.why__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-12);
}

@media (min-width: 40rem) {
  .why__list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-12) var(--space-8);
  }
}

@media (min-width: 64rem) {
  .why__list {
    grid-template-columns: repeat(4, minmax(0, 1fr));
  }
}

.why__item {
  padding-top: var(--space-6);
  border-top: 2px solid var(--colour-marker);
}

.why__n {
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  color: var(--colour-text-inverse-muted); /* the orange is 2.86:1 on this band */
}

.why__title {
  font-size: var(--text-xl);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  margin: var(--space-2) 0 var(--space-4);
}

/* An h3 by request, but it reads as a paragraph, so it is set as one. */
.why__text {
  font-family: inherit;
  font-size: var(--text-base);
  font-weight: var(--weight-regular);
  line-height: var(--leading-body);
  color: var(--colour-text-inverse-muted);
  text-wrap: pretty;
}

/* The About page's lead-in and list: five short lines, each with a tick.
   Across the page once each line has room for itself, in two columns before. */
.why .h2 {
  max-width: none;
}

@media (min-width: 64rem) {
  .why__list--five {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 72rem) {
  .why__list--five {
    grid-template-columns: repeat(5, minmax(0, 1fr));
  }
}

.why__get {
  display: flex;
  gap: var(--space-3);
  align-items: flex-start;
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
}

.why__get .icon {
  width: 1.25rem;
  height: 1.25rem;
  margin-top: 0.15em;
  color: var(--colour-marker);
}

/* The same list on a light band. */
.why__list--light .why__text {
  color: var(--colour-text-muted);
}

/* -- Category pages: tiles, detail overlay, carousel ------------------------ */
/* Replaces the arcade. The grid is the page; a project opens over it.

   The whole section is built so that WITHOUT SCRIPTING it is a grid of anchors
   followed by every project in full, in the flow — a complete page. Everything
   that turns it into an overlay sits inside `@media (scripting: enabled)`, so
   there is no flash of hidden content and no render-blocking head script to
   set a class. */

.tiles {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-8);
}

@media (min-width: 40rem) {
  .tiles {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 64rem) {
  .tiles {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-12) var(--space-8);
  }
}

/* Gateway: two tiles, one per site, at two columns from 40rem up. */
@media (min-width: 64rem) {
  .tiles--two {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* The gateway's two tiles: a short, wide photo (2:1, set in the template) and
   the site's name as the largest type on the page after the heading. */
.tiles--two .tile__title {
  font-size: var(--text-3xl);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
}

@media (min-width: 48rem) {
  .tiles--two .tile__title {
    font-size: var(--text-4xl);
  }
}

/* Projects page: one section per service, its title then its tiles. Tiles
   are one column on a phone and three from 48rem — never two, which would
   leave a third tile alone on a line. */
.projects-flow {
  display: grid;
  gap: var(--space-16);
}

@media (min-width: 64rem) {
  .projects-flow {
    gap: var(--space-24);
  }
}

.projects-group > .eyebrow--section {
  margin-bottom: var(--space-8);
}

/* Jump buttons above the sections. They wrap rather than scroll sideways on
   a phone. */
.jumpnav {
  margin-bottom: var(--space-16);
}

.jumpnav ul {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  list-style: none;
  margin: 0;
  padding: 0;
}

.tiles--row {
  grid-template-columns: minmax(0, 1fr);
}

@media (min-width: 48rem) {
  .tiles--row {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

.tile__link {
  display: block;
  color: inherit;
  text-decoration: none;
  position: relative;
}

.tile__link .photo-frame,
.tile__link .missing-photo {
  overflow: hidden;
}

/* The one hover effect in the system is the action affordance. Here the
   affordance IS the tile, so it gets the ochre rule and the cue — not a zoom on
   the photograph, which the motion spec rules out. */
.tile__link .photo,
.tile__link .missing-photo {
  transition: filter var(--duration-base) var(--ease-out);
}

.tile__link:hover .photo {
  filter: brightness(0.92);
}

.tile__cap {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding-top: var(--space-4);
  margin-top: var(--space-4);
  border-top: var(--rule) solid var(--colour-border);
  transition: border-color var(--duration-base) var(--ease-out);
}

.tile__link:hover .tile__cap,
.tile__link:focus-visible .tile__cap {
  border-top-color: var(--colour-action);
}

.tile__title {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
}

.tile__meta {
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

.tile__desc {
  margin-top: var(--space-2);
  font-size: var(--text-sm);
  line-height: var(--leading-body);
  text-wrap: pretty;
}

/* Project tiles line up across a row. Each tile spans the same five rows of
   the grid (photo, title, meta, description, cue), so a two-line meta in one
   tile moves every description in that row down together. The grid's rows sit
   edge to edge, because a gap there would open inside each tile as well; the
   space between one row of tiles and the next is padding instead. The
   gateway's two tiles have no description, so they keep the plain layout. */
@supports (grid-template-rows: subgrid) {
  .tiles:not(.tiles--two) {
    --tile-row-gap: var(--space-8);
    row-gap: 0;
    margin-bottom: calc(-1 * var(--tile-row-gap));
  }

  @media (min-width: 64rem) {
    .tiles:not(.tiles--two) {
      --tile-row-gap: var(--space-12);
    }
  }

  .tiles:not(.tiles--two) > .tile,
  .tiles:not(.tiles--two) .tile__link {
    display: grid;
    grid-row: span 5;
    grid-template-rows: subgrid;
  }

  .tiles:not(.tiles--two) > .tile {
    padding-bottom: var(--tile-row-gap);
  }

  .tiles:not(.tiles--two) .tile__cap {
    display: grid;
    grid-row: span 3;
    grid-template-rows: subgrid;
  }

  .tiles:not(.tiles--two) .tile__cue {
    justify-self: start;
  }
}

/* -- Hover preview ---------------------------------------------------------- */
/* Photos 2..n stack over photo 1 and cross-fade. Absolutely positioned, so a
   swap can never move anything: the frame's height comes from photo 1 alone.
   Only opacity animates, which the compositor handles without a repaint. */
.tile__media {
  position: relative;
  overflow: hidden;
}

/* A track that slides, not a stack that fades. One transform moves every
   photograph, so the work does not grow with the photo count, and the
   compositor does it without a repaint. */
.preview__track {
  display: flex;
  transition: transform 500ms var(--ease-out);
}

.preview__slide,
.tile__extra {
  flex: 0 0 100%;
  width: 100%;
  /* Fixed by the frame, so a slide can never change the tile's height. */
  aspect-ratio: 4 / 3;
  object-fit: cover;
  /* The tile is one click target. These must never swallow the pointer. */
  pointer-events: none;
}

.preview__slide picture,
.preview__slide .photo {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Sits above the stack so it stays legible whichever photo is showing. */
.tile__count {
  position: absolute;
  right: 0;
  bottom: 0;
  z-index: 2;
  padding: var(--space-1) var(--space-3);
  background-color: rgb(35 39 42 / 0.72);
  color: var(--colour-stone-0);
  font-family: var(--font-display);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

.photo__badge {
  z-index: 2;
}

.tile__cue {
  display: inline-block;
  margin-top: var(--space-2);
  font-family: var(--font-display);
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--colour-action-text);
  opacity: 0;
  transform: translateX(-4px);
  transition:
    opacity var(--duration-base) var(--ease-out),
    transform var(--duration-base) var(--ease-out);
}

.tile__link:hover .tile__cue,
.tile__link:focus-visible .tile__cue {
  opacity: 1;
  transform: translateX(0);
}

/* -- Missing photograph ----------------------------------------------------- */
/* Deliberately loud. A grey block naming what is absent is useful; a broken
   image is not, and a stock photograph standing in silently would be a claim
   about work the client has not shown us. */
.missing-photo {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  padding: var(--space-6);
  background-color: var(--colour-concrete);
  color: var(--colour-stone-900);
  text-align: center;
}

.missing-photo__tag {
  font-family: var(--font-display);
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

.missing-photo__desc {
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
  color: var(--colour-stone-700);
  max-width: 32ch;
}

/* -- Detail panels ---------------------------------------------------------- */
/* No-JS baseline: every project in the flow, separated by a rule. */
.details {
  margin-top: var(--space-24);
}

.detail {
  padding-top: var(--space-16);
  margin-top: var(--space-16);
  border-top: 2px solid var(--colour-stone-900);
}

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

.detail__head {
  margin-bottom: var(--space-8);
}

.detail__title {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: var(--weight-bold);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  margin: var(--space-4) 0 var(--space-2);
}

@media (min-width: 48rem) {
  .detail__title {
    font-size: var(--text-3xl);
  }
}

.detail__meta {
  color: var(--colour-text-muted);
  font-size: var(--text-sm);
}

.detail__body {
  margin-top: var(--space-8);
  max-width: var(--measure);
}

.detail__sub {
  font-family: var(--font-display);
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--colour-text-muted);
  margin-bottom: var(--space-3);
}

.detail__note {
  margin-top: var(--space-4);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

.detail__cta {
  margin-top: var(--space-8);
}

.detail__close {
  display: none;
}

/* ===========================================================================
   Scripted presentation: grid plus overlay
   =========================================================================== */
@media (scripting: enabled) {
  .details {
    position: fixed;
    inset: 0;
    z-index: 60;
    margin: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    background-color: rgb(35 39 42 / 0.62);
    backdrop-filter: blur(3px);
    visibility: hidden;
    opacity: 0;
    transition:
      opacity var(--duration-base) var(--ease-out),
      visibility 0s linear var(--duration-base);
  }

  .details.is-open {
    visibility: visible;
    opacity: 1;
    transition-delay: 0s;
  }

  .detail {
    display: none;
    border-top: 0;
    padding: 0;
    margin: var(--space-16) auto;
    max-width: 64rem;
  }

  .detail.is-open {
    display: block;
  }

  .detail:focus-visible {
    outline: none;
  }

  .detail__inner {
    position: relative;
    background-color: var(--colour-bg);
    padding: var(--space-12) var(--space-6) var(--space-16);
    max-width: none;
  }

  @media (min-width: 48rem) {
    .detail__inner {
      padding: var(--space-16) var(--space-12) var(--space-16);
    }
  }

  .detail__close {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: absolute;
    top: var(--space-4);
    right: var(--space-4);
    min-width: var(--touch-min);
    min-height: var(--touch-min);
    padding-inline: var(--space-4);
    background-color: var(--colour-stone-900);
    color: var(--colour-text-inverse);
    font-family: var(--font-display);
    font-size: var(--text-sm);
    font-weight: var(--weight-semibold);
    text-decoration: none;
  }

  .detail__close:hover {
    background-color: var(--colour-action);
    color: var(--colour-on-action);
  }

  /* The page behind must not scroll while a project is open. */
  body.has-open-project {
    overflow: hidden;
  }

  /* The mobile action bar would sit over the panel. */
  body.has-open-project .actionbar {
    display: none;
  }
}

/* -- Carousel --------------------------------------------------------------- */
.carousel {
  position: relative;
  overflow: hidden;
  background-color: var(--colour-stone-100);
}

.carousel__track {
  display: flex;
  list-style: none;
  margin: 0;
  padding: 0;
  /* Transform only — the slide never triggers layout. */
  transition: transform var(--duration-slow) var(--ease-out);
}

.carousel__slide {
  flex: 0 0 100%;
  position: relative;
  min-width: 0;
}

.carousel__slide img {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
}

/* In a project's pop-up every photograph is shown whole, never trimmed: it is
   contained in a 4:3 frame (the most common shape among the project photos),
   and the carousel's grey fills the rest, so wide before-and-after shots and
   upright phone photos keep all their edges. One frame for every slide keeps
   the arrows and caption still as the slides change. The height cap leaves
   room for the panel's margin and title above it (about 18rem on a laptop),
   so the whole photo is on screen the moment the panel opens; the 14rem floor
   stops it shrinking to a strip on a phone held sideways. */
.detail .carousel__slide img {
  aspect-ratio: 4 / 3;
  object-fit: contain;
  max-height: max(14rem, calc(100svh - 18rem));
}

/* No script: the track cannot transform, so the slides become a vertical
   stack and every photograph stays reachable. */
@media (scripting: none) {
  .carousel__track {
    display: grid;
    gap: var(--space-2);
  }

  .carousel__arrow,
  .carousel__bar {
    display: none;
  }
}

.carousel__arrow {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  align-items: center;
  justify-content: center;
  width: var(--touch-min);
  height: var(--touch-min);
  padding: 0;
  border: 0;
  background-color: rgb(255 255 255 / 0.88);
  color: var(--colour-stone-900);
  cursor: pointer;
  transition: background-color var(--duration-fast) var(--ease-out);
}

.carousel__arrow:hover {
  background-color: var(--colour-action);
  color: var(--colour-on-action);
}

.carousel__arrow svg {
  width: 1rem;
  height: 1rem;
}

.carousel__arrow--prev {
  left: var(--space-3);
}

.carousel__arrow--next {
  right: var(--space-3);
}

.carousel__bar {
  position: absolute;
  left: var(--space-3);
  right: var(--space-3);
  bottom: var(--space-3);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
}

.carousel__dots {
  display: flex;
  gap: var(--space-1);
  list-style: none;
  margin: 0;
  padding: 0;
}

.carousel__dots button {
  display: flex;
  align-items: center;
  width: 1.75rem;
  height: var(--touch-min);
  padding: 0;
  background: none;
  border: 0;
  cursor: pointer;
}

/* A class, not `.carousel__dots span` — an element selector there would
   outscore .visually-hidden and blow out any hidden label placed inside. */
.carousel__dot {
  display: block;
  width: 100%;
  height: 3px;
  background-color: rgb(255 255 255 / 0.55);
  transition: background-color var(--duration-base) var(--ease-out);
}

.carousel__dots button[aria-current='true'] .carousel__dot {
  background-color: var(--colour-action);
}

/* -- Work grid -------------------------------------------------------------- */
.work {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--space-8);
}

@media (min-width: 40rem) {
  .work {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (min-width: 64rem) {
  .work {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-12) var(--space-8);
  }
}

.work__group {
  margin-top: var(--space-24);
}

.work__group:first-of-type {
  margin-top: var(--space-12);
}

/* -- Process: six steps that slide in behind a marker line ----------------- */
/* The line draws across (down, on a phone) while each step slides in as the
   line reaches it. It plays once, in about four seconds, so it needs no pause
   control. With scripting off the steps are simply there: every offset below
   sits inside `scripting: enabled`. */
.steps-wrap {
  position: relative;
  --step-gap: 0.55s;
  /* The steps start 3rem to the right and slide in. Without clipping, that
     start position widens the page on a phone until they are revealed. */
  overflow-x: clip;
}

.steps {
  list-style: none;
  margin: 0;
  padding: 0 0 0 var(--space-8);
  display: grid;
  gap: 0;
}

@media (min-width: 64rem) {
  .steps {
    grid-template-columns: repeat(var(--step-count), minmax(0, 1fr));
    padding: 0;
  }
}

/* The track the marker runs along. */
.steps-wrap::before,
.steps__line {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  width: 2px;
  height: 100%;
}

.steps-wrap::before {
  background-color: var(--colour-border);
}

.steps__line {
  background-color: var(--colour-action);
  transform-origin: top;
}

@media (min-width: 64rem) {
  .steps-wrap::before,
  .steps__line {
    width: 100%;
    height: 2px;
  }

  .steps-wrap::before {
    background-color: var(--colour-stone-900);
  }

  .steps__line {
    transform-origin: left;
  }
}

.step {
  position: relative;
}

/* A tick where the line meets each step. */
.step::before {
  content: '';
  position: absolute;
  left: calc(-1 * var(--space-8) - 5px);
  top: var(--space-8);
  width: 12px;
  height: 12px;
  background-color: var(--colour-bg-raised);
  border: 2px solid var(--colour-action);
}

@media (min-width: 64rem) {
  .step::before {
    left: 0;
    top: -5px;
  }
}

.step__inner {
  padding-block: var(--space-6);
  height: 100%;
}

@media (min-width: 64rem) {
  .step__inner {
    padding: var(--space-8) var(--space-6) 0 0;
  }
}

.step__n {
  font-family: var(--font-display);
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  color: var(--colour-action-text);
}

.step__name {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  margin: var(--space-2) 0 var(--space-3);
}

.step__detail {
  font-size: var(--text-sm);
  line-height: var(--leading-body);
  color: var(--colour-text-muted);
}

@media (scripting: enabled) {
  .steps__line {
    transform: scaleY(0);
    transition: transform calc(var(--step-gap) * var(--step-count)) linear;
  }

  .step {
    transform: translateX(3rem);
    transition: transform var(--duration-slow) var(--ease-out);
    transition-delay: calc(var(--i) * var(--step-gap));
  }

  .step::before {
    background-color: var(--colour-bg-raised);
    transition: background-color var(--duration-base) var(--ease-out);
    transition-delay: calc(var(--i) * var(--step-gap) + var(--step-gap));
  }

  .steps-wrap.is-shown .steps__line {
    transform: scaleY(1);
  }

  .steps-wrap.is-shown .step {
    transform: none;
  }

  .steps-wrap.is-shown .step::before {
    background-color: var(--colour-action);
  }

  @media (min-width: 64rem) {
    .steps__line {
      transform: scaleX(0);
    }

    .steps-wrap.is-shown .steps__line {
      transform: scaleX(1);
    }
  }
}

/* The wrapper slides sideways, not up, so the whole section reads as one
   horizontal motion. */
.steps-wrap.reveal {
  transform: none;
}

.steps__note {
  margin-top: var(--space-12);
  font-size: var(--text-sm);
  color: var(--colour-text-muted);
}

/* -- Quote band ------------------------------------------------------------- */
.quoteband {
  background-color: var(--colour-bg-inverse);
  color: var(--colour-text-inverse);
  padding-block: var(--space-24);
}

@media (min-width: 48rem) {
  .quoteband {
    padding-block: var(--space-32);
  }
}

.quoteband__inner {
  display: grid;
  gap: var(--space-8);
}

@media (min-width: 48rem) {
  .quoteband__inner {
    grid-template-columns: 1fr auto;
    align-items: end;
  }
}

.quoteband .eyebrow {
  color: var(--colour-text-inverse);
}

.quoteband__lead {
  margin-top: var(--space-6);
  color: var(--colour-text-inverse-muted);
  max-width: var(--measure);
}

.quoteband__lead + .quoteband__lead {
  margin-top: var(--space-4);
}

.quoteband__line {
  margin-top: var(--space-6);
  max-width: var(--measure);
  font-family: var(--font-display);
  font-weight: var(--weight-semibold);
  color: var(--colour-text-inverse);
}

.quoteband__actions {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-4);
}

/* One size on every page, whatever the words: wide enough for the longest label,
   "Call 0431 851 350". */
.quoteband__actions .btn {
  width: 12rem;
}

.quoteband__whatsapp {
  color: var(--colour-text-inverse-muted);
  font-size: var(--text-sm);
  text-underline-offset: 0.25em;
}

.quoteband__whatsapp:hover {
  color: var(--colour-text-inverse);
}

/* -- Contact ---------------------------------------------------------------- */
/* One column of direct contact options; the narrow width keeps each label
   close to its number or address. */
.contact__inner > div {
  max-width: var(--container-narrow);
}

.direct {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  border-top: var(--rule) solid var(--colour-border);
}

.direct a {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding-block: var(--space-4);
  min-height: var(--touch-min);
  border-bottom: var(--rule) solid var(--colour-border);
  color: var(--colour-text);
  text-decoration: none;
}

.direct a:hover {
  color: var(--colour-action-text);
}

/* Contact options carry the real brand marks, a size up from the inline
   icons used elsewhere. */
.brand-icon {
  width: 2rem;
  height: 2rem;
  flex-shrink: 0;
}

.direct__label {
  font-family: var(--font-display);
  font-size: var(--text-xs);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--colour-text-muted);
  min-width: 6rem;
}

/* The value takes the remaining width and sits hard right, so the four rows
   line up as a table rather than drifting with each label's length. */
.direct__value {
  margin-left: auto;
  text-align: right;
}

.direct__value {
  font-family: var(--font-display);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
}

/* -- FAQ: one native <details> per question ---------------------------------- */
/* From 64rem the heading holds still on the left while the questions open on
   the right, so a long answer never pushes it out of view. */
.faq__split {
  display: grid;
  gap: var(--space-8);
  margin-top: var(--space-8);
}

.faq__sub {
  margin-top: 0;
}

@media (min-width: 64rem) {
  .faq__split {
    grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
    gap: var(--space-16);
    align-items: start;
  }

  .faq__sub {
    position: sticky;
    top: calc(var(--nav-h) + var(--space-8));
  }
}

.faq__item {
  border-top: var(--rule) solid var(--colour-border);
}

.faq__item:last-child {
  border-bottom: var(--rule) solid var(--colour-border);
}

.faq__q {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  padding-block: var(--space-6);
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-snug);
  cursor: pointer;
  list-style: none;
}

/* Remove the default disclosure triangle in every engine. */
.faq__q::-webkit-details-marker {
  display: none;
}

.faq__q::marker {
  content: '';
}

.faq__q:hover {
  color: var(--colour-action-text);
}

.faq__n {
  flex: 0 0 1.5rem;
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  line-height: inherit;
  color: var(--colour-action-text);
}

.faq__text {
  flex: 1;
}

/* The plus turns into a cross when the answer is open. */
.faq__q .icon {
  width: 1.5rem;
  height: 1.5rem;
  color: var(--colour-action-text);
  transition: transform var(--duration-base) var(--ease-out);
}

.faq__item[open] .icon {
  transform: rotate(45deg);
}

.faq__a {
  padding-bottom: var(--space-6);
  color: var(--colour-text-muted);
  max-width: var(--measure);
}

.faq__a:not(:last-child) {
  padding-bottom: var(--space-4);
}

/* Lined up under the question once there is width to spare for it. */
@media (min-width: 40rem) {
  .faq__a {
    padding-left: calc(1.5rem + var(--space-4));
  }
}

/* A browser that can animate to `auto` slides the answer open; any other just
   shows it. */
@media (prefers-reduced-motion: no-preference) {
  @supports (interpolate-size: allow-keywords) {
    .faq__item {
      interpolate-size: allow-keywords;
    }

    .faq__item::details-content {
      block-size: 0;
      overflow: clip;
      transition: block-size var(--duration-base) var(--ease-out), content-visibility var(--duration-base) allow-discrete;
    }

    .faq__item[open]::details-content {
      block-size: auto;
    }
  }
}

/* =============================================================================
   9  UTILITIES
   ============================================================================= */

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

/* Keeps a word in one piece, such as "fit-outs" at its hyphen. */
.nowrap {
  white-space: nowrap;
}

/* -- Scroll reveal ---------------------------------------------------------- */
/* TRANSFORM ONLY, NEVER OPACITY. An opacity:0 initial state means blank
   sections if the script never runs. A translate leaves the content fully
   legible and merely 24px out of place, which is a defect you can live with. */
.reveal {
  transform: translateY(24px);
  transition: transform var(--duration-slow) var(--ease-out);
}

.reveal.is-shown {
  transform: translateY(0);
}

/* =============================================================================
   10  NO JAVASCRIPT
   The contract that must hold if no JavaScript ever runs. The device's
   reduce-motion setting is deliberately NOT honoured anywhere on this site:
   the client chose to show the motion to every visitor.
   ============================================================================= */

@media (scripting: none) {
  .reveal {
    transform: none;
  }

  /* The hero collapses to one screen: no pin, no track, no canvas. It shows an
     outcome rather than a process, which is the honest static equivalent. */
  .hero {
    --hero-track: auto;
    height: auto;
  }

  .hero__stage {
    position: relative;
    height: 100svh;
    min-height: 34rem;
  }

  .hero__still--start,
  .hero__canvas,
  .hero__cue,
  .hero__progress {
    display: none;
  }

  .hero__still--end {
    display: block;
  }

  .hero__act {
    grid-area: auto;
  }

  .hero__steps,
  .hero__act--outro {
    display: none;
  }
}
