﻿/* ============================================================
   Urbankriti — reset, design tokens, base styles, components
   ============================================================ */

/* ---------- Reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
* { margin: 0; padding: 0; }
html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
body { min-height: 100vh; -webkit-font-smoothing: antialiased; }
img, picture, video, canvas, svg { display: block; max-width: 100%; }
input, button, textarea, select { font: inherit; }
a { color: inherit; text-decoration: none; }
ul, ol { list-style: none; }
button { background: none; border: none; cursor: pointer; }

/* ---------- Design tokens ---------- */
:root {
  /* Colour — anchors */
  --uk-brown:      #5C3A21;
  --uk-umber:      #3F2422;

  /* Colour — accents */
  --uk-ochre:      #C8811C;
  --uk-terracotta: #7E3239;

  /* Colour — neutrals */
  --uk-clay:       #B08560;
  --uk-taupe:      #8D746C;
  --uk-sandstone:  #E5D9C3;
  --uk-bone:       #FAF7F2;
  --uk-white:      #FFFFFF;

  /* Colour — semantic */
  --text-primary:   var(--uk-umber);
  /* Raw taupe as body text measures 4.06:1 on bone and 3.11:1 on sandstone —
     both under the floor. Deepened toward umber: 6.5:1 on bone, 4.9:1 on
     sandstone, still clearly softer than the umber headings. --uk-taupe itself
     is untouched. */
  --text-secondary: color-mix(in srgb, var(--uk-taupe) 60%, var(--uk-umber));
  --text-on-dark:   #EFE7DA;
  --surface:        var(--uk-bone);
  --surface-alt:    var(--uk-sandstone);
  --surface-dark:   var(--uk-umber);
  --border:         rgba(92, 58, 33, 0.14);
  --border-strong:  rgba(92, 58, 33, 0.30);
  --border-on-dark: rgba(239, 231, 218, 0.12);

  /* Clay is the eyebrow colour, but on umber it measures 4.28:1 — under the
     4.5 floor for 12px type. Lifted toward the cream for dark sections only:
     same hue, derived from existing tokens, 5.2:1. Not a new palette colour. */
  --clay-on-dark:   color-mix(in srgb, var(--uk-clay) 82%, var(--text-on-dark));
  /* Ochre has the same problem, less obviously: on umber it measures 4.448:1,
     which misses the floor by a hair. Lifted the same way, same hue, 4.8:1.
     Only for ochre used as type on a dark ground — buttons and rules still
     take the raw token, where 3:1 applies. */
  --ochre-on-dark:  color-mix(in srgb, var(--uk-ochre) 92%, var(--text-on-dark));
  /* The image mount on a dark ground — bone would blaze, so a lifted umber. */
  --surface-dark-2: color-mix(in srgb, var(--uk-umber) 92%, var(--text-on-dark));
  /* Terracotta is the palette's warning colour and on umber it measures
     1.62:1 — not marginal, invisible. Lifted toward the cream the same way
     clay and ochre are above: same hue, 4.3:1, which clears the 3:1 graphics
     floor with room. Section 4's programme gaps are the only thing using it,
     and they are the one place the page raises its voice. */
  --terracotta-on-dark: color-mix(in srgb, var(--uk-terracotta) 55%, var(--text-on-dark));

  /* The same problem inverted. On light grounds clay falls to 3.1:1 (bone) and
     2.4:1 (sandstone), and ochre to 3.0:1 and 2.3:1 — so neither can carry
     type there. Deepened toward brown: clay reaches 4.8:1 on sandstone, ochre
     5.1:1 on bone. Use these for text only; the raw tokens still do the fills,
     borders and buttons, where the floor is 3:1. */
  --clay-on-light:  color-mix(in srgb, var(--uk-clay) 35%, var(--uk-brown));
  --ochre-on-light: color-mix(in srgb, var(--uk-ochre) 50%, var(--uk-brown));
  /* Terracotta at 1.6:1-ish on bone, same problem as clay/ochre above,
     same fix: deepened toward brown until it can carry text. */
  --terracotta-on-light: color-mix(in srgb, var(--uk-terracotta) 55%, var(--uk-brown));

  /* Keyword accent (round 15). The serif keyword in a heading carries the
     accent, one system sitewide. On light grounds: the deep copper the
     Renovations hero has always used (#96532A, 5.4:1 on paper, 4.4:1 on
     sandstone). On dark grounds: the lifted ochre the "Learn more" links use
     (4.8:1 on umber). Both clear the 3:1 large-text floor with room. */
  --keyword-on-light: #96532A;
  --keyword-on-dark:  var(--ochre-on-dark);

  /* Type (round 12, 2026-10-09). One family for every ordinary word:
     --font-display  Plus Jakarta Sans: headings, statements, large numerals.
                     Light (300) for h1/h2-scale type, regular for h3s.
     --font-body     Plus Jakarta Sans: running text, UI, buttons, small text.
     --font-mono     kept as a seam for the old label selectors; the sans.
     --font-keyword  Instrument Serif italic, for KEYWORDS ONLY: the one word
                     marked <em> inside a heading (or .uk-em). Nothing else on
                     the site is set in the serif. */
  --font-display: 'Plus Jakarta Sans', system-ui, sans-serif;
  --font-body:    'Plus Jakarta Sans', system-ui, sans-serif;
  --font-mono:    'Plus Jakarta Sans', system-ui, sans-serif;
  --font-keyword: 'Instrument Serif', Georgia, serif;

  /* ---- The six accents ----

     One colour per plate in Section 5, so each of the six reads as its own
     thing rather than as one idea printed six times. Every one is MIXED FROM
     THE FOUR PALETTE COLOURS — ochre, terracotta, clay, brown — so this is a
     ramp through the palette, not a fifth colour smuggled in. Gold through
     amber, rust, terracotta, mauve, clay: the spread of a warm material
     board, which is the right reference for this brand.

     Contrast on bone, all measured, all against the 3:1 graphics floor
     (these draw the marks; they are not body copy):

       01 gold  3.5:1   02 amber 4.0:1   03 rust  5.1:1
       04 terra 8.2:1   05 mauve 5.0:1   06 clay  4.2:1

     Raw ochre is deliberately NOT in the list: it measures 2.97:1 here and
     misses the floor by a hair, which is exactly the kind of near-miss that
     looks fine on a designer's monitor and disappears on a laptop at an
     angle. 01 is ochre deepened just far enough to clear it. */
  --acc-1: color-mix(in srgb, var(--uk-ochre) 85%, var(--uk-brown));
  --acc-2: color-mix(in srgb, color-mix(in srgb, var(--uk-ochre) 70%, var(--uk-terracotta)) 100%, transparent);
  --acc-3: color-mix(in srgb, var(--uk-ochre) 45%, var(--uk-terracotta));
  --acc-4: var(--uk-terracotta);
  --acc-5: color-mix(in srgb, var(--uk-terracotta) 50%, var(--uk-clay));
  --acc-6: color-mix(in srgb, var(--uk-clay) 70%, var(--uk-brown));

  /* The logotype, and ONLY the logotype. Never a heading, a label or a word
     of copy — this is not a fourth typeface in the sense CLAUDE.md means.

     The logo artwork is set in Century Gothic. Not Quicksand, which used to
     sit here on that assumption: overlay the two against the PNG's own
     wordmark and Century Gothic traces it letter for letter, while Quicksand
     comes out wider, rounder in the stroke ends and visibly different at the
     K and the r. Century Gothic is licensed and cannot be served, so it is
     tried locally first — a great many machines have it — with Questrial, the
     closest free geometric to it, as the served fallback.

     The header wordmark no longer depends on any of this: it paints the
     logo's own lettering (see .hdr-word). What is left for this stack is the
     drawing-titleblock mark and the fallback path where masks are missing. */
  --font-logo:    'Century Gothic', 'Questrial', sans-serif;

  /* Display scale (round 15). Every display size below, and every page's
     own heading/statement font-size, is multiplied by --k-display. The sizes
     were tuned for Instrument Serif, a narrow face; Plus Jakarta Sans is
     about 1.35x wider at the same px, so at the old sizes the headings broke
     onto extra lines. .8 brings each heading back to the serif's footprint
     (same line count, similar block width and height). Body, nav, buttons
     and small labels are NOT scaled. */
  --k-display:  .82;
  /* Heading measures set in ch were tuned on the serif, whose "0" is 0.46em;
     the sans's is 0.72em, so an Nch measure grew wider than the text did and
     headings lost a line. --k-measure brings such a measure back to the
     serif's characters-per-line. Used only where a heading's line count
     depended on it. */
  --k-measure:  .86;
  --fs-hero:    calc(clamp(3.1rem, 8vw, 6.4rem) * var(--k-display));
  /* min(vw, vh) — not vw alone.

     Every size on this page scaled on width only, which is fine on a desktop
     monitor and wrong on a laptop: a 1920x830 screen is WIDE, so 4.5vw gave
     an 86px heading, and then there were 830px of height to fit it and a
     pinned image and a caption into. Width told the type it had room that
     the height did not have.

     Taking the smaller of the two means a tall screen renders exactly as
     before (the vw term still binds) and a short one steps the type down
     instead of pushing content off the bottom. */
  --fs-h2:      calc(clamp(2.1rem, min(5.4vw, 7.6vh), 4rem) * var(--k-display));
  --fs-h3:      calc(clamp(1.35rem, min(2.3vw, 3vh), 1.9rem) * var(--k-display));
  --fs-lead:    clamp(1rem, min(1.6vw, 2.1vh), 1.3rem);
  --fs-body:    1.0625rem;
  --fs-small:   0.9375rem;
  --fs-eyebrow: 0.75rem;
  --fs-number:  calc(clamp(2.9rem, 5.6vw, 4.6rem) * var(--k-display));

  /* Spacing and layout */
  --pad-page:    clamp(1.25rem, 5vw, 5rem);
  --pad-section: clamp(4.5rem, 10vw, 9rem);
  --gap-sm: 0.75rem;
  --gap-md: 1.5rem;
  --gap-lg: 3rem;
  --gap-xl: 5rem;
  --maxw: 1320px;
  /* Header-01 reskin: the reference bar is a compact `py-3` row, not a tall
     masthead. Reduced from 170px. --hdr-mark-h is NOT touched — the mark is a
     crop window over the logo PNG and its 51/76 pairing is load-bearing (see
     the note above .hdr-mark). A shorter bar can only help the heroes, which
     pad against --hdr-safe below and now clear it with room to spare. */
  --hdr-h: 136px;
  --hdr-mark-h: 76px;
  /* Header-01: `mt-2` on the inner container, and `rounded-2xl` on it once
     the page is scrolled. */
  --hdr-gap-top: 0.5rem;
  --hdr-radius: 1rem;
  --radius: 2px;
  --radius-img: 4px;

  /* The height the fixed header occupies once it has shrunk, plus a little
     air. EVERY pinned or sticky full-viewport stage has to subtract this.

     This exists because making the header fixed broke every one of them at
     once: a stage pinned to `top top` and sized to 100svh centres its
     contents in a box whose top 88px is underneath the bar, so the first
     line of the heading is always hidden. It looked like six unrelated
     layout bugs and it was one.

     Kept as a token rather than a number in six places so the day the header
     changes height, the stages follow. If you change the header's stuck
     min-height, change this. */
  --hdr-safe: 92px;

  /* Motion */
  --dur-fast: .25s;
  --dur-base: .6s;
  --dur-slow: .9s;
  --ease-out: cubic-bezier(.22, 1, .36, 1);
}

/* ---------- Base ---------- */

/* The header is fixed now, so every in-page anchor would otherwise land with
   its heading underneath it. scroll-margin on the sections rather than
   scroll-padding here, so it applies to the targets themselves and Lenis
   honours it too. */
:target,
section[id] { scroll-margin-top: 96px; }

body {
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: 1.7;
  color: var(--text-primary);
  background: var(--surface);
}

/* Only the faces that are loaded may render: the sans has no italic and the
   keyword serif has no bold, and a synthesised oblique or bold is the fastest
   way to make either look cheap. */
html { font-synthesis: none; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: 300;
  line-height: 1.08;
  letter-spacing: -0.02em;
  color: var(--text-primary);
}

h1 {
  font-size: var(--fs-hero);
  font-weight: 300;
  line-height: 1.05;
  letter-spacing: -0.025em;
}

h2 { font-size: var(--fs-h2); }
h3 { font-size: var(--fs-h3); font-weight: 400; line-height: 1.12; letter-spacing: -0.01em; }
h4 { font-weight: 400; }

/* Keywords (round 12). The one word or phrase marked <em> in a heading is
   the only serif on the site: Instrument Serif italic, a touch larger than
   the sans so its smaller x-height sits level with it. Colour comes from
   each page's own em rule (copper/clay), or the heading's colour. */
h1 em, h2 em, h3 em, .uk-em {
  font-family: var(--font-keyword);
  font-style: italic;
  font-weight: 400;
  font-size: 1.08em;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--keyword-on-light);
}
/* Dark sections (umber/walnut) are the ones marked data-nav-theme="dark". */
[data-nav-theme="dark"] :is(h1, h2, h3) em,
[data-nav-theme="dark"] .uk-em { color: var(--keyword-on-dark); }

p { max-width: 62ch; }

a { transition: color var(--dur-fast) ease; }

:focus-visible {
  outline: 2px solid var(--uk-ochre);
  outline-offset: 3px;
}

/* ---------- Components ---------- */

.container {
  max-width: var(--maxw);
  margin-inline: auto;
  padding-inline: var(--pad-page);
}

/* Eyebrow labels (the dash + small label above a heading) are retired
   sitewide (round 6, 2026-10-08): headings stand on their own. The markup
   has been removed; anything left over stays out of sight rather than
   render as stray text. */
.eyebrow { display: none !important; }

.frame {
  border: 1px solid var(--uk-brown);
  padding: 10px;
  background: var(--uk-bone);
}
.frame img {
  display: block;
  width: 100%;
  border-radius: var(--radius-img);
}

/* One shared grade across every photograph on the page — docs/07 §2. This is
   what stops thirty-nine separately generated images reading as a pile of
   stock rather than one shoot.

   `main img` reaches every content photograph and nothing else: the header
   and footer logos sit outside <main>, and the film is a <canvas>, not an
   <img>. Deliberately only two type selectors, so the two rules below —
   which are class-based and therefore stronger — keep overriding it without
   needing !important. Replaces the narrower .frame/.s3-card-media pairing,
   which left the Section 8 and 9 photographs ungraded. */
main img {
  filter: saturate(0.92) contrast(1.03) sepia(0.04);
}

/* Demo-mode placeholder: js/main.js tags an image whose file is missing, and
   the coloured block behind it stands in at the same aspect ratio. */
img.is-missing { display: none; }

/* An empty .frame with nothing behind a hidden image reads as broken, not
   placeholder — a bare bordered rectangle with no content at all. This
   gives it an actual designed empty state instead: a fixed height so it
   doesn't collapse to 20px of padding, a faint diagonal hatch instead of
   dead white space, and a small mono label so it's unmistakably "photo not
   in yet" rather than "something failed to load". */
.frame:has(img.is-missing) {
  min-height: 220px;
  display: flex;
  align-items: center;
  justify-content: center;
  background-image: repeating-linear-gradient(
    135deg,
    transparent, transparent 9px,
    var(--border) 9px, var(--border) 10px
  );
  position: relative;
}
.frame:has(img.is-missing)::after {
  content: "Photography pending";
  font-family: var(--font-mono);
  font-size: 0.9rem;
  letter-spacing: 0;
  color: var(--uk-clay);
  background: var(--uk-bone);
  padding: .5rem 1rem;
  border: 1px solid var(--border-strong);
}

/* Read by screen readers, invisible on screen. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Frame on a dark ground: bone mount and brown border both disappear, so the
   border steps up to clay and the mount to a slightly lifted umber. */
.frame--on-dark {
  border-color: var(--uk-clay);
  background: var(--surface-dark-2);
}


/* Mono link with a trailing arrow that steps right on hover. */
.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: .01em;
  color: var(--ochre-on-light);
}
.link-arrow-mark {
  transition: transform var(--dur-fast) var(--ease-out);
}
@media (hover: hover) {
  .link-arrow:hover .link-arrow-mark,
  .s3-card:hover .link-arrow-mark { transform: translateX(4px); }
}

.btn {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.8125rem;
  letter-spacing: .01em;
  padding: 1rem 2rem;
  border-radius: var(--radius);
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  transition: background var(--dur-fast) ease, color var(--dur-fast) ease, border-color var(--dur-fast) ease;
}
.btn--primary {
  background: var(--uk-ochre);
  color: #2A1A06;
  border: 1px solid var(--uk-ochre);
}
.btn--primary:hover {
  background: #B0700F;
  border-color: #B0700F;
}
.btn--ghost {
  background: transparent;
  color: var(--uk-brown);
  border: 1px solid var(--border-strong);
}
.btn--ghost:hover {
  border-color: var(--uk-brown);
  background: rgba(92,58,33,.05);
}
.btn--on-dark {
  color: var(--text-on-dark);
  border-color: rgba(239,231,218,.35);
}

/* ---------- Reduced motion ---------- */
@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; }
}

/* UrbanKriti — scroll film styles.
   Append to css/styles.css AFTER the design tokens.
   Depends on: --uk-umber, --uk-ochre, --uk-clay, --text-on-dark,
   --font-display, --font-mono, --fs-hero. */

/* ── track + stage ─────────────────────────────────────── */

/* One screen, not a 720vh track. The height used to be ~120vh per chapter
   × 6 because scroll position drove the frame index; nothing is driven by
   scroll any more, so the section is exactly as tall as the hero it holds.
   svh, not dvh: a phone's collapsing address bar must not resize the hero
   mid-scroll and shift the headline under the reader. */
#track {
  position: relative;
  background: var(--uk-umber);
}

#stage {
  position: relative;
  height: 100vh;
  height: 100svh;
  min-height: 34rem;
  max-height: 60rem;
  overflow: hidden;
  background: var(--uk-umber);
  display: grid;
  align-items: end;
  justify-items: center;
  /* The two buttons are the only page content on this screen, and they sit
     low. The film's own burned-in titling runs across the middle of the
     frame for almost its whole length, so anything centred here would land
     on top of it. */
  padding: clamp(6rem, 13vh, 10rem) 0 clamp(3rem, 9vh, 6rem);
}

/* The hero film. Now the client's mp4 rather than a scrubbed canvas, so it
   needs the cover crop the canvas used to do in JS. object-position sits
   slightly below centre: the footage's horizon reads better there, and it
   keeps the frame's own subject clear of the centred captions. The umber
   ground shows through until the poster paints, so the stage never flashes
   white. */
#film {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  object-position: 50% 55%;
  background: var(--uk-umber);
  pointer-events: none;
  /* The canvas draw used to apply brightness(1.22) saturate(1.1) to every
     frame — that is what made the film read as lit rather than as dusk, and
     it went when the canvas went. This puts it back, a little gentler,
     because the client's footage is already better exposed than the
     generated frames were. */
  filter: brightness(1.16) saturate(1.06);
}

/* Scrim. Warm umber over the film, and as little of it as the job needs.

   What this used to protect was an HTML headline sitting across the middle
   of the frame; it was carrying almost half a stop of density to keep cream
   type off bright footage. That headline is now visually hidden — the film
   carries its own titling, already treated by whoever cut it — so the centre
   drops from .46 to .20 and the upper mid-band from .12 to .05. The film is
   substantially brighter, which is the point.

   What is left is doing two specific jobs, not a general dimming: the top
   band (.46) keeps the fixed header's cream nav legible over whatever the
   film is showing behind it, and the bottom band (.62) both seats the two
   buttons and carries the hero into the section below. The very soft radial
   is a vignette, not a scrim — it stops the corners glaring.

   The top band gets its own second stop (.62 at the very top, still .44 at
   12%, clear by 30%). One frame of the film is a full-bleed floor plan in
   near-white line work, and the header's cream nav sat straight on top of
   it. The band has to cover the header's height specifically, not fade out
   of it — hence the extra stop rather than a steeper single ramp. */
#film-vignette {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background:
    radial-gradient(70% 66% at 50% 50%, rgba(30,18,14,.20) 0%, rgba(30,18,14,.13) 55%, rgba(30,18,14,.06) 100%),
    linear-gradient(180deg, rgba(63,36,34,.62) 0%, rgba(63,36,34,.44) 12%, rgba(63,36,34,.06) 30%, rgba(63,36,34,.14) 62%, rgba(63,36,34,.62) 100%);
}

/* ── the hero's content ────────────────────────────────────

   Almost nothing, now. What used to live here was the caption system:
   .caption / .cap-center / .cap-top / .cap-bottom absolutely positioned and
   opacity-0 by default, with film.js writing opacity and a translate onto
   each one every frame against scroll progress; plus .film-line for the four
   chapter verbs, .film-close, .film-eyebrow, #scroll-cue and the
   #film-loader panel.

   Then, briefly, a .hero-static__title — a real HTML headline at
   clamp(2.9rem, 8.8vw, 8.7rem) with a soft ::before plate under it to hold
   its contrast off the footage. That went too, and for a better reason than
   the rest: the client's film already burns the same headline into the
   picture, so ours was a second copy of the same words in a second typeface.
   The h1 is still in the markup, carrying the wording for search engines and
   screen readers, but it is .visually-hidden.

   What is left on the screen is two buttons. */

.hero-static {
  position: relative;
  z-index: 2;
  width: min(96vw, 1500px);
  padding-inline: clamp(1rem, 2.5vw, 2rem);
  text-align: center;
}

.hero-static__actions {
  display: flex;
  gap: 1rem;
  justify-content: center;
  flex-wrap: wrap;
}

/* ── header ────────────────────────────────────────────── */

/* Fixed, so it stays pinned for the whole page. It was absolute and scrolled
   away with the film; the nav is the only way around a single page this long,
   and having it available exactly once was the problem.

   Still out of flow, which is what the absolute version was really buying:
   it must not push the 720vh film track down by its own height and open a
   gap above the canvas.

   Two states, and the second one is not optional. Over the film the bar is
   transparent and the cream type sits on a dark frame. Past it the page runs
   bone, sandstone, umber, bone — cream type on nothing would vanish on four
   of those. So .is-stuck brings a near-opaque umber bar with it, and the
   header shrinks at the same time: 170px is a title sequence, not something
   to give up permanently at the top of every viewport.

   js/main.js sets the class off scroll position, not off .past-film — the
   film reports that flag at 98.5% of a 720vh track, thousands of pixels after
   the bar needs a background.

   This block previously read:

     body:not(.past-film) .site-header { opacity: 0; pointer-events: none; }

   — nothing but the film while the film played. The header is visible from
   the first frame now. That one line restores the original behaviour if the
   film should ever own the screen alone again. */

.site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  /* Under the film loader, which sits at 200. */
  z-index: 100;

  /* Two variables drive every colour in the bar, and the theme classes below
     do nothing except reassign them. The alternative — re-stating the colour
     on the wordmark, the links, the caret, the burger and the rule in each
     theme — is five chances to miss one, and the one you miss is the one
     that goes invisible.

     --hdr-ink   the type, and via currentColor the caret and burger bars
     --hdr-bar   the bar fill, already including its own alpha
     --hdr-edge  the hairline under it */
  --hdr-ink:  var(--text-on-dark); --hdr-mark-dark: 0;
  --hdr-bar:  rgba(63, 36, 34, 0);
  --hdr-edge: rgba(239, 231, 218, 0);

  /* ---- Header-01 reskin ----
     https://21st.dev/@designali-in/components/header-01

     In the reference the <nav> is the full-width fixed shell and carries NO
     fill of its own; the bar you see is the INNER container, which is
     `mx-auto mt-2` and — only once scrolled — gains
     `max-w-5xl rounded-2xl border backdrop-blur-xl px-3` plus a 50% fill.
     So the shell is transparent here and --hdr-bar / --hdr-edge are consumed
     by .hdr-inner instead. The variables themselves are untouched: the
     .is-stuck / .hdr-on-dark / .hdr-on-light theme switching that
     main.js drives is exactly as it was. */
  background: transparent;
  padding-inline: 0.75rem;             /* px-3 */

  /* Faster than --dur-base. The bar crosses a section boundary in a few
     hundred milliseconds and the ink has to change with it; a slow crossfade
     spends that time somewhere between the two themes, which is exactly
     where contrast is worst. Short enough not to be caught, long enough not
     to read as a flicker. */
  transition: color .32s var(--ease-out);
}

@media (min-width: 768px) {
  .site-header { padding-inline: 1rem; }   /* md:px-4 */
}

/* The blur is what buys the transparency. At .94 opaque the bar was reading
   as a solid slab bolted over the page; the blur lets it sit at .62–.68 and
   still hold type, because what shows through is a wash of the section rather
   than its detail. Progressive: a browser without backdrop-filter falls back
   to the flat alpha, which is lighter than ideal but still legible.

   saturate() before blur keeps the warm ground warm — blurring alone greys
   a palette out.

   Header-01 puts the blur on the inner card (`backdrop-blur-xl`), so that is
   where it lives now.

   POLISH PASS — two fixes here.

   1. This rule was dead. The reskin pass left a stray `*​/` above it, which
      closed the comment early and turned the following prose into a selector
      that swallowed the whole block. The bar has had NO backdrop blur since;
      what looked like glass was only the flat alpha. Comment repaired.

   2. The bar was reading too thin — page text and images stayed legible
      through it as content scrolled under. Blur raised to 24px and the fill
      alphas below raised to ~.92, which puts the glass around 90% effective.
      Still a wash of the section rather than a flat slab. */
.site-header.is-stuck .hdr-inner {
  backdrop-filter: blur(24px) saturate(1.15);
  -webkit-backdrop-filter: blur(24px) saturate(1.15);
  box-shadow: 0 6px 24px -12px rgba(63, 36, 34, .35);
}

/* The shell itself frosts too, once stuck. Header-01's card floats on `mt-2`
   inside a `px-3` gutter, so there is always a strip above it and a margin
   either side — and page content scrolling under those strips stayed sharp
   and readable, which is the "content visible outside the header bar"
   problem. Frosting the shell covers the gutter without touching the card's
   shape: the crisp rounded bar still sits on top, and the few pixels around
   it are washed rather than clear. No fill of its own beyond a faint tint, so
   the theme colour still comes from the card. */
.site-header.is-stuck {
  backdrop-filter: blur(14px) saturate(1.1);
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
  background: color-mix(in srgb, var(--hdr-bar) 55%, transparent);
}

/* ---- the two themes ----

   js/main.js reads which section is under the bar and sets one of these. The
   dark one is the default, and is also what the header wears over the film.

   Contrast, worst case, measured against the section showing through:

     on-dark   cream #EFE7DA over umber at .62 on top of bone (the worst
               a dark bar can sit on, mid-transition) → 5.9:1
     on-light  umber #3F2422 over bone at .68 on top of umber (same, the
               other way round) → 6.4:1

   Both clear the 4.5 floor with the section at its most hostile, which is
   the only figure worth quoting — settled over its own section each is far
   higher.

   POLISH PASS: .62 / .68 let the section behind stay readable through the
   bar. Raised to .92 / .93 — with the gradient on .hdr-inner easing the
   bottom edge to ~.87, the glass now sits around 90% effective. Contrast can
   only have improved: each theme is now closer to its own settled colour than
   the figures above, which were measured at the old, thinner alphas. The
   edges come up with them so the bar still has a drawn boundary at 92% fill. */
.site-header.is-stuck.hdr-on-dark {
  --hdr-bar:  rgba(63, 36, 34, .92);
  --hdr-edge: rgba(239, 231, 218, .16);
  --hdr-ink:  var(--text-on-dark); --hdr-mark-dark: 0;
}

.site-header.is-stuck.hdr-on-light {
  --hdr-bar:  rgba(250, 247, 242, .93);
  --hdr-edge: rgba(92, 58, 33, .18);
  --hdr-ink:  var(--uk-umber); --hdr-mark-dark: 1;
}

/* Not stuck — transparent over the film — but the ink still has to be told
   what it is, because the film's last frames are not reliably dark and the
   hero sits over a photograph either way. Dark ink over the film would be
   unreadable, so the top of the page is always cream regardless of theme. */
.site-header:not(.is-stuck) { --hdr-ink: var(--text-on-dark); --hdr-mark-dark: 0; }

/* Header-01's inner container: `mx-auto mt-2`, and the row inside it
   `relative flex items-center justify-between gap-3 py-3`. This element is
   now what paints the bar — see the note on .site-header above. */
.hdr-inner {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: space-between;      /* justify-between */
  gap: 0.75rem;                        /* gap-3 */
  margin-top: var(--hdr-gap-top);      /* mt-2 */
  margin-inline: auto;                 /* mx-auto */
  min-height: var(--hdr-h);
  padding-inline: var(--pad-page);
  /* POLISH PASS: a gradient rather than a flat fill, so the bar's bottom edge
     eases off instead of stopping dead. --hdr-bar already carries its own
     alpha, so these percentages multiply into it — at the .92/.93 the themes
     now set, that reads as ~.92 at the top easing to ~.87 at the bottom.
     Un-stuck the variable is fully transparent, so the gradient is too. */
  background: linear-gradient(
    180deg,
    color-mix(in srgb, var(--hdr-bar) 100%, transparent),
    color-mix(in srgb, var(--hdr-bar) 94%, transparent)
  );
  border: 1px solid transparent;
  border-radius: 0;
  transition:
    min-height var(--dur-base) var(--ease-out),
    max-width .32s var(--ease-out),
    background-color .32s var(--ease-out),
    border-color .32s var(--ease-out),
    border-radius .32s var(--ease-out),
    padding .32s var(--ease-out),
    backdrop-filter .32s var(--ease-out);
}

/* Scrolled: the reference collapses the inner container into a floating,
   centred, rounded card — `max-w-5xl rounded-2xl border backdrop-blur-xl px-3`.

   POLISH PASS: max-width dropped. The reference's `max-w-5xl` made the glass a
   centred pill with the page showing either side of it, so content scrolling
   past was visible outside the bar. The card now spans the shell, which is the
   viewport minus its own 12/16px gutter — full-width glass, Header-01's
   rounded corners and mt-2 float kept. The inline padding grows with it so the
   logo and nav are not thrown against the edges at 12px. */
.site-header.is-stuck .hdr-inner {
  max-width: none;
  border-radius: var(--hdr-radius);    /* rounded-2xl */
  border-color: var(--hdr-edge);
  padding-inline: clamp(1rem, 3vw, 2.25rem);
}

/* The shrink, recalculated for a stacked lockup. Side by side the bar only
   had to clear the taller of the two; stacked it has to clear both plus the
   gap, so the mark comes down further than it used to.

   At 32px wide the PNG renders 55.5px tall, and a 47px window over the top of
   it shows 84.7% — inside the 83.6–86.6% band the crop note below defines,
   so the mark is complete and cut in the gap above the type. Stack height is
   then 47 + 6 gap + ~25 of wordmark = 78px inside an 88px bar.

   Both numbers move together. Change one and the crop lands on the PNG's own
   wordmark, and the header shows the name twice. */
.site-header.is-stuck .hdr-inner { min-height: 88px; }
.site-header.is-stuck .hdr-mark { width: 29px; height: 42px; }
.site-header.is-stuck .hdr-word { font-size: 1.05rem; }
.site-header.is-stuck .hdr-logo { gap: .3rem; }

/* Landscape phones (mobile pass, 2026-10-01). A 136px bar on a 390px-tall
   screen took over a third of it before anything was scrolled. Short and
   wide, the bar starts at its stuck size instead: the same lockup, the same
   88px bar, from the top. --hdr-h follows, so every band and hero that
   clears the header with it (Contact, About, Why) closes up too. */
@media (orientation: landscape) and (max-height: 500px) {
  :root { --hdr-h: 88px; }
  .site-header .hdr-mark { width: 29px; height: 42px; }
  .site-header .hdr-word { font-size: 1.05rem; --hdr-word-h: 15px; }
  .site-header .hdr-logo { gap: .3rem; }
  .site-header.is-open .hdr-drawer,
  .site-header.is-open.is-stuck .hdr-drawer { top: 88px; }
}

/* ---- .hdr-rule is gone ----
   It was a full-width hairline drawn under the bar as its own element. It
   read as a second underline sitting beside the nav's own active marker, and
   it is one of the artifacts this pass was asked to remove. Header-01 carries
   no full-width rule in its scrolled state either — the edge is the floating
   card's own border, which .hdr-inner now draws from --hdr-edge. The element
   has been removed from both header renderers; no JS referenced it. */

/* ── the load-in ───────────────────────────────────────────
   Three beats over 1.5s, and the silence between the first two is the point.

     0.00 → 0.30   the mark fades up, alone
     0.30 → 0.60   held. Nothing else on screen but the mark
     0.60 → 1.00   the name settles open beside it
     1.10 → 1.50   the rule draws in from the right; the nav arrives with it

   The name is a letter-spacing settle rather than a slide: tracking is the
   wordmark's defining property, so opening it from .06em to its final .16em
   under a fade reads as the type composing itself rather than as an object
   being moved into place. It is also the only one of the three candidates
   that does not fight the mark sitting still beside it.

   The nav is on the last beat, not the second. The brief's pause only works
   if the mark really is alone during it, and seven nav items arriving early
   would be the loudest thing on screen through the whole hold.

   backwards fill is what makes the staging safe: before its delay elapses an
   element takes the from-state, and after the animation it simply is what it
   is. No class to add and no flash of a later beat before a script gets to
   it. Under reduced motion the global animation:none leaves everything in its
   natural state — visible — rather than stranded at opacity 0. */

/* POLISH PASS: the staged entrance held the wordmark at opacity 0 for .6s and
   the nav for 1.1s. With `backwards` fill that is a real blank interval, not a
   perceptual one — anything landing on the page inside it, a screenshot
   included, saw an empty bar. Delays cut to .06s and .12s: the beat survives,
   the blank does not. */
.hdr-mark { animation: hdr-mark-in .3s var(--ease-out) backwards; }
.hdr-word { animation: hdr-word-in .4s var(--ease-out) .06s backwards; }

.hdr-nav,
.hdr-actions,
.hdr-burger { animation: hdr-nav-in .4s var(--ease-out) .12s backwards; }

@keyframes hdr-mark-in { from { opacity: 0; } }
/* The letter-spacing settle only means anything on the type fallback — the
   wordmark is artwork now, and artwork has no tracking to loosen. The 2px
   rise is what the beat reads as in both cases; the tracking stays for the
   fallback, where it is still the nicer of the two. */
@keyframes hdr-word-in { from { opacity: 0; letter-spacing: .06em; transform: translateY(2px); } }
@keyframes hdr-nav-in  { from { opacity: 0; } }

/* Held while the film loader is still up, so a three-second reveal is not
   spent behind an opaque panel. js/main.js sets this class immediately and
   drops it when the loader reports ready — or after four seconds regardless,
   so a stalled film cannot strand the header.

   The default is running, not paused, so with JavaScript off the sequence
   still plays and the header still ends up visible. */
body.hdr-hold .hdr-mark,
body.hdr-hold .hdr-word,
body.hdr-hold .hdr-nav,
body.hdr-hold .hdr-actions,
body.hdr-hold .hdr-burger { animation-play-state: paused; }

/* ── branding ──────────────────────────────────────────── */

/* Mark above, name below — the arrangement of the logo artwork itself, which
   is a stacked lockup. The two halves are still separate elements (that is
   what makes the staged reveal possible at all); they are simply stacked the
   way the PNG stacks them instead of set side by side.

   Centred, not left-aligned: the name is about twice the width of the mark,
   and the artwork centres the two on a shared axis. Flush left would put the
   mark over the U and leave the rest of the name hanging off it. */
.hdr-logo {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: .4rem;
  flex: 0 0 auto;
}

/* The PNG is a stacked lockup — mark above, wordmark below. The mark's frame
   ends at about 84% of the 924px height. At 51px wide the whole image renders
   88.4px tall, so the 76px window over the top shows 86.0% of it: the mark
   complete, cut in the gap above the type. Change one of these two numbers
   and the crop lands on the wordmark, so they move together — the safe band
   is 83.6% to 86.6%. */
/* LOGO SVG (Projects album pass): the mark is now the client's own vector
   artwork (now urbankriti-mark.svg / -light.svg) — mark only, no wordmark and no
   background, so there is nothing to crop. The window is simply the mark's
   own 462:516 box, sized to sit inside the same --hdr-mark-h band. */
/* AUTHENTIC LOGO PASS: the mark is now the brand artwork itself (frame only,
   361:522, from the client PSD/.ai) — the stock file's out-of-frame petals
   are gone, so the box is narrower but the frame renders at the same 67px
   height as before and the bar does not move. Two copies of the mark sit on
   top of each other: white linework for the dark bar / over the film, the
   client's brown linework for the light bar. --hdr-mark-dark is set beside
   every --hdr-ink assignment, so the mark follows the ink exactly. */
.hdr-mark {
  position: relative;
  display: block;
  width: 47px;
  height: 67px;
  overflow: visible;
  flex: 0 0 auto;
  transition:
    width var(--dur-base) var(--ease-out),
    height var(--dur-base) var(--ease-out);
}
.hdr-mark img {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  transition: opacity .32s var(--ease-out);
}
.hdr-mark-img--light { opacity: calc(1 - var(--hdr-mark-dark)); }
.hdr-mark-img--dark  { opacity: var(--hdr-mark-dark); }

/* The wordmark: "UrbanKriti", one line, sitting under the mark.

   Two layers to this block. What follows immediately is the TYPE fallback —
   the name set in --font-logo, which is what renders if CSS masks are not
   available. The @supports block after it is the real thing: the logo's own
   lettering, cut out of the PNG, painted as a mask. Everything the fallback
   sets (size, tracking, colour) is overridden there, so the two only have to
   agree on one number — the wordmark ends up ~112px wide either way, and the
   bar does not reflow between them.

   The .06em start of the load animation resolves against whatever the
   letter-spacing below is, so the settle keeps working if it is retuned. */
.hdr-word {
  display: flex;
  flex-direction: row;
  align-items: baseline;
  font-family: var(--font-logo);
  /* Larger than the 1.15rem the capitals ran at. Mixed case spends most of
     its height on lowercase, so matching the old cap-height means going up.
     Under the mark rather than beside it there is also width to spend — the
     lockup is now as wide as the name, not as wide as name plus mark. */
  font-size: 1.45rem;
  /* 400, not the 500 this ran at. Questrial ships one weight and the logo's
     own strokes are lighter than a regular, so anything above 400 is the
     browser faking a weight the logotype does not have. */
  font-weight: 400;
  /* Near-normal, with a touch of air. The .2em the capitals needed was doing
     the work caps always need doing; lowercase set that loose falls apart
     into separate letters. .03em is the logo's own spacing — open enough to
     breathe under the mark, nowhere near tracked. */
  letter-spacing: .03em;
  line-height: 1.2;
  white-space: nowrap;
  color: var(--hdr-ink);
  transition:
    font-size var(--dur-base) var(--ease-out),
    color .32s var(--ease-out);
}

/* One weight across both halves. The split used to carry a 300/600 break to
   stop a row of tracked capitals reading as a flat block — a problem mixed
   case does not have, and a break the logo itself does not make. The spans
   stay because the load animation targets the wordmark as a unit and the
   capital K is still the seam if anything ever needs to address it.

   No negative margin either: that was compensating for a .2em trailing gap
   after the last letter, and at .01em there is nothing left to take back. */
.hdr-word-1,
.hdr-word-2 { display: block; font-weight: 400; }

/* THE WORDMARK AS ARTWORK.

   assets/icons/wordmark.png is the logo's own "UrbanKriti", cut out of
   logo.png at its ink bounding box (x 5–518, y 808–899 of the 533×924
   original) and saved greyscale+alpha, because only the alpha is read. It is
   514×92, so it carries better than 2x for the ~112px it renders at.

   A mask rather than an <img> for one reason: the bar changes colour. Over
   the dark sections the ink is cream and over the light ones it is umber, and
   the wordmark in the PNG is near-black — as an image it would sink into the
   umber bar. Masked, the element is a rectangle of var(--hdr-ink) with the
   letters cut out of it, so it takes whatever colour the header is wearing
   and transitions between them exactly like the type it replaced did.

   ONE number drives the lockup: --hdr-word-h. Width is that height times the
   artwork's 514:92, so the two can never drift out of the PNG's proportions.
   20px matches the width the type it replaced occupied (~112px), which is why
   nothing around it had to move.

   The spans still hold the name for screen readers — a mask paints no text —
   so they are clipped rather than removed. */
@supports ((-webkit-mask-image: url("")) or (mask-image: url(""))) {
  .hdr-word {
    --hdr-word-h: 20px;
    display: block;
    position: relative;
    width: calc(var(--hdr-word-h) * 347.3 / 64.1);
    height: var(--hdr-word-h);
    background-color: var(--hdr-ink);
    /* Vector wordmark: the client's own outlined glyphs (Urbankirti Text.ai) — alpha only is read. */
    -webkit-mask: url(/assets/brand/urbankriti-wordmark.svg) center / contain no-repeat;
            mask: url(/assets/brand/urbankriti-wordmark.svg) center / contain no-repeat;
    transition:
      width var(--dur-base) var(--ease-out),
      height var(--dur-base) var(--ease-out),
      background-color .32s var(--ease-out);
  }

  /* Same shrink the type did, driven from the one number: 15px lands the
     wordmark at ~84px, which is where 1.05rem of the old face landed. */
  .site-header.is-stuck .hdr-word { --hdr-word-h: 15px; }

  .hdr-word-1,
  .hdr-word-2 {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }
}

/* ── nav ───────────────────────────────────────────────── */

/* Sits directly beside the branding. No margin-left: auto — that is what
   threw the nav to the far edge and opened the gap this was meant to close. */
.hdr-nav { display: none; }

/* Header-01's <NavigationMenuList>: `flex items-center justify-center
   space-x-1` — the items are chips that sit almost touching, not a widely
   tracked row. */
.hdr-menu {
  display: flex;
  align-items: center;
  gap: 0.25rem;                        /* space-x-1 */
}

.hdr-item { position: relative; }

/* Header-01's navigationMenuTriggerStyle():
     inline-flex h-9 w-max items-center justify-center rounded-md px-4 py-2
     text-sm font-medium transition-colors
     hover:bg-accent hover:text-accent-foreground
     focus:bg-accent data-[active]:bg-accent/50 data-[state=open]:bg-accent/50
   — a filled chip, and NO underline anywhere in the component. Dropping the
   ::after underline is what removes this pass's "duplicate underline" and
   "broken active underline" artifacts: the active page is now marked the way
   the reference marks it, with a soft filled chip. */
.hdr-link {
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: .35rem;
  height: 2.25rem;                     /* h-9 */
  padding: 0.5rem 1rem;                /* py-2 px-4 */
  border-radius: 0.375rem;             /* rounded-md */
  background: transparent;
  font-family: var(--font-body);
  font-size: 0.875rem;                 /* text-sm */
  font-weight: 500;                    /* font-medium */
  letter-spacing: .01em;
  line-height: 1;
  white-space: nowrap;
  /* currentColor carries this down to the caret as well. */
  color: var(--hdr-ink);
  transition:
    color .32s var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}

/* `bg-accent` mapped to the palette: a wash of the bar's own ink, so the chip
   works in both the on-dark and the on-light theme without a second colour. */
.hdr-link[aria-current],
.hdr-item--menu:focus-within > .hdr-link,
.hdr-toggle[aria-expanded="true"] {
  background: color-mix(in srgb, var(--hdr-ink) 14%, transparent);
}
.hdr-link:focus-visible {
  background: color-mix(in srgb, var(--hdr-ink) 14%, transparent);
}
@media (hover: hover) {
  .hdr-link:hover { background: color-mix(in srgb, var(--hdr-ink) 20%, transparent); }
}

.hdr-caret {
  width: 6px;
  height: 6px;
  border-right: 1px solid currentColor;
  border-bottom: 1px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform var(--dur-fast) var(--ease-out);
}
.hdr-item--menu:hover .hdr-caret,
.hdr-item--menu:focus-within .hdr-caret { transform: translateY(1px) rotate(225deg); }

/* ── dropdowns ─────────────────────────────────────────── */

/* A bone panel over the film: the links inside get one contrast figure to
   hold rather than whatever frame happens to be playing behind them. */
/* Header-01's <NavigationMenuViewport>:
     mt-1.5 rounded-md border bg-popover shadow overflow-hidden
   and its content padding `p-2`. */
.hdr-sub {
  position: absolute;
  top: 100%;
  left: 50%;
  transform: translateX(-50%);
  min-width: 15rem;
  margin-top: 0.375rem;                /* mt-1.5 */
  padding: .5rem;                      /* p-2 */
  background: var(--uk-bone);
  border: 1px solid var(--border);
  border-radius: 0.375rem;             /* rounded-md */
  box-shadow: 0 4px 12px -2px rgba(63, 36, 34, .14);
  overflow: hidden;
  opacity: 0;
  visibility: hidden;
  transform: translateX(-50%) translateY(6px);
  transition: opacity var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out),
              visibility var(--dur-fast);
}

/* Hover and focus-within, so a pointer and a Tab key reach it the same way.
   The JS only keeps aria-expanded honest and handles Escape — the panel
   itself never depends on a script. */
.hdr-item--menu:hover > .hdr-sub,
.hdr-item--menu:focus-within > .hdr-sub {
  opacity: 1;
  visibility: visible;
  transform: translateX(-50%);
}

/* Bridges the gap between trigger and panel, which would drop the hover. */
.hdr-item--menu::after {
  content: "";
  position: absolute;
  top: 100%;
  left: 0;
  right: 0;
  height: 0.375rem;
}

.hdr-sub a {
  display: block;
  padding: .6rem .75rem;
  font-family: var(--font-body);
  font-size: 0.9375rem;
  color: var(--uk-brown);
  border-radius: var(--radius);
  transition: color var(--dur-fast) ease, background-color var(--dur-fast) ease;
}
.hdr-sub a:hover,
.hdr-sub a:focus-visible {
  color: var(--ochre-on-light);
  background: rgba(92, 58, 33, .05);
}
/* The Services subpage you are on (client, 2026-10-02): the same glass chip
   the active top-level item wears, in the panel's own ink, plus a copper
   tick so it reads as "you are here". */
.hdr-sub a { white-space: nowrap; }
.hdr-sub a[aria-current="page"] {
  position: relative;
  font-weight: 500;
  color: var(--uk-umber);
  background: color-mix(in srgb, var(--uk-brown) 12%, transparent);
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
}
.hdr-sub a[aria-current="page"]::before {
  content: "";
  position: absolute;
  left: .3rem;
  top: 50%;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--uk-ochre);
  transform: translateY(-50%);
}
/* mobile drawer: the current subpage reads as selected too */
.hdr-acc-panel a[aria-current="page"] {
  color: var(--text-on-dark);
  font-weight: 600;
  padding-inline: .6rem;
  border-radius: var(--radius);
  background: color-mix(in srgb, var(--text-on-dark) 12%, transparent);
}

/* ── burger ────────────────────────────────────────────── */

/* 44px square: this is the tap target, the bars inside are what you see. */
.hdr-burger {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin-left: auto;
  margin-right: -10px;   /* optically aligns the bars to the page gutter */
  /* The three bars are currentColor, so this one line themes the whole
     button. */
  color: var(--hdr-ink);
  transition: color .32s var(--ease-out);
}

.hdr-burger-mark,
.hdr-burger-mark::before,
.hdr-burger-mark::after {
  display: block;
  width: 22px;
  height: 1px;
  background: currentColor;
  transition: transform var(--dur-fast) var(--ease-out);
}
.hdr-burger-mark { position: relative; }
.hdr-burger-mark::before,
.hdr-burger-mark::after {
  content: "";
  position: absolute;
  left: 0;
}
.hdr-burger-mark::before { top: -6px; }
.hdr-burger-mark::after  { top: 6px; }

.hdr-burger[aria-expanded="true"] .hdr-burger-mark { background: transparent; }
.hdr-burger[aria-expanded="true"] .hdr-burger-mark::before { transform: translateY(6px) rotate(45deg); }
.hdr-burger[aria-expanded="true"] .hdr-burger-mark::after  { transform: translateY(-6px) rotate(-45deg); }

/* ── mobile drawer ─────────────────────────────────────── */

/* The one place the header is pinned, and only while the menu is a modal:
   the drawer is a fixed overlay, so the bar carrying its close button has to
   be fixed with it or the control scrolls out from under the reader's thumb.
   It returns to absolute the moment the menu closes. */
/* POLISH PASS: while the drawer is open the bar and the drawer have to read as
   one full-screen surface. The floating card's mt-2 float, its rounded corners
   and the shell's own 12/16px gutter all left slivers of the page showing
   around the top of the menu, which is what made it feel cut and narrow. Open,
   the bar goes full-bleed and the drawer starts flush under it. */
.site-header.is-open { position: fixed; padding-inline: 0; }
.site-header.is-open .hdr-inner {
  /* Snap, don't crossfade. Radius, border and background are in .hdr-inner's
     transition list for the scroll state change, which would make the bar
     take .32s to square off after the menu is already open — the corners and
     the page's colour visibly linger behind a modal that should feel instant. */
  transition: none;
  margin-top: 0;
  border-radius: 0;
  border-color: transparent;
  background: var(--uk-umber);
  box-shadow: none;
}
.site-header.is-open .hdr-drawer { top: var(--hdr-h); }

/* Opening the menu after scrolling used to break it. The stuck shell carries
   a backdrop-filter (the frosted gutter, above), and a filtered ancestor
   becomes the containing block for position: fixed descendants — so the
   drawer was laid out inside the ~96px header instead of the viewport, and
   only a sliver of it showed. Open, the bar is an opaque umber slab anyway,
   so the frost has nothing to do and comes off. The drawer also starts under
   the bar's STUCK height, not the 136px of the top-of-page bar, or a 48px
   strip of page showed between the two. */
.site-header.is-open,
.site-header.is-open .hdr-inner {
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
}
.site-header.is-open.is-stuck .hdr-drawer { top: 88px; }

/* Open, the bar is always umber, so its ink is always cream. Without this
   the theme under the bar still decided: over a light section (or the
   Services pages' light heroes, see services.shared.css) the ink was umber
   on umber and the close button and wordmark vanished. !important because
   three theme rules of higher specificity, one in a later stylesheet, set
   these same two variables. */
.site-header.is-open {
  --hdr-ink: var(--text-on-dark) !important;
  --hdr-mark-dark: 0 !important;
}

.hdr-drawer {
  position: fixed;
  /* Clears the bar plus Header-01's `mt-2` offset on the inner container. */
  top: calc(var(--hdr-h) + var(--hdr-gap-top));
  left: 0;
  right: 0;
  bottom: 0;
  /* Full-bleed and scrollable in its own right: top/bottom pinning already
     gives it exactly the viewport below the bar, and dvh-correct on mobile
     because `bottom: 0` follows the dynamic viewport as the URL bar moves.
     overscroll-behavior stops a flick at the end of the list from scrolling
     the page underneath. */
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  /* Bottom padding was 4rem of dead space under the last link. The drawer
     now ends on its own action block (.hdr-drawer-foot), so it only needs
     to clear the home indicator. */
  padding: clamp(1.5rem, 6vw, 3rem) var(--pad-page) max(1.5rem, env(safe-area-inset-bottom));
  /* Opaque, not translucent: nothing behind the menu should be readable. */
  background: var(--uk-umber);
  color: var(--text-on-dark);
}
.hdr-drawer[hidden] { display: none; }

/* Opening (mobile pass, 2026-10-01): the drawer used to snap in. It now
   fades up over .28s while the links rise 10px into place one after
   another, the foot last; transform and opacity only, CSS only, so it plays
   from main.js's plain `hidden` toggle with nothing to coordinate. Closing
   stays instant: the tap that closes it is usually a navigation. */
@media (prefers-reduced-motion: no-preference) {
  .hdr-drawer:not([hidden]) { animation: hdr-drawer-in .28s var(--ease-out) both; }
  .hdr-drawer:not([hidden]) .hdr-drawer-nav > ul > li,
  .hdr-drawer:not([hidden]) .hdr-drawer-foot { animation: hdr-drawer-rise .5s var(--ease-out) both; }
  .hdr-drawer-nav > ul > li:nth-child(1) { animation-delay: .04s !important; }
  .hdr-drawer-nav > ul > li:nth-child(2) { animation-delay: .08s !important; }
  .hdr-drawer-nav > ul > li:nth-child(3) { animation-delay: .12s !important; }
  .hdr-drawer-nav > ul > li:nth-child(4) { animation-delay: .16s !important; }
  .hdr-drawer-nav > ul > li:nth-child(5) { animation-delay: .2s !important; }
  .hdr-drawer-nav > ul > li:nth-child(6) { animation-delay: .24s !important; }
  .hdr-drawer-foot { animation-delay: .28s !important; }
  /* the Services list opening inside it */
  .hdr-acc-panel:not([hidden]) { animation: hdr-drawer-rise .35s var(--ease-out) both; }
}
@keyframes hdr-drawer-in { from { opacity: 0; } }
@keyframes hdr-drawer-rise { from { opacity: 0; transform: translateY(10px); } }


.hdr-drawer-nav > ul > li + li { border-top: 1px solid var(--border-on-dark); }

.hdr-drawer-link {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  width: 100%;
  padding-block: 1.1rem;
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 6vw, 2rem);
  font-weight: 300;
  line-height: 1.2;
  text-align: left;
  color: var(--text-on-dark);
}

/* Short phones (an SE, a 568px Android, landscape): the six links close up
   so the foot with both phone numbers is on screen with less scrolling. */
@media (max-height: 700px) and (max-width: 1099px) {
  .hdr-drawer { padding-top: 1rem; }
  .hdr-drawer-link { padding-block: .7rem; }
}

.hdr-acc-toggle .hdr-caret {
  width: 9px;
  height: 9px;
  flex: 0 0 auto;
}
.hdr-acc-toggle[aria-expanded="true"] .hdr-caret { transform: translateY(2px) rotate(225deg); }

.hdr-acc-panel { padding: 0 0 1.25rem .25rem; }
.hdr-acc-panel[hidden] { display: none; }

.hdr-acc-panel a {
  display: block;
  padding-block: .55rem;
  font-size: var(--fs-body);
  color: var(--clay-on-dark);
  transition: color var(--dur-fast) ease;
}
.hdr-acc-panel a:hover,
.hdr-acc-panel a:focus-visible { color: var(--text-on-dark); }

/* The drawer has its own overflow-y, so the page behind it is the only thing
   frozen. js/main.js stops Lenis alongside this — overflow: hidden alone does
   not stop a smooth-scroll library. */
html.nav-open,
html.nav-open body { overflow: hidden; }

/* ── the desktop bar, from 1100px ──────────────────────────

   Was 900px. The bar carries the icon rail on the right as well as the logo
   and the nav (five items since 2026-10-01; it was seven), and between 900
   and 1099 the old seven-item nav sat crammed against the right edge. That
   range gets the tablet bar instead: logo, the icon rail, the burger.
   main.js's drawer matchMedia moved to 1100 with this.

   The nav is in flow now, not absolutely centred. Header-01 centred it on the
   row (`absolute inset-0 m-auto size-fit`); with a cluster on the right that
   centre could run into it at narrow desktop widths. In flow, .hdr-inner's
   `justify-between` gives logo | nav | actions equal gaps, which centres the
   nav between the two ends — balanced against what is actually beside it. */
@media (min-width: 1100px) {
  .hdr-nav { display: block; }
  .hdr-burger { display: none; }
  .hdr-drawer { display: none !important; }
}

/* ── utility cluster ───────────────────────────────────────

   The right-hand end of the bar: one slim rail holding email, Instagram and
   WhatsApp. The "Get a quote" pill that used to follow it was removed
   (client feedback, 2026-10-01) and the rail moved right into its place.
   The rail is built from the bar's own --hdr-ink, like everything else in
   the header, so it follows the dark/light theme switch and the Services
   light-hero override without a rule of its own.

     under 640px   hidden; the drawer carries it (.hdr-drawer-foot)
     640–1099px    the rail beside the burger
     1100px up     the rail at the right-hand end of the desktop bar

   Hover is kept to what a drawing office would do: the icon takes an ochre
   tint and a soft wash. No scale, no bounce. */

.hdr-actions {
  display: flex;
  align-items: center;
  gap: .5rem;
  flex: 0 0 auto;
  margin-left: auto;
}
.hdr-actions > .hdr-social { display: none; }

.hdr-social {
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 3px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--hdr-ink) 22%, transparent);
  background: color-mix(in srgb, var(--hdr-ink) 6%, transparent);
  /* A light frost so the icons hold over a busy hero photograph, where the
     bar itself is still transparent. */
  -webkit-backdrop-filter: blur(8px);
          backdrop-filter: blur(8px);
  transition:
    border-color .32s var(--ease-out),
    background-color .32s var(--ease-out);
}

.hdr-social .uk-social__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  color: var(--hdr-ink);
  transition:
    color var(--dur-fast) var(--ease-out),
    background-color var(--dur-fast) var(--ease-out);
}
.hdr-social svg { width: 17px; height: 17px; }

/* The tint is ochre mixed INTO the ink: lifted toward cream on the dark bar,
   deepened toward umber on the light one, so one rule clears the 3:1 graphics
   floor in both themes. */
.hdr-social a.uk-social__link:focus-visible {
  outline-offset: 1px;
  color: color-mix(in srgb, var(--uk-ochre) 72%, var(--hdr-ink));
}
@media (hover: hover) {
  .hdr-social:hover { border-color: color-mix(in srgb, var(--hdr-ink) 34%, transparent); }
  .hdr-social a.uk-social__link:hover {
    color: color-mix(in srgb, var(--uk-ochre) 72%, var(--hdr-ink));
    background: color-mix(in srgb, var(--hdr-ink) 12%, transparent);
  }
}

/* With the drawer open the drawer's own icon row is the way to reach the
   studio; the bar's copy would be the same rail twice, one above the other.
   Hidden, not removed, so the bar does not reflow under the close button. */
.site-header.is-open .hdr-actions { visibility: hidden; }

/* The burger follows the cluster at every width now that the call button
   is always in it (the script tag after the cluster is display: none, so
   the sibling combinator has to step over it). */
.hdr-actions ~ .hdr-burger { margin-left: .25rem; }

@media (min-width: 640px) {
  .hdr-actions > .hdr-social { display: flex; }
  .hdr-actions ~ .hdr-burger { margin-left: .5rem; }
}

/* Touch tablets get the rail too (640–1099 and up): 32px icons are a
   cursor's size, so a coarse pointer gets 38px ones in a 46px rail, the
   same height as the call disc beside it. */
@media (pointer: coarse) {
  .hdr-social .uk-social__link { width: 38px; height: 38px; }
}

/* ── the call button ───────────────────────────────────────
   (mobile pass, 2026-10-01; behaviour: shared/components/header/
   header-call.js). The one filled shape in the bar: the footer CTA's ochre
   with its dark ink (#2A1A06 on #C8811C, 5.5:1), so it reads as the
   action and never as a fourth social icon. A 44px tap box holding a 40px
   disc; the panel it opens is the Services dropdown's bone card, anchored
   under the disc on a desktop and across the bar on a phone. */
.hdr-call { position: relative; flex: 0 0 auto; }

.hdr-call-btn {
  position: relative;
  display: inline-grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: #2A1A06;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.hdr-call-btn::before {
  content: "";
  position: absolute;
  inset: 2px;
  border-radius: 50%;
  background: var(--uk-ochre);
  box-shadow: 0 0 0 1px rgb(42 26 6 / .12), 0 8px 18px -10px rgb(200 129 28 / .9);
  transition:
    transform .35s var(--ease-out),
    background-color var(--dur-fast) var(--ease-out),
    box-shadow .35s var(--ease-out);
}
.hdr-call-btn svg {
  position: relative;
  width: 19px;
  height: 19px;
  transition: transform .35s var(--ease-out);
}
@media (hover: hover) {
  .hdr-call-btn:hover::before {
    transform: scale(1.06);
    box-shadow: 0 0 0 1px rgb(42 26 6 / .12), 0 12px 22px -10px rgb(200 129 28 / .95);
  }
  .hdr-call-btn:hover svg { transform: rotate(-12deg); }
}
.hdr-call-btn:active::before { transform: scale(.94); }
.hdr-call-btn:focus-visible { outline-offset: 2px; }
/* Open: the disc darkens to the deep ochre and takes a ring in the bar's
   own ink, so the button shows what it is attached to. */
.hdr-call.is-open .hdr-call-btn::before {
  background: color-mix(in srgb, var(--uk-ochre) 82%, #2A1A06);
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--hdr-ink) 55%, transparent);
}
.hdr-call.is-open .hdr-call-btn { color: var(--uk-bone); }
/* Touch: the disc takes the whole 44px box, level with the 46px rail. */
@media (pointer: coarse) {
  .hdr-call-btn::before { inset: 0; }
  .hdr-call-btn svg { width: 20px; height: 20px; }
}

.hdr-call-panel {
  position: absolute;
  z-index: 2;
  top: calc(100% + .6rem);
  right: 0;
  width: max-content;
  min-width: 15.5rem;
  padding: .55rem;
  background: var(--uk-bone);
  border: 1px solid var(--border);
  border-radius: .75rem;
  box-shadow: 0 18px 40px -18px rgba(63, 36, 34, .45), 0 4px 12px -4px rgba(63, 36, 34, .14);
  color: var(--uk-umber);
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px) scale(.97);
  transform-origin: top right;
  transition:
    opacity .2s var(--ease-out),
    transform .28s var(--ease-out),
    visibility 0s linear .28s;
}
.hdr-call.is-open .hdr-call-panel {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition:
    opacity .2s var(--ease-out),
    transform .28s var(--ease-out),
    visibility 0s;
}
.hdr-call-title {
  padding: .45rem .65rem .5rem;
  font-family: var(--font-mono);
  font-size: 0.894rem;
  font-weight: 500;
  letter-spacing: 0;
  line-height: 1.2;
  color: var(--text-secondary);
}
.hdr-call-list { display: grid; gap: 2px; }
.hdr-call-num {
  display: flex;
  align-items: center;
  gap: .8rem;
  min-height: 52px;
  padding: .45rem .9rem .45rem .5rem;
  border-radius: .5rem;
  color: var(--uk-umber);
  transition: background-color var(--dur-fast) ease, color var(--dur-fast) ease;
}
.hdr-call-ico {
  display: inline-grid;
  place-items: center;
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--uk-ochre) 16%, transparent);
  color: var(--ochre-on-light);
  transition: background-color var(--dur-fast) ease, color var(--dur-fast) ease;
}
.hdr-call-ico svg { width: 17px; height: 17px; }
.hdr-call-txt { display: grid; line-height: 1.2; }
.hdr-call-verb {
  font-size: .75rem;
  font-weight: 500;
  color: var(--text-secondary);
}
.hdr-call-digits {
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: .01em;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
.hdr-call-num:hover,
.hdr-call-num:focus-visible { background: rgba(92, 58, 33, .06); }
.hdr-call-num:hover .hdr-call-ico,
.hdr-call-num:focus-visible .hdr-call-ico { background: var(--uk-ochre); color: #2A1A06; }
.hdr-call-num:active { background: rgba(92, 58, 33, .1); }

/* Phones: the panel drops from the bar's right edge, as wide as the bar
   allows, so a 320px screen never pushes it off the left. */
@media (max-width: 639px) {
  .hdr-call { position: static; }
  .hdr-call-panel {
    right: .5rem;
    width: auto;
    min-width: 0;
    max-width: calc(100% - 1rem);
    left: auto;
  }
}

@media (prefers-reduced-motion: reduce) {
  .hdr-call-btn::before,
  .hdr-call-btn svg,
  .hdr-call-panel { transition: none !important; }
  .hdr-call-panel { transform: none; }
}

@media (min-width: 1100px) {
  /* justify-between spaces the three; an auto margin here would pull the
     nav across to the logo. */
  .hdr-actions { margin-left: 0; }
}

/* ── the drawer's foot ─────────────────────────────────────
   The menu used to trail off after "Contact". It now ends on the icon row
   and the studio phone (the quote pill that led it was removed, 2026-10-01) — pushed to
   the bottom of the drawer when there is room (margin-top: auto in a column
   flex), so on a tall phone the nav reads first and the actions sit where a
   thumb already is. */
.hdr-drawer { display: flex; flex-direction: column; }
.hdr-drawer-nav { flex: 0 0 auto; }

.hdr-drawer-foot {
  display: grid;
  gap: .875rem;
  margin-top: auto;
  padding-top: 1.25rem;
}

/* A tablet drawer is tall enough that bottom-anchoring strands the actions
   a third of a screen below the list; there they follow it instead. */
@media (min-width: 640px) {
  .hdr-drawer-foot { margin-top: 0; padding-top: 2.5rem; }
}

.hdr-drawer-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: .75rem 1.25rem;
  padding-top: .875rem;
  border-top: 1px solid var(--border-on-dark);
}

.hdr-drawer-social {
  display: flex;
  gap: .5rem;
}
.hdr-drawer-social .uk-social__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid rgba(239, 231, 218, .2);
  color: var(--text-on-dark);
  transition:
    color var(--dur-fast) ease,
    border-color var(--dur-fast) ease;
}
.hdr-drawer-social svg { width: 18px; height: 18px; }
.hdr-drawer-social a.uk-social__link:hover,
.hdr-drawer-social a.uk-social__link:focus-visible {
  color: var(--ochre-on-dark);
  border-color: var(--ochre-on-dark);
}

/* Both studio numbers (mobile pass, 2026-10-01; it was the first one only),
   each a tel: link set as a slim ochre-ringed pill so it reads as "tap to
   call" and not as a printed line. They wrap under the icon row on a narrow
   phone and sit beside it where there is room. */
.hdr-drawer-contact {
  display: flex;
  flex-wrap: wrap;
  gap: .5rem;
  max-width: none;
  font-family: var(--font-mono);
  font-size: .8125rem;
  font-weight: 500;
  letter-spacing: .02em;
}
.hdr-drawer-contact a {
  display: inline-flex;
  align-items: center;
  gap: .5rem;
  min-height: 44px;
  padding: 0 .85rem 0 .7rem;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--uk-ochre) 55%, transparent);
  background: rgba(200, 129, 28, .08);
  color: var(--text-on-dark);
  white-space: nowrap;
  transition: color var(--dur-fast) ease, background-color var(--dur-fast) ease, border-color var(--dur-fast) ease;
}
.hdr-drawer-contact svg {
  flex: 0 0 auto;
  width: 17px;
  height: 17px;
  color: var(--ochre-on-dark);
}
.hdr-drawer-contact a:hover,
.hdr-drawer-contact a:focus-visible,
.hdr-drawer-contact a:active {
  border-color: var(--ochre-on-dark);
  background: rgba(200, 129, 28, .18);
}

/* ---- artifacts removed site-wide ----

   .uk-page-progress is a 2px fixed bar injected into <body> by
   src/shared/scripts/subpages.shared.js and painted with --pg-accent, which
   resolves to ochre or terracotta per page. Pinned to the top of the viewport
   at z-index 1001 — above the header at 100 — it read as a red/orange rule
   stuck to the header, and as a loading/progress indicator the site does not
   otherwise have. Hidden here rather than removed from the script so the
   injector, its scroll listener and the router's re-mount path are all left
   exactly as they were. */
.uk-page-progress { display: none !important; }

/* The duplicate #s1-fallback hero used to live here — a second full-screen
   hero (six generated Ken Burns stills, its own copy of the headline, a lead
   paragraph and four highlight phrases) that only ever rendered under
   body.no-film, i.e. for prefers-reduced-motion. With the scroll film gone
   there is nothing to fall back FROM: the hero is one static screen, and
   reduced motion is served by pausing the video on its poster
   (assets/film/film.js). The markup and this block went with it. */

/* ============================================================
   Highlights — the bridge between the hero film and About. Four
   short proof lines loop at 2.7s each (timeline in main.js
   initHighlights). Sandstone breaks up the umber run either side
   of it per the section-rhythm rule. Brown type on sandstone
   (7.4:1).
   ============================================================ */

/* Home + Insights pass: a compact "project pulse" rather than a second
   headline band. Shorter padding, a smaller line, a small crop from a real
   project record that crossfades with each line, and a thin segmented
   progress rule. The photograph atmosphere behind stays as it was. */
#highlights {
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--uk-sandstone);
  color: var(--text-primary);
  padding-block: clamp(1.75rem, 4.5vw, 2.75rem);
}

.highlights-inner {
  display: flex;
  align-items: center;
  gap: clamp(1rem, 3vw, 1.75rem);
  width: min(100% - 2rem, 640px);
}

.highlights-thumbs {
  position: relative;
  flex: none;
  width: clamp(4rem, 12vw, 5.5rem);
  aspect-ratio: 1;
  overflow: hidden;
  border: 1px solid rgba(92, 58, 33, .22);
  background: var(--uk-bone);
}
.highlights-thumb {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transform: scale(1.06);
  transition: opacity .7s var(--ease-out), transform 2.4s var(--ease-out);
}
.highlights-thumb:first-child,
.highlights-thumb.is-active { opacity: 1; transform: scale(1); }
.highlights-thumbs:has(.is-active) .highlights-thumb:not(.is-active) { opacity: 0; transform: scale(1.06); }

.highlights-text { flex: 1; min-width: 0; }

.highlights-label {
  margin: 0 0 .35rem;
  font-family: var(--font-mono);
  font-size: 0.884rem;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--uk-brown);
  opacity: .8;
}

/* Fixed-height window; each line is absolutely stacked inside it so the
   exiting and entering lines share the space mid-transition. Two lines tall
   on a phone, where the longest phrase wraps; one line from 768px. */
.highlights-rotator {
  position: relative;
  height: 2.5em;
  font-size: clamp(1.2rem, 3.4vw, 1.75rem);
  overflow: hidden;
}

/* js/main.js flips the inline transform/opacity each tick; the transition
   does the glide. */
.highlights-rotator-item {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1em;
  line-height: 1.2;
  letter-spacing: -.015em;
  color: var(--uk-brown);
  opacity: 0;
  transform: translateY(100%);
  will-change: transform, opacity;
  transition:
    transform .7s cubic-bezier(.4, 0, .2, 1),
    opacity .7s cubic-bezier(.4, 0, .2, 1);
}
.highlights-rotator-item:first-child {
  opacity: 1;
  transform: translateY(0);
}

.highlights-progress {
  display: flex;
  gap: .35rem;
  margin-top: .5rem;
  max-width: 12rem;
}
.highlights-seg {
  position: relative;
  flex: 1;
  height: 2px;
  overflow: hidden;
  background: rgba(92, 58, 33, .18);
}
.highlights-seg > span {
  position: absolute;
  inset: 0;
  background: var(--uk-brown);
  transform: scaleX(0);
  transform-origin: left;
}
.highlights-seg.is-done > span { transform: scaleX(1); }
#highlights.is-running .highlights-seg.is-active > span {
  animation: hl-fill var(--hl-interval, 2700ms) linear forwards;
}
@keyframes hl-fill { to { transform: scaleX(1); } }

@media (min-width: 768px) {
  .highlights-rotator { height: 1.5em; }
}

@media (prefers-reduced-motion: reduce) {
  .highlights-progress { display: none; }
}

/* ============================================================
   Section 2 — About. Deep umber, light type. Measured on
   #3F2422: cream #EFE7DA 11.5:1, bone 12.4:1, lifted clay
   5.2:1, ochre rule 4.4:1 (a 2px graphic, floor is 3:1).
   Plain --uk-clay is deliberately NOT used for type here —
   it lands at 4.28:1. See --clay-on-dark in the tokens.
   ============================================================ */

.s2 {
  background: var(--uk-umber);
  color: var(--text-on-dark);
  padding-block: var(--pad-section);
}

.s2-grid {
  display: grid;
  gap: clamp(2.5rem, 6vw, 3.5rem);
}

/* Mobile order: image first, then the copy. */
.s2-media { position: relative; margin: 0; }

/* Mobile: the detail image tucks under the portrait's right edge with a
   negative margin — no absolute positioning, so it can never overflow the
   page gutter and the figure still reserves its own height. */
.s2-media-detail {
  width: 58%;
  margin-left: auto;
  margin-top: -14%;
}

/* The holder carries the crop and the placeholder fill, so a missing
   photograph reads as a solid clay block at the right ratio rather than a
   collapsed box. 3:2 on mobile, portrait 4:5 from 900px. */
.s2-media-holder {
  position: relative;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  border-radius: var(--radius-img);
  background: var(--uk-clay);
}
.s2-media-holder--detail { aspect-ratio: 4 / 3; }

.s2-media-holder img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.s2-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

.s2-body h2 {
  margin-top: 1.5rem;
  font-weight: 300;
  color: var(--text-on-dark);
  max-width: 20ch;
}
/* Client (2026-10-01): "Design and build, planned together." on ONE line
   from tablets up. The column is a size container, so the heading scales
   down just enough to fit its own column (the line is ~15.5em long) and
   never grows past the site's h2 size. Phones keep the natural wrap. */
@media (min-width: 768px) {
  .s2-body { container-type: inline-size; }
  .s2-body h2 {
    max-width: none;
    white-space: nowrap;
    font-size: calc(min(clamp(2.4rem, 5.2vw, 3.8rem), 7.4cqi) * var(--k-display));
    line-height: 1;
  }
}

/* :not(.eyebrow) matters — the eyebrow is a direct child <p> too, and this
   rule would otherwise outrank .eyebrow--on-dark and paint it cream. */
.s2-body > p:not(.eyebrow) {
  margin-top: 1.5rem;
  color: var(--text-on-dark);
  max-width: 62ch;
}

/* Routes onward from the About band: the full Why argument and the studio.
   These replace the homepage Why sheet and Team band, which did the same
   job at far greater length. */
.s2-links { display: flex; flex-wrap: wrap; gap: 1rem 2rem; margin-top: 2.25rem; }
.s2-links .link-arrow { color: var(--ochre-on-dark); }

/* Ochre rather than terracotta — terracotta on umber is near-invisible. */
.s2-quote {
  margin-top: 2.5rem;
  padding-left: 1.5rem;
  border-left: 2px solid var(--uk-ochre);
}
.s2-quote p {
  font-family: var(--font-display);
  font-style: normal;
  font-weight: 400;
  font-size: clamp(1.45rem, 1.75vw, 1.7rem);
  line-height: 1.25;
  letter-spacing: -0.005em;
  color: var(--text-on-dark);
  max-width: 40ch;
}

@media (min-width: 900px) {
  .s2-grid {
    grid-template-columns: 5fr 7fr;
    gap: clamp(3rem, 6vw, 5rem);
    align-items: center;
  }
  .s2-media-holder { aspect-ratio: 4 / 5; }

  /* Desktop: the detail breaks out of the column into the gutter. Offsets are
     right/bottom rather than a transform, so the parallax below is free to own
     the transform property without fighting this rule. */
  .s2-media-detail {
    position: absolute;
    right: -10%;
    bottom: -8%;
    width: 56%;
    margin: 0;
  }
}

/* ============================================================
   Section 3 — Services. Sandstone, four bone cards, 2x2 from
   768px. Measured: umber heading 10.1:1 on sandstone, deepened
   taupe intro 4.9:1, card body 6.5:1 on bone, deepened clay
   marker 6.3:1, deepened ochre link 5.1:1.
   ============================================================ */

.s3 {
  background: var(--uk-sandstone);
  color: var(--text-primary);
  padding-block: var(--pad-section);
}

.s3-header { margin-bottom: clamp(2.5rem, 6vw, 4rem); }
/* One line, at every width. max-width: 20ch was breaking it after "Your", and
   nowrap on its own would simply have run it off a phone.

   So the size comes off --fs-h2 and onto a vw ramp that keeps the whole
   string inside the gutter instead: 29 characters of Manrope 300 measure
   about 15.1em, so at 5vw the line wants ~0.76 of the viewport, and the
   container gives it ~0.89 at 375px and more above that. The 3.25rem ceiling
   is --fs-h2's own, so nothing changes on a large screen — this only bites
   where the line would otherwise have had to wrap. */
.s3-header h2 {
  margin-top: 0;
  font-size: calc(clamp(2rem, 8vw, 4.4rem) * var(--k-display));
  line-height: 1;
  white-space: nowrap;
}

.s3-intro {
  margin-top: 1.5rem;
  font-size: var(--fs-lead);
  line-height: 1.6;
  color: var(--text-secondary);
  max-width: 62ch;
}

/* ---- The stack ----
   Every card is sticky, each pinned a step lower than the one before, so the
   four settle into a visible stack of edges. The card's own margin-bottom is
   what creates the scroll travel between reveals — no wrapper element, and
   because sticky releases at the container's bottom edge, card 04 finishes on
   top with 01–03 tucked behind it. Later siblings paint above earlier ones,
   which is what makes the covering work without a single z-index. */
.s3-stack {
  /* max() against --hdr-safe: the cards stick at this offset, and at short
     viewports the 4.5rem floor put them under the header. The same value
     does the job on a phone — it is the header height either way. */
  --stack-top:  max(var(--hdr-safe), clamp(4.5rem, 12vh, 7.5rem));
  --head-h:     2.75rem;
  /* The step IS the title strip's height, so a covered card leaves exactly
     its number and name showing. Any other value leaves a meaningless sliver
     of whatever happens to sit at the top of the card. */
  --stack-step: var(--head-h);
}

.s3-card {
  position: sticky;
  display: flex;
  flex-direction: column;
  margin-bottom: clamp(1.5rem, 6vh, 4rem);
  background: var(--uk-bone);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;     /* keeps the image corners inside the card radius */
  transition: border-color .4s var(--ease-out);
}
.s3-card:last-child { margin-bottom: 0; }

.s3-card:nth-child(1) { top: var(--stack-top); }
.s3-card:nth-child(2) { top: calc(var(--stack-top) + var(--stack-step)); }
.s3-card:nth-child(3) { top: calc(var(--stack-top) + var(--stack-step) * 2); }
.s3-card:nth-child(4) { top: calc(var(--stack-top) + var(--stack-step) * 3); }

/* focus-within so the border reacts to keyboard arrival too, not just a mouse. */
.s3-card:focus-within { border-color: var(--uk-brown); }
@media (hover: hover) {
  .s3-card:hover { border-color: var(--uk-brown); }
  .s3-card:hover .s3-card-media img { transform: scale(1.03); }
}

/* The wrapper carries the 16:10 crop and the placeholder fill, so the zoom is
   clipped and a missing file still holds its space as a clay block. */
.s3-card-media {
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--uk-clay);
}
.s3-card-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .4s var(--ease-out);
}

.s3-card-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  flex: 1;
  padding: 1.5rem;
}

/* The title strip. It is the only part of a card that stays visible once the
   next card has covered it, so the four of them accumulate into a running
   index down the top of the stack — like tabs on a set of drawings. */
.s3-card-head {
  display: flex;
  align-items: center;
  height: var(--head-h);
  padding-inline: clamp(1.25rem, 2.5vw, 2rem);
  border-bottom: 1px solid var(--border);
  background: var(--uk-bone);
}

/* The tab names the service (round 6: no "01 /" numbering, no caps). */
.s3-card-marker {
  font-family: var(--font-display);
  font-style: normal;
  font-size: clamp(1.05rem, 1.3vw, 1.25rem);
  font-weight: 400;
  letter-spacing: 0;
  line-height: 1.3;
  color: var(--clay-on-light);
}

/* Each card now holds the frame on its own, so the headline carries more
   size than the 2x2 grid allowed. */
.s3-card-body h3 {
  font-size: calc(clamp(1.5rem, 3vw, 2.7rem) * var(--k-display));
  font-weight: 300;
  line-height: 1.04;
  letter-spacing: -0.01em;
  max-width: 22ch;
}

.s3-card-body p {
  margin-top: 1rem;
  font-size: var(--fs-small);
  color: var(--text-secondary);
}

/* margin-top:auto pins the link to the bottom edge, so all four line up
   however long the paragraph above it runs. */
.s3-card-body .link-arrow { margin-top: auto; padding-top: 1.5rem; }

@media (min-width: 900px) {
  .s3-stack { --head-h: 3.25rem; }

  /* Landscape card: title strip across the top, image left, copy right.
     Keeps each card short enough to read whole in the frame at once. */
  .s3-card {
    display: grid;
    grid-template-columns: 5fr 7fr;
    grid-template-rows: auto 1fr;
    align-items: stretch;
    /* The card is pinned as far down as --stack-top plus three title strips,
       so that much of the viewport is already spoken for before it starts.
       Its height is the remainder, not a number picked independently of it —
       which is what let the fourth card run off the bottom of the screen.
       min() keeps it from growing past a comfortable reading height on a
       tall monitor; max() stops it collapsing on a very short one. */
    height: max(300px, min(52vh,  calc(100vh  - var(--stack-top) - var(--stack-step) * 3 - 3rem)));
    height: max(300px, min(52svh, calc(100svh - var(--stack-top) - var(--stack-step) * 3 - 3rem)));
    /* --stack-top already carries --hdr-safe, so the header is subtracted
       here through it rather than twice. */
    min-height: 0;
  }
  .s3-card-head { grid-column: 1 / -1; }
  .s3-card-media { aspect-ratio: auto; height: 100%; min-height: 0; }
  /* min-height:0 lets a grid item shrink below its content; without it the
     copy would push the card taller again and undo the cap above. */
  .s3-card-body {
    padding: clamp(1.25rem, 2.5vw, 2.75rem);
    justify-content: center;
    min-height: 0;
    overflow: hidden;
  }
  .s3-card-body .link-arrow { margin-top: 0; padding-top: 2rem; }
}

/* No sticking on a phone. The stack is a desktop effect: on a narrow screen
   four sticky cards each hold the viewport in turn, which on a touch device
   reads as the page refusing to scroll rather than as a designed transition
   — and CLAUDE.md is explicit that nothing may strand a mobile viewport.
   Below 900px they are a plain stack, which is what the markup describes
   anyway.

   This was previously reset ONLY under prefers-reduced-motion, so every
   phone that had not switched that on got the sticky behaviour. */
@media (max-width: 899px) {
  /* The cards stack on a phone too.

     I had made these static below 900px on the grounds that four sticky
     cards each holding the viewport reads as the page jamming on a touch
     device. That is a real risk, but it is not what happened — it removed
     the section's only animation and left a plain list, which is worse. The
     stack IS the effect here, and it should not be desktop-only.

     What it needs instead is mobile-appropriate geometry: the cards stick
     below the header rather than at the top of the screen, and each one is
     short enough that the whole card is readable in the frame at once
     instead of being taller than the phone it is stuck to. */
  .s3-card {
    /* A card taller than the viewport can never be read while it is stuck,
       and its bottom would be permanently off screen. */
    max-height: calc(100svh - var(--stack-top) - var(--stack-step) * 3 - 1.5rem);
  }

  .s3-card-media {
    /* Landscape rather than the tall default: the copy underneath is what
       has to fit, and a portrait image eats the height it needs. */
    aspect-ratio: 16 / 10;
    flex: 0 0 auto;
  }

  .s3-card-body {
    /* If a paragraph still runs long on a small phone, the card scrolls
       inside itself rather than pushing its own link out of reach. */
    overflow: auto;
    -webkit-overflow-scrolling: touch;
  }
}

/* No sticking when motion is reduced — the four cards become a plain stack. */
@media (prefers-reduced-motion: reduce) {
  .s3-card { position: static; margin-bottom: var(--gap-md); }
}

/* ============================================================
   Section 4 — The client problem. Umber. The problem only:
   the copy states it, the tangle shows it, and neither of them
   answers it. Measured on #3F2422: cream body and heading
   11.5:1, lifted clay eyebrow 5.2:1, label boxes and text 8.9:1,
   clay tangle lines 3.4:1 and the grey ones 3.1:1 (graphics,
   floor is 3:1).
   ============================================================ */

.s4 {
  background: var(--uk-umber);
  color: var(--text-on-dark);
  position: relative;
}

/* ---- the runway and the stage ----

   The runway's height IS the animation's length. The stage inside it is
   sticky, so scrolling 300vh past a stage that stays put is what drives the
   sequence — the reader performs it rather than watching it play.

   position: sticky, not a GSAP pin. Section 6 already settled this argument
   for the page: a pin inserts a spacer and rewrites the document height,
   which the other ScrollTriggers then have to be refreshed around. Sticky is
   layout, so nothing else on the page has to know about it.

   100svh, not 100vh: on a phone the address bar collapsing mid-scroll would
   otherwise resize the stage and jump the whole composition. */
.s4-runway { height: 300vh; }

/* padding-top clears the fixed header. The stage is sticky at top:0, so
   without it the first line of the heading sits underneath the bar — which
   is exactly what was happening: "Building" was being cut off.

   box-sizing is border-box globally, so the padding comes out of the 100svh
   rather than adding to it, and the stage still fills exactly one screen.

   overflow is visible, not hidden. Hidden here is what silently amputated
   the copy when it ran tall; the tree does its own clipping in its own box,
   which is where clipping belongs. */
.s4-stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  display: flex;
  align-items: center;
  padding-top: max(var(--hdr-safe), 11vh);
  padding-bottom: clamp(1rem, 3vh, 2.5rem);
}

.s4-inner {
  display: grid;
  gap: clamp(2rem, 4vw, 3.5rem);
  width: 100%;
  align-items: center;
}

/* Smaller than --fs-h2's ceiling, and deliberately so: this heading lives
   in a 32fr column inside a sticky one-screen stage, where the page default
   wrapped it to six lines and pushed the whole composition past 100vh. A
   heading that has to share a fixed viewport cannot use the same scale as
   one with a whole section to itself. */
.s4-copy h2 {
  margin-top: 0;
  font-weight: 300;
  font-size: clamp(2.1rem, 3.3vw, 2.9rem);
  line-height: 1.04;
  color: var(--text-on-dark);
  max-width: 15ch;
}

.s4-copy > p:not(.eyebrow) {
  margin-top: 1rem;
  color: var(--text-on-dark);
  max-width: 44ch;
  font-size: clamp(.9375rem, 1.2vw, 1.0625rem);
  line-height: 1.6;
}

/* The paragraphs and the turn are driven by scroll position, so their
   resting state is visible: no JS and reduced motion both get all three. */
.s4-para, .s4-turn { transition: opacity .3s linear; }

/* ---- the tree ----

   One square-ish stage holding four layers, all sharing the same box so the
   canvas, the branches, the hub and the labels stay in register:

     canvas          the mess
     .s4-field-layer the ten roles
     svg             the exploded assembly and its leaders, viewBox 1200x660
     .s4-tree-hub    the mark, dead centre
     ul              the six outcomes, placed by the same numbers as the SVG

   The labels are HTML rather than SVG <text> so they wrap, scale with the
   page's type and stay selectable. They are positioned in percentages taken
   from the SVG's own coordinates — every --x is a leader's start point
   divided by 1200 — which is the only reason a label always lands on the end
   of its own leader. Both come out of tools/assembly_gen.py; do not edit
   either by hand or they drift apart. */
/* The box is back. This is the framed field from the version before the
   tree went in, and dropping it was the regression: without a border and
   without overflow:hidden the drawing fragments and the ten role labels spread over
   the whole section instead of being contained, and the figure stopped
   reading as one object.

   Sized by HEIGHT first. `width: 100%` plus `max-height` does not clamp an
   aspect-ratio box — the width stays put and the ratio simply breaks, which
   is what turned the field into a wide smear. Driving the width from the
   available height keeps 1200:660 true at every viewport, and 100% still
   caps it in a narrow column. */
.s4-tree {
  position: relative;
  width: min(100%, calc(58vh * 1200 / 660));
  aspect-ratio: 1200 / 660;
  margin-inline: auto;
  overflow: hidden;
  border: 1px solid rgba(239, 231, 218, .14);
  border-radius: var(--radius);
  background: rgba(239, 231, 218, .025);
}

/* On desktop the wrapper simply fills the tree box, so the canvas, the
   branches and the mark sit exactly where they always did and the labels
   still orbit them. It earns its keep on mobile. */
.s4-tree-graphic { position: absolute; inset: 0; }

.s4-tree-svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
}

/* ---- the collision: ten contributors, ten drawings, one sheet ----

   This replaced a canvas particle mesh. The mesh was a picture of a NETWORK,
   and the section's complaint is not that the vendors are networked — it is
   that ten people are drawing on the same sheet and nobody is reconciling
   them. So the field is ten actual drawings: a plan, an interior layout, a
   structural grid, revision clouds, material swatches, a circuit route, a
   service run, setting-out marks, a schedule and a section. They accumulate
   until the space cannot hold them, then collapse into the mark.

   The chaos has to come from COMPOSITION, not from effects. Nothing here
   glitches, jitters or explodes; the density is the whole device. Which is
   also why the line weights below are so tightly grouped — five near-identical
   greys is what a pile of drawings looks like, and any one stroke shouting
   would turn the pile into a poster.

   Geometry, placement and label positions all come out of
   tools/collision_gen.py. Do not hand-edit the markup. */

.frag-field { fill: none; }

/* transform-box so the script's collapse works in viewBox units, and
   transform-origin is written per-sheet by the generator — each drawing
   shrinks toward its own centre while travelling to the middle, which is what
   makes it read as folding up rather than sliding away. */
.frag {
  transform-box: view-box;
}

/* The empty sheet everything lands on. Barely there — it is the quiet the
   first phase needs in order for the accumulation to register as arrival. */
.fr-base   { stroke: color-mix(in srgb, var(--text-on-dark) 11%, transparent); stroke-width: 1; }

/* Cut material: walls in plan, slabs in section. The heaviest line on a
   drawing is always the thing you would bump into. */
.fr-wall   { stroke: color-mix(in srgb, var(--clay-on-dark) 74%, transparent); stroke-width: 1.5; }
.fr-thin   { stroke: color-mix(in srgb, var(--clay-on-dark) 56%, transparent); stroke-width: 1; }
.fr-grid   { stroke: color-mix(in srgb, var(--text-on-dark) 26%, transparent); stroke-width: .9; }
.fr-hatch  { stroke: color-mix(in srgb, var(--clay-on-dark) 38%, transparent); stroke-width: .8; }
.fr-dim    { stroke: color-mix(in srgb, var(--text-on-dark) 26%, transparent); stroke-width: .8; }
.fr-mark   { stroke: color-mix(in srgb, var(--clay-on-dark) 52%, transparent); stroke-width: 1; }
.fr-route  { stroke: color-mix(in srgb, var(--ochre-on-dark) 55%, transparent);
             stroke-width: 1.3; stroke-linejoin: round; stroke-linecap: round; }
.fr-dash   { stroke: color-mix(in srgb, var(--clay-on-dark) 46%, transparent);
             stroke-width: 1; stroke-dasharray: 6 4; }
.fr-dashdot{ stroke: color-mix(in srgb, var(--clay-on-dark) 46%, transparent);
             stroke-width: 1; stroke-dasharray: 12 4 2 4; stroke-linejoin: round; }
.fr-solid  { fill: color-mix(in srgb, var(--clay-on-dark) 34%, transparent); stroke: none; }
.fr-dot    { fill: color-mix(in srgb, var(--clay-on-dark) 60%, transparent); stroke: none; }

/* ---- the ten role names ---- */

.s4-field-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Each name sits ON its own drawing rather than orbiting the middle. The
   plate behind it is what keeps it readable once six other drawings are
   crossing underneath — without it the names dissolve into the density at
   exactly the moment there is most to read.

   --r is a degree or two, taken from its sheet's own rotation so the name
   lies with the drawing it belongs to. Any more and it stops being a label
   on a drawing and starts being a design flourish. */
.s4-field-layer span {
  position: absolute;
  left: var(--x);
  top: var(--y);
  transform: translate(-50%, -50%) rotate(var(--r, 0deg));
  white-space: nowrap;
  font-family: var(--font-body);
  font-weight: 500;
  /* .6875rem = 11px is the floor. .625rem rendered at 10px on a phone,
     which is below what anyone reads comfortably at arm's length. */
  font-size: clamp(.6875rem, 1.05vw, .78rem);
  line-height: 1.2;
  padding: .25em .5em;
  border-radius: 2px;
  background: color-mix(in srgb, var(--uk-umber) 86%, transparent);
  color: color-mix(in srgb, var(--text-on-dark) 82%, transparent);
}

/* ---- the exploded assembly ----

   This replaced a radiating "tree" whose six terminals read as something
   organic and unfortunate. The fix was not a better terminal shape: it was
   dropping the metaphor. The figure is now what the section is actually about
   — a project drawn the way a project is drawn, pulled apart along one axis so
   every layer can be pointed at and named.

   Everything here is line weight, and the weights are the argument:

     1.6   plate outlines        the parts themselves
     1.0   internal detail       what each part contains
     0.9   dimensions, leaders   the drawing talking about itself

   Nothing is filled except two facade panels and a whisper under each plate.
   That is deliberate. Section 7 is the page's one showy moment and it earns
   that by being photographic; keeping this at wireframe weight means the two
   never compete for the same kind of attention. */

.asm-stack,
.asm-devices,
.asm-leaders { fill: none; }

/* Each layer is its own <g> so the explosion is one transform per layer
   rather than a transform per path. data-lift carries its travel in viewBox
   units; the script reads it and animates translateY from lift to 0. */
.asm-layer {
  transform-box: view-box;
  transform-origin: center;
}

/* Ground under each plate, and it has to be nearly opaque. At 55% the stack
   rendered as a wireframe mesh: every plate visible through every other plate,
   five sets of edges overlapping, no read of what is in front of what. 88%
   still lets a hint of the layer below through — which is what keeps it a
   drawing rather than five solid slabs — while giving the stack real depth
   order. This is the single value that decides whether the figure looks
   three-dimensional or looks like a tangle. */
.asm-fill {
  fill: color-mix(in srgb, var(--uk-umber) 88%, transparent);
  stroke: none;
}

.asm-plate {
  stroke: var(--clay-on-dark);
  stroke-width: 1.6;
  stroke-linejoin: miter;
}

/* Structural grid and interior partitions — inside the plate, so lighter than
   the plate's own edge or the layer stops reading as one object. Lighter, but
   not by much: at 62% the middle layers rendered as empty sheets and the whole
   claim of the figure is that each layer HAS contents worth naming. */
.asm-detail {
  stroke: color-mix(in srgb, var(--clay-on-dark) 80%, transparent);
  stroke-width: 1.05;
}

.asm-column {
  stroke: var(--clay-on-dark);
  stroke-width: 1.4;
  stroke-linecap: round;
}

.asm-run {
  stroke: color-mix(in srgb, var(--clay-on-dark) 78%, transparent);
  stroke-width: 1.3;
  stroke-linejoin: round;
  stroke-linecap: round;
}

.asm-node {
  fill: var(--surface-dark);
  stroke: var(--clay-on-dark);
  stroke-width: 1.2;
}

.asm-panel {
  stroke: color-mix(in srgb, var(--clay-on-dark) 66%, transparent);
  stroke-width: .95;
}

/* The two called-out facade panels. The only real fill in the drawing, which
   is what makes "structured cost and material decisions" have something
   material to point at. */
.asm-sample {
  fill: color-mix(in srgb, var(--uk-clay) 34%, transparent);
  stroke: color-mix(in srgb, var(--clay-on-dark) 70%, transparent);
  stroke-width: .9;
}

/* Site boundary: dashed, because a boundary is a legal line rather than a
   built one, and that is exactly how it is drawn on a real site plan. */
.asm-site {
  stroke: color-mix(in srgb, var(--clay-on-dark) 62%, transparent);
  stroke-width: 1;
  stroke-dasharray: 7 5;
}

.asm-mark {
  stroke: color-mix(in srgb, var(--clay-on-dark) 55%, transparent);
  stroke-width: 1;
}

/* ---- the drawing's own annotations ----

   Dimensions and leaders are drawn in bone rather than clay. On a real sheet
   the notation is a different ink from the building, and here it also stops
   the six leaders competing with the five plates. */

/* Dash-dot: the centre-line convention. This is the axis the layers separate
   along, which is what "one consolidated execution plan" is pointing at. */
.asm-axis {
  stroke: color-mix(in srgb, var(--text-on-dark) 34%, transparent);
  stroke-width: 1;
  stroke-dasharray: 14 4 2 4;
}

.asm-node-sq {
  fill: color-mix(in srgb, var(--text-on-dark) 42%, transparent);
  stroke: none;
}

.asm-ext,
.asm-dim {
  stroke: color-mix(in srgb, var(--text-on-dark) 38%, transparent);
  stroke-width: .9;
}

.asm-dim--overall { stroke: color-mix(in srgb, var(--text-on-dark) 55%, transparent); }

/* 45-degree obliques, not arrowheads. This is the architectural dimension
   terminator; an engineering arrow would be the wrong dialect. */
.asm-tick {
  stroke: color-mix(in srgb, var(--text-on-dark) 62%, transparent);
  stroke-width: 1.2;
}

.asm-arrow {
  fill: color-mix(in srgb, var(--text-on-dark) 55%, transparent);
  stroke: none;
}

/* pathLength normalises every leader to 1 whatever its real length, so one
   dashoffset draws the short right-hand leaders and the long forked left one
   at the same rate. Set by the script, along with the dasharray. */
.note-leader {
  stroke: color-mix(in srgb, var(--text-on-dark) 60%, transparent);
  stroke-width: .9;
  stroke-linejoin: miter;
  stroke-linecap: butt;
}

/* The dot where a leader meets its text. Small, but it is what makes the
   annotation land on the sentence instead of pointing vaguely at it. */
.note-dot {
  fill: color-mix(in srgb, var(--text-on-dark) 70%, transparent);
  stroke: none;
}

/* ---- the trunk ---- */

/* AUTHENTIC LOGO PASS: no plate any more. The white tile existed only
   because the old PNG had a near-black wordmark (1.5:1 on umber). The hub is
   now the light lockup (white linework, white wordmark) straight on the
   umber, so the tile is gone and the branches grow from the artwork itself.
   The box below keeps its size and position — the branch clearances in the
   notes that follow were measured against it, and the artwork sits inside it.

   Historical note on the box:
*/
/* The plate takes the logo's OWN proportions instead of being a circle.

   A circle is the wrong container for this mark: the artwork is a portrait
   tile at 533:924, so a round plate had to shrink it to about two-thirds of
   the diameter just to fit, leaving it stranded in the middle of a lot of
   white. The plate now echoes the framed tile the logo already is — same
   portrait ratio, softly rounded corners — so the white reads as the mark's
   own mount rather than as a disc it is floating on, and the branches look
   like they grow out of the logo instead of out of a badge behind it.

   168 x 292 in viewBox units, up from 116 x 202 — about 45% larger in each
   direction, which is what makes the wordmark inside it legible rather than
   a smudge. Growing it meant moving the branches: R_HUB went 88 to 112 in
   the generator and every branch path was regenerated to start further out.

   The three numbers are one constraint, not three choices. The plate's reach
   along a branch direction is 107 at 38 degrees and 84 at 0, against a
   branch start of 112 — so every branch still emerges from open ground. Its
   top edge sits at y=184 against the upper tiles at y=154, so it clears
   those too. 170 x 296 is the ceiling; past that the diagonals start
   underneath the plate and the tree stops looking like it grows from the
   mark. */
.s4-tree-hub {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  display: grid;
  place-items: center;
  width: 14%;                   /* 168 / 1200 */
  aspect-ratio: 168 / 292;
  background: none;
}

/* The plate's ratio (0.574) is within a percent of the artwork's (0.577), so
   the mark can fill it almost edge to edge with even padding on all four
   sides — which is the whole reason for matching the ratio. 88% against the
   old 68% of a smaller circle is roughly half again as much logo. */
.s4-tree-hub img {
  width: 90%;
  height: auto;
  max-height: 90%;
  object-fit: contain;
  /* A soft shadow that follows the artwork's own edges, so it lifts off the
     linework without reintroducing a rectangle. */
  filter: drop-shadow(0 8px 22px rgba(20, 10, 8, .45));
}

/* ---- the six outcomes ---- */

.s4-tree-labels {
  position: absolute;
  inset: 0;
  pointer-events: none;
}

/* Anchored to where its own leader starts: left labels end at the anchor,
   right labels begin at it. The generator's verify pass proves no anchor sits
   over the drawing and no leader crosses a plate, so a label can always be
   traced back to the component it names.

   The six are deliberately NOT mirrored. Their x and y both vary by a few
   units per row, because a mechanically symmetrical annotation layout reads as
   a diagram and an architectural sheet does not look like that. The variation
   is small enough to be felt rather than seen.

   20% and not the old 22%: the right column now clears a stage dimension and
   an overall dimension, so its anchor sits further out than the leaf tips did.
   These stay full sentences at full weight — they are the section's message,
   not dimension text, and the drawing exists to support them. */
.tree-label {
  position: absolute;
  left: var(--x);
  top: var(--y);
  width: 20%;
  font-family: var(--font-body);
  font-weight: 500;
  font-size: clamp(.7rem, 1.15vw, .9rem);
  line-height: 1.3;
  color: var(--text-on-dark);
}

.tree-label--l {
  transform: translate(-100%, -50%);
  padding-right: 1.1rem;
  text-align: right;
}

.tree-label--r {
  transform: translate(0, -50%);
  padding-left: 1.1rem;
  text-align: left;
}

/* ---- resting state ----

   No JS, no GSAP, reduced motion: the finished drawing. The mess is the part
   that needs a script, so the mess is what is absent by default — a reader
   who never runs the animation is left with the answer, which is the half
   that carries the product.

   The mark goes with it. Its whole job in this section is the handover at
   0.54-0.66, where it and the collapsed stack share the same pixels and the
   identity is seen becoming the project. With no script there is no handover
   to watch, and a white plate parked over the middle of the assembly hides
   three of the five layers and every leader that lands on them. So the mark
   is absent by default and the script is what brings it in — the exact
   inverse of how it was written when the figure was a tree growing out of it.
   The logo still sits in the nav and the footer either way. */
/* The collision without a script is a still: every drawing at once, which is
   the peak frame and completely unreadable under the assembly sitting on top
   of it. Held right down it becomes what it should be with no animation to
   carry it — a calm architectural texture the answer is drawn over, so a
   reduced-motion reader still sees that the answer came out of something.

   The ten names go, because at full strength they would collide with the six
   annotations. Nothing is lost: all ten are named in the section's own
   paragraph, which is where they came from. */
.frag-field { opacity: .18; }
.s4-field-layer span { opacity: 0; }

/* .is-live is the script taking ownership of every opacity in the figure. */
.s4-tree.is-live .frag-field { opacity: 1; }
.s4-tree-hub { opacity: 0; }

/* ---- the hand-off into Section 5 ----

   umber to bone, graded rather than cut. The tree spends 300vh arguing that
   the problem resolves; a hard colour edge immediately after it reads as the
   page changing subject instead of finishing a sentence. */
.s4-handoff {
  height: clamp(4rem, 12vh, 9rem);
  /* Grades into the sandstone method section, which now follows directly
     (the bone "What holds on every project" section was removed). */
  background: linear-gradient(to bottom, var(--uk-umber), var(--uk-sandstone));
}

@media (min-width: 900px) {
  .s4-inner {
    grid-template-columns: 32fr 68fr;
    gap: clamp(2rem, 4vw, 4rem);
  }
}

/* ---- narrow screens ----

   No runway and no sticky. A 300vh scroll hijack on a phone is the thing
   CLAUDE.md warns about, and the tree at phone width cannot carry six
   labels around a centre anyway. The section becomes what it is: the copy,
   then the finished tree, stacked and still. */
@media (max-width: 899px) {
  /* The runway and the sticky stage stay. They were removed here on the
     grounds that a scroll-driven section is a hijack on a phone — but the
     effect IS the section, and taking the scrub away took the sequence with
     it. 220vh rather than the desktop 300: the same four beats, a shorter
     distance to pull them through, because a phone scrolls faster per
     gesture. */
  .s4-runway { height: 220vh; }

  /* The box stops being a fixed-ratio frame and becomes a column: the
     graphic keeps the ratio, the labels sit below it. overflow has to go
     visible or the list is clipped by a box sized only for the tree. */
  /* Copy above, figure below, inside one sticky screen. Both have to fit,
     so the figure takes what the copy leaves rather than a fixed ratio. */
  .s4-copy h2 { font-size: calc(clamp(1.6rem, 7.4vw, 2.1rem) * var(--k-display)); margin-top: 0; }
  .s4-copy > p:not(.eyebrow) { margin-top: .6rem; font-size: .8125rem; line-height: 1.5; }

  .s4-inner { gap: 1rem; }

  .s4-tree {
    max-height: none;
    margin-top: .5rem;
    width: 100%;
    aspect-ratio: auto;
    overflow: visible;
    padding: clamp(.9rem, 3vw, 1.25rem);
  }

  /* The graphic carries the ratio now. position: relative, not absolute —
     it has to take up real height so the labels have something to sit
     under; absolute would collapse it and stack them on top again, which is
     the bug this is fixing. */
  .s4-tree-graphic {
    position: relative;
    inset: auto;
    width: 100%;
    aspect-ratio: 1200 / 660;
  }

  /* THE SAME FIGURE AS DESKTOP. Same 1200:660 geometry, the same six
     branches, the same six labels orbiting the mark — not a rearrangement
     of it.

     Two earlier attempts got this wrong. Stacking the labels into a list
     under the tree was a different composition wearing the same parts.
     Squaring the box and generating a portrait tree failed on arithmetic:
     in a 760-wide portrait frame the middle-row labels have nowhere
     outboard to go, because their own terminal already reaches the edge.

     What actually buys the room is WIDTH, and the page was giving it away.
     The figure sat inside the container gutter AND its own padding, so on a
     390px phone it had about 328px to work in. Full-bleed it has 390px, and
     at 22% each label gets 86px — enough to wrap "Structured cost and
     material decisions" to four lines inside a row band 61px tall.

     The fix was to stop indenting it, not to redraw it. */
  .s4-tree {
    max-height: none;
    margin-top: 2rem;
    /* Break out by exactly the container's own gutter, rather than with the
       usual 100vw / calc(50% - 50vw) trick. 100vw counts the scrollbar and
       clientWidth does not, so on a desktop-class browser that pair makes the
       figure ~15px wider than the viewport and gives the whole page a
       horizontal scroll. Cancelling the padding cannot overshoot, because it
       is measured from the same box it is undoing. */
    width: auto;
    margin-inline: calc(-1 * var(--pad-page));
    padding: 0;
    /* Edge to edge, so side borders and a radius would only draw attention
       to the seam. */
    border-left: 0;
    border-right: 0;
    border-radius: 0;
    aspect-ratio: auto;
    overflow: hidden;
  }

  /* Landscape again — the desktop ratio, so every label percentage the
     generator produced still lands on its own tile. */
  .s4-tree-graphic {
    position: relative;
    inset: auto;
    width: 100%;
    aspect-ratio: 1200 / 660;
  }

  /* The ten roles stay. "Ten different vendors" is the claim the section
     makes, and anonymous drifting dots do not make it. 10px is under the
     11px floor the rest of the page holds to, and is justified here only
     because these exact ten words sit in the paragraph directly above. */
  .s4-field-layer span {
    font-size: .625rem;
    padding: .15em .35em;
    letter-spacing: 0;
  }

  /* The six outcomes orbit, exactly as on desktop. */
  .s4-tree-labels {
    position: absolute;
    inset: 0;
    display: block;
    margin: 0;
  }

  /* 24%, up from 22%. The assembly is a narrower object than the tree it
     replaced — 448..752 against the old 318..882 — so there is more room out
     here than there used to be, and on a 390px phone that is 94px per label
     instead of 86px. The generator asserts this width still fits. */
  .tree-label {
    position: absolute;
    left: var(--x);
    top: var(--y);
    width: 24%;
    font-size: .625rem;
    line-height: 1.25;
    border-left: 0;
    padding: 0;
  }

  .tree-label--l {
    transform: translate(-100%, -50%);
    text-align: right;
    padding-right: .4rem;
  }

  .tree-label--r {
    transform: translate(0, -50%);
    text-align: left;
    padding-left: .4rem;
  }

  .s4-tree-svg { opacity: .9; }
}

/* A very narrow phone gets its label width from the empty centre of the
   figure rather than from the type, which is already at its floor. */
@media (max-width: 479px) {
  .tree-label { width: 26%; }
}

@media (prefers-reduced-motion: reduce) {
  /* Belt and braces: the runway collapses so nothing is scroll-driven, and
     the finished tree is simply on the page. */
  .s4-runway { height: auto; }
  .s4-stage { position: static; height: auto; padding-block: var(--pad-section); }
}

/* ============================================================
/* ============================================================
   Section 10 — Team. Copy on bone, then a fixed-background band.

   No sticky, no pin, no JS. background-attachment: fixed paints the
   photograph against the viewport instead of the element, so it
   appears to hold still while the page scrolls past at its normal
   rate — the scroll is never held or slowed.

   Measured on bone: h2 13.2:1, body 6.5:1, eyebrow 6.3:1, pills
   13.2:1. Over the band, worst case a pure white photograph
   beneath: bone heading 5.9:1, ochre button 5.3:1 (its own fill, so
   whatever is behind it does not matter).
   ============================================================ */

.s10 {
  background: var(--uk-bone);
  color: var(--text-primary);
}

/* ---- the copy ---- */

.s10-text { padding-block: var(--pad-section); }

.s10-text h2 { margin-top: 1.5rem; max-width: 22ch; }

/* :not(.eyebrow) — the eyebrow is a sibling <p>, and this rule would
   otherwise outrank .eyebrow--on-light and repaint it. */
.s10-text > p:not(.eyebrow) {
  margin-top: 1.5rem;
  color: var(--text-secondary);
  max-width: 62ch;
}

/* ---- the drafting workspace ----
   The eight disciplines as entities placed on a sheet. Thin lines only, no
   fills, no shadows, no icons. */

.s10-workspace {
  position: relative;
  /* Clips the guide lines, which are deliberately far wider than the panel so
     they read as running off the edge of the sheet rather than stopping. */
  overflow: hidden;
  margin-top: clamp(2.5rem, 6vw, 3.5rem);
  padding: clamp(2rem, 5vw, 3.5rem);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background-color: var(--uk-bone);
  background-image:
    repeating-linear-gradient(to right, rgba(92, 58, 33, .07) 0 1px, transparent 1px 40px),
    repeating-linear-gradient(to bottom, rgba(92, 58, 33, .07) 0 1px, transparent 1px 40px);
}

.s10-workspace-note {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin-bottom: clamp(2rem, 4vw, 3rem);
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: .8rem;
  letter-spacing: 0;
  /* Deepened clay: raw --uk-clay is 3.1:1 on bone. */
  color: var(--clay-on-light);
}

.s10-objects {
  display: grid;
  gap: clamp(2rem, 4vw, 3rem) clamp(1.5rem, 3vw, 2.5rem);
}

.s10-obj {
  position: relative;
  transition: opacity .4s var(--ease-out);
}

/* ---- layer colours ----
   CAD colour-codes its layers, so this sheet does too. Each category owns one
   accent from the palette and spends it on its outline, its guides, its ticks
   and its layer code — 1px linework and annotation only, never a fill. The
   workspace stays bone.

   The order is arranged so that no two neighbouring objects share a colour in
   any of the three layouts: one column, two columns and four columns. That
   means no repeat at a distance of 1 (side by side, and the single-column
   stack), 2 (vertical neighbours at two columns) or 4 (vertical neighbours at
   four columns) — which is why the sequence is not a simple rotation.

   Two variables per object, and the second is not decoration: the raw accents
   are line colours, not type colours. On bone, ochre measures 3.0:1 and clay
   3.1:1 — both under the 4.5:1 floor the layer code needs as text. --accent
   draws the lines, --accent-ink sets the type, reusing the deepened tokens
   already defined at the top of this file. Terracotta (8.2:1) and brown
   (9.4:1) clear the floor untouched, so for those two the pair is one colour.

   Line colours sit at 3:1 or better against bone as non-text, and every one
   of these does. */
.s10-obj:nth-child(1),
.s10-obj:nth-child(6) { --accent: var(--uk-ochre);      --accent-ink: var(--ochre-on-light); }

.s10-obj:nth-child(2),
.s10-obj:nth-child(5) { --accent: var(--uk-terracotta); --accent-ink: var(--uk-terracotta); }

.s10-obj:nth-child(3),
.s10-obj:nth-child(8) { --accent: var(--uk-clay);       --accent-ink: var(--clay-on-light); }

.s10-obj:nth-child(4),
.s10-obj:nth-child(7) { --accent: var(--uk-brown);      --accent-ink: var(--uk-brown); }

/* ---- coordinate ticks ----
   A measurement scale along the top and left edges of the selected object:
   1px marks, 8px apart, in its own layer colour. They run outward from the
   corner on hover, the way a dimension is pulled off a drawing. Hidden and
   zero-scaled at rest, so the sheet stays quiet until something is selected. */
.s10-obj::before,
.s10-obj::after {
  content: "";
  position: absolute;
  opacity: 0;
  pointer-events: none;
  transition: transform .45s var(--ease-out), opacity .45s var(--ease-out);
}
.s10-obj::before {
  /* 20px clears the outline, which sits 12px off the box, by a visible margin
     and still stays inside the row and column gaps. */
  top: -20px;
  left: 0;
  right: 0;
  height: 5px;
  background: repeating-linear-gradient(to right, var(--accent) 0 1px, transparent 1px 8px);
  transform: scaleX(0);
  transform-origin: left center;
}
.s10-obj::after {
  top: 0;
  bottom: 0;
  left: -20px;
  width: 5px;
  background: repeating-linear-gradient(to bottom, var(--accent) 0 1px, transparent 1px 8px);
  transform: scaleY(0);
  transform-origin: center top;
}

/* ---- guide lines ----
   Anchored to the object, extended a viewport past the panel in each
   direction, and scaled from the centre so they draw outward from the entity
   toward the sheet edges. The panel's overflow does the trimming. */
.s10-guide {
  position: absolute;
  background: var(--accent);
  opacity: 0;
  transition: transform .45s var(--ease-out), opacity .45s var(--ease-out);
  pointer-events: none;
}
.s10-guide--h {
  top: 50%;
  left: -100vw;
  right: -100vw;
  height: 1px;
  transform: scaleX(0);
}
.s10-guide--v {
  left: 50%;
  top: -100vh;
  bottom: -100vh;
  width: 1px;
  transform: scaleY(0);
}

/* ---- the entity itself ---- */

/* The resting outline carries the layer colour at low strength — enough to
   read as coloured linework rather than grey, not enough to turn eight
   rectangles into a pattern. Hover takes it to full. */
.s10-obj-box {
  position: relative;
  display: block;
  padding: 0.35rem 0.5rem;
  outline: 1px solid color-mix(in srgb, var(--accent) 38%, transparent);
  outline-offset: 12px;
  transition: outline-color .4s var(--ease-out);
}

/* Corner brackets: the layer colour, held back at rest so each name reads as a
   placed object, full strength on hover so it reads as a selected one. */
.s10-obj-box::before,
.s10-obj-box::after {
  content: "";
  position: absolute;
  width: 10px;
  height: 10px;
  border: 0 solid color-mix(in srgb, var(--accent) 60%, transparent);
  transition: border-color .4s var(--ease-out), opacity .4s var(--ease-out);
}
.s10-obj-box::before {
  top: -6px;
  left: -6px;
  border-top-width: 1px;
  border-left-width: 1px;
}
.s10-obj-box::after {
  bottom: -6px;
  right: -6px;
  border-bottom-width: 1px;
  border-right-width: 1px;
}

.s10-obj-name {
  display: block;
  font-family: var(--font-display);
  font-weight: 400;
  font-size: clamp(0.95rem, 1.5vw, 1.0625rem);
  line-height: 1.25;
  color: var(--text-primary);
}

.s10-obj-code {
  display: block;
  margin-top: 0.5rem;
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: var(--fs-eyebrow);
  letter-spacing: .16em;
  color: var(--accent-ink);
  transition: opacity .4s var(--ease-out);
}

/* ---- hover ----
   Pointer devices only. There is nothing to tap here: these are labels, not
   controls, and every name and code is fully legible at rest — the highlight
   adds emphasis, never information. */
@media (hover: hover) {
  /* Two levels of dimming, and the split is a contrast decision.
     The linework and the layer codes go to roughly 0.4 — 0.72 on the object
     times 0.55 on the part. The category names stay at 0.72, which holds them
     at 5.58:1 on bone; taking the names themselves to 0.4 would put seven of
     the eight at 2.3:1 for as long as the pointer rests anywhere in the panel.
     The codes can take the full dim because they are aria-hidden decoration
     and repeat nothing the name does not already say. */
  .s10-objects:hover .s10-obj { opacity: .72; }
  .s10-objects:hover .s10-obj:hover { opacity: 1; }

  .s10-objects:hover .s10-obj:not(:hover) .s10-obj-code,
  .s10-objects:hover .s10-obj:not(:hover) .s10-obj-box::before,
  .s10-objects:hover .s10-obj:not(:hover) .s10-obj-box::after { opacity: .55; }

  /* The selected layer, at full strength in its own colour. */
  .s10-obj:hover .s10-guide { opacity: 1; }
  .s10-obj:hover .s10-guide--h { transform: scaleX(1); }
  .s10-obj:hover .s10-guide--v { transform: scaleY(1); }

  .s10-obj:hover::before,
  .s10-obj:hover::after { opacity: 1; }
  .s10-obj:hover::before { transform: scaleX(1); }
  .s10-obj:hover::after  { transform: scaleY(1); }

  .s10-obj:hover .s10-obj-box { outline-color: var(--accent); }
  .s10-obj:hover .s10-obj-box::before,
  .s10-obj:hover .s10-obj-box::after { border-color: var(--accent); }

  /* The code brightens by weight rather than by colour: the raw accent would
     drop ochre to 3.0:1 and clay to 3.1:1, and the ink is already as light as
     4.5:1 allows. Against neighbours at 0.4 it reads as lit regardless. */
  .s10-obj:hover .s10-obj-code { font-weight: 500; }
}

@media (min-width: 600px) {
  .s10-objects { grid-template-columns: repeat(2, 1fr); }
}

@media (min-width: 900px) {
  .s10-objects { grid-template-columns: repeat(4, 1fr); }
  /* A half-step drop on alternate columns: still aligned to the guides, but
     placed rather than tabulated. */
  .s10-obj:nth-child(even) { margin-top: 1.75rem; }
}

/* ---- the photo band ---- */

/* Sections are already full-bleed children of <main>, so 100% is edge to
   edge. 100vw is avoided on purpose: it counts the vertical scrollbar's
   width, overflowing the page by ~15px on Windows and giving the whole
   document a horizontal scrollbar. */
.s10-band {
  position: relative;
  width: 100%;
  height: 70vh;
  min-height: 360px;
}

.s10-band-photo {
  position: absolute;
  inset: 0;
  /* Clay shows through until the photograph exists, so the band is testable
     now. It is a background rather than the shorthand so the image can be
     added or removed without disturbing the fallback colour. */
  background-color: var(--uk-clay);
  background-image: url(/assets/images/projects/site-process/site-work-01.webp);
  background-size: cover;
  background-position: center;
  background-attachment: fixed;
  /* The shared grade. A CSS background cannot pick it up from `main img`, so
     it carries its own copy — and because contrast(1.03) lifts the bright
     areas, the scrim's radial was re-measured against the graded pixels and
     nudged from .55 to .58 to hold the heading above 4.5:1. */
  filter: saturate(0.92) contrast(1.03) sepia(0.04);
}

/* The scrim rides on the content layer rather than a pseudo-element: ::after
   is generated as the last child, so it would paint over the heading and the
   button instead of under them. */
.s10-band-content {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 24px;
  padding: var(--pad-page);
  text-align: center;
  /* Two layers, and both numbers are measured against the actual photograph
     rather than chosen by eye.

     The flat wash grades the whole band and is what decides whether the
     photograph reads as a photograph. It was .40, which flattened a warm
     brown image into something near-indistinguishable from the clay
     placeholder underneath it. At .25 the picture comes back.

     The radial above it darkens the middle, where the type sits. It cannot
     come down as far: the bone heading needs 4.5:1, and against the bright
     areas of this image a 45% total leaves it at 3.4:1. Measured at the
     heading's centre, its outer letters and its far edge, against the
     brightest pixels in the crop behind it — and re-measured after the shared
     grade was added, which costs about 0.13:1 — .58 over the .25 wash is the
     lightest pair that clears the floor everywhere: 6.5:1, 5.0:1 and 4.7:1.
     The geometry is wider than it was so the falloff across the heading is
     gentler; a tighter radial starves the outer letters.

     Retune both if the photograph is replaced with a brighter one. The
     button is unaffected either way — it carries its own opaque ochre fill,
     so it holds 5.3:1 whatever is behind it. */
  background:
    radial-gradient(64% 70% at 50% 50%, rgba(30, 18, 14, .58) 0%, rgba(30, 18, 14, 0) 100%),
    linear-gradient(rgba(63, 36, 34, .25), rgba(63, 36, 34, .25));
}

.s10-band-content h2 {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(2rem, 5vw, 3.5rem);
  line-height: 1.1;
  letter-spacing: -.012em;
  color: var(--uk-bone);
  text-shadow: 0 2px 30px rgba(30, 18, 14, .6);
}

.s10-band-btn {
  display: inline-flex;
  align-items: center;
  padding: 1rem 2.25rem;
  border-radius: 88px;
  border: 1.6px solid var(--uk-ochre);
  background-color: var(--uk-ochre);
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: 0.8125rem;
  letter-spacing: .01em;
  color: #2A1A06;
  cursor: pointer;
  transition: border-color .4s, background-color .4s;
}
.s10-band-btn:hover {
  background-color: #B0700F;
  border-color: #B0700F;
}

/* The band behaves the same on every device. It used to drop to
   background-attachment: scroll below 900px and shrink to 55vh, which made
   the section a different thing on a phone than on a monitor.

   svh rather than vh so a phone's collapsing address bar cannot resize the
   band mid-scroll, which is the real problem on mobile — not the fixed
   attachment itself. */
@media (max-width: 900px) {
  .s10-band {
    height: 70vh;
    height: 70svh;
  }
}

/* The held-still image is a parallax effect by another name, so it goes when
   motion is switched off. */
@media (prefers-reduced-motion: reduce) {
  .s10-band-photo { background-attachment: scroll; }
}

/* ============================================================
   Section 9 — Featured projects. Stacked card scroll.

   Rules above the "stack mode" divider are the default AND the
   fallback: five medium cards straight down the page. .is-stack is
   added by the script only at 1100px, and only then do the cards
   overlap or anything pin.

   Measured over the card scrim at its .92 floor, worst case a pure
   white photograph beneath: clay category 4.5:1, ochre link 4.7:1,
   cream name and location 12.1:1.
   ============================================================ */

.s9 {
  background: var(--uk-sandstone);
  color: var(--text-primary);
  padding-block: var(--pad-section);
}

.s9-head h2 { margin-top: 0; max-width: 20ch; }

.s9-note {
  margin-top: 1.25rem;
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: .8rem;
  letter-spacing: 0;
  color: var(--text-secondary);
}

.s9-stack {
  display: grid;
  gap: clamp(1.5rem, 4vw, 2.5rem);
  margin-top: clamp(2.5rem, 6vw, 4rem);
}

.s9-card {
  position: relative;
  overflow: hidden;
  border-radius: var(--radius);
  /* The one shadow on the page. Overlapping cards need a depth cue that a
     1px border cannot give — without it the stack reads as a flat collage.
     Kept low-contrast and warm so it lifts rather than announces itself. */
  box-shadow: 0 18px 40px rgba(63, 36, 34, .18);
}

.s9-media {
  aspect-ratio: 4 / 3;
  /* Full-size stand-in when the file is missing, so the stack is testable
     with no photography at all. */
  background: linear-gradient(150deg, var(--uk-clay), var(--uk-brown));
}
.s9-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Opaque where the type sits, clear by two-thirds up, so the photograph is
   only darkened where it has to be. */
.s9-card-body {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: clamp(3.5rem, 10vh, 6rem) clamp(1.25rem, 3vw, 2.25rem) clamp(1.25rem, 3vw, 2rem);
  background: linear-gradient(180deg,
    rgba(30, 18, 14, 0) 0%,
    rgba(30, 18, 14, .62) 45%,
    rgba(30, 18, 14, .92) 100%);
}

.s9-cat {
  font-family: var(--font-display);
  font-style: normal;
  font-size: 1.1rem;
  font-weight: 400;
  line-height: 1.2;
  letter-spacing: 0;
  color: color-mix(in srgb, var(--text-on-dark) 78%, transparent);
}

.s9-card-body h3 {
  margin-top: 0.2rem;
  font-weight: 400;
  font-size: calc(clamp(1.6rem, 2.6vw, 2.3rem) * var(--k-display));
  line-height: 1.05;
  color: var(--text-on-dark);
}

.s9-loc {
  margin-top: 0.35rem;
  font-size: var(--fs-small);
  color: var(--text-on-dark);
}

.s9-link {
  display: inline-block;
  margin-top: 1rem;
  font-family: var(--font-body);
  font-size: .9rem;
  font-weight: 500;
  letter-spacing: 0;
  color: var(--uk-ochre);
}
.s9-link span {
  display: inline-block;
  transition: transform var(--dur-fast) var(--ease-out);
}
@media (hover: hover) {
  .s9-link:hover span { transform: translateX(4px); }
}

.s9-actions {
  margin-top: clamp(2.5rem, 6vw, 4rem);
  text-align: center;
}

/* ---------- stack mode: 1100px and up, with motion allowed ---------- */

.s9.is-stack .s9-stage {
  min-height: 100vh;
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-top: var(--hdr-safe);     /* pinned at top top — see Section 7 */
}

.s9.is-stack .s9-stack {
  position: relative;
  display: block;
  /* Was a flat 58vh that took no account of the heading and note sitting
     above it inside the same 100svh stage, so the bottom of every card fell
     past the fold. */
  height: max(260px, min(58vh,  calc(100vh  - 15rem - var(--hdr-safe))));
  height: max(260px, min(58svh, calc(100svh - 15rem - var(--hdr-safe))));
}

/* Centred with left/right/margin rather than a translate, because GSAP owns
   the transform property here — scale, y and yPercent are all in play, and a
   centring translateX would be the first thing overwritten. */
.s9.is-stack .s9-card {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  /* Third term holds the card to its photograph's 4:3 shape once height is
     the binding constraint, so the image is scaled down rather than cropped
     to a letterbox slot. */
  width: min(74vw, 1000px, calc(max(260px, min(58vh,  calc(100vh  - 15rem))) * 4 / 3));
  width: min(74vw, 1000px, calc(max(260px, min(58svh, calc(100svh - 15rem))) * 4 / 3));
  height: 100%;
  margin-inline: auto;
}

.s9.is-stack .s9-media {
  aspect-ratio: auto;
  height: 100%;
}

/* ============================================================
   Section 7 — Design to execution. The signature interaction.

   Everything below the "wipe mode" divider applies only once the
   script has confirmed a 1100px viewport and added .is-wipe. The
   rules above it are the fallback and are what mobile, reduced
   motion and a failed CDN all get: five pairs down the page, design
   beside built, every label readable, no clip, no pin.

   Measured on umber: cream headings, captions and body 11.5:1,
   lifted clay eyebrow and mono labels 5.2:1, ochre wipe edge 4.4:1
   as a graphic against a 3:1 floor.
   ============================================================ */

.s7 {
  background: var(--uk-umber);
  color: var(--text-on-dark);
  padding-block: var(--pad-section);
}

.s7-head h2 {
  margin-top: 0;
  font-weight: 300;
  color: var(--text-on-dark);
  max-width: 18ch;
}
/* Round 12: the sans is wider, so the 18ch measure would let "Resolved"
   climb onto the first line; the answer stays whole on its own line. */
.s7-head h2 em { white-space: nowrap; }

.s7-frame { margin-top: clamp(2.5rem, 6vw, 4rem); }

/* The mono stage headers belong to the wipe; in the stacked fallback each
   pair carries its own caption, which says the same thing closer to hand. */
.s7-meta { display: none; }

.s7-pairs {
  display: grid;
  gap: clamp(2.5rem, 6vw, 4rem);
  padding-inline: var(--pad-page);
  max-width: var(--maxw);
  margin-inline: auto;
}

.s7-media {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2px;
}

.s7-layer {
  position: relative;
  overflow: hidden;
  aspect-ratio: 4 / 3;
}
.s7-layer img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Solid blocks stand in when a file is missing, so the mechanic is testable
   with no photography at all — and the two colours differ, so the wipe reads
   as a wipe rather than as nothing happening. */
.s7-layer--design { background: var(--uk-clay); }
.s7-layer--exec   { background: var(--uk-brown); }

/* The render treatment, per docs/07-DEMO-MODE.md §4: the same photograph
   reads as a visualisation on the design side and stays untouched on the
   execution side, which is what makes the geometry match perfectly. */
.s7-layer--design img { filter: saturate(0.72) contrast(1.12) brightness(1.08); }
.s7-layer--design::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg, rgba(200, 215, 235, .15), rgba(255, 250, 240, .10));
  mix-blend-mode: screen;
  pointer-events: none;
}

.s7-edge { display: none; }

.s7-caption {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 0.9rem;
  font-family: var(--font-display);
  font-weight: 300;
  font-size: var(--fs-small);
  color: var(--text-on-dark);
}
.s7-caption span:last-child { text-align: right; }

.s7-support {
  margin-top: clamp(3rem, 8vw, 5rem);
  text-align: center;
}
.s7-support p {
  margin-inline: auto;
  max-width: 62ch;
  color: var(--text-on-dark);
}
.s7-support p + p { margin-top: 1.25rem; }

/* ---------- wipe mode: 1100px and up, with motion allowed ---------- */

.s7.is-wipe .s7-stage {
  /* height, not min-height: "at least a viewport" let the contents push the
     stage taller than the screen it is pinned to, which is the same bug in a
     different place. */
  height: 100vh;
  height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: clamp(.75rem, 2vh, 1.75rem);
  /* --hdr-safe on top: the stage is pinned at top top, so without it the
     heading sits under the fixed header for the whole of the pin — which is
     the entire section. box-sizing is border-box, so this comes out of the
     100svh and the stage still fills exactly one screen; justify-content
     then centres the contents in what is left, below the bar. */
  padding-top: max(var(--hdr-safe), clamp(.75rem, 2vh, 1.5rem));
  padding-bottom: clamp(.75rem, 2vh, 1.5rem);
}
.s7.is-wipe .s7-head { flex: 0 0 auto; }
.s7.is-wipe .s7-frame { flex: 0 0 auto; }

.s7.is-wipe .s7-frame {
  /* The meta row above the image, the caption below it, and a little air. */
  /* --s7-avail is written by js/main.js: the height genuinely left inside
     the pinned stage once the heading, the meta row and the caption have
     taken theirs. It cannot be computed in CSS because the heading is fluid
     type — clamp(2rem, 4.5vw, 3.25rem) over two lines is a different height
     on every screen, and guessing at it is what left the image running off
     the bottom. The fallback only applies for the first frame before the
     measurement lands. */
  --s7-avail: 52vh;
  /* Width was the only constraint before, so on any screen shorter than about
     1.9x its width the 16:9 image ran straight off the bottom of the viewport.
     The third term is the width at which a 16:9 box exactly fills the height
     that is actually left, so whichever limit bites first now wins. */
  /* Third term is the width at which a 16:9 box exactly fills the height
     that is actually free, so the whole image always lands inside the fold. */
  width: min(70vw, 1100px, calc(var(--s7-avail) * 16 / 9));
  max-width: 100%;
  margin-inline: auto;
}

.s7.is-wipe .s7-meta {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  margin-bottom: 0.9rem;
  font-family: var(--font-display);
  font-style: normal;
  font-weight: 400;
  font-size: clamp(1.05rem, 1.4vw, 1.3rem);
  letter-spacing: 0;
  color: var(--clay-on-dark);
}
.s7.is-wipe .s7-meta-right { display: flex; align-items: baseline; gap: 1.25rem; }
.s7.is-wipe .s7-counter {
  font-family: var(--font-body);
  font-style: normal;
  font-size: .85rem;
  font-variant-numeric: tabular-nums;
}
.s7.is-wipe .s7-counter-now { color: var(--text-on-dark); }

/* First pair stays in flow and gives the stack its height; the other four
   overlay it. Same content shape, so the heights agree. */
.s7.is-wipe .s7-pairs {
  display: block;
  position: relative;
  padding-inline: 0;
}
.s7.is-wipe .s7-pair {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
}
.s7.is-wipe .s7-pair:first-child { position: relative; }

.s7.is-wipe .s7-media {
  display: block;
  position: relative;
  aspect-ratio: 16 / 9;
}
.s7.is-wipe .s7-layer {
  position: absolute;
  inset: 0;
  aspect-ratio: auto;
}

.s7.is-wipe .s7-edge {
  display: block;
  position: absolute;
  top: 0;
  bottom: 0;
  width: 2px;
  margin-left: -1px;
  background: var(--uk-ochre);
}

.s7.is-wipe .s7-caption { font-size: var(--fs-body); }

/* ============================================================
   Section 5 — Why Urbankriti. Bone. Measured on #FAF7F2: umber
   titles 13.2:1, deepened taupe body 6.5:1, deepened clay
   eyebrow and numbers 6.3:1. Raw clay would have been 3.1:1
   here, which is why the numbers use --clay-on-light.
   ============================================================ */

.s5 {
  background: var(--uk-bone);
  color: var(--text-primary);
  padding-block: var(--pad-section);
}

/* Label above, heading below, exactly as every other section header on the
   page is set. --fs-h2 and the 20ch measure are the shared values, not
   section-specific ones: this header should be indistinguishable in
   structure from Section 3's or Section 10's.

   ---- the appear ----

   Both lines animate in: the label arrives first, then the heading's own
   words lift into place behind it. js/main.js splits the words and runs the
   timeline once on scroll.

   .is-split is the script taking ownership. Without it — no JS, no GSAP —
   the header is ordinary visible type, which is why no hidden state lives on
   these elements by default. */
.s5-header { margin-bottom: clamp(2.5rem, 6vw, 4rem); }
.s5-header h2 { margin-top: 1.5rem; max-width: 20ch; }

.s5-header.is-split .s5-word { display: inline-block; }
.s5-header.is-split .s5-word-inner { display: inline-block; will-change: transform; }

/* ---- The sheet ---------------------------------------------------------

   Six plates that arrive loose and settle into one ruled drawing.

   The sheet is the <ul> itself with a title block under it — the plates are
   the six <li>s, not a decorative shell around them. That is what makes the
   no-JS and reduced-motion paths correct rather than merely acceptable:
   there is no "assembled" state to reach, because assembled is how it is
   built. The script only ever adds the loose state and takes it away again.

   Ruled, not boxed. Internal hairlines between the plates and a border round
   the outside give a drawing sheet; six bordered cards would give a pricing
   table. The difference is entirely in which edges are shared. */

.s5-sheet {
  border: 1px solid var(--border);
  border-radius: var(--radius);

  /* A drafting grid under the plates. Borrowed technique — 21st.dev's
     grid-pattern cards build the same texture into a card surface — but at a
     fraction of their weight, because here it has to sit under body copy
     rather than under a headline.

     .045 on white is about the faintest a hairline can be and still register;
     the point is that you feel the sheet is ruled without ever reading the
     rules. Anything heavier and it competes with the hairlines that actually
     divide the plates, which are load-bearing.

     24px pitch, chosen against the type rather than for a round number: body
     text here is 15px at 1.7, so a 24px grid never lands in lockstep with the
     baselines and cannot set up a moire against them. */
  background-color: var(--uk-white);
  background-image:
    repeating-linear-gradient(to right,  rgba(92, 58, 33, .045) 0 1px, transparent 1px 24px),
    repeating-linear-gradient(to bottom, rgba(92, 58, 33, .045) 0 1px, transparent 1px 24px);
  /* Clips the plates' loose state so nothing escapes the sheet on its way
     in — the drawing is assembling itself, not being flown in from off the
     page. */
  overflow: hidden;
}

/* The rules between plates are the plates' own borders, collapsed onto each
   other with a negative gap. Cheaper and more robust than pseudo-elements:
   the lines cannot fall out of register with the cells they divide, because
   they ARE the cells. */
.s5-grid {
  display: grid;
  gap: 0;
}

.s5-item {
  /* Overrides the old card treatment: the top border becomes the shared rule
     and the padding becomes the plate's own margin inside the sheet. */
  position: relative;
  border-top: 1px solid var(--border);
  padding: clamp(1.5rem, 3.2vw, 2.25rem);
  /* Each plate declares its own accent once; the mark, the number and the
     hover wash all read from here, so recolouring a plate is one line. */
  --acc: var(--clay-on-light);
  transition: background-color var(--dur-base) var(--ease-out);
}

.s5-item:nth-child(1) { --acc: var(--acc-1); }
.s5-item:nth-child(2) { --acc: var(--acc-2); }
.s5-item:nth-child(3) { --acc: var(--acc-3); }
.s5-item:nth-child(4) { --acc: var(--acc-4); }
.s5-item:nth-child(5) { --acc: var(--acc-5); }
.s5-item:nth-child(6) { --acc: var(--acc-6); }

/* A wash of the plate's own colour on hover. 5% — enough to feel the plate
   pick up its colour, far too little to touch the body text underneath,
   which stays on white and keeps its measured contrast. */
@media (hover: hover) {
  .s5-item:hover { background-color: color-mix(in srgb, var(--acc) 5%, transparent); }
}

/* A colour bar down the leading edge, drawn on hover. The plates are ruled
   into one sheet, so the accent arrives as an edge rather than as a border
   round a card — it marks the plate without unpicking the drawing. */
.s5-item::before {
  content: "";
  position: absolute;
  left: 0;
  top: -1px;
  bottom: 0;
  width: 2px;
  background: var(--acc);
  transform: scaleY(0);
  transform-origin: top center;
  transition: transform var(--dur-base) var(--ease-out);
}
@media (hover: hover) {
  .s5-item:hover::before { transform: scaleY(1); }
}
/* No rule above the first plate — the sheet's own border is already there. */
.s5-item:first-child { border-top: 0; }

/* ---- The six marks ------------------------------------------------------

   Each plate animates the thing it says. That is the whole brief for these:
   not six decorated boxes, but six different ideas each shown moving in the
   way it actually works — a line being built, a sequence falling into order,
   options resolving to one, parties merging, checks running along the way,
   updates arriving on a beat.

   One shared motif repeated six times would be decoration. Six different
   mechanics is the section having something to say per plate, which is the
   individuality the grid was missing.

   All CSS keyframes on inline SVG. No script and no library: these are
   ambient, they run forever, and anything driven from JS would mean six
   timers running whether or not the section is on screen.

   Every animation is slow (5–9s) and low-contrast on purpose. Six things
   moving at once in a reader's peripheral vision is a nervous page; at this
   speed they read as alive rather than as animated. */

/* The mark sits on a wash of its own colour. Six pale colour blocks down the
   sheet is what actually makes the section read as colourful from across a
   room — a 1.6px stroke, however saturated, is not enough area to register.

   9% keeps it a tint rather than a swatch: the sheet stays a drawing with
   marks on it, not six coloured cards. The mark itself is full strength
   against it, so nothing is lost to the wash. */
.s5-glyph {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 104px;
  height: 64px;
  margin-bottom: 1.25rem;
  border-radius: 3px;
  background: color-mix(in srgb, var(--acc) 9%, transparent);
  transition: background-color var(--dur-base) var(--ease-out);
}

@media (hover: hover) {
  .s5-item:hover .s5-glyph {
    background: color-mix(in srgb, var(--acc) 16%, transparent);
  }
}

.s5-glyph svg {
  width: 76px;
  height: 44px;
  overflow: visible;
}

/* Shared stroke language, so six different mechanics still read as one
   family. Clay for the working parts, deepened for anything carrying weight. */
.s5-glyph path,
.s5-glyph line,
.s5-glyph rect,
.s5-glyph circle {
  /* currentColor, set once on the plate, so a mark cannot end up a different
     colour from its own number. */
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  fill: none;
}

.s5-glyph { color: var(--acc); }

/* Marks scale up a touch when their plate is hovered — the plate answers,
   without anything moving far enough to disturb the sheet. */
.s5-glyph svg {
  transform-origin: center;
  transition: transform var(--dur-base) var(--ease-out);
}
@media (hover: hover) {
  .s5-item:hover .s5-glyph svg { transform: scale(1.06); }
}

/* ---- 01  the drawing becomes the build ---- */
.gl-sketch {
  stroke-dasharray: 3 4;
  opacity: .55;
}
.gl-built {
  stroke: currentColor;
  stroke-width: 2;
  stroke-dasharray: 96;
  stroke-dashoffset: 96;
  animation: gl-build 6s var(--ease-out) infinite;
}
/* Draws, holds while it is a finished edge, then clears and begins again.
   The hold is most of the cycle — the built state is the point, not the
   drawing of it. */
@keyframes gl-build {
  0%        { stroke-dashoffset: 96; }
  45%, 78%  { stroke-dashoffset: 0; }
  92%, 100% { stroke-dashoffset: -96; }
}

/* ---- 02  the sequence falls into order ---- */
.gl-seq {
  fill: currentColor;
  stroke: none;
  transform-box: fill-box;
  transform-origin: left center;
  animation: gl-seq 5.5s var(--ease-out) infinite;
}
.gl-seq--2 { animation-delay: .28s; }
.gl-seq--3 { animation-delay: .56s; }
/* Each bar arrives from behind the one before it and settles into its slot,
   which is what "dependencies considered before work begins" looks like. */
@keyframes gl-seq {
  0%, 8%    { transform: translateX(-14px) scaleX(.3); opacity: 0; }
  26%, 76%  { transform: translateX(0) scaleX(1); opacity: 1; }
  94%, 100% { transform: translateX(0) scaleX(1); opacity: 0; }
}

/* ---- 03  the options resolve to one ---- */
.gl-cost {
  fill: currentColor;
  stroke: none;
  opacity: .38;
  transform-box: fill-box;
  transform-origin: bottom center;
  animation: gl-cost 6.5s var(--ease-out) infinite;
}
.gl-cost--2 { animation-delay: .5s; }
.gl-cost--3 { animation-delay: 1s; }
.gl-cost-rule { stroke: var(--border-strong); stroke-width: 1; }
/* Each option is weighed in turn — lifts, brightens, settles back. Nothing
   is eliminated: the decision is informed, not narrowed for you. */
@keyframes gl-cost {
  0%, 12%   { opacity: .38; transform: scaleY(1); }
  22%, 34%  { opacity: 1;   transform: scaleY(1.08); }
  46%, 100% { opacity: .38; transform: scaleY(1); }
}

/* ---- 04  many parties, one plan ---- */
.gl-merge {
  stroke-dasharray: 80;
  stroke-dashoffset: 80;
  animation: gl-merge 7s var(--ease-out) infinite;
}
.gl-merge--1 { animation-delay: 0s; }
.gl-merge--2 { animation-delay: .12s; }
.gl-merge--3 { animation-delay: .24s; }
.gl-merge--4 { animation-delay: .36s; }
.gl-merge--5 { animation-delay: .48s; }
.gl-merge-hub {
  fill: currentColor;
  stroke: none;
  transform-box: fill-box;
  transform-origin: center;
  animation: gl-hub 7s var(--ease-out) infinite;
}
/* The five draw inward and arrive; the hub only appears once they are all
   there, because the single point of contact is a consequence of the
   coordination, not a thing that exists beforehand. */
@keyframes gl-merge {
  0%        { stroke-dashoffset: 80; }
  40%, 80%  { stroke-dashoffset: 0; }
  95%, 100% { stroke-dashoffset: -80; }
}
@keyframes gl-hub {
  0%, 34%   { opacity: 0; transform: scale(.3); }
  46%, 80%  { opacity: 1; transform: scale(1); }
  92%, 100% { opacity: 0; transform: scale(.3); }
}

/* ---- 05  checked along the way ---- */
.gl-stage-rule { stroke: var(--border-strong); stroke-width: 1; }
.gl-check circle { stroke: currentColor; fill: var(--uk-bone); }
.gl-check path   { stroke: currentColor; stroke-width: 1.9; }
.gl-check {
  transform-box: fill-box;
  transform-origin: center;
  animation: gl-check 7s var(--ease-out) infinite;
}
.gl-check--2 { animation-delay: .5s; }
.gl-check--3 { animation-delay: 1s; }
.gl-check--4 { animation-delay: 1.5s; }
.gl-check--5 { animation-delay: 2s; }
/* Five checks running left to right along the whole line — the section's
   point is that quality is not one inspection at the end. */
@keyframes gl-check {
  0%, 6%    { opacity: .28; }
  16%, 82%  { opacity: 1; }
  94%, 100% { opacity: .28; }
}

/* ---- 06  updates on a regular beat ---- */
.gl-beat-rule { stroke: var(--border-strong); stroke-width: 1; }
.gl-tick { stroke: currentColor; opacity: .45; }
.gl-pulse {
  fill: currentColor;
  stroke: none;
  animation: gl-pulse 5s linear infinite;
}
/* Linear, not eased, and evenly spaced — "structured updates" means a
   rhythm you can rely on, so the one thing this must not do is arrive at
   irregular intervals. */
@keyframes gl-pulse {
  0%        { cx: 16px; opacity: 0; }
  8%        { cx: 16px; opacity: 1; }
  25%       { cx: 32px; }
  50%       { cx: 48px; }
  75%       { cx: 64px; }
  92%, 100% { cx: 64px; opacity: 0; }
}

/* Reduced motion: every mark holds still in its resolved state. They are
   diagrams first and animations second, so stopping them costs the meaning
   nothing — the built edge is drawn, the sequence is in order, the parties
   are merged, the checks are checked. */
@media (prefers-reduced-motion: reduce) {
  .gl-built, .gl-seq, .gl-cost, .gl-merge, .gl-merge-hub, .gl-check, .gl-pulse {
    animation: none;
  }
  .gl-built  { stroke-dashoffset: 0; }
  .gl-seq    { opacity: 1; transform: none; }
  .gl-cost   { opacity: .55; }
  .gl-merge  { stroke-dashoffset: 0; }
  .gl-merge-hub { opacity: 1; }
  .gl-check  { opacity: 1; }
  .gl-pulse  { cx: 48px; opacity: 1; }
}

/* The number takes its plate's colour too, so the accent is stated twice —
   once in the mark and once in the index — and the plate reads as a unit
   rather than as a coloured drawing above unrelated black text. */
/* p.s5-item-num, not .s5-item-num. The number is a <p>, so the plate's body
   rule `.s5-item p` (0,1,1) outranked a bare class (0,1,0) and painted all
   six numbers --text-secondary — the accent was being set and silently
   thrown away. The type selector buys the one point that settles it. */
p.s5-item-num {
  font-family: var(--font-mono);
  font-size: var(--fs-small);
  font-weight: 600;
  letter-spacing: .14em;
  color: var(--acc);
}

.s5-item h3 {
  margin-top: 1rem;
  font-weight: 400;
  line-height: 1.25;
}

/* :not(.s5-item-num) — the number is a <p> too, and bumping its selector to
   p.s5-item-num only drew level with this one (both 0,1,1), so source order
   handed it straight back. Excluding it here is the fix that actually holds:
   the body rule now cannot match the number at all, whatever the order. */
.s5-item p:not(.s5-item-num) {
  margin-top: 0.75rem;
  font-size: var(--fs-small);
  color: var(--text-secondary);
}

/* The title block. What turns six plates into one document, and the reason
   the figure says "one coordinated plan" instead of the section having to.

   Sandstone rather than white so it reads as a different register from the
   plates above — the strip a drawing set puts its identity in, not another
   cell of content. */
.s5-titleblock {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: .5rem 1.5rem;
  padding: clamp(.9rem, 2vw, 1.15rem) clamp(1.5rem, 3.2vw, 2.25rem);
  border-top: 1px solid var(--border-strong);
  background: var(--uk-sandstone);
}

.s5-titleblock-mark {
  font-family: var(--font-logo);
  /* 400: the served fallback for --font-logo has one weight, and asking for
     500 only buys a synthesised one. */
  font-weight: 400;
  font-size: 1.05rem;
  letter-spacing: .03em;
  color: var(--text-primary);
}

.s5-titleblock-note {
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: .8rem;
  letter-spacing: 0;
  color: var(--clay-on-light);
}

@media (min-width: 700px) {
  .s5-grid { grid-template-columns: repeat(2, 1fr); }
  /* Vertical rules come from the plates too. :nth-child(even) takes a left
     border rather than every plate taking a right one, so the sheet never
     ends up with a rule against its own outer edge. */
  .s5-item:nth-child(even) { border-left: 1px solid var(--border); }
  .s5-item:nth-child(-n+2) { border-top: 0; }
}

@media (min-width: 1100px) {
  .s5-grid { grid-template-columns: repeat(3, 1fr); }
  /* Reset the two-column rules before laying the three-column ones, or a
     plate keeps a border from the narrower arrangement. */
  .s5-item:nth-child(even) { border-left: 0; }
  .s5-item:nth-child(-n+2) { border-top: 1px solid var(--border); }
  .s5-item:not(:nth-child(3n+1)) { border-left: 1px solid var(--border); }
  .s5-item:nth-child(-n+3) { border-top: 0; }
}

/* The loose state, added by js/main.js and animated away. It lives here
   rather than in the script so the offsets are readable as design values
   instead of magic numbers in a tween — and so that if the script ever
   fails between adding the class and running the timeline, the plates are
   still legible where they sit. */
.s5-sheet.is-loose .s5-item {
  opacity: 0;
}

/* ============================================================
   TOUCH TARGETS

   Anything tapped with a thumb needs a box a thumb can hit. The link styles
   on this page are set for a cursor, which lands several of them at around
   20px tall — fine for a mouse, a miss on a phone.

   The padding is vertical only and is applied with a coarse-pointer query,
   so a desktop layout is untouched: this fires on touch devices, not merely
   on small windows. Line-height stays put so nothing reflows.
   ============================================================ */

/* `pointer: coarse` is the right question to ask — it is about the input
   device, not the window size. But a narrow window on a desktop is also
   where a phone layout gets tested and shipped from, so the same sizing
   applies below 900px regardless of pointer. Either condition is enough. */
@media (pointer: coarse), (max-width: 899px) {
  .hdr-drawer-link,
  .ftr-links a,
  .ftr-legal a,
  .s3-card-body .link-arrow,
  .s12-card a,
  .link-arrow {
    /* min-height rather than a fixed height: these must still grow with
       their own text when a label wraps. */
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }

  /* The drawer's accordion toggles are the deepest in the nav and the most
     likely to be mis-tapped. */
  .hdr-acc-toggle,
  .hdr-acc-panel a { min-height: 44px; }

  /* The testimonial controls are icon-only, so they have no text to give
     them height. */
  .s11-btn { min-width: 44px; min-height: 44px; }

  /* The rest, found by measuring rather than guessing: the project links and
     every unclassed <a> inside the drawer, the dropdowns and the footer.
     Naming selectors from memory missed all seventeen of them. */
  .s9-link,
  .hdr-sub a,
  .hdr-acc-panel a,
  .site-footer a {
    min-height: 44px;
    display: inline-flex;
    align-items: center;
  }
}

/* ============================================================
   SHORT VIEWPORTS

   A laptop is not a small screen, it is a SHORT one — 1920x830 is wider
   than most desktops and 250px shorter. Everything above sizes on width, so
   that combination gets desktop-sized type in a viewport that cannot hold
   it, and the pinned sections push their own content past the fold.

   Width-based breakpoints cannot see this at all: at 1920px wide every
   min-width query says "you have plenty of room". Only a height query can.

   These rules tighten the vertical rhythm of the pinned stages and nothing
   else. They do not fire on a desktop monitor.
   ============================================================ */

@media (max-height: 900px) and (min-width: 1100px) {
  /* The section rhythm is the biggest single consumer of height on the page
     and the easiest to give back. */
  :root { --pad-section: clamp(3rem, 6vw, 5rem); }

  /* The heading blocks inside pinned stages. Each of these sits above a
     piece of media that is sized from whatever is left, so every pixel
     saved here goes straight to the image. */
  .s7.is-wipe .s7-head,
  .s9.is-stack .s9-head { margin-bottom: clamp(1rem, 2.5vh, 2rem); }

  .s7.is-wipe .s7-head h2,
  .s9.is-stack .s9-head h2 {
    margin-top: 0;
    font-size: calc(clamp(2rem, 6vh, 3.4rem) * var(--k-display));
    line-height: 1;
  }

  /* The intro/note lines under those headings. On a short screen they are
     the first thing that can afford to be smaller. */
  .s9.is-stack .s9-note,
  .s7.is-wipe .s7-caption { font-size: .8125rem; }

  /* Section 3's sticky cards are sized off the viewport already; the stack
     offset is what needs to come in, or the cards start below the fold. */
  .s3-stack { --stack-top: max(var(--hdr-safe), 6.5rem); }

  /* The project cards are capped at 58vh for a tall screen, which on a short
     one is most of the viewport before the heading above them is counted.
     48vh leaves room for the heading, the note and the section's own
     padding without the bottom card row falling past the fold. */
  .s9.is-stack .s9-stack {
    height: max(220px, min(48vh,  calc(100vh  - 14rem - var(--hdr-safe))));
    height: max(220px, min(48svh, calc(100svh - 14rem - var(--hdr-safe))));
  }

  /* Section 4's tree gets the same treatment: it is sized from height
     already, so this just stops the copy beside it setting the floor. */
  .s4-copy h2 { font-size: clamp(2.1rem, 6vh, 3rem); }
  .s4-tree { width: min(100%, calc(52vh * 1200 / 660)); }
}

/* Shorter still — 720p laptops and anything with a lot of browser chrome. */
@media (max-height: 760px) and (min-width: 1100px) {
  :root { --pad-section: clamp(2.5rem, 5vw, 4rem); }

  .s7.is-wipe .s7-head h2,
  .s9.is-stack .s9-head h2 { font-size: calc(clamp(1.7rem, 5vh, 2.6rem) * var(--k-display)); }

  .s4-tree { width: min(100%, calc(48vh * 1200 / 660)); }
}

/* ============================================================
   Interlude 1 — cinematic band between Sections 5 and 6.
   Measured over the scrim's darkest region, where the text sits:
   cream line 10.3:1, lifted clay eyebrow 4.7:1. Both figures are
   against the gradient fallback; a bright photograph dropped in
   later would lower them, so check them again then.
   ============================================================ */

.interlude {
  position: relative;
  display: flex;
  align-items: flex-end;
  overflow: hidden;
  /* svh so a phone's collapsing address bar cannot resize the band
     mid-scroll; the vh line above it is the fallback. */
  height: 58vh;
  height: 58svh;
  min-height: 360px;
  /* Shows through when the photograph is missing, so the band is testable
     before any asset exists. */
  background: linear-gradient(160deg, var(--uk-brown), var(--uk-umber));
}

/* No inset shorthand: the parallax below overrides height and top, and a
   bottom:0 from inset would over-constrain the box. */
.interlude-media {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Third stop is beyond the brief's .45 → .65. The band's copy sits at the
   bottom, and a .65 ceiling leaves the eyebrow at 4.4:1 — just under the
   floor. Deepening only the last third fixes it without lightening the
   image overall. */
.interlude::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
    rgba(63, 36, 34, .45) 0%,
    rgba(63, 36, 34, .65) 65%,
    rgba(63, 36, 34, .80) 100%);
}

.interlude-inner {
  position: relative;
  z-index: 1;
  width: 100%;
  padding-bottom: clamp(2.5rem, 6vw, 4.5rem);
}

.interlude-line {
  margin-top: 1.25rem;
  font-family: var(--font-display);
  font-weight: 300;
  font-size: var(--fs-h3);
  line-height: 1.3;
  letter-spacing: -0.01em;
  color: var(--text-on-dark);
  max-width: 30ch;
}

@media (min-width: 768px) {
  .interlude {
    height: 70vh;
    height: 70svh;
  }
}

/* Oversized only where the parallax runs, so the ±24px travel can never
   expose an edge. Below 1100px the image is exactly 100% and static. */
@media (min-width: 1100px) {
  .interlude-media {
    height: 112%;
    top: -6%;
  }
}

/* ============================================================
   Section 6 — The Urbankriti Method. Sandstone. Measured on
   #E5D9C3: umber titles 10.1:1, deepened taupe body 4.9:1,
   deepened clay numbers and counter 4.8:1, terracotta active
   number and rail fill 6.3:1, ochre button 5.3:1.

   The active step is signalled by colour DEPTH, not by fading
   the others out — see the note on .s6-step below.
   ============================================================ */

.s6 {
  background: var(--uk-sandstone);
  color: var(--text-primary);
  padding-block: var(--pad-section);
}

.s6-grid {
  display: grid;
  gap: clamp(2.5rem, 6vw, 4rem);
  align-items: start;   /* required, or the sticky column stretches and never sticks */
}

.s6-rail h2 { margin-top: 0; max-width: calc(16ch * var(--k-measure)); font-size: calc(clamp(2.4rem, 4.6vw, 4rem) * var(--k-display)); line-height: 1; }

/* ---- Progress rail ----
   Horizontal by default, vertical from 900px. One custom property drives
   both, so the script sets a single number and never has to know which
   orientation is on screen. */
.s6-progress {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-top: 2.5rem;
}

.s6-track {
  position: relative;
  flex: 1;
  height: 2px;
  background: var(--border);
}

.s6-fill {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  width: var(--progress, 0%);
  background: var(--uk-terracotta);
  transition: width .45s var(--ease-out), height .45s var(--ease-out);
}

.s6-counter {
  font-family: var(--font-body);
  font-weight: 500;
  font-size: .85rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  color: var(--clay-on-light);
  white-space: nowrap;
}
.s6-counter-now { color: var(--uk-terracotta); }

/* ---- Steps ---- */

.s6-step {
  display: grid;
  gap: 0.5rem;
}

.s6-step + .s6-step {
  margin-top: clamp(2.5rem, 5vw, 4rem);
  padding-top: clamp(2.5rem, 5vw, 4rem);
  border-top: 1px solid var(--border);
}

.s6-step-num {
  font-family: var(--font-display);
  font-size: var(--fs-number);
  font-weight: 400;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  color: var(--clay-on-light);
  transition: color var(--dur-fast) ease;
}

.s6-step-body h3 { font-weight: 400; font-size: calc(clamp(1.5rem, 2.2vw, 2rem) * var(--k-display)); }

.s6-step-body p {
  margin-top: 0.75rem;
  color: var(--text-secondary);
  max-width: 62ch;
}

.s6-cta { margin-top: clamp(3rem, 6vw, 4.5rem); }

@media (min-width: 700px) {
  .s6-step {
    grid-template-columns: minmax(5rem, auto) 1fr;
    gap: clamp(1.5rem, 3vw, 2.5rem);
    align-items: start;
  }
}

@media (min-width: 900px) {
  .s6-grid { grid-template-columns: 38fr 62fr; }

  .s6-rail {
    position: sticky;
    top: 25vh;
  }

  .s6-progress {
    align-items: flex-start;
    margin-top: 3rem;
  }
  .s6-track {
    flex: 0 0 auto;
    width: 2px;
    height: clamp(140px, 22vh, 240px);
  }
  .s6-fill {
    width: 100%;
    height: var(--progress, 0%);
  }

}

/* The active step advances rather than the others receding: its title deepens
   from the body colour to full umber and its number goes terracotta. Dimming
   the rest with opacity — the obvious way to do this — drops them to about
   2:1 on sandstone, which would make six of the seven steps unreadable at any
   given moment. Both states here clear 4.5:1.

   OUTSIDE the 900px query, deliberately. This block used to sit inside it, so
   on a phone initMethod faithfully toggled .is-active down all seven steps
   and absolutely nothing was styled to respond — the section looked inert
   because visually it was. The observer was never the problem. */
.s6-step-body h3 {
  color: var(--text-secondary);
  transition: color var(--dur-fast) ease;
}
.s6-step.is-active .s6-step-body h3 { color: var(--text-primary); }
.s6-step.is-active .s6-step-num { color: var(--uk-terracotta); }

/* On a phone there is no sticky rail to show progress, so the active step
   marks itself: a clay edge that grows down its left side as it becomes the
   one being read. */
@media (max-width: 899px) {
  .s6-step {
    position: relative;
    padding-left: 1rem;
    transition: padding-left var(--dur-fast) var(--ease-out);
  }
  .s6-step::before {
    content: "";
    position: absolute;
    left: 0;
    top: .2em;
    bottom: .2em;
    width: 2px;
    background: var(--uk-clay);
    transform: scaleY(0);
    transform-origin: top center;
    transition: transform var(--dur-base) var(--ease-out);
  }
  .s6-step.is-active::before { transform: scaleY(1); }
}

/* ============================================================
   Section 11 — Testimonials. Bone ground, one quote featured.

   Calm on purpose. A quote, a hairline, an attribution, and a slim
   index beside it — no cards, no shadows, no quote-mark graphics.

   The three slides sit in one grid cell and crossfade. Opacity, not
   visibility: the outgoing slide has to stay painted while it fades,
   and CSS visibility would cut it dead on the first frame.
   aria-hidden keeps the two inactive quotes out of the accessibility
   tree, and pointer-events keeps them out of a cursor's way.

   Measured on bone — heading and quote 13.2:1, eyebrow, note, theme
   label and index 5.5:1, project reference and every inactive entry
   6.5:1. See the note on the index for why the inactive entries take
   a colour step rather than the requested opacity.
   ============================================================ */

.s11 {
  background: var(--uk-bone);
  color: var(--text-primary);
  padding-block: var(--pad-section);

  /* GSAP's power3.out as a bezier. The crossfade is done in CSS rather than
     in GSAP on purpose: a transition always lands on the value its class
     implies, so interrupting one — hovering two entries quickly, or clicking
     during an auto-advance — can never strand a slide at a half-faded inline
     opacity. Everything else on the page uses the house --ease-out; this one
     curve is here because the brief specifies power3.out for the swap. */
  --ease-power3: cubic-bezier(.165, .84, .44, 1);
}

.s11-head h2 { margin-top: 1.5rem; max-width: 20ch; }

/* Same treatment as the Section 9 placeholder note — a technical annotation
   about the build, not part of the client's copy. */
.s11-note {
  margin-top: 1.25rem;
  font-family: var(--font-mono);
  font-weight: 500;
  font-size: .8rem;
  letter-spacing: 0;
  color: var(--clay-on-light);
}

.s11-layout {
  display: grid;
  gap: clamp(2.5rem, 6vw, 4rem);
  margin-top: clamp(2.5rem, 6vw, 4rem);
}

/* ---- the featured quote ---- */

.s11-stage { display: grid; }

/* Three resting states, not two. A slide waiting its turn sits 14px low, so
   it rises as it arrives; the one being replaced gets .is-leaving and goes
   14px high, so it lifts away rather than sinking back the way it came.
   JS drops .is-leaving once the transition is spent. */
.s11-slide {
  grid-area: 1 / 1;
  opacity: 0;
  pointer-events: none;
  transform: translateY(14px);
  transition: opacity .5s var(--ease-power3),
              transform .5s var(--ease-power3);
}
.s11-slide.is-active {
  opacity: 1;
  pointer-events: auto;
  transform: none;
}
.s11-slide.is-leaving {
  opacity: 0;
  transform: translateY(-14px);
}

/* The editorial flourish, kept to a hairline. A big quotation glyph would
   compete with Section 7, which is the one thing on this page allowed to be
   loud. */
.s11-rule {
  display: block;
  width: 48px;
  height: 1px;
  margin-bottom: clamp(1.75rem, 3.5vw, 2.5rem);
  background: var(--uk-ochre);
}

.s11-quote p {
  max-width: 26ch;
  font-family: var(--font-display);
  font-size: clamp(1.5rem, 2.8vw, 2.4rem);
  font-weight: 300;
  line-height: 1.35;
  letter-spacing: -0.015em;
  color: var(--text-primary);
}

.s11-attr {
  max-width: 26ch;
  margin-top: clamp(2rem, 4vw, 2.75rem);
  padding-top: clamp(1.25rem, 2.5vw, 1.75rem);
  border-top: 1px solid var(--border);
}

.s11-theme {
  display: block;
  font-family: var(--font-mono);
  font-size: .8rem;
  font-weight: 500;
  letter-spacing: 0;
  color: var(--clay-on-light);
}

.s11-ref {
  display: block;
  margin-top: .6rem;
  font-style: normal;   /* <cite> italicises by default */
  color: var(--text-secondary);
}

.s11-sample {
  display: inline-block;
  margin-top: 1rem;
  padding: .1rem .5rem;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  font-family: var(--font-mono);
  font-size: 0.894rem;
  font-weight: 500;
  letter-spacing: 0;
  line-height: 1.6;
  color: var(--clay-on-light);
}

/* ---- the index ----

   The active entry advances rather than the others receding, the same
   decision as the Section 6 rail and for the same reason: opacity is far
   more expensive on a light ground than on a dark one. At the 0.5 the brief
   asks for, an inactive theme measures 2.9:1 on bone and its project
   reference 2.2:1, and unlike a hover state these two entries sit there for
   as long as the section is on screen. So the step is in colour — the active
   theme goes to full umber, the inactive ones hold the body colour — and
   every state clears 4.5:1. The ochre bar and the filling progress line
   carry the rest of the distinction. */

.s11-entry {
  position: relative;
}
.s11-entry + .s11-entry { border-top: 1px solid var(--border); }

.s11-entry-btn {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: clamp(1rem, 2vw, 1.5rem);
  align-items: baseline;
  width: 100%;
  text-align: left;
  padding: clamp(1.25rem, 2.5vw, 1.75rem) 0 clamp(1.25rem, 2.5vw, 1.75rem) clamp(1rem, 2vw, 1.25rem);
}

/* The active marker: a 2px ochre rule down the left edge. It is a second
   signal, not the only one — the theme's colour step carries the state on its
   own, which is what keeps this legible for anyone who cannot pick ochre out
   against bone. */
.s11-entry-btn::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--uk-ochre);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-fast) var(--ease-out);
}
.s11-entry.is-active .s11-entry-btn::before { transform: scaleX(1); }

.s11-entry-num {
  font-family: var(--font-mono);
  font-size: var(--fs-eyebrow);
  font-weight: 500;
  letter-spacing: .16em;
  color: var(--clay-on-light);
}

.s11-entry-theme {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-h3);
  font-weight: 400;
  line-height: 1.25;
  color: var(--text-secondary);
  transition: color var(--dur-fast) ease;
}
.s11-entry.is-active .s11-entry-theme { color: var(--text-primary); }

.s11-entry-ref {
  display: block;
  margin-top: .4rem;
  font-size: var(--fs-small);
  color: var(--text-secondary);
}

@media (hover: hover) {
  .s11-entry-btn:hover .s11-entry-theme { color: var(--text-primary); }
}

/* ---- the progress line ----
   Sits on the entry's own bottom edge, so what is filling is visibly the row
   you are looking at. Only the active entry's line ever moves. */
.s11-entry-progress {
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;   /* over the divider, not above it */
  height: 1px;
  overflow: hidden;
}
.s11-entry-fill {
  display: block;
  width: 100%;
  height: 100%;
  background: var(--uk-ochre);
  transform: scaleX(0);
  transform-origin: left center;
}

/* Nothing advances on its own under reduced motion, so a line counting down
   to an event that never comes would be a lie. The entries are the whole
   navigation there. */
@media (prefers-reduced-motion: reduce) {
  .s11-entry-progress { display: none; }
}

@media (min-width: 900px) {
  .s11-layout {
    grid-template-columns: 60fr 40fr;
    align-items: start;
  }
}

/* ============================================================
   Section 12 — Insights. Umber ground, three cards.

   Deliberately the plainest section on the page: image, category,
   title, link. Same card mechanics as Section 3 — 16:10 wrapper
   carrying the crop, the clay placeholder fill and the overflow that
   clips the hover zoom.

   The card keeps the section's own umber rather than a lifted dark
   tone, and that is a contrast decision, not a stylistic one. The
   category is the tightest thing here: ochre only just clears the
   floor on umber, and every step the card is lifted spends what
   little margin it has. On --surface-dark-2, the lifted tone this
   file uses for image mounts, even the corrected ochre falls to
   3.9:1. On flat umber it holds 4.8:1. So the border does the work
   of separating the card instead.

   Measured on umber — heading, titles and body 11.5:1, category
   4.8:1 (see --ochre-on-dark; raw ochre is 4.45:1 and misses),
   read link and eyebrow 5.3:1 (raw clay is 4.28:1 and misses),
   button label 11.5:1.
   ============================================================ */

.s12 {
  background: var(--uk-umber);
  color: var(--text-on-dark);
  padding-block: var(--pad-section);
}

/* The base h2/h3 rules paint headings umber, which on umber is nothing. */
.s12-head h2 {
  margin-top: 1.5rem;
  max-width: 20ch;
  color: var(--text-on-dark);
}

.s12-grid {
  display: grid;
  gap: clamp(1.5rem, 3vw, 2rem);
  margin-top: clamp(2.5rem, 6vw, 4rem);
}

.s12-card {
  display: flex;
  background: var(--uk-umber);
  border: 1px solid rgba(239, 231, 218, .15);
  border-radius: var(--radius);
  overflow: hidden;     /* keeps the image corners inside the card radius */
  transition: border-color .4s var(--ease-out);
}

/* focus-within so the border reacts to keyboard arrival too, not just a
   mouse. The link fills the card, so this fires whenever it is focused. */
.s12-card:focus-within { border-color: var(--uk-ochre); }
@media (hover: hover) {
  .s12-card:hover { border-color: var(--uk-ochre); }
  .s12-card:hover .s12-card-media img { transform: scale(1.03); }
  .s12-card:hover .s12-read-mark { transform: translateX(4px); }
}

.s12-card-link {
  display: flex;
  flex-direction: column;
  width: 100%;
}

/* The wrapper carries the 16:10 crop and the placeholder fill, so the zoom is
   clipped and a missing file still holds its space as a clay block. */
.s12-card-media {
  display: block;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--uk-clay);
}
.s12-card-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform .4s var(--ease-out);
}

.s12-card-body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  flex: 1;
  padding: 1.5rem;
}

.s12-cat,
.s12-read {
  font-family: var(--font-mono);
  font-size: .8rem;
  font-weight: 500;
  letter-spacing: 0;
}

/* Raw ochre measures 4.448:1 on umber — short of the floor by a hair. This is
   the same colour lifted toward the cream; the token carries the working. */
.s12-cat { color: var(--ochre-on-dark); }

.s12-title {
  margin-top: 1rem;
  font-size: var(--fs-h3);
  font-weight: 400;
  color: var(--text-on-dark);
}

.s12-read {
  display: inline-flex;
  align-items: center;
  gap: .6rem;
  /* auto, so the link sits on the card's bottom edge whatever the title's
     length — three cards of unequal copy still line their links up. */
  margin-top: auto;
  padding-top: 1.5rem;
  letter-spacing: .08em;
  /* Raw clay is 4.28:1 on umber, which is the shortfall --clay-on-dark was
     added to the tokens for in the first place. */
  color: var(--clay-on-dark);
}
.s12-read-mark { transition: transform var(--dur-fast) var(--ease-out); }

.s12-cta { margin-top: clamp(3rem, 6vw, 4.5rem); }

@media (min-width: 900px) {
  .s12-grid { grid-template-columns: repeat(3, 1fr); }
}

/* ============================================================
   Section 13 — Final call to action. Sandstone.

   Section 12 is umber and the footer will be dark, so this light
   band is what keeps the two apart — the colour is doing structural
   work, not just decoration.

   Mobile first — the buttons are stacked and full width by default
   and become a centred row at 600px.

   Measured on sandstone — heading 10.1:1, body 4.95:1, ghost label
   7.2:1, primary button label 5.3:1 against its own ochre fill.

   The background layer is blended, and that is a contrast decision.
   On a light ground the danger is the photograph darkening it: a
   plain 9% layer over a dark image drops the ground to #D0C5B1 and
   takes the body copy to 4.05:1, under the floor. Even 5% misses.
   Screen blending means the layer can only ever lighten — the
   ground moves between sandstone and #E7DCC8, and the body copy
   between 4.95:1 and 5.09:1 — so the texture is free. isolation
   keeps the blend inside this section.

   Two non-text values sit below 3:1 and both are shared components
   left as they are page-wide: the ghost border, --border-strong at
   1.63:1 on sandstone, and the ochre button fill at 2.28:1. Each
   button is identified by its label — 7.2:1 and 5.3:1 — rather than
   by its edge. The global ochre focus ring is 2.28:1 here too; that
   is worth fixing across the whole page rather than in one section.
   ============================================================ */

.s13 {
  position: relative;
  overflow: hidden;
  isolation: isolate;   /* contains the blend below to this section */
  background: var(--uk-sandstone);
  color: var(--text-primary);
  padding-block: var(--pad-section);
}

/* A background-image rather than an <img>: a missing file paints nothing and
   leaves the sandstone exactly as it was, which is the graceful skip. The
   grade is the one every other photograph on the page carries — docs/07 §2. */
.s13-bg {
  position: absolute;
  inset: 0;
  opacity: .09;
  background-image: url("/assets/img/s13-bg.webp");
  background-size: cover;
  background-position: center;
  /* Same grade as the content photographs, so this texture belongs to the
     same set. It is a CSS background rather than an <img>, so it cannot pick
     the rule up from `main img` and carries its own copy. */
  filter: saturate(0.92) contrast(1.03) sepia(0.04);
  /* Lighten only — see the contrast note above. */
  mix-blend-mode: screen;
  pointer-events: none;
}

.s13-inner {
  position: relative;   /* above the photograph */
  text-align: center;
}

/* --fs-hero stepped down once: this is the last word on the page, not the
   first, and it should not outrun the hero it echoes. */
.s13 h2 {
  margin-inline: auto;
  max-width: 18ch;
  font-size: clamp(2.25rem, 5.5vw, 4.25rem);
  font-weight: 200;
  line-height: 1.05;
  letter-spacing: -.012em;
  color: var(--text-primary);
}

.s13-body {
  margin: clamp(1.5rem, 3vw, 2rem) auto 0;
  max-width: 56ch;
  font-size: var(--fs-lead);
  color: var(--text-secondary);
}

.s13-actions {
  display: flex;
  flex-direction: column;
  gap: var(--gap-sm);
  margin-top: clamp(2.5rem, 5vw, 3.5rem);
}

/* Full width on a phone, which means the label has to centre itself — .btn is
   an inline-flex that packs to the left otherwise. */
.s13-actions .btn { justify-content: center; }

@media (min-width: 600px) {
  .s13-actions {
    flex-direction: row;
    justify-content: center;
    gap: var(--gap-md);
  }
}

/* ============================================================
   Footer — the closing plate
   (markup: src/shared/components/footer/footer.js)

   The last sheet of a drawing set rather than a link dump. Four
   bands, top to bottom:

     .ftr-lead    the brand line set large over a chain dimension
                  (Design | Build | Handover), and beside it the
                  "start your project" plate, marked out by crop
                  corners instead of a boxed border
     .ftr-index   brand, studio, explore, services — numbered like
                  sheet references — over a faint elevation drawing
     .ftr-base    copyright, place, back to top. Its top rule is the
                  elevation's ground line: the drawing stands on it.

   Ground: umber deepening slightly toward the base, with a low
   ochre warmth in the top-left corner. Contrast, measured on the
   lighter (umber) end: body and links 11.5:1, the muted cream
   6.5:1, column heads and chain labels (--clay-on-dark) 5.2:1,
   link hover (--ochre-on-pattern) 5.3:1. The elevation sits under
   the index at ~9% and never under the statement or the plate.
   ============================================================ */

.site-footer {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  color: var(--text-on-dark);
  background:
    radial-gradient(80% 60% at 0% 0%, rgba(200, 129, 28, .07), transparent 62%),
    linear-gradient(180deg, var(--uk-umber) 0%, color-mix(in srgb, var(--uk-umber) 84%, #000) 100%);
  border-top: 1px solid rgba(239, 231, 218, .08);

  /* The footer's own ochre for type, one step lighter than --ochre-on-dark,
     kept from the previous footer: it holds 4.5:1 even where a warm ground
     lifts behind the text. Scoped here because only the footer needs it. */
  --ochre-on-pattern: color-mix(in srgb, var(--uk-ochre) 80%, var(--text-on-dark));
  --ftr-muted: color-mix(in srgb, var(--text-on-dark) 72%, var(--uk-umber));
  --ftr-rule: rgba(239, 231, 218, .13);
  --elev-o: .16;
}

.ftr-inner {
  position: relative;
  padding-top: clamp(4.5rem, 8vw, 6.5rem);
}

/* ---- 1 · the statement ---- */

.ftr-lead {
  display: grid;
  gap: clamp(3rem, 7vw, 4.5rem);
}

/* Statement at the top of its column, chain at the foot: beside the plate the
   two meet its top and bottom edges, so the band reads as one composed
   sheet rather than a headline floating in space. */
.ftr-statement-wrap {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: clamp(2rem, 4vw, 2.75rem);
}

/* Three set lines — short, long, medium — rather than two that wrap where
   the column decides. Large enough that the column is the statement and the
   chain, with no dead band between them beside the plate. */
.ftr-statement {
  max-width: none;
  font-family: var(--font-display);
  font-size: calc(clamp(3.1rem, 7.4vw, 6rem) * var(--k-display));
  font-weight: 300;
  line-height: 1.02;
  letter-spacing: -.03em;
  color: var(--text-on-dark);
}
.ftr-statement span { display: block; }
/* The answer steps back in tone, not in size: the line is one thought, and
   the quieter clay lets "built right" land as the reply to it. */
.ftr-statement .ftr-statement-answer { color: var(--clay-on-dark); }

/* The chain dimension. Labels above the line, as dimension text sits on a
   drawing; a 45° ochre tick at each station. The line is one element so the
   reveal can draw it left to right with a single scaleX. */
.ftr-chain {
  position: relative;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  max-width: 34rem;
  padding-bottom: .9rem;
}
.ftr-chain-line {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: color-mix(in srgb, var(--text-on-dark) 42%, transparent);
  transform-origin: left center;
}
.ftr-chain-seg {
  position: relative;
  font-family: var(--font-display);
  font-style: normal;
  font-size: 1.15rem;
  font-weight: 400;
  letter-spacing: 0;
  line-height: 1;
  text-align: center;
  color: var(--clay-on-dark);
}
.ftr-chain-seg::before,
.ftr-chain-seg:last-child::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: calc(-.9rem - 6px);
  width: 1px;
  height: 13px;
  background: var(--uk-ochre);
  transform: rotate(45deg);
}
.ftr-chain-seg:last-child::after { left: auto; right: 0; }

/* ---- 2 · the plate ---- */

/* Crop corners, not a box: eight 1px strokes painted as backgrounds, plus a
   hairline so faint it only defines the field. */
.ftr-plate {
  --corner: color-mix(in srgb, var(--text-on-dark) 50%, transparent);
  --corner-l: 18px;
  position: relative;
  padding: clamp(1.75rem, 3.2vw, 2.5rem);
  background-color: rgba(239, 231, 218, .035);
  background-image:
    linear-gradient(var(--corner), var(--corner)), linear-gradient(var(--corner), var(--corner)),
    linear-gradient(var(--corner), var(--corner)), linear-gradient(var(--corner), var(--corner)),
    linear-gradient(var(--corner), var(--corner)), linear-gradient(var(--corner), var(--corner)),
    linear-gradient(var(--corner), var(--corner)), linear-gradient(var(--corner), var(--corner));
  background-repeat: no-repeat;
  background-size:
    var(--corner-l) 1px, 1px var(--corner-l),
    var(--corner-l) 1px, 1px var(--corner-l),
    var(--corner-l) 1px, 1px var(--corner-l),
    var(--corner-l) 1px, 1px var(--corner-l);
  background-position:
    0 0, 0 0,
    100% 0, 100% 0,
    0 100%, 0 100%,
    100% 100%, 100% 100%;
  box-shadow: inset 0 0 0 1px rgba(239, 231, 218, .05);
}

.ftr-plate-title {
  /* the plate is narrow: one line, as on the serif, needs the tighter scale */
  --k-display: .72;
  margin-top: 0;
  font-size: calc(clamp(1.8rem, 2.6vw, 2.3rem) * var(--k-display));
  font-weight: 300;
  line-height: 1.08;
  letter-spacing: -.02em;
  color: var(--text-on-dark);
  text-wrap: balance;
}

.ftr-plate-text {
  margin-top: .75rem;
  font-size: .9375rem;
  line-height: 1.65;
  color: var(--ftr-muted);
}

.ftr-plate-actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: .9rem 1.5rem;
  margin-top: 1.75rem;
}

/* The header pill's big sibling: same ochre, same disc, same settle. */
.ftr-cta {
  display: inline-flex;
  align-items: center;
  gap: .7rem;
  height: 48px;
  padding: 0 6px 0 1.35rem;
  border-radius: 999px;
  background: var(--uk-ochre);
  color: #2A1A06;
  font-size: .875rem;
  font-weight: 600;
  letter-spacing: .01em;
  line-height: 1;
  white-space: nowrap;
  transition:
    transform .35s var(--ease-out),
    box-shadow .35s var(--ease-out);
}
.ftr-cta-disc {
  display: inline-grid;
  place-items: center;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: rgba(42, 26, 6, .12);
  transition:
    background-color .35s var(--ease-out),
    color .35s var(--ease-out);
}
.ftr-cta-disc svg {
  width: 15px;
  height: 15px;
  transition: transform .35s var(--ease-out);
}
@media (hover: hover) {
  .ftr-cta:hover {
    transform: translateY(-1px);
    box-shadow: 0 12px 26px -14px rgba(200, 129, 28, .85);
  }
  .ftr-cta:hover .ftr-cta-disc { background: #2A1A06; color: var(--uk-ochre); }
  .ftr-cta:hover .ftr-cta-disc svg { transform: translateX(2px); }
}
.ftr-cta:focus-visible { outline-offset: 4px; }

.ftr-plate-alt {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  font-size: .875rem;
  color: var(--text-on-dark);
}
.ftr-plate-alt svg {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  color: var(--clay-on-dark);
  transition: color var(--dur-fast) ease;
}
/* A resting hairline at 35% that takes the full ochre on hover. */
.ftr-plate-alt span:not(.visually-hidden) {
  --u: color-mix(in srgb, var(--text-on-dark) 35%, transparent);
  padding-bottom: 2px;
  background: linear-gradient(var(--u), var(--u)) 0 100% / 100% 1px no-repeat;
}
.ftr-plate-alt:hover,
.ftr-plate-alt:focus-visible { color: var(--ochre-on-pattern); }
.ftr-plate-alt:hover svg,
.ftr-plate-alt:focus-visible svg { color: var(--ochre-on-pattern); }
.ftr-plate-alt:hover span:not(.visually-hidden),
.ftr-plate-alt:focus-visible span:not(.visually-hidden) { --u: currentColor; }

/* ---- 3 · the index ---- */

/* --elev-h is the drawing band: the elevation's height, which the index
   reserves under its columns so no line ever runs behind a word. */
.ftr-index {
  --elev-h: max(118px, calc((min(1320px, 100vw) - 2 * var(--pad-page)) / 6));
  position: relative;
  display: grid;
  /* Phones: brand and studio full width, then explore and services paired —
     two short lists side by side instead of one long column. */
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 2.75rem 1.5rem;
  margin-top: clamp(4rem, 8vw, 6.5rem);
  padding-top: clamp(2.5rem, 5vw, 3.5rem);
  padding-bottom: calc(var(--elev-h) + clamp(2.5rem, 5vw, 3.5rem));
  border-top: 1px solid var(--ftr-rule);
}

/* The elevation. Bottom of the index, full width, so y=200 in the drawing —
   every wall, pier, trunk and fin ends there — lands on .ftr-base's top rule.
   `slice` from the right: at full width the whole street fits; narrower, it
   crops from the left and the house stays. z-index -1 inside the footer's
   isolated stacking context: above the ground colour, beneath everything. */
.ftr-elev-wrap {
  position: absolute;
  z-index: -1;
  left: 0;
  right: 0;
  bottom: 0;
  height: var(--elev-h);
  color: var(--text-on-dark);
  opacity: var(--elev-o);
  pointer-events: none;
}
.ftr-elev {
  width: 100%;
  height: 100%;
}
.ftr-elev * { vector-effect: non-scaling-stroke; }
.ftr-elev text {
  fill: currentColor;
  stroke: none;
  font-family: var(--font-mono);
  font-size: 7px;
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: uppercase;
}
.ftr-elev-context { opacity: .55; }
.ftr-elev-detail { opacity: .7; }
.ftr-elev-notes { opacity: .6; }

.ftr-brand,
.ftr-col--studio { grid-column: 1 / -1; }
.ftr-brand { max-width: 22rem; }

.ftr-logo {
  display: inline-block;
  width: 164px;
}
.ftr-logo img {
  display: block;
  width: 100%;
  height: auto;
}

.ftr-tagline {
  margin-top: 1.25rem;
  max-width: 20ch;
  font-size: 1.125rem;
  font-weight: 300;
  line-height: 1.45;
  color: var(--text-on-dark);
}

.ftr-social {
  display: flex;
  gap: .5rem;
  margin-top: 1.75rem;
}
.ftr-social .uk-social__link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 42px;
  height: 42px;
  min-height: 0;
  border-radius: 50%;
  border: 1px solid rgba(239, 231, 218, .2);
  color: var(--text-on-dark);
  transition:
    color var(--dur-fast) ease,
    border-color var(--dur-fast) ease,
    background-color var(--dur-fast) ease,
    transform .35s var(--ease-out);
}
.ftr-social svg { width: 17px; height: 17px; }
.ftr-social a.uk-social__link:hover,
.ftr-social a.uk-social__link:focus-visible {
  color: var(--ochre-on-pattern);
  border-color: color-mix(in srgb, var(--uk-ochre) 70%, transparent);
  background: rgba(200, 129, 28, .08);
}
@media (hover: hover) {
  .ftr-social a.uk-social__link:hover { transform: translateY(-1px); }
}

.ftr-col-head {
  display: flex;
  align-items: baseline;
  gap: .7rem;
  /* Client (2026-10-01): the column titles in the same font as the content
     under them, a step larger (17px over 14px) so they stand out, and they
     light up when the pointer is over their column. */
  font-family: var(--font-body);
  font-size: 1.0625rem;
  font-weight: 600;
  letter-spacing: .01em;
  line-height: 1.2;
  color: var(--clay-on-dark);
  background: linear-gradient(var(--ochre-on-dark), var(--ochre-on-dark)) 0 100% / 0 1px no-repeat;
  padding-bottom: .35rem;
  width: max-content;
  transition: color var(--dur-fast) ease, background-size .45s var(--ease-out);
}
.ftr-col:hover .ftr-col-head,
.ftr-col:focus-within .ftr-col-head {
  color: var(--text-on-dark);
  background-size: 100% 1px;
}
@media (prefers-reduced-motion: reduce) {
  .ftr-col-head { transition: none; }
}
/* Round 6: the "01 / 02 / 03" column numbers are gone; the title word sits
   on the same line as the content under it. */

.ftr-address {
  margin-top: 1.4rem;
  font-style: normal;   /* <address> italicises by default */
}
/* Column body text steps down to 14px (was 15.5px; client feedback,
   2026-10-01) — address, phone and email lines, and the Explore and
   Services links share the one size. Colours are unchanged, so contrast is
   too; the 44px tap boxes on phones come from the touch-target rule. */
.ftr-address p {
  font-size: .875rem;
  line-height: 1.65;
  color: var(--ftr-muted);
}

.ftr-list,
.ftr-lines {
  display: grid;
  gap: .65rem;
  margin-top: 1.4rem;
}
.ftr-lines { margin-top: 1.1rem; }
.ftr-lines li {
  display: flex;
  align-items: center;
  gap: .7rem;
}
/* Phone and envelope glyphs from the shared icon set, in the column-head
   clay, marking each line the way the mono T / E letters used to. */
.ftr-key {
  display: inline-flex;
  flex: 0 0 auto;
  color: var(--clay-on-dark);
}
.ftr-key svg { width: 15px; height: 15px; }

/* The phone lines: tap-to-call, and made to look it (mobile pass,
   2026-10-01). A copper phone in a 32px ringed disc and the number at 16px
   in full cream, a step up from the 14px address; the disc fills on hover
   or tap. Contrast on the umber ground: number 11.5:1, the copper glyph
   (--ochre-on-pattern) 5.3:1. The email line under them keeps the quieter
   key glyph. */
.ftr-lines li.ftr-line--phone { display: block; }
.ftr-lines li.ftr-line--phone + li:not(.ftr-line--phone) { margin-top: .35rem; }
.ftr-call {
  display: inline-flex;
  align-items: center;
  gap: .75rem;
  min-height: 36px;
  color: var(--text-on-dark);
  font-size: 1rem;
  font-weight: 500;
  letter-spacing: .01em;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  transition: color var(--dur-fast) ease;
}
.ftr-call-ico {
  display: inline-grid;
  place-items: center;
  flex: 0 0 auto;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--uk-ochre) 60%, transparent);
  background: rgba(200, 129, 28, .12);
  color: var(--ochre-on-pattern);
  transition: background-color var(--dur-fast) ease, color var(--dur-fast) ease, border-color var(--dur-fast) ease, transform .35s var(--ease-out);
}
.ftr-call-ico svg { width: 16px; height: 16px; }
.ftr-call-num {
  padding-bottom: 2px;
  background: linear-gradient(currentColor, currentColor) 0 100% / 0 1px no-repeat;
  transition: background-size .45s var(--ease-out);
}
.ftr-call:hover,
.ftr-call:focus-visible { color: var(--ochre-on-pattern); }
.ftr-call:hover .ftr-call-num,
.ftr-call:focus-visible .ftr-call-num { background-size: 100% 1px; }
.ftr-call:hover .ftr-call-ico,
.ftr-call:focus-visible .ftr-call-ico,
.ftr-call:active .ftr-call-ico {
  background: var(--uk-ochre);
  border-color: var(--uk-ochre);
  color: #2A1A06;
}
@media (hover: hover) {
  .ftr-call:hover .ftr-call-ico { transform: rotate(-10deg); }
}

/* Links draw a hairline in from the left on hover — the one piece of motion
   every link in the footer shares. */
.ftr-link {
  color: var(--text-on-dark);
  font-size: .875rem;
  line-height: 1.4;
  background: linear-gradient(currentColor, currentColor) 0 100% / 0 1px no-repeat;
  padding-bottom: 2px;
  transition:
    color var(--dur-fast) ease,
    background-size .45s var(--ease-out);
}
.ftr-link:hover,
.ftr-link:focus-visible {
  color: var(--ochre-on-pattern);
  background-size: 100% 1px;
}

/* ---- 4 · the base ---- */

.ftr-base {
  display: flex;
  flex-direction: column;
  gap: .35rem;
  padding-block: 1.4rem 1.6rem;
  /* The ground line — a shade stronger than the rules above it. */
  border-top: 1px solid rgba(239, 231, 218, .24);
  font-family: var(--font-body);
  font-size: .8rem;
  font-weight: 400;
  letter-spacing: 0;
  color: var(--ftr-muted);
}
.ftr-base p { max-width: none; }

.ftr-top {
  display: inline-flex;
  align-items: center;
  gap: .55rem;
  align-self: flex-start;
  letter-spacing: 0;
  font-size: 0.894rem;
  color: var(--text-on-dark);
  transition: color var(--dur-fast) ease;
}
.ftr-top svg {
  width: 14px;
  height: 14px;
  transition: transform .35s var(--ease-out);
}
.ftr-top:hover,
.ftr-top:focus-visible { color: var(--ochre-on-pattern); }
.ftr-top:hover svg { transform: translateY(-2px); }

/* ---- the reveal (armed by main.js initFooter) ----
   Nothing is hidden until .is-armed is on the footer, and main.js only sets
   it when motion is allowed — so no-JS and reduced motion get the finished
   footer, not an invisible one. */
.site-footer.is-armed [data-ftr-reveal] {
  opacity: 0;
  transform: translateY(14px);
  transition:
    opacity .9s var(--ease-out),
    transform .9s var(--ease-out);
  transition-delay: calc(var(--ftr-i, 0) * 90ms);
}
.site-footer.is-armed [data-ftr-reveal].is-in {
  opacity: 1;
  transform: none;
}
.site-footer.is-armed .ftr-chain-line {
  transform: scaleX(0);
  transition: transform 1.3s var(--ease-out) .35s;
}
.site-footer.is-armed .ftr-chain-seg::before,
.site-footer.is-armed .ftr-chain-seg::after {
  opacity: 0;
  transition: opacity .5s ease 1.1s;
}
.site-footer.is-armed .is-in .ftr-chain-line { transform: none; }
.site-footer.is-armed .is-in .ftr-chain-seg::before,
.site-footer.is-armed .is-in .ftr-chain-seg::after { opacity: 1; }
.site-footer.is-armed .ftr-elev-wrap {
  opacity: 0;
  transition: opacity 1.8s ease .2s;
}
.site-footer.is-armed .ftr-index.is-in .ftr-elev-wrap { opacity: var(--elev-o); }

@media (prefers-reduced-motion: reduce) {
  .site-footer.is-armed [data-ftr-reveal],
  .site-footer.is-armed .ftr-chain-line { opacity: 1; transform: none; }
  .site-footer.is-armed .ftr-elev-wrap { opacity: var(--elev-o); }
}

/* ---- phones: the touch-target rule above gives every footer link a 44px
   box, so the lists close their own gaps to compensate. ---- */
@media (pointer: coarse), (max-width: 899px) {
  .ftr-list,
  .ftr-lines { gap: 0; margin-top: .9rem; }
  .ftr-social .uk-social__link { width: 44px; height: 44px; }
  .ftr-call { min-height: 48px; }
  .ftr-cta,
  .ftr-plate-alt { min-height: 48px; }
  .ftr-top { min-height: 44px; }
}

@media (min-width: 700px) {
  /* Tablet: the brand becomes a strip across the top — logo, tagline,
     socials in one row — and the three lists share the row below. */
  .ftr-index { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .ftr-col--studio { grid-column: auto; }
  .ftr-brand {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 1rem 2.5rem;
    max-width: none;
  }
  .ftr-brand .ftr-tagline { margin-top: 0; }
  .ftr-brand .ftr-social { margin: 0 0 0 auto; }

  .ftr-base {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: 1.5rem;
  }
  .ftr-top { align-self: auto; }
}

@media (min-width: 1100px) {
  .ftr-lead {
    grid-template-columns: minmax(0, 1fr) minmax(21rem, 26rem);
    column-gap: clamp(3rem, 6vw, 6.5rem);
  }

  .ftr-index {
    grid-template-columns: minmax(0, 1.5fr) minmax(0, 1.1fr) minmax(0, .85fr) minmax(0, 1.1fr);
    column-gap: clamp(2rem, 3.5vw, 3.5rem);
  }
  .ftr-brand { grid-column: auto; display: block; max-width: 22rem; }
  .ftr-brand .ftr-tagline { margin-top: 1.25rem; }
  .ftr-brand .ftr-social { margin: 1.75rem 0 0; }
}

/* Contact page only (client feedback, 2026-10-01): the "start your project"
   plate only links back to the same page, so there it gives way to an onward
   plate (projects, why UrbanKriti). Every other page keeps the original.
   body[data-page] is kept current by router.js. */
.ftr-plate--onward,
body[data-page="contact"] .ftr-plate { display: none; }
body[data-page="contact"] .ftr-plate--onward { display: block; }

/* ── Site-wide image atmosphere ─────────────────────────────────────
   Solid colour first, image second, text always first.

   The section keeps its own solid ground; a real UrbanKriti image emerges
   into it from one side or the corners, and the copy sits on a calm zone of
   near-solid colour. Four stacked backgrounds, top to bottom:

     1. seam   — solid ground at the top and bottom edges, so neighbouring
                 sections meet on colour, never on a cut photograph;
     2. zone   — an optional near-solid content field (mode "content");
     3. wash   — the ground colour, solid on the text side and thinning
                 towards the image side;
     4. image  — a pre-softened derivative from /assets/images/atmosphere/
                 (tools/build_atmosphere_assets.mjs).

   How much image shows is one number per level — its "reach", the most the
   image can ever contribute at its clearest point:

     light  (light, paper, linen, sand)   16%
     dark   (umber, walnut, night)        28%
     hero   (data-uk-atmosphere-mode="hero")  38% light / 50% dark

   and the mode decides where along the section that reach is spent:

     side (default)  image emerges on the right; copy side ~97% solid
     left            the same, mirrored
     edge            image held to the outer quarter (controls, tabs, atlases)
     edge-left       the same, on the left
     field           an even, quiet veil (dense card grids that cover it anyway)
     content         image only at the corners, a solid field in the middle
                     (long copy, forms, FAQs, centred CTAs)
     hero            immersive, with the copy side still protected

   data-uk-atmosphere-strength="soft" lowers reach by a quarter for busy
   images; "rich" raises it slightly for very calm ones.

   Markup:  data-uk-atmosphere="<key>"  data-uk-atmosphere-tone="…"
            data-uk-atmosphere-mode="…"  [data-uk-atmosphere-grid]
   Audit per section: docs/SITEWIDE_IMAGE_ATMOSPHERE_AUDIT.md */
[data-uk-atmosphere] {
  --uk-atm-image: none;
  --uk-atm-pos: 72% 50%;
  --uk-atm-tint: 250 247 242;
  --uk-atm-reach: .16;
  --uk-atm-scale: 1;
  --uk-atm-k1: .2;
  --uk-atm-k2: .6;
  --uk-atm-k3: 1;
  --uk-atm-hold: 38%;
  --uk-atm-dir: 90deg;
  --uk-atm-seam: 88px;
  --uk-atm-zone-pos: 50% 50%;
  --uk-atm-zone: 0;
  --uk-atm-attach: scroll;

  --uk-atm-r: calc(var(--uk-atm-reach) * var(--uk-atm-scale));
  --uk-atm-seam-layer: linear-gradient(180deg,
      rgb(var(--uk-atm-tint) / 1) 0,
      rgb(var(--uk-atm-tint) / 0) var(--uk-atm-seam),
      rgb(var(--uk-atm-tint) / 0) calc(100% - var(--uk-atm-seam)),
      rgb(var(--uk-atm-tint) / 1) 100%);
  --uk-atm-zone-layer: radial-gradient(ellipse 62% 70% at var(--uk-atm-zone-pos),
      rgb(var(--uk-atm-tint) / var(--uk-atm-zone)) 0%,
      rgb(var(--uk-atm-tint) / calc(var(--uk-atm-zone) * .9)) 48%,
      rgb(var(--uk-atm-tint) / 0) 78%);
  --uk-atm-wash: linear-gradient(var(--uk-atm-dir),
      rgb(var(--uk-atm-tint) / calc(1 - var(--uk-atm-r) * var(--uk-atm-k1))) 0%,
      rgb(var(--uk-atm-tint) / calc(1 - var(--uk-atm-r) * var(--uk-atm-k1))) var(--uk-atm-hold),
      rgb(var(--uk-atm-tint) / calc(1 - var(--uk-atm-r) * var(--uk-atm-k2))) 72%,
      rgb(var(--uk-atm-tint) / calc(1 - var(--uk-atm-r) * var(--uk-atm-k3))) 100%);

  background-color: rgb(var(--uk-atm-tint)) !important;
  background-image: var(--uk-atm-seam-layer), var(--uk-atm-zone-layer), var(--uk-atm-wash), var(--uk-atm-image) !important;
  background-position: center, center, center, var(--uk-atm-pos) !important;
  background-size: 100% 100%, 100% 100%, 100% 100%, cover !important;
  background-repeat: no-repeat !important;
  background-attachment: scroll, scroll, scroll, var(--uk-atm-attach) !important;
}

/* Tones — each is the ground colour the section already had. */
[data-uk-atmosphere-tone="paper"] { --uk-atm-tint: 251 247 239; }
[data-uk-atmosphere-tone="linen"] { --uk-atm-tint: 244 238 227; }
[data-uk-atmosphere-tone="sand"] { --uk-atm-tint: 229 217 195; }
[data-uk-atmosphere-tone="umber"],
[data-uk-atmosphere-tone="dark"] { --uk-atm-tint: 63 36 34; --uk-atm-reach: .28; }
[data-uk-atmosphere-tone="walnut"] { --uk-atm-tint: 49 32 29; --uk-atm-reach: .28; }
[data-uk-atmosphere-tone="night"] { --uk-atm-tint: 31 23 19; --uk-atm-reach: .28; }

/* Modes — where the reach is spent. */
[data-uk-atmosphere][data-uk-atmosphere-mode="left"] { --uk-atm-dir: 270deg; }
[data-uk-atmosphere][data-uk-atmosphere-mode="edge"] { --uk-atm-k1: .08; --uk-atm-k2: .4; --uk-atm-k3: .95; --uk-atm-hold: 55%; }
[data-uk-atmosphere][data-uk-atmosphere-mode="edge-left"] { --uk-atm-k1: .08; --uk-atm-k2: .4; --uk-atm-k3: .95; --uk-atm-hold: 55%;  --uk-atm-dir: 270deg; }
[data-uk-atmosphere][data-uk-atmosphere-mode="field"] { --uk-atm-k1: .72; --uk-atm-k2: .72; --uk-atm-k3: .72; }
[data-uk-atmosphere][data-uk-atmosphere-mode="content"] { --uk-atm-k1: .95; --uk-atm-k2: .95; --uk-atm-k3: 1; --uk-atm-zone: .94; }
[data-uk-atmosphere][data-uk-atmosphere-mode="hero"] { --uk-atm-reach: .38; --uk-atm-k1: .25; --uk-atm-k2: .7; --uk-atm-hold: 30%; }
[data-uk-atmosphere-mode="hero"][data-uk-atmosphere-tone="umber"],
[data-uk-atmosphere-mode="hero"][data-uk-atmosphere-tone="walnut"],
[data-uk-atmosphere-mode="hero"][data-uk-atmosphere-tone="night"] { --uk-atm-reach: .5; }

[data-uk-atmosphere][data-uk-atmosphere-strength="soft"] { --uk-atm-scale: .75; }
[data-uk-atmosphere][data-uk-atmosphere-strength="rich"] { --uk-atm-scale: 1.15; }

/* The Services drafting grid survives the treatment, drawn over the wash. */
[data-uk-atmosphere][data-uk-atmosphere-grid] {
  background-image:
    linear-gradient(var(--svc-line, rgba(92, 58, 33, .18)) 1px, transparent 1px),
    linear-gradient(90deg, var(--svc-line, rgba(92, 58, 33, .18)) 1px, transparent 1px),
    var(--uk-atm-seam-layer), var(--uk-atm-zone-layer), var(--uk-atm-wash), var(--uk-atm-image) !important;
  background-position: 0 0, 0 0, center, center, center, var(--uk-atm-pos) !important;
  background-size: min(7vw, 96px) min(7vw, 96px), min(7vw, 96px) min(7vw, 96px), 100% 100%, 100% 100%, 100% 100%, cover !important;
  background-repeat: repeat, repeat, no-repeat, no-repeat, no-repeat, no-repeat !important;
  background-attachment: scroll, scroll, scroll, scroll, scroll, var(--uk-atm-attach) !important;
}

/* On a large pointer screen the image holds against the viewport, so it
   drifts quietly behind the content and a very tall section shows a sensible
   crop. Touch devices and reduced motion keep it with the section. */
@media (min-width: 1100px) and (hover: hover) and (prefers-reduced-motion: no-preference) {
  [data-uk-atmosphere] { --uk-atm-attach: fixed; }
}

/* Phones: copy spans the full width, so the image is held to the lower part
   of each section and never sits under a heading. */
@media (max-width: 767px) {
  [data-uk-atmosphere][data-uk-atmosphere] {
    --uk-atm-dir: 180deg;
    --uk-atm-k1: .1;
    --uk-atm-k2: .4;
    --uk-atm-k3: .85;
    --uk-atm-hold: 45%;
    --uk-atm-seam: 48px;
    --uk-atm-zone: 0;
  }
}

/* Images. Positions favour the architecture in each frame, not its centre. */
[data-uk-atmosphere="home-about"] { --uk-atm-image: url("/assets/images/atmosphere/home-about.webp"); --uk-atm-pos: 60% 40%; }
[data-uk-atmosphere="home-services"] { --uk-atm-image: url("/assets/images/atmosphere/home-services.webp"); --uk-atm-pos: 70% 50%; }
[data-uk-atmosphere="home-problem"] { --uk-atm-image: url("/assets/images/atmosphere/home-problem.webp"); --uk-atm-pos: 60% 50%; }
[data-uk-atmosphere="home-process"] { --uk-atm-image: url("/assets/images/atmosphere/home-process.webp"); --uk-atm-pos: 60% 45%; }
[data-uk-atmosphere="home-wipe"] { --uk-atm-image: url("/assets/images/atmosphere/home-wipe.webp"); --uk-atm-pos: 50% 40%; }
[data-uk-atmosphere="home-projects"] { --uk-atm-image: url("/assets/images/atmosphere/home-projects.webp"); --uk-atm-pos: 50% 30%; }

[data-uk-atmosphere="services-intro"] { --uk-atm-image: url("/assets/images/atmosphere/services-intro.webp"); --uk-atm-pos: 60% 40%; }
[data-uk-atmosphere="services-list"] { --uk-atm-image: url("/assets/images/atmosphere/services-list.webp"); --uk-atm-pos: 80% 50%; }
[data-uk-atmosphere="services-conclusion"] { --uk-atm-image: url("/assets/images/atmosphere/services-conclusion.webp"); --uk-atm-pos: 60% 40%; }
[data-uk-atmosphere="design-layers"] { --uk-atm-image: url("/assets/images/atmosphere/design-layers.webp"); --uk-atm-pos: 60% 50%; }
[data-uk-atmosphere="design-closing"] { --uk-atm-image: url("/assets/images/atmosphere/design-closing.webp"); --uk-atm-pos: 60% 50%; }
[data-uk-atmosphere="construction-programme"] { --uk-atm-image: url("/assets/images/atmosphere/construction-programme.webp"); --uk-atm-pos: 60% 50%; }
[data-uk-atmosphere="construction-closing"] { --uk-atm-image: url("/assets/images/atmosphere/construction-closing.webp"); --uk-atm-pos: 50% 40%; }
[data-uk-atmosphere="renovation-model"] { --uk-atm-image: url("/assets/images/atmosphere/renovation-model.webp"); --uk-atm-pos: 60% 50%; }
[data-uk-atmosphere="renovation-scope"] { --uk-atm-image: url("/assets/images/atmosphere/renovation-scope.webp"); --uk-atm-pos: 70% 50%; }
[data-uk-atmosphere="renovation-closing"] { --uk-atm-image: url("/assets/images/atmosphere/renovation-closing.webp"); --uk-atm-pos: 60% 40%; }
[data-uk-atmosphere="planning-scope"] { --uk-atm-image: url("/assets/images/atmosphere/planning-scope.webp"); --uk-atm-pos: 76% 48%; }
[data-uk-atmosphere="planning-closing"] { --uk-atm-image: url("/assets/images/atmosphere/planning-closing.webp"); --uk-atm-pos: 40% 50%; }

[data-uk-atmosphere="why-problem"] { --uk-atm-image: url("/assets/images/atmosphere/why-problem.webp"); --uk-atm-pos: 70% 50%; }
[data-uk-atmosphere="why-difference"] { --uk-atm-image: url("/assets/images/atmosphere/why-difference.webp"); --uk-atm-pos: 60% 50%; }
[data-uk-atmosphere="why-faq"] { --uk-atm-image: url("/assets/images/atmosphere/why-faq.webp"); --uk-atm-pos: 70% 50%; }
[data-uk-atmosphere="about-principle"] { --uk-atm-image: url("/assets/images/atmosphere/about-principle.webp"); --uk-atm-pos: 70% 45%; }
[data-uk-atmosphere="about-team"] { --uk-atm-image: url("/assets/images/atmosphere/about-team.webp"); --uk-atm-pos: 70% 45%; }
[data-uk-atmosphere="contact-brief"] { --uk-atm-image: url("/assets/images/atmosphere/contact-brief.webp"); --uk-atm-pos: 76% 38%; }


/* ── Call features: Contact page only (owner, 2026-10-01) ───────────────
   The header call button, the second number in the mobile drawer and the
   highlighted footer phone lines show on /contact/ only; every other page
   keeps its original header, drawer and footer. body[data-page] is kept
   current by router.js, so this holds across in-page navigation. */
body:not([data-page="contact"]) .hdr-call { display: none; }
body:not([data-page="contact"]) .hdr-drawer-contact li:nth-child(n+2) { display: none; }
body:not([data-page="contact"]) .ftr-line--phone,
body[data-page="contact"] .ftr-line--plain { display: none; }

/* Keeps a short phrase on one line (e.g. "planned together."). */
.nowrap { white-space: nowrap; }

/* Contact page: the email line takes the same ringed disc, gap and size as
   the two highlighted phone lines above it, so all three texts start on one
   line (client: "does it look aligned"). */
body[data-page="contact"] .ftr-lines li:not(.ftr-line--phone):not(.ftr-line--plain) { gap: .75rem; align-items: center; }
body[data-page="contact"] .ftr-lines li:not(.ftr-line--phone):not(.ftr-line--plain) .ftr-key {
  display: inline-grid; place-items: center; flex: 0 0 auto;
  width: 32px; height: 32px; border-radius: 50%;
  border: 1px solid color-mix(in srgb, var(--uk-ochre) 60%, transparent);
  background: rgba(200, 129, 28, .12); color: var(--ochre-on-pattern);
}
body[data-page="contact"] .ftr-lines li:not(.ftr-line--phone):not(.ftr-line--plain) .ftr-key svg { width: 16px; height: 16px; }
body[data-page="contact"] .ftr-lines li:not(.ftr-line--phone):not(.ftr-line--plain) .ftr-link { font-size: 1rem; }


/* Footer logo lockup (client, 2026-10-02): the emblem on the left; the
   name and the tagline as ONE block, centred vertically against the
   emblem, with a clear gap between emblem and text. The emblem and the
   wordmark are the original logo artwork split into two files
   (urbankriti-emblem-light.svg, urbankriti-wordmark-light.svg), so the
   name keeps its exact letterforms. The tagline is set in the brand kit's
   Caviar Dreams, justified to exactly the wordmark's width. Every size is
   a share of the lockup's width (container units), so the proportions
   hold at any size. */
/* The brand's own tagline face, from the client's logo kit (Caviar Dreams,
   free for commercial use; Latin subset, 5.7 KB). Timeburner, the other kit
   font, may not be redistributed, so it is not embedded. */
@font-face {
  font-family: 'Caviar Dreams';
  src: url('/assets/fonts/caviar-dreams-latin.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
.ftr-lockup {
  display: inline-block;
  width: clamp(200px, 17vw, 240px);
  container-type: inline-size;
}
.ftr-lockup .ftr-logo {
  display: flex;
  align-items: center;          /* the text block centred on the emblem */
  gap: 7cqi;                    /* wider than the artwork's own 3cqi gap */
  width: 100%;
}
.ftr-lockup .ftr-logo .ftr-lockup-emblem { display: block; flex: none; width: 24cqi; height: auto; }
.ftr-lockup-text { display: flex; flex-direction: column; gap: 3.2cqi; flex: 1 1 auto; min-width: 0; }
.ftr-lockup .ftr-logo .ftr-lockup-word { display: block; width: 100%; height: auto; }
.ftr-brand .ftr-lockup .ftr-tagline,
.ftr-lockup .ftr-tagline {
  display: block;
  margin: 0;
  max-width: none;
  font-family: 'Caviar Dreams', var(--font-logo);
  font-size: 6.45cqi;   /* ~15.5px on desktop, ~12.9px on phones */
  font-weight: 400;
  line-height: 1;
  /* letters spread evenly to (almost) the word's width; justify only takes
     up the last pixel or two */
  letter-spacing: .06em;
  text-align: justify;
  text-align-last: justify;
  white-space: nowrap;
  color: rgba(255, 255, 255, .82);
}

/* Client, 2026-10-08: no drag-out or iOS long-press save on images (main.js
   blocks the right-click menu). */
img, video { -webkit-user-drag: none; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; }

/* Netlify injects a "Powered by Netlify" badge (an iframe from
   /.netlify/scripts/hud) on the live site. Owner, 2026-10-09: remove it. */
#nl-badge-frame,
#nl-hud-frame { display: none !important; }
