/* === 01-fonts.css === */
/* ---------------------------------------------------------------------------
   Fonts — self-hosted, latin subset only.

   Newsreader for everything editorial, Archivo for labels, eyebrows and
   buttons. They replaced Cormorant Garamond and Inter in the August 2026
   redesign, and the old files are deleted rather than left in the folder.

   Every design file in the handoff loads these two from fonts.googleapis.com.
   They are self-hosted instead, and that is not a preference. A Google Fonts
   link is a DNS lookup and two connections before a single glyph is drawn, it
   hands a third party the IP address of every visitor, and it puts a request
   outside the origin on a site whose cookie-free position depends on there not
   being one. No request ever leaves the origin for a typeface.

   The files are committed to the repo and refreshed with `npm run fonts`.
   The URLs below are rewritten to content-hashed names at build time by
   lib/assets.mjs, so a changed weight can never be served from a year-long
   cache of the old one.
   --------------------------------------------------------------------------- */

/*
   NEWSREADER IS ONE VARIABLE FACE, NOT FIVE STATIC ONES.

   `font-weight: 200 800` is a RANGE, not a mistake. A variable font declares
   the span it can render and the browser interpolates inside it, so 300, 400
   and 500 all come from this one file and no separate face is needed for each.

   The reason it is the variable face at all is the OPTICAL SIZE axis, which
   the designs ask for explicitly:

     Newsreader:ital,opsz,wght@0,6..72,300;400;500;1,6..72,300;400

   `6..72` is opsz. Optical sizing is not scaling — it is a different drawing
   of the letter at a different size, with more open counters and sturdier
   hairlines small, and a tighter fit with finer strokes large. A 17px caption
   and a 76px page title are meant to look like relatives, not like the same
   shape at two magnifications.

   Until 10 August 2026 this shipped five static instances, which gave the site
   one drawing at every size. Set beside the design that reads as a different
   typeface in the small sizes, which is how it was reported.

   `font-optical-sizing: auto` is the initial value in every browser that
   supports the axis, so the element's own font-size drives it and there is
   nothing to declare. No rule elsewhere in this stylesheet has to know.

   ARCHIVO STAYS STATIC. The designs load it as `Archivo:wght@400;500` — no
   optical size, no width axis, two weights. A variable Archivo would be a
   larger file for an axis nothing asks for.

   Two Newsreader files replace five: 279KB against 116KB, paid once for the
   whole site, and still self-hosted and content-hashed. No request for a
   typeface ever leaves the origin.
   --------------------------------------------------------------------------- */

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

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

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

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

/* === 02-site.css === */
/* ===========================================================================
   THE AUTHORITATIVE STYLESHEET.

   This is design_handoff_lapunne_site/site.css, verbatim and unmerged. The
   handoff is explicit that it REPLACES the previous stylesheet rather than
   being merged into it, and that anything in the old one with no equivalent
   here is a pattern the redesign no longer uses. Nothing was carried over: the
   fifteen partials that used to live in this folder were deleted, not adapted.

   Two competing sets of tokens is how a site drifts, and this repo has already
   paid for that once — `.journey` existed in two page stylesheets and a stale
   cache put one over the other's markup.

   The @font-face rules are in 01-fonts.css, because they point at files this
   repo hashes and the handoff loads from Google. Everything else that follows
   is per-page work the handoff says is deliberately NOT in this file: the two
   Ready by Completion timelines, the Innovation ecosystem diagram, and the
   project page tables, whose proportions carry meaning and are laid out per
   instance.

   Edit the design bundle, not this file, if a value here is ever wrong.
   =========================================================================== */

/* Lapunne — site stylesheet
   The authoritative styling for the redesigned site. This replaces the previous
   site.css. Every value here is taken from the design files in ./design/.

   The designs are styled inline (a constraint of the tool they were made in).
   This file is the same styling expressed as tokens and classes. Where a design
   file and this stylesheet disagree, this file is the one to build against. */

/* ---------------------------------------------------------------- tokens */

:root {
  /* Colour */
  --ivory:        #FAF8F4;  /* page background */
  --panel:        #F1ECE4;  /* alternating sections, bio blocks */
  --stone:        #E4DDD3;  /* placeholders, chart bars, inactive tabs */
  --stone-mid:    #D6CCBF;  /* secondary chart bars */
  --coffee:       #3A2C24;  /* primary text, dark panels, buttons */
  --coffee-hover: #6B563F;
  --body:         #5A4F46;  /* body copy */
  --muted:        #6E6157;  /* labels, captions, secondary text */
  --caramel:      #6B563F;  /* links, accents, eyebrow rules */

  /*
     Three colours the designs use that the token table in the handoff does
     not name. They are not inventions: each is stated in a design file and
     the sweep found all three missing from the build.

       --bio-ink   the fact-strip value on every project page, and the .bio
                   block. 8.79:1 on panel.
       --on-dark-warm  the phase numerals in the home page's dark band, and
                   the link under them. 6.66:1 on coffee.
       --on-photo-warm the eyebrow over the Our Notes hero photograph, which
                   hero:contrast measures against the rendered pixels.
  */
  --bio-ink:        #4A3E35;
  --on-dark-warm:   #C6B49F;
  --on-photo-warm:  #E7DCCB;
  --on-dark:      #FAF8F4;
  --on-dark-soft: #E4DDD3;

  /*
     The design draws NINE alphas of this one colour and the token set names
     three. Two more are added here because the project pages need them and a
     literal in a page stylesheet is how the palette drifts:

       --rule-hair  0.14, the commonest rule in the whole bundle (69 uses):
                    the divider between two cells of a fact strip.
       --rule-mid   0.20 (39 uses): the border around a fact strip and above an
                    outcome block.

       --rule-line  0.18 (42 uses): the rule under a date line, between two
                    FAQ answers, and around a bordered panel.

     Two are left — 0.28 (17 uses), and 0.24 and 0.35 once each — and they are
     drawn with whichever token is nearest. Nothing measures a border:
     `render:pages` compares type, so a rule that is one hundredth of an alpha
     out fails no check and is found by reading the design file.
  */
  --rule:         rgba(58, 44, 36, 0.16);
  --rule-soft:    rgba(58, 44, 36, 0.12);
  --rule-hair:    rgba(58, 44, 36, 0.14);
  --rule-line:    rgba(58, 44, 36, 0.18);
  --rule-mid:     rgba(58, 44, 36, 0.20);
  --rule-strong:  rgba(58, 44, 36, 0.30);

  /* Type */
  --font-serif: "Newsreader", Georgia, "Times New Roman", serif;
  --font-sans:  "Archivo", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;

  /* Space */
  --section-y:   96px;
  --section-x:   56px;
  --section-x-s: 24px;

  /* Measure */
  --w-page:    1360px;
  --w-prose:   820px;
  --w-index:   920px;   /* the Our Notes list, which the design draws wider */
  --w-legal:   760px;
  --w-article: 720px;

  /* Focus */
  --focus: 2px solid var(--coffee);
}

/* ------------------------------------------------------------ base reset */

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

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--ivory);
  color: var(--coffee);
  font-family: var(--font-serif);
  font-size: 20px;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img, svg, video { display: block; max-width: 100%; }
img { width: 100%; height: 100%; object-fit: cover; }

a { color: var(--caramel); text-decoration: none; }
a:hover { color: var(--coffee); }
a:focus-visible,
button:focus-visible,
summary:focus-visible,
[role="tab"]:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  outline: var(--focus);
  outline-offset: 3px;
}

/* Prose links carry an underline; navigation and button links do not. */
.prose a, .legal a, .article__body a { text-decoration: underline; text-underline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}

/* --------------------------------------------------------------- layout */

.section {
  padding: var(--section-y) var(--section-x);
}
.section--panel { background: var(--panel); }
.section--dark  { background: var(--coffee); color: var(--on-dark); }
.section--dark a { color: var(--on-dark); }
/*
   ── THE BLANKET CAME OFF, 15 AUGUST 2026 ──────────────────────────────────
   This was `.section--dark p { color: var(--on-dark-soft) }`, which is right
   on About and wrong six times on the home page. Two dark bands exist and the
   design gives them different reversed values: About's closing lede is
   #E4DDD3, and the home band's paragraphs are three different things —
   0.78 on the supporting sentence, solid ivory on the claim under it, and
   0.72 on the four phases. One rule covering two populations again.

   So About's instance is named, and the home band's are in 04-pages.css beside
   the phase list they belong to. What a future paragraph in a dark band
   inherits is now `--on-dark`, the section's own solid ivory: the highest
   contrast available, which is the safe thing to be wrong in the direction of.
   A blanket guessing "soft" is not.
*/
.section--dark .lede { color: var(--on-dark-soft); }
.section--tight { padding-top: 0; }

.container   { max-width: var(--w-page);    margin-inline: auto; }
.container--prose   { max-width: var(--w-prose);   margin-inline: auto; }
.container--legal   { max-width: var(--w-legal);   margin-inline: auto; }
.container--article { max-width: var(--w-article); margin-inline: auto; }

/* Sidebar-and-content, and equal columns. Both collapse on their own. */
/*
   260px and a 40/80 gap on About, Our Approach and Ready by Completion.
   Contact's "What happens next" is 320px on 56/80, so it says so with
   `--split` rather than taking the shared one: a 320px heading column beside a
   three-across step list is a different proportion, not a near miss.
*/
.split {
  display: grid;
  grid-template-columns: minmax(0, var(--split, 260px)) minmax(0, 1fr);
  gap: var(--split-gap, 40px 80px);
  align-items: start;
}
.cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: 56px 80px;
  align-items: start;
}
/* 40px between rows and 32 between columns, which is what Home draws. It was
   the other way round, and four cards 8px too narrow followed from it. */
.cols--narrow { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 40px 32px; }

/*
   ── AND IT DOES NOT CARRY A MEASURE, WHICH WAS TRIED TWICE ────────────────
   `max-width` on `.stack` is wrong twice over. Written as `var(--measure, …)`
   it leaks: a custom property inherits, so capping the stack caps every
   paragraph and heading inside it as well, and nine measures went the wrong
   way. Written as `var(--stack, none)` it still breaks `.container--legal
   .stack`, because `none` is a real declaration and `.stack` is the later
   rule — three page titles jumped from 760px to the full 1328.

   A layout primitive does not get a width. Where the design caps a column,
   the cap goes on `.measured` below, which is only ever written where it is
   wanted.
*/
.stack       { display: grid; gap: 22px; }
/*
   A column the design caps on the WRAPPER rather than on each paragraph in it.

   In pixels, and that is not laziness. The design writes `max-width:66ch` on a
   div whose font-size is the document's default 16px, and this build's body is
   20px — so the same declaration draws a wider column here. `ch` cannot simply
   be scaled either: Newsreader is a variable face with an optical size axis,
   so the advance of "0" at 20px is not 20/16 of its advance at 16px, and
   66 × 16/20 = 52.8ch comes out 38px short. 598px is what the design draws.
*/
.measured { max-width: var(--stack, 598px); }
.stack--tight{ display: grid; gap: 14px; }
.row         { display: flex; gap: 16px 24px; flex-wrap: wrap; align-items: center; }

.skip-link {
  position: absolute; left: -9999px;
  background: var(--coffee); color: var(--on-dark);
  padding: 12px 20px; font-family: var(--font-sans); font-size: 11px;
  letter-spacing: 0.2em; text-transform: uppercase; z-index: 100;
}
.skip-link:focus { left: 16px; top: 16px; }

/* ----------------------------------------------------------------- type */

/* ═══════════════════════════════════════════════════════════════════════════
   CLASS 5 · THE HEADING SCALE, AS THE DESIGN STATES IT

   The design draws SEVEN h1 steps and NINE h2 steps. The build had three and
   two, and everything else was pulled to the nearest one it had. That is 70 of
   the 165 size differences and all 27 weight differences, and it is the fourth
   time a class in this build has covered more populations than the design's
   equivalent.

   The whole ladder, with what each step is for and how many times the bundle
   draws it, so the next person can see there is a system and where a heading
   belongs in it:

     h1                                        lh     ×
     ── clamp(42px,5.4vw,76px)  --hero        1.04   10   About, Contact, Our
                                                          Approach, 6 projects
     ── clamp(40px,5vw,72px)    .hero__title  1.05    1   Home
     ── clamp(38px,4.6vw,64px)  --notes       1.06    1   Our Notes
     ── clamp(38px,4.6vw,62px)  .title        1.08    4   404, Innovation and
                                                          the two form replies
     ── clamp(36px,4.4vw,60px)  --work        1.08    1   Our Work
     ── clamp(36px,4.2vw,56px)  --legal       1.08    3   Privacy, Cookies,
                                                          Terms
     ── clamp(32px,3.8vw,50px)  --article     1.14    8   the journal

     h2                                        lh  fw    ×
     ── clamp(28px,3.4vw,48px)               1.12 300    1   About's closing
     ── clamp(30px,3.4vw,46px)  --display    1.18 300    3   Home
     ── clamp(28px,3.2vw,44px)               1.18 300    1   Our Work's closing
     ── clamp(28px,3vw,40px)    --section    1.15 300   35   every project
                                                             chapter, and the
                                                             principal heading
                                                             on four pages
     ── clamp(28px,2.9vw,40px)  .cols--work  1.18 300    6   the Our Work cards
     ── clamp(24px,2.4vw,32px)               1.25 400    8   the Our Notes index
     ── clamp(24px,2.4vw,30px)  .h2          1.20 300   13   Contact, Innovation,
                                                             Our Approach
     ── clamp(24px,2.2vw,30px)  article h2   1.28 400   38   the journal
     ── clamp(22px,2.2vw,28px)  legal .h2    1.20 300   20   Privacy, Cookies,
                                                             Terms

   THE DEFAULT IS THE SMALL STEP, DELIBERATELY. A heading that lands here
   without a modifier is drawn at the bottom of the ladder, which is the wrong
   size in a way somebody notices, rather than the wrong size in a way that
   looks intentional. Being wrong in the safe direction is the same choice as
   a paragraph on the dark band inheriting solid ivory.

   AND THE WEIGHT IS ON THE CLASS, NOT THE TAG. `h1, h2, h3, h4 { 300 }` is an
   ELEMENT selector, so `<p class="h3">` and `<figcaption class="h3">` never
   saw it and took the browser's 400. Seven headings on six pages were bold
   because of where the declaration sat rather than what it said. A class that
   names a heading level now carries the whole heading, not half of it.
   ═══════════════════════════════════════════════════════════════════════════ */

h1, h2, h3, h4 { font-weight: 300; letter-spacing: -0.01em; text-wrap: balance; margin: 0; }

/*
   THE MEASURE GOES ON THE STEP, NOT ON `.title`.

   The design caps a page title on ten of the twenty-eight pages and leaves the
   other eighteen alone, and the split is by STEP rather than by page: every
   `--hero` title is capped, no `--article` or `--legal` one is. A default on
   `.title` would be inherited by all four modifiers and would be wrong eleven
   times, so the base carries a hook and no value.
*/
.title {
  font-size: clamp(38px, 4.6vw, 62px);
  line-height: 1.08;
  letter-spacing: -0.02em;
  font-weight: 300;
  max-width: var(--measure, none);
}
/* 16ch on six of the ten, including five of the six project pages. The other
   four state theirs: 18ch on Our Approach, About and Ready by Completion,
   15ch on the major transformation. */
.title--hero { font-size: clamp(42px, 5.4vw, 76px); line-height: 1.04; max-width: var(--measure, 16ch); }
.title--article { font-size: clamp(32px, 3.8vw, 50px); line-height: 1.14; letter-spacing: -0.02em; }
.title--legal { font-size: clamp(36px, 4.2vw, 56px); }
.title--work  { font-size: clamp(36px, 4.4vw, 60px); max-width: var(--measure, 18ch); }

/*
   CLASS 3 · THE HEADING MEASURE IS PER INSTANCE, AND A DEFAULT WAS TRIED AND
   REVERTED.

   The design caps 36 of its 55 h2s at 22ch, 6 at 20, 5 at 24 and 8 at 34, and
   the build caps none — which is most of the width differences in
   `npm run render:pages` and, on the pages where a heading should be two
   lines, is why it is one long one.

   `max-width: var(--measure, 22ch)` was the obvious fix and it is recorded
   here because it was MEASURED AND IT WAS WORSE: it corrected 26 headings and
   broke 32, because `.h2` in this build is also used on headings the design
   leaves uncapped, and the two populations do not line up. Measure went 122 to
   128 and width 358 to 383.

   ── AND HERE IS WHY, READ ON 15 AUGUST 2026 ───────────────────────────────
   The number was never the difficult part. `.h2` was one class over four of
   the design's steps, and the design leaves two of them ENTIRELY UNCAPPED: 38
   journal headings and 20 legal ones, 58 headings with no measure at all
   against 35 capped at 22ch. No single default can be right for that
   population, which is exactly the 26-against-32 the experiment measured.

   Class 5 gave those steps their own classes. The measures then went in
   without a fight, one population at a time:

     .chapter__head .h2   22ch default, 26 of 30 · 4 stated in chapter data
     .h2--section         22ch default, 2 of 5   · 3 stated at the element
     .h2--display         20ch default, 3 of 5   · 2 stated at the element
     .h2 (this rule)      NO default, 11 of 13   · 2 stated at the element
     .article__body h2    no measure, 38 of 38
     .article__body .h2   no measure, 20 of 20
     .notes-index__title  34ch, 8 of 8
     .cols--work .h2      22ch, 6 of 6

   126 h2 elements, every one now drawn at the measure its own design file
   states. Measure 116 to 74, width 316 to 275, lines 166 to 136.

   The lesson is not "read every instance". It is that a blanket is only
   answerable once the class underneath it holds ONE population. Splitting the
   sizes was what made the measures a five-minute job.
*/
/*
   NO DEFAULT MEASURE, DELIBERATELY. The design leaves 11 of the 13 headings at
   this step uncapped, and the two that are capped say so where they are. This
   is the class the reverted class-3 experiment was applied to; `var(--measure,
   none)` is how the exception gets in without a default that is wrong 11 times.
*/
.h2 {
  font-size: var(--h2, clamp(24px, 2.4vw, 30px));
  line-height: var(--h2-lh, 1.2);
  font-weight: 300;
  max-width: var(--measure, none);
}
/*
   The 35-instance step, and the one the build did not have. It is every
   project chapter heading and the principal heading on Home, About, Our
   Approach and Ready by Completion — the largest single population in the
   bundle, drawn at 30px because `.h2` was the only class there was.
*/
.h2--section {
  font-size: clamp(28px, 3vw, 40px);
  line-height: var(--h2-lh, 1.15);
  /* 22ch on 28 of the 35 headings at this step. The five outside the project
     pages each state their own where they differ, including the one the design
     leaves uncapped. */
  max-width: var(--measure, 22ch);
}
.h3 { font-size: clamp(21px, 2.1vw, 26px); line-height: 1.45; font-weight: 300; max-width: var(--measure, none); }

/*
   ── 46ch, AND 52 WAS A TRANSCRIPTION ──────────────────────────────────────
   This said "52ch is the design's most common lede measure". Read out of the
   rendered design rather than the handoff, it is not: 46ch is stated on nine
   of the eighteen ledes the bundle draws, and 52ch on two. Six project pages,
   both Ready by Completion ledes and the Contact opening are all 46.

   The seven that differ say so where they are: 52ch twice on About, 62ch three
   times and 56ch once on Our Approach, 54 on the could-not-send page, 50 on
   the 404, and Innovation's is uncapped.
*/
.lede {
  font-size: var(--lede, 22px);
  line-height: 1.6;
  max-width: var(--measure, 46ch);
  text-wrap: pretty;
}

/*
   ── THE ONE PLACE THIS FILE IS OVERRULED, AND WHY ─────────────────────────
   `max-width: 66ch` was a blanket cap on every prose paragraph on the site.
   The design files state a measure PER ELEMENT — 62ch, 56ch, 52ch, 46ch,
   44ch, 22ch and a dozen others — and the two cannot both be true. The sweep
   found 66ch on 21 of 28 pages and 73 stated measures never produced.

   The handoff's arbitration clause says this stylesheet wins on styling.
   Pearl overruled it on 10 August 2026, and the reasoning is worth keeping:
   the inline-styled design files are what she reviewed and approved, page by
   page. This stylesheet was written afterwards as a tokenised interpretation
   of them, and in doing so it generalised something nobody reviewed. So where
   a blanket rule here contradicts a value the design states per instance, the
   design wins.

   `--measure` is how a per-element value gets back in: the template sets it
   where the design states one, and where the design states none the paragraph
   is capped by its container instead, which is what the eight article pages
   do — they set no ch measure anywhere and rely on --w-article at 720px.
*/
/*
   ── 1.78 IS THE JOURNAL'S, AND IT WAS APPLIED TO EVERY PROSE BLOCK ────────
   The design sets `font-size:20px; line-height:1.78` on the container of an
   ARTICLE, and nothing else in the bundle uses 1.78 — 80 paragraphs are 1.7,
   55 are 1.75 in the legal column, and the home page's are 1.68. One value
   transcribed off one page and made the default for all of them.

   So the two are separated: `.article__body p` keeps the journal's, and
   `.prose p` takes 1.7, which the design states more often than every other
   value put together. `--prose-lh` carries the rest.
*/
.article__body p {
  margin: 0 0 26px;
  font-size: var(--prose, 20px);
  line-height: var(--prose-lh, 1.78);
  max-width: var(--measure, none);
  text-wrap: pretty;
}
.prose p {
  margin: 0 0 26px;
  font-size: var(--prose, 20px);
  line-height: var(--prose-lh, 1.7);
  max-width: var(--measure, none);
  text-wrap: pretty;
}
.prose p:last-child, .article__body p:last-child { margin-bottom: 0; }
/*
   THE SECOND REGISTER IS THREE SIZES, NOT ONE. The design writes this quieter
   voice at 20px where it is carrying the argument, 19px where it is qualifying
   one, and 18px where it is a standfirst over a list. 19 is the commonest and
   stays the default; `--prose` is how the other two get stated at the block
   that uses them, the same way `--measure` states a paragraph's own measure.
*/
.prose--secondary p { font-size: var(--prose, 19px); line-height: var(--prose-lh, 1.7); color: var(--body); }

/*
   TWO KINDS OF DOCUMENT SHARE THIS COLUMN, AND THE DESIGN SETS THEIR HEADINGS
   DIFFERENTLY.

   `.article__body` is the reading column for the eight journal articles AND
   for privacy, cookies and terms, which is why the legal pages read as well as
   they do. The design draws a journal section heading at clamp(24,2.2vw,30)
   in 400 and a legal one at clamp(22,2.2vw,28) in 300 — a step smaller and a
   weight lighter, because a legal document's headings are signposts and an
   article's are the argument.

   One rule at 400 covered both, so all twenty legal headings were a size too
   large and a weight too heavy. They are told apart by what they already are:
   a journal heading is a bare `<h2>` written by Markdown, a legal one is
   `<h2 class="h2">` written by hand. The margin is the same for both and stays
   on the shared rule.

   Scoped to the container rather than given a modifier so the next heading
   added to a legal page is right without anybody remembering to say so.
*/
.article__body h2 { margin: 52px 0 22px; }
.article__body h2:not([class]) { font-size: clamp(24px, 2.2vw, 30px); line-height: 1.28; font-weight: 400; }
.article__body .h2 { font-size: clamp(22px, 2.2vw, 28px); line-height: 1.2; }

/*
   0.2em, which the design states on 146 of its 149 ten-pixel labels and 114 of
   its eleven-pixel ones. 0.22em was a transcription and it is two pixels of
   width per word — enough to wrap the services line on /our-work/ onto a
   second line. `--label-ls` carries the six instances the design tracks
   differently.
*/
.label {
  font-family: var(--font-sans);
  font-size: 11px;
  letter-spacing: var(--label-ls, 0.2em);
  text-transform: uppercase;
  color: var(--muted);
}
.label--tight { font-size: 10px; letter-spacing: var(--label-ls, 0.2em); }
.label--accent { color: var(--caramel); }

.rule       { width: 64px; height: 1px; background: var(--caramel); border: 0; margin: 0; }
.rule--full { width: 100%; background: var(--rule); }

.quote {
  margin: 14px 0 0;
  padding-left: 24px;
  border-left: 2px solid var(--caramel);
  /* About's. The four project pull quotes are a step up and say so in
     12-project.css, which owns the chapter they sit in. */
  font-size: clamp(24px, 2.5vw, 32px);
  line-height: 1.3;
  font-weight: 300;
  font-style: italic;
  /* Stated on every blockquote in the bundle. A blockquote is not one of the
     four tags the heading reset reaches, so it was the only display-sized type
     on the site setting at the default tracking — 14px wider over a 42
     character line, which is what put one pull quote onto a second line. */
  letter-spacing: -0.01em;
  text-wrap: pretty;
}

.note {
  font-size: 16px;
  line-height: 1.68;
  color: var(--body);
  border-top: 1px solid var(--rule);
  padding-top: 28px;
}
.note strong { color: var(--coffee); font-weight: 500; }

/*
   The one disclaimer in the journal, on the renovation-budget article. The
   design sets it at 18px in --bio-ink, a size down and a shade back from the
   article it interrupts, because it is the article stepping out of its own
   voice to say "check this with somebody qualified". Raw HTML in the Markdown
   because that is the only way to name one paragraph, and it is named rather
   than selected by position for the reason `.lede + .lede` stopped being.
*/
.article__body p.disclaimer { font-size: 18px; line-height: 1.65; color: var(--bio-ink); }
/*
   ── `.note` IS TWO THINGS, AND THE DESIGN SIZES THEM DIFFERENTLY ──────────
   The rule above is the editorial note under a journal article: 16px on 1.68
   in `--body`, which is what the design states and what it should stay.

   The sign-off at the foot of a legal page and the line under the 404's link
   list share the markup and nothing else. The design draws both a step up and
   a shade back — 17px in `--muted` — because they are asides to a document
   rather than part of it. Four instances, all previously drawn as if they were
   the journal's.

   This is also the four colour rows `npm run render:pages` has been carrying:
   the size and the colour were one substitution, not two.
*/
.note--aside { font-size: 17px; line-height: var(--note-lh, 1.6); color: var(--muted); }
.note--aside p { max-width: var(--measure, none); }

.bio { padding: 28px; background: var(--panel); font-size: 17px; line-height: 1.68; color: #4A3E35; }

/* -------------------------------------------------------------- buttons */

.btn {
  display: inline-flex;
  align-items: center;
  min-height: 48px;
  /* 15px 30px on a link and 16px 32px on the one submit button, which is what
     the two design components state. `--btn-pad` carries the difference. */
  padding: var(--btn-pad, 15px 30px);
  border: 0;
  border-radius: 0;              /* no radius anywhere on this site */
  background: var(--coffee);
  color: var(--on-dark);
  font-family: var(--font-sans);
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  text-decoration: none;
  cursor: pointer;
}
.btn:hover { background: var(--coffee-hover); color: var(--on-dark); }

.btn--ghost { background: transparent; color: var(--coffee); border: 1px solid var(--rule-strong); }
.btn--ghost:hover { background: transparent; border-color: var(--coffee); color: var(--coffee); }

.section--dark .btn { background: var(--on-dark); color: var(--coffee); }
.section--dark .btn:hover { background: var(--on-dark-soft); }
.section--dark .btn--ghost { background: transparent; color: var(--on-dark); border-color: rgba(250,248,244,0.5); }
.section--dark .btn--ghost:hover { border-color: var(--on-dark); }

.link-arrow {
  font-family: var(--font-sans);
  font-size: 11px;
  letter-spacing: var(--label-ls, 0.2em);
  text-transform: uppercase;
  text-decoration: none;
}

/* ----------------------------------------------------------------- nav */

.nav {
  position: fixed;
  inset: 0 0 auto;
  z-index: 50;
  padding: 22px var(--section-x);
  transition: transform 0.3s ease, background 0.3s ease, border-color 0.3s ease;
  border-bottom: 1px solid transparent;
}
.nav[data-scrolled="true"] { background: var(--ivory); border-bottom-color: var(--rule); }
.nav[data-hidden="true"]   { transform: translateY(-100%); }
.nav__inner { max-width: var(--w-page); margin-inline: auto; display: flex; align-items: center; justify-content: space-between; gap: 32px; }
.nav__links { display: flex; gap: 36px; flex-wrap: wrap; }
.nav__link {
  font-family: var(--font-sans); font-size: 11px; letter-spacing: 0.18em;
  text-transform: uppercase; color: var(--muted); padding: 6px 0; text-decoration: none;
}
.nav__link:hover, .nav__link[aria-current="page"] { color: var(--coffee); }
.nav__toggle { display: none; }

/* --------------------------------------------------------------- footer */

.footer { border-top: 1px solid var(--rule); padding: 32px var(--section-x) 44px; }
.footer__inner {
  max-width: var(--w-page); margin-inline: auto;
  display: flex; align-items: center; justify-content: space-between;
  gap: 32px; flex-wrap: wrap;
  font-family: var(--font-sans); font-size: 11px;
  letter-spacing: 0.18em; text-transform: uppercase; color: var(--muted);
}
.footer__links { display: flex; gap: 32px; flex-wrap: wrap; }
.footer__links a { color: var(--muted); padding: 4px 0; text-decoration: none; }
.footer__links a:hover { color: var(--coffee); }

/* -------------------------------------------------------------- figures */

figure { margin: 0; }
.figure__frame { overflow: hidden; background: var(--stone); }
.figure__frame--4x5 { aspect-ratio: 4 / 5; }
.figure__frame--16x9 { aspect-ratio: 16 / 9; }
/* 60ch is this file's generalisation of a value the designs state per
   figure: 46ch in a pair, 70ch beside a single or a full-bleed image. Same
   ruling as the paragraph measure above — the per-instance value wins. */
figcaption { margin-top: 14px; font-size: 17px; line-height: 1.6; color: var(--muted); max-width: var(--measure, 60ch); text-wrap: pretty; }

.placeholder {
  display: flex; align-items: center; justify-content: center;
  background: var(--ivory); border: 1px solid rgba(58,44,36,0.16);
}

/* --------------------------------------------------------------- tables */

.table { width: 100%; border-collapse: collapse; text-align: left; }
.table th {
  padding: 0 24px 12px 0;
  border-bottom: 1px solid var(--rule-strong);
  font-family: var(--font-sans); font-size: 10px; font-weight: 500;
  letter-spacing: 0.2em; text-transform: uppercase; color: var(--muted);
}
.table td {
  padding: 16px 24px 16px 0;
  border-bottom: 1px solid var(--rule-soft);
  font-size: 18px; color: var(--body); vertical-align: top;
}
.table td:first-child { color: var(--coffee); }
.table tr:last-child td { border-bottom: 0; }

/* ------------------------------------------------------------ tab groups */

.tabs { display: flex; gap: 8px; flex-wrap: wrap; }
.tab {
  padding: 12px 22px;
  border: 0; border-radius: 0;
  background: var(--stone);
  color: var(--coffee);
  font-family: var(--font-sans); font-size: 11px;
  letter-spacing: 0.18em; text-transform: uppercase;
  cursor: pointer;
}
.tab[aria-selected="true"] { background: var(--coffee); color: var(--on-dark); }
.tab:hover:not([aria-selected="true"]) { background: var(--stone-mid); }
.tabpanel[hidden] { display: none; }

/* --------------------------------------------------------------- details */

details { border-bottom: 1px solid var(--rule); }
details summary {
  display: flex; align-items: baseline; justify-content: space-between; gap: 24px;
  padding: 22px 0; cursor: pointer; list-style: none;
  font-size: 21px; line-height: 1.4; font-weight: 300; color: var(--coffee);
}
details summary::-webkit-details-marker { display: none; }
details summary::after {
  content: "+";
  font-family: var(--font-sans); font-size: 18px; color: var(--caramel); line-height: 1;
}
details[open] summary::after { content: "\2212"; }
details .prose { padding-bottom: 22px; }

/* ----------------------------------------------------------------- forms */

.field { display: grid; gap: 8px; }
/*
   A LABEL STATES NO LEADING IN THE DESIGN, SO IT STATES NONE HERE.

   Every Archivo label, legend, chart bar and eyebrow in the bundle is written
   with a font-family, a size and a tracking, and no line-height at all — so it
   draws at the face's own `normal`. Inheriting the body's 1.7 onto a 10px
   label puts seven pixels of air inside a one-line control, which is 28 of the
   leading rows in `npm run render:pages` and the same fault the house rules
   already record for captions.
*/
.field__label {
  font-family: var(--font-sans); font-size: 10px;
  letter-spacing: 0.2em; text-transform: uppercase; color: var(--muted);
  line-height: normal;
}
.field__hint { font-size: 16px; line-height: 1.6; color: var(--muted); }

input[type="text"], input[type="email"], input[type="tel"], input[type="date"],
select, textarea {
  width: 100%;
  min-height: 48px;
  padding: 13px 14px;
  border: 1px solid var(--rule-strong);
  border-radius: 0;
  background: var(--ivory);
  color: var(--coffee);
  font-family: var(--font-serif);
  font-size: 18px;
  line-height: 1.5;
}
textarea { min-height: 160px; resize: vertical; }
select { appearance: none; background-image: none; }
input:hover, select:hover, textarea:hover { border-color: var(--coffee); }

.radios { display: flex; gap: 12px 28px; flex-wrap: wrap; }
.radio { display: inline-flex; align-items: center; gap: 10px; font-size: 19px; min-height: 44px; line-height: normal; }
.radio input { width: 20px; height: 20px; min-height: 0; accent-color: var(--coffee); }

/* Honeypot. Off-screen, never display:none, never named for autofill. */
.honeypot { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* ------------------------------------------------------------ responsive */

@media (max-width: 1200px) {
  .split { gap: 32px 48px; }
}

@media (max-width: 980px) {
  :root { --section-y: 72px; }
  .chart { display: none; }
  .chart-fallback { display: block; }
}

@media (max-width: 880px) {
  :root { --section-x: var(--section-x-s); }
  .split, .cols { grid-template-columns: minmax(0, 1fr); }
  .nav__links { display: none; }
  .nav__toggle { display: inline-flex; }
  .nav[data-open="true"] .nav__links {
    display: flex; flex-direction: column; gap: 20px;
    width: 100%; margin-top: 24px;
  }
  .nav[data-open="true"] .nav__inner { flex-wrap: wrap; }
  body { font-size: 19px; }
}

/* ---------------------------------------------------------------- print */

@media print {
  .nav, .skip-link, .btn, .tabs { display: none; }
  body { background: #fff; font-size: 11pt; }
  .section { padding: 0 0 24pt; }
  a { color: inherit; text-decoration: underline; }
}

/* === 03-shell.css === */
/* ===========================================================================
   The shell, where the authoritative stylesheet stops short.

   site.css styles `.nav` for its solid state and gives it `data-scrolled`,
   `data-hidden` and `data-open` to switch on. What it does not carry is the
   TRANSPARENT state — the nav sitting over a hero photograph, ivory wordmark,
   ivory links, and the scrim underneath them that makes those legible. That
   behaviour is specified in Site Nav.dc.html and nowhere else, so it is ported
   from the design file, exactly as the handoff says to do with the timelines
   and the ecosystem diagram.

   ── Why the default is SOLID, and why that is not a deviation ──────────────
   Read literally, `.nav[data-scrolled="true"] { background: var(--ivory) }`
   means the nav is transparent until you scroll — on every page, hero or not.
   The design's own logic says otherwise: `solid` starts true and only goes
   false when the `transparent` prop is set AND the hero is still on screen.
   This repo has already paid for getting that backwards. A transparent-by-
   default header shipped once and produced 128 contrast failures across 26
   pages, because a header with no background over a pale canvas is a header
   with no contrast guarantee at all.

   So: solid is the default, transparent is opt-in per page, and it is scoped
   to `body:has(.hero)` so a page without a hero cannot accidentally get it
   even if the flag is set wrongly in front matter.
   =========================================================================== */

/* The nav is fixed, so every page without a hero owes it the height it covers.
   A hero page owes nothing: the photograph is meant to run under it. */
.shell--solid { padding-top: 84px; }

/* --------------------------------------------------------- transparent nav */

body:has(.hero) .nav:not([data-scrolled='true']) {
  background: transparent;
  border-bottom-color: transparent;
}
body:has(.hero) .nav:not([data-scrolled='true']) .nav__link { color: var(--on-dark); }
/*
   `:not([open])` because the ivory trigger is for a trigger sitting ON a
   photograph. Once the menu is open the photograph is behind a full-screen
   ivory panel, and ivory on ivory is a Close control nobody can see. This was
   the state the specificity fight was about: the transparent-hero rule is one
   element more specific than anything scoped to the open menu, so the right
   fix is to narrow this rule rather than to out-weigh it.
*/
body:has(.hero) .nav:not([data-scrolled='true']) .nav__menu:not([open]) .nav__toggle { color: var(--on-dark); }

/* The wordmark is two files, not one recoloured: the ivory mark is drawn for a
   dark ground and is not the coffee mark with a filter on it. */
.nav__mark--ivory { display: none; }
body:has(.hero) .nav:not([data-scrolled='true']) .nav__mark--coffee { display: none; }
body:has(.hero) .nav:not([data-scrolled='true']) .nav__mark--ivory { display: block; }
/* The panel's mark is the coffee one and has no ivory twin to switch with. */
.nav__panel-head .nav__mark { display: block; }

/*
   THE SCRIM, AND WHY IT IS MEASURED RATHER THAN EYEBALLED.

   The hero's own gradient runs left to right and is thinnest at the right
   edge, which is exactly where the navigation links sit. Without this the top
   right of a hero page is ivory 11px type over whatever the photograph happens
   to be doing there, and "whatever the photograph happens to be doing" is not
   a contrast ratio. `npm run hero:contrast` measures the nav label and the
   wordmark against the rendered pixels at five widths.
*/
.nav__scrim {
  position: absolute;
  inset: 0 0 auto;
  height: 150px;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s ease;
  background: linear-gradient(
    180deg,
    rgba(24, 17, 13, 0.62) 0%,
    rgba(24, 17, 13, 0.28) 55%,
    rgba(24, 17, 13, 0) 100%
  );
}
body:has(.hero) .nav:not([data-scrolled='true']) .nav__scrim { opacity: 1; }

.nav__inner { position: relative; }
/*
   ── THE MARK IS BIGGER THAN THE DESIGN DRAWS IT, DELIBERATELY ──────────────
   The design states 40px. Pearl's ruling of 12 August 2026 is 56, and the
   reason is what the mark IS on this site rather than what it looks like: on
   every page except the home page it is the ONLY route home, and at 40px it
   read as decoration rather than as the control it is. On a phone it is also
   the only route home that is visible at all, because the menu panel covers
   it once open.

   56px costs nothing anywhere. `.nav__inner` has `min-height: 84px` and the
   brand pays 6px of padding each side, so the row was 52px inside an 84px
   floor: there were 32px of slack and this takes 16 of them. The header does
   not grow at any width.

   The tap target is the <a>, not the image, and it was already over 44 tall
   through that padding. It is now 44 in BOTH directions with room to spare,
   which is what WCAG 2.2 SC 2.5.8 asks and what `npm run layout` asserts at
   four widths.
*/
.nav__mark { height: 56px; width: auto; object-fit: contain; }

/*
   ── AND THE IVORY MARK WAS DRAWING ON EVERY PAGE WITHOUT A HERO ────────────
   Found while enlarging it. `.nav__mark--ivory { display: none }` sits fifty
   lines above `.nav__mark { … display: block }`, both are one class, and the
   later one wins — so the ivory wordmark, which exists only for a dark ground,
   was rendering UNDER the coffee one on every solid-header page since the
   port. It is `alt=""` and `aria-hidden`, so no screen reader ever announced
   it and nothing measured it, but it was a second copy of the logo on the page
   and it doubled the height of the header: 92px of brand inside a 137px header
   where 52 and 113 were intended.

   `display: block` is on the two rules that decide WHICH mark shows, and not
   on the rule that decides how big they are. A base rule must not restate a
   property its own modifiers exist to switch.
*/
.nav__mark--coffee { display: block; }
.nav__brand { display: block; flex: 0 0 auto; padding: 6px 0; }

/* ------------------------------------------------------------ mobile menu */

/*
   A native <details>. No JavaScript opens or closes this, which is the point:
   the menu is the only way to reach three of the four sections on a phone, and
   a menu that depends on a script is a site that depends on a script.
*/
.nav__toggle {
  align-items: center;
  gap: 10px;
  min-width: 44px;
  min-height: 44px;
  cursor: pointer;
  list-style: none;
  font-family: var(--font-sans);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  justify-content: flex-end;
}
.nav__toggle::-webkit-details-marker { display: none; }
.nav__burger { display: flex; flex-direction: column; gap: 5px; width: 22px; }
.nav__burger span { display: block; height: 1px; background: currentColor; }

/*
   ── THE WAY OUT ───────────────────────────────────────────────────────────
   The panel is `position: fixed; inset: 0; z-index: 70`, so by default it
   covers its own trigger and the menu has no close control at all. That is
   what shipped: a full-screen menu whose only exit was to navigate somewhere.

   The summary is raised above it and relabelled instead of a separate button,
   because a <button> inside a <details> needs JavaScript to close it and this
   menu is the only route to three of the four sections on a phone. As the
   summary it closes on click, on Enter and on Space with no script, it keeps
   focus while it does so — which is why focus is on the trigger afterwards
   without anything putting it there — and it lands where the design draws
   Close: 22px from the top and 32px from the right, the panel's own padding.

   44×44 against the 24×24 that WCAG 2.2 SC 2.5.8 asks for, and `npm run
   controls` measures it.
*/
.nav__toggle-label--close { display: none; }
.nav__menu[open] .nav__toggle-label--open,
.nav__menu[open] .nav__burger { display: none; }
.nav__menu[open] .nav__toggle-label--close { display: inline; }

/*
   The colour needs the `.nav` in the selector to outweigh the transparent-hero
   rule above, which is `body:has(.hero) .nav:not([data-scrolled]) .nav__toggle`
   and paints the trigger ivory for a photograph. Over the open panel that is
   ivory on ivory: the word was there and unreadable, which is the same fault
   as no word at all.

   `top: 15px` rather than 22 so the control sits on the wordmark's centre
   line, not below it: the panel pays 22px of top padding, the wordmark is 30px
   tall, so its centre is at 37px and a 44px-tall control starts at 15. The
   target stays 44×44 against the 24×24 minimum.

   `content: none` because `details summary::after` in 02-site.css draws a
   plus, and a minus when open. That is right for the FAQ disclosures it was
   written for and wrong here: it drew a stray dash beside the word Close.
*/
.nav .nav__menu[open] .nav__toggle {
  position: fixed;
  top: 15px;
  right: 32px;
  z-index: 80;
  padding: 12px;
  color: var(--coffee);
}
.nav__menu > .nav__toggle::after { content: none; }
.nav__menu { border-bottom: 0; }

.nav__menu { position: static; }
.nav__panel {
  position: fixed;
  inset: 0;
  z-index: 70;
  background: var(--panel);
  display: flex;
  flex-direction: column;
  padding: 22px 32px 40px;
  animation: navFade 240ms ease both;
}
@keyframes navFade {
  from { opacity: 0; }
  to { opacity: 1; }
}
.nav__panel-head { display: flex; align-items: center; justify-content: space-between; gap: 24px; }
/* And in the open panel, where it is the only thing identifying the site.
   40px rather than the design's 30, for the same reason and in proportion. */
.nav__panel-head .nav__mark { height: 40px; }
/*
   `.nav__close` was here, styling a button the markup never rendered, from the
   day the panel was ported until 13 August. A rule for an element that does
   not exist is not a harmless leftover: it read as evidence that the control
   was present, and it is why the missing close control survived a build, a
   review and a launch. The summary is the control now — see the block above.
*/
.nav__panel-links { margin-top: auto; display: flex; flex-direction: column; gap: 6px; }
.nav__panel-links a {
  font-size: 38px;
  line-height: 1.25;
  font-weight: 300;
  letter-spacing: -0.01em;
  color: var(--coffee);
  padding: 8px 0;
}
.nav__statement {
  margin: 36px 0 0;
  font-size: 19px;
  line-height: 1.55;
  color: var(--body);
  max-width: 26ch;
  text-wrap: pretty;
}
.nav__panel-cta {
  margin-top: 28px;
  padding-top: 22px;
  border-top: 1px solid var(--rule-strong);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
  color: var(--coffee);
  font-family: var(--font-sans);
  font-size: 12px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  min-height: 44px;
}

/* A full-screen menu that scrolls the page behind it is a menu that loses your
   place. No script needed for this either. */
html:has(.nav__menu[open]) { overflow: hidden; }

@media (min-width: 55.0625rem) {
  .nav__menu { display: none; }
}
@media (max-width: 55rem) {
  .nav__toggle { display: flex; }
}

/* ----------------------------------------------------------------- hero */

.hero { position: relative; }
.hero__frame {
  width: 100%;
  height: clamp(520px, 46vw, 760px);
  overflow: hidden;
  background: var(--stone);
}
/*
   THE HERO SCRIM, AND THE NARROW-WIDTH CASE THE DESIGN DOES NOT SPECIFY.

   Left to right, deep at the left where the text starts and almost clear at
   the right where the photograph should be seen. That is the design file's
   gradient, verbatim, and on a wide screen it is right: the headline occupies
   16ch on the left and never reaches the pale end.

   On a phone it is wrong, and measurably so. The text block spans the full
   width, so its right-hand end sits on the 0.22 stop — the headline measured
   2.72:1 at 375px against a target of 3.0, and the sub-line 2.45:1 against
   4.5. Not a judgement call: `npm run hero:contrast` reads the rendered
   pixels.

   Below 55rem the gradient turns vertical instead. Same colour, same intent,
   and it follows the text rather than the composition — which is the honest
   response to a layout that has stopped being two columns. This is the
   "responsive behaviour the design implies but does not specify" clause, and
   it is the one place on this site where a scrim was chosen by measurement
   rather than by eye, because the alternative was shipping a headline nobody
   with average sight could read outdoors.
*/
.hero__scrim {
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    rgba(28, 20, 16, 0.82) 0%,
    rgba(28, 20, 16, 0.62) 46%,
    rgba(28, 20, 16, 0.22) 100%
  );
}

@media (max-width: 55rem) {
  .hero__scrim {
    background: linear-gradient(
      180deg,
      rgba(28, 20, 16, 0.38) 0%,
      rgba(28, 20, 16, 0.62) 46%,
      rgba(28, 20, 16, 0.86) 100%
    );
  }
}

/*
   The eyebrow over a photograph cannot be `--muted`. #6E6157 reaches 3.51:1
   against pure black, so as small text needing 4.5:1 there is no scrim deep
   enough to rescue it — the failure is in the colour, not the ground. On dark
   it becomes stone, which the audit already measures at 9.95:1 on coffee.
*/
/*
   #E7DCCB, which is what the Our Notes design states for the eyebrow over its
   hero photograph. It was --on-dark-soft, one of the same class of quiet
   substitutions as the phase numeral. hero:contrast measures this against the
   rendered pixels rather than against a flat ground, which is the only
   measurement that means anything over a photograph.
*/
.hero .label { color: var(--on-photo-warm); }
.hero__body {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-end;
  padding: 64px var(--section-x) 72px;
}
.hero__inner { max-width: var(--w-page); margin: 0 auto; width: 100%; }
.hero__title {
  margin: 0;
  max-width: 16ch;
  font-size: clamp(40px, 5vw, 72px);
  line-height: 1.05;
  font-weight: 300;
  letter-spacing: -0.02em;
  color: var(--on-dark);
  text-wrap: balance;
}
/*
   Our Notes is the second page with a photographic hero and the design sets its
   headline a step down: clamp(38px,4.6vw,64px) on 1.06, against the home page's
   clamp(40px,5vw,72px) on 1.05. Home's line is four words and this one is
   eleven, which is what the step is for.
*/
/* Uncapped, where Home's is 16ch. Home's headline is four words and this one
   is eleven: a 16ch cap would break it into four lines. */
.hero__title--notes { font-size: clamp(38px, 4.6vw, 64px); line-height: 1.06; max-width: none; }
.hero__lede {
  margin: 26px 0 0;
  /* 50ch on Home, 56ch on Our Notes. */
  max-width: var(--measure, 50ch);
  font-size: clamp(18px, 1.6vw, 21px);
  line-height: 1.6;
  color: var(--on-dark);
  text-wrap: pretty;
}
.hero .btn--ghost {
  margin-top: 32px;
  border-color: rgba(250, 248, 244, 0.6);
  color: var(--on-dark);
}
.hero .btn--ghost:hover {
  background: var(--on-dark);
  color: var(--coffee);
  border-color: var(--on-dark);
}

/* === 04-pages.css === */
/* ===========================================================================
   What the authoritative stylesheet does not carry.

   02-site.css is the handoff's site.css verbatim and 03-shell.css is the nav's
   transparent state and the hero, ported from Site Nav.dc.html. Between them
   they cover most of the redesign. This file is the remainder found while
   porting Home, Our Work, Our Notes and Our Model, and every rule below names
   the design file it comes from and why the authoritative sheet has no
   equivalent.

   It is a SHARED stylesheet, not a page's: everything here is used by at least
   two of those four pages, and `checkClassOwnership` in scripts/audit.mjs lists
   it under GLOBAL for that reason. Nothing page-specific belongs here — the
   moment a rule is true of exactly one page it goes in a numbered scoped
   partial, like the timelines and the ecosystem diagram already do.

   Values are taken from the design files only where the authoritative sheet
   offers nothing at all. Where it offers something close, its value wins: the
   preamble to 02-site.css is explicit that it is the file to build against when
   it and a design file disagree.
   =========================================================================== */

/* ------------------------------------------------------------ media frames */

/*
   The design files put a bare <img> inside every media frame and 02-site.css
   answers with `img { width: 100%; height: 100%; object-fit: cover }`. The
   `picture` shortcode and partials/work-image.njk both wrap that <img> in a
   <picture>, which is inline by default, so the image's percentage height has
   no box to resolve against and the crop silently stops happening.

   Making the wrapper a block that fills its parent restores exactly the box the
   design draws. Where the parent has no height of its own this resolves to
   auto, which is the behaviour every other image on the site already has.
*/
picture { display: block; height: 100%; }

/*
   FOUR BY THREE. Every card and every project image in Home.dc.html and
   Our Work.dc.html sits in `aspect-ratio:4/3`. 02-site.css carries 3x2, 4x5 and
   16x9 and not this one, so it is added rather than a near ratio substituted:
   the four home cards and the six project rows are the only images on those two
   pages and they all share it.
*/
.media-4x3 {
  aspect-ratio: 4 / 3;
  overflow: hidden;
  background: var(--stone);
}
.media-4x3 figure { height: 100%; }

/* The project images keep partials/work-image.njk, which renders a <figure>.
   Its caption is only ever set on `existing` and `construction` images, and no
   card image is one, so nothing is being hidden here. */

.media-link { display: block; }

/* ------------------------------------------------------------- hero scrim */

/*
   A SECOND SCRIM DIRECTION, BECAUSE THE TWO HEROES SIT THEIR TYPE DIFFERENTLY.

   `.hero__scrim` in 03-shell.css is Home.dc.html's gradient and runs left to
   right: the home hero's text is a column against the left edge, so the darkest
   band belongs down that edge and the photograph is left alone on the right.

   Our Notes.dc.html sets its type across the full width at the foot of the
   frame instead, and draws its own gradient at 180deg for that reason. A
   left-to-right scrim would leave the end of the headline and the whole right
   half of the sub-line sitting on unmodified photograph, which is the exact
   arrangement that produced 128 contrast failures in August.

   So this is a modifier rather than an edit: the home page depends on the
   horizontal one, and one page's composition is not the other's.

   ── WHY THE STOPS ARE NOT THE DESIGN FILE'S ────────────────────────────────
   Our Notes.dc.html draws 0.52 / 0.44 / 0.74, and that gradient DIPS in the
   middle: it is darkest at the very top and at the very foot, and lightest
   across the band between them. In the design file the headline sits below that
   band, because its frame is clamp(440px, 40vw, 620px). This site's hero frame
   is 03-shell.css's, clamp(520px, 46vw, 760px), so the same text block sits
   higher up a taller box and the headline lands on the dip. Measured at 2.57:1
   against a target of 3.0 at 1024px, where the frame is shortest and the text
   therefore highest.

   The type did not move and was not lightened. The dip was filled: 0.44 to
   0.54, and 0.52 to 0.58 to keep the top marginally the deeper of the two as
   the design has it. 0.54 is the shallowest point anywhere on the gradient now,
   and against a blown-out window at 255 it is what 3:1 costs. The foot stays at
   the design's 0.74 because the sub-line already cleared 4.5 there and paying
   for contrast that is not needed is paying in photograph.
*/
.hero__scrim--down {
  background: linear-gradient(
    180deg,
    rgba(28, 20, 16, 0.58) 0%,
    rgba(28, 20, 16, 0.54) 40%,
    rgba(28, 20, 16, 0.74) 100%
  );
}

/* --------------------------------------------------------------- rhythm */

/*
   `.stack`, `.row`, `.cols` and `.split` in 02-site.css set a gap and stop
   there. The design files set `margin:0` on every single element and let the
   container own the rhythm, so a <p> dropped into one of these is spaced twice:
   once by the gap and once by its own default margin. Zeroing DIRECT children
   only, so `.prose p` keeps the 26px it is given deliberately.
*/
.stack > *,
.stack--tight > *,
.row > *,
.cols > *,
.split > * {
  margin: 0;
}

/*
   The hairline between sections. Home.dc.html rules off the statement, Ready by
   Completion and the notes; Our Work.dc.html rules off the introduction and
   every project. It is `rgba(58,44,36,0.12)`, which is --rule-soft. 02-site.css
   has --section-y, --section-x and the two panel modifiers but no ruled one.
*/
.section--ruled { border-bottom: 1px solid var(--rule-soft); }

/*
   The same hairline above rather than below, with the design's 48px of air
   under it. Our Approach.dc.html opens every section after the first this way,
   which is how a stacked run of bands is built: `.section--tight` from 02-site.css takes
   the top padding off and the rule carries the separation instead.
*/
.band {
  border-top: 1px solid var(--rule);
  padding-top: 48px;
}
/* The design gives the block that closes a band the same 48px of air above it
   that the band's own rule gets below it, so the section reads as three
   measures rather than two and a stray. */
.band__close { margin-top: 48px; }

/* --------------------------------------------------------------- headings */

/*
   THE ONE TYPE SCALE THE AUTHORITATIVE SHEET DOES NOT CARRY.

   02-site.css has `.title` for a page title (38–62px) and `.h2` for a section
   heading (24–30px). `.h2` is exactly Our Approach.dc.html's section heading, so
   the sheet is not wrong — but Home.dc.html and Our Work.dc.html set their
   principal section headings at clamp(30px,3.4vw,46px) and clamp(28px,3.2vw,44px),
   a display scale that sits between the two and has no class.

   Setting those in `.h2` would render "Designing a home is only one part of
   delivering it." at a little over half the size it is drawn at, on the first
   thing a reader meets below the hero. That is not a disagreement over a value,
   it is a missing step, so the step is added. Both files' ranges are close
   enough to be one class; the wider of the two is used.
*/
/*
   ── AND TWO SINGLE INSTANCES SIT BESIDE IT, 2px EITHER WAY ────────────────
   About's closing heading is `clamp(28px,3.4vw,48px)` on 1.12 and Our Work's
   is `clamp(28px,3.2vw,44px)`. One instance each, two pixels from this one at
   page width and further apart below it.

   Not worth a class each, and not something to round away either: `--h2` is
   how a per-instance value gets back in, the same mechanism `--measure` gives
   a paragraph and `--cap` gives a caption. The template states what the design
   states, at the element the design states it about.
*/
.h2--display {
  font-size: var(--h2, clamp(30px, 3.4vw, 46px));
  line-height: var(--h2-lh, 1.18);
  letter-spacing: -0.015em;
  /* Three of the five at this size are 20ch and two are 22ch. The default is
     the tighter one, so a display heading that arrives without a measure
     breaks early rather than running the width of the page. */
  max-width: var(--measure, 20ch);
}

/* ---------------------------------------------------------------- columns */

/* `.cols` aligns to the top. Home.dc.html's closing section and Our Work's
   align their two columns on the baseline of the last line instead, so the
   heading and the button finish level. */
.cols--end { align-items: end; }

/* Our Work.dc.html centres the text block against the image beside it, which is
   what stops a three-line entry sitting at the top of a 4:3 photograph. */
.cols--centre { align-items: center; }

/*
   The contact page's opening: the words at 808 and the photograph at 440, as
   the design states them. Below 55rem it collapses like every other two-column
   thing on this site.
*/
.cols--intro { grid-template-columns: minmax(0, 808px) minmax(0, 440px); }
@media (max-width: 55rem) {
  .cols--intro { grid-template-columns: minmax(0, 1fr); }
}

/*
   The six listing rows on /our-work/ take a 40/64 gutter, not the 56/80 that
   `.cols` states. All six say so in the design and nothing else in the bundle
   does — About and the home page are the two 320px grids that keep the wider
   one. It drew each card 8px narrow.
*/
/*
   The header is `position: fixed`, so an anchor that lands flush at the top of
   the viewport lands UNDER it. `scroll-margin-top` is the whole fix, and it
   belongs on the TARGET rather than on anything that links to it.

   160px, because the header measures 137 at 1440 — 22px of padding each side
   of a 92px inner row, not the 84px the shell comment quotes — and less than
   that on a phone. This is the one number here that is a clearance rather than
   a design value: it has to be at least the tallest the header ever gets, and
   `npm run layout` asserts that every anchor lands clear of it at three
   widths.
*/
.section--ruled[id] { scroll-margin-top: 160px; }

.cols--work { gap: 40px 64px; }

/*
   And the card's own type, which the design states on the card and the shared
   rules generalise:

     · the headline is 22ch and `clamp(28px,2.9vw,40px)`. `.h2--display` is the
       page-heading step and drew it at 46px with no measure, so a two-line
       title became one long line and the card lost its shape.
     · the headline LINK is coffee, hovering to caramel. `a { color: caramel }`
       is right for a link in a sentence and wrong for a heading that happens
       to be one: it made every project title on the listing read as a link
       first and a title second.
     · the description is 48ch. Uncapped it ran to the full 632px column.
*/
.cols--work .h2--display { font-size: clamp(28px, 2.9vw, 40px); line-height: 1.18; max-width: 22ch; }
.cols--work .h2--display a { color: var(--coffee); }
.cols--work .h2--display a:hover { color: var(--caramel); }
.cols--work .prose--secondary p { max-width: 48ch; }

/* ------------------------------------------------------------ section head */

/*
   Home.dc.html's "Our work" band opens with a label and a link on one rule.
   `.row` is the closest thing in 02-site.css and is the wrong shape: it centres
   its items, does not space them apart, and carries no rule.
*/
.section-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 32px;
  padding-bottom: 18px;
  margin-bottom: 40px;
  border-bottom: 1px solid var(--rule);
}
/* The design sets this heading at the medium sans weight. It is an <h2>, so it
   would otherwise inherit the 300 the sheet gives every heading, and at 11px
   uppercase that is the difference between a label and a whisper. */
.section-head h2 { font-weight: 500; }

/* ----------------------------------------------------------------- cards */

/*
   The four category cards on the home page: a whole block is the link, so the
   photograph and the words are one target rather than two adjacent ones.

   The colour override is the point of the class. `a { color: var(--caramel) }`
   is right for a link inside a sentence and wrong for a card, where the heading
   would turn caramel and read as emphasis. Home.dc.html sets and holds
   `#3A2C24` on both the resting and the hover state for exactly that reason.
*/
.card { display: block; color: var(--coffee); }
.card:hover { color: var(--coffee); }
/*
   The eyebrow carries the design's 18px above the card's words; the title then
   sits directly under it. Written as two rules rather than one so a card
   without an eyebrow — there are none today — still gets its air.
*/
.card__eyebrow { margin: 18px 0 0; }
.card__title { margin: 18px 0 0; font-weight: 400; }
.card__eyebrow + .card__title { margin-top: 8px; }
.card__note {
  margin: 8px 0 0;
  font-size: 17px;
  line-height: 1.6;
  color: var(--muted);
  max-width: 34ch;
  text-wrap: pretty;
}

/* ------------------------------------------------------------ phase list */

/*
   ── THE REVERSED VALUES ON THE HOME BAND, READ ONE AT A TIME ──────────────
   The house rule says secondary text is `--muted` and nothing lighter, and it
   names 0.72 opacity as one of the things that caused it to be written. That
   rule is about LIGHT GROUNDS, and it now says so in CLAUDE.md: the three
   regressions behind it were opacity used to fake a lighter grey on sand and
   ivory, where thinning the ink moves it towards the background and contrast
   falls. Reversed on coffee the arithmetic runs the other way — thinning ivory
   moves it towards the dark ground it is measured against: 7.35:1 at 0.72 and
   8.33:1 at 0.78, against the 4.5 they need. Measured, not assumed, and
   `npm run contrast` composites both against the coffee on every run.

   The design states four values in this band and they are four decisions:

     0.78  the supporting sentence, which explains
     1.0   the claim under it, which is the point of the band
     0.72  the four phase texts, which are detail
     #C6B49F  the link out, and the numerals above it

   Flattening those to one `--on-dark-soft` is the same substitution the
   `.phase__name` comment below records being caught in.
*/
.phase-intro p { color: rgba(250, 248, 244, 0.78); }
.phase-intro p.phase-intro__claim { color: var(--on-dark); }

/*
   The four phases in the dark band on the home page. Not `.cols`: the design
   packs these at minmax(240px,1fr) so four sit in a row at page width and two
   at tablet, where `.cols` would drop straight to two and then one.

   The rule above each is `rgba(250,248,244,0.24)`, a reversed hairline the token
   set has no name for. 02-site.css writes the same kind of literal for
   `.section--dark .btn--ghost`, so this follows the file it extends.
*/
.phase-list {
  margin: 72px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 40px 32px;
}
.phase-list > li {
  border-top: 1px solid rgba(250, 248, 244, 0.24);
  padding-top: 20px;
}

/*
   A <div> rather than a <p> carrying `.label`, because `.section--dark p` sets
   the paragraph colour and would win on specificity over `.label`.

   ── THE COLOUR WAS SUBSTITUTED, AND THE OLD COMMENT SAID SO ───────────────
   It read: "The design uses #C6B49F here; --on-dark-soft is ... the nearest
   thing to it that is a token rather than a fifth warm neutral." That is the
   substitution the rule forbids, written down and shipped. Nothing caught it:
   both colours clear their target on coffee, so contrast was content.

   The design says #C6B49F, so it is #C6B49F, and it is now a token —
   --on-dark-warm — because being un-tokenised was the only argument against
   it. 6.66:1 on coffee, measured.
*/
.phase__name {
  font-family: var(--font-sans);
  font-size: 11px;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--on-dark-warm);
  line-height: normal;
}
.phase__text { margin: 14px 0 0; font-size: 17px; line-height: 1.65; color: rgba(250, 248, 244, 0.72); }
.phase-list__more { margin: 52px 0 0; }
/*
   The same #C6B49F as the numerals above it, and for the same reason: the
   design draws the label and the way out of the band in one warm colour.
   `.section--dark a` is ivory and is right for the two buttons on About; this
   is the same weight and later in the bundle, so it takes this one instance.
   The hover is the design's, and it is the ivory the rest of the band uses.
*/
.phase-list__more a { color: var(--on-dark-warm); }
.phase-list__more a:hover { color: var(--on-dark); }

/* -------------------------------------------------------------- note list */

/*
   The three most recent notes on the home page: title and date on one line,
   one hairline per row. Distinct from the index on /our-notes/ below, which is
   a different component at a different size with a standfirst — they are not
   one thing with a modifier, and pretending otherwise is how a shared class
   ends up owned by two pages.
*/
.note-list { margin: 0; padding: 0; list-style: none; }
.note-list__item { border-top: 1px solid var(--rule); }
.note-list__item:last-child { border-bottom: 1px solid var(--rule); }
.note-list__link {
  display: flex;
  gap: 32px;
  align-items: baseline;
  justify-content: space-between;
  padding: 22px 0;
  color: var(--coffee);
}
.note-list__link:hover { color: var(--caramel); }
.note-list__title { font-size: 21px; line-height: 1.4; max-width: 46ch; }
.note-list__date {
  font-family: var(--font-sans);
  font-size: 11px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  line-height: normal;
}

/* ------------------------------------------------------------ notes index */

/*
   /our-notes/. The whole entry is the link, including the standfirst, which is
   why the date is inside it rather than beside it.
*/
/* `margin-block` rather than `margin`, because this list also carries
   `.container--prose` and a `margin: 0` here would land after it in the bundle
   and quietly undo its `margin-inline: auto`. */
/* 920px, which the design states on the list itself. It was `.container--prose`
   at 820, so every article title and standfirst on the index broke a hundred
   pixels early. */
.notes-index { max-width: var(--w-index); margin-inline: auto; margin-block: 0; padding: 0; list-style: none; }
.notes-index__item { border-top: 1px solid var(--rule-strong); }
.notes-index__item:last-child { border-bottom: 1px solid var(--rule-strong); }
.notes-index__link { display: block; padding: 34px 0; color: var(--coffee); }
.notes-index__link:hover { color: var(--caramel); }
.notes-index__title {
  font-size: clamp(24px, 2.4vw, 32px);
  line-height: 1.25;
  font-weight: 400;
  letter-spacing: -0.01em;
  max-width: 34ch;
}
.notes-index__standfirst {
  margin: 14px 0 0;
  font-size: 19px;
  line-height: 1.65;
  color: var(--body);
  max-width: 62ch;
  text-wrap: pretty;
}
/* A block, as the design writes it: `<time>` is inline by default and the
   date read as 95px of content rather than as a line under the standfirst. */
.notes-index__date {
  display: block;
  margin: 16px 0 0;
  font-family: var(--font-sans);
  font-size: 11px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--muted);
  line-height: normal;
}

/* -------------------------------------------------------------- checklist */

/*
   Our Approach.dc.html's two ruled lists: the eleven things one appointment
   leads, and the six things a client gets. Two columns of ruled rows with no
   bullets and no vertical gap, so the hairlines line up across the columns.
   `.cols` cannot do it — it sets a 56px row gap, which would break the rules
   into stripes.
*/
.checklist {
  margin: 48px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 0 56px;
}
.checklist li {
  padding: 16px 0;
  border-top: 1px solid var(--rule-soft);
  font-size: 18px;
  line-height: 1.55;
  color: var(--body);
}
/* The values list is set in the primary ink, not the body colour: it is a list
   of qualities rather than a list of tasks. */
.checklist--ink li { color: var(--coffee); }

/* ------------------------------------------------------------- tab groups */

/*
   THE ONE RULE WITHOUT WHICH THE STAGE TABS ARE A LIE.

   src/assets/js/site.js ships the tablist `hidden` and unhides it only once it
   can honour it: four buttons that cannot switch anything are four buttons that
   lie. `hidden` is a UA rule — `[hidden] { display: none }` — and an author rule
   beats a UA rule whatever its specificity, so `.tabs { display: flex }` in
   02-site.css silently defeats it and the controls render for a reader whose
   JavaScript never arrives.

   02-site.css carries `.tabpanel[hidden]` for exactly this reason on the other
   half of the widget. This is the missing twin, and it is here rather than there
   because that file is the handoff verbatim.
*/
.tabs[hidden] { display: none; }

/*
   TWO TAB BARS, ONE CLASS, AND THEY ARE NOT THE SAME SHAPE.

   `.tabs` in 02-site.css is `display: flex; gap: 8px; flex-wrap: wrap`, which
   packs the controls from the left at whatever width their words need. That is
   right for Ready by Completion, where the design states exactly that shape
   for its two timeline buttons — but its gap is 2px, not 8.

   It is wrong for the four stages on Our Approach. The design states
   `repeat(4,1fr)` with a 2px gutter, so the four phases divide the whole
   measure and read as a bar: Discover at the left edge under "least costly to
   change" and Deliver at the right edge under "most costly". Drawn as four
   content-width chips hugging the left, that argument disappears — the scale
   line underneath still spans the page, pointing at nothing.

   The rest is the design too: 16px 18px rather than 12px 22px, the name left
   aligned inside its cell, and 10px on 0.2em rather than 11px on 0.18em,
   because in this bar the name is a caption over a second line rather than a
   control label on its own.
*/
.tabs { gap: 2px; }
.tab { padding: 14px 22px; }

.tabs--stages { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
.tabs--stages .tab { padding: 16px 18px; text-align: left; font-size: 10px; letter-spacing: 0.2em; }
@media (max-width: 55rem) {
  .tabs--stages { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/*
   The sub-label inside a stage tab: "Stages 0 and 1", "Stage 2", and so on.

   `.tab` is a sans, 11px, uppercase, letter-spaced control, which is right for
   the stage NAME and wrong for the line under it — the design sets that second
   line in the serif at 15px, in ordinary sentence case, because it is a piece of
   information rather than a control label. Colour is inherited on purpose, so
   the pair reads correctly in both the selected and unselected states without
   this rule knowing which one it is in.
*/
.tab__note {
  display: block;
  margin-top: 6px;
  font-family: var(--font-serif);
  font-size: 15px;
  letter-spacing: 0;
  text-transform: none;
}

/*
   The scale printed under the stage tabs: "Least costly to change" at one end
   and "Most costly to change" at the other. `.row` is the closest thing in
   02-site.css and is the wrong shape — it packs its items together from the
   left, and the whole point of these two is the distance between them.

   Deliberately NOT inside the tablist and therefore not hidden with it: it is a
   statement about the four stages, which are on the page whether the controls
   are or not.
*/
.tabs__scale {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
  margin: 10px 0 0;
}

/* ---------------------------------------------------------- table caption */

/*
   A caption is centred by default, and every table on this site is set flush
   left. 02-site.css styles `.table` th and td and stops there, because the
   design files it was taken from draw the caption inline on the element.
*/
.table caption {
  text-align: left;
  padding-bottom: 20px;
  font-size: 19px;
  line-height: 1.6;
  color: var(--muted);
}

/* --------------------------------------------------------------- headings */

/*
   A heading that carries `.label` must not also carry the heading weight.

   Our Approach.dc.html titles each service with an 11px uppercase sans line,
   which is `.label`; it is still an <h3> because it titles a block in the
   document outline whatever size it is set at. The heading reset sets every
   h1 to h4 at 300, and at 11px uppercase that is the difference between a label
   and a whisper. Same reasoning, and the same value, as `.section-head h2`.
*/
h3.label { font-weight: 500; }

/* -------------------------------------------------------------- callout */

/*
   A panel block inset inside a section, rather than a panel-coloured section.

   Our Approach.dc.html closes on a #F1ECE4 block with 56px of padding sitting
   inside an ivory band, so an ivory margin runs around it. `.section--panel`
   paints the whole band edge to edge and cannot express that; they are two
   different drawings and the design uses both.

   The padding is `--section-x` rather than a literal 56px so it collapses to 24
   with everything else at 880px. A 56px inset on a 375px screen leaves the words
   about 260px to live in.
*/
.callout {
  background: var(--panel);
  padding: var(--section-x);
}

/* ------------------------------------------------------- ecosystem figure */

/*
   THE ECOSYSTEM DIAGRAM OWNS ITSELF NOW.

   Two `.eco` rules lived here — the caption's margin and a recolour of `.label`
   inside the coffee-filled hub — because an earlier pass was asked not to edit
   11-ecosystem.css. Both are gone: the caption's 48px is stated once in that
   file, and there is no `.eco__hub` any more. The hub was a padded coffee box
   with a label inside it; the design draws a filled circle with the label
   BELOW it on the page ground, so there is no reversed text to recolour.
*/

/* =========================================================================
   SECOND PASS: Contact, Ready by Completion, the project pages and the notes.

   Everything below was found while porting design/Contact.dc.html,
   design/Ready by Completion.dc.html, the six design/Project*.dc.html files
   and the eight design/Note*.dc.html files. Same test as the rules above: it
   is here only because 02-site.css, 03-shell.css, 10-charts.css and
   12-project.css between them have no equivalent.

   Two of these blocks are honestly page-specific rather than shared — the
   ruled definition list and the timelines' qualification note are true of
   /ready-by-completion/ alone, and the article footer is true of the eight
   notes alone. The file's own preamble says such a rule belongs in a numbered
   SCOPED partial. It is here because the brief for this pass is explicit that
   04-pages.css is the one stylesheet it may add to, and because a new scoped
   partial would have to be declared in `checkClassOwnership` — which lives in
   scripts/audit.mjs, outside this pass. Recorded rather than hidden: if a
   later session opens 10-charts.css or adds 14-article.css, the article and
   timeline blocks below are the ones to move.
   ========================================================================= */

/* ----------------------------------------------------------------- forms */

/*
   The row grid inside both enquiry forms.

   Contact Form.dc.html and Feasibility Form.dc.html both lay their fields out
   in `repeat(auto-fit, minmax(240px, 1fr))` rows at a 26px gap, so two
   questions sit side by side at page width and stack on a phone with no media
   query. `.cols--narrow` in 02-site.css is the same shape at minmax(280px) and
   a 32/40px gap, which is a page-section rhythm rather than a form's: at 240px
   the pairs survive to a narrower viewport, which is the whole point of
   pairing Name with Email rather than stacking eight questions.
*/
.fields {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
  gap: 26px;
}

/*
   A fieldset is the correct element for a radio group and the UA gives it a
   border, padding and a margin that no design on this site draws. `min-width:
   0` because a fieldset's default `min-width: min-content` stops a grid child
   from ever shrinking, which is how a two-column form row refuses to collapse
   on a phone.
*/
fieldset { margin: 0; padding: 0; border: 0; min-width: 0; }
legend { padding: 0; }

/*
   "(optional)" and "(if known)" inside a field label. The label itself is
   11px uppercase and letter-spaced; the qualifier is drawn in sentence case in
   both design files, because it is an aside to the question rather than part
   of it.
*/
.field__optional { text-transform: none; letter-spacing: 0.02em; line-height: normal; }

/* The privacy line under the submit button. Both form designs set it at 15px
   in --muted with a 60ch measure; `.field__hint` is the same size and colour
   but is a hint attached to one input, and this is a statement about the whole
   form. */
.form__privacy {
  font-size: 15px;
  line-height: 1.6;
  color: var(--muted);
  max-width: 60ch;
}

/* --------------------------------------------------------- ruled bands */

/*
   A hairline with a statement at one end and a link at the other.

   Contact.dc.html uses it twice — "Every project on this site was led by us"
   with "See our work", and the innovation line with "Innovation" — and both
   are `align-items: baseline; justify-content: space-between` on a wrapping
   flex row. `.row` in 02-site.css packs its items from the left and centres
   them, which is the right default for a group of buttons and the wrong one
   here: the distance between the two ends is the composition.
*/
.row--split {
  justify-content: space-between;
  align-items: baseline;
}

/*
   Content with a narrower column beside it.

   The enquiry block on Contact.dc.html is `minmax(0,1fr) minmax(0,340px)`: the
   form takes the width it needs and the address and legal links sit in a fixed
   column beside it. `.split` in 02-site.css is the mirror image (a 260px
   sidebar FIRST) and `.cols` is equal columns, so neither draws this.
*/
.with-aside {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 340px);
  gap: 48px 80px;
  align-items: start;
}
@media (max-width: 880px) {
  .with-aside { grid-template-columns: minmax(0, 1fr); }
}

/*
   The feasibility form on Ready by Completion, capped where the design caps
   it. The contact form needs no equivalent: it is already held to its share by
   `.with-aside`, which is how that page's design constrains it.
*/
.form-block { max-width: 900px; }

/* ===========================================================================
   CLASS 1 · SMALL-CAPS TYPE HAS NO LEADING TO SET.

   The single biggest class in `npm run render:pages`: 360 of 748 differing
   elements at 1440, on every page. Every Archivo label, eyebrow, nav link,
   footer link, button and link-arrow inherits the body's `line-height: 1.7`,
   and the design states no line-height on any of them — which means `normal`,
   which for Archivo is about 1.09.

   On 11px type that is a 19px line box around a 12px letter: seven pixels of
   air the design does not have, under every eyebrow and inside every control.
   It is invisible one label at a time and it is why a card, a caption or a
   fact strip sits lower than the design puts it, and why a two-word label
   occasionally takes two lines' worth of room.

   It is the same fault fixed for `.pfig__label` on 11 August, at site scale.
   The fix is the same sentence: a label is one line of small caps and has no
   leading to set.

   `line-height: normal` rather than a number, because the correct value
   differs per size and the browser already knows it. Anything genuinely
   multi-line — the nav statement, the panel links — is not in this list.
   =========================================================================== */
.label,
.link-arrow,
.nav__link,
.nav__toggle,
.footer__inner,
.footer__links a,
.tab,
.btn,
/* The same type wearing a page's own class name: the chapter marks and fact
   labels on a project page, the phase numerals on the home page, the eyebrow
   inside a card. Each is Archivo small caps and each was inheriting 1.7. */
.chapter__mark,
.project__fact dt,
.outcome__list dt,
.comp__label,
.card__eyebrow {
  line-height: normal;
}

/* ===========================================================================
   CLASS 2 · THE FOUR LONG-READ PAGES TAKE MORE LEADING, AND ONLY THOSE FOUR.

   `.prose--secondary p` is 19px on 1.7, which is what the design states in 80
   places. On Privacy, Cookies, Terms and Innovation — and nowhere else — it
   states 1.75, in 55 places and with no 1.7 among them.

   That is a decision rather than a slip, and it is legible once you see which
   four pages they are: the two legal texts, the cookie statement and the
   innovation essay are the only continuous long-form reading on the site.
   Everything else is prose in short runs between headings and photographs.
   More leading is what a page you read straight through wants, and it is worth
   0.05 of an em only there.

   A modifier rather than a change to `.prose--secondary`, because a global
   swap would have moved 80 paragraphs the design does not move.
   =========================================================================== */
.prose--reading p { line-height: var(--prose-lh, 1.75); }

/* ===========================================================================
   CLASS 4 · GROUP 1 · BODY WHERE THE BUILD INHERITED COFFEE — 25 rows.

   Three elements, one sentence: the design writes `#5A4F46` and the build
   wrote nothing, so each inherited the page's coffee.

     .prose--secondary The FAQ answers on Our Approach, the /our-work/ intro
                       and the note beside the contact form are --body in the
                       design, and `.prose--secondary` is already exactly that
                       component: 19px on 1.7 in --body. They are marked as it
                       in the templates rather than being given a colour here.

                       `.prose p { color: var(--body) }` was tried first and
                       REVERTED, for the same reason as the h2 measure in class
                       3: `.prose` covers several populations. It corrected the
                       nine it was aimed at and broke eleven — the dark band's
                       reversed paragraphs, the closing emphasis line on three
                       pages, and four muted sign-offs. Recorded because it is
                       the obvious move and it is wrong twice over.

     .lede + .lede     A SECOND lede paragraph is --body. The first is coffee
                       in the design and the second is not, on /our-work/ and
                       on the two project pages that open on two paragraphs.
                       It is the design saying "and then, more quietly".

     .outcome__list dd The outcome definitions, and the services line beside
                       them. 18px coffee in the build, --body in the design.
                       That one is fixed in 12-project.css, which owns the
                       class and loads after this file — stating it here as
                       well would be a rule that never applies, which is the
                       class of thing this whole pass is about.

     .lede--quiet      /our-work/'s opening line. Its lede is --body, and it is
                       the only lede on the page, so `+ .lede` cannot reach it.

   None of these fails contrast either way, which is why all 25 shipped:
   --body is 7.6:1 on ivory and --coffee is 11.8:1, so the check that reads
   colour saw two passes and nothing to say.
   =========================================================================== */
/*
   ── `.lede + .lede` WAS A DOCUMENT-ORDER RULE, AND IT IS NOW A NAMED ONE ───
   It read "the second lede is --body", which was true of the three project
   pages that have two and would have been silently wrong the moment anybody
   put a paragraph between them. A rule that depends on position breaks without
   saying so, which is the failure this whole conversion keeps producing.

   Named instead, and given the rest of what the design states about it: the
   second paragraph of a project's opening is 21px on a 44ch measure in --body,
   a step down from the 22px/46ch/--coffee lede above it. Three instances, on
   the new home, the interior transformation and the major transformation.
*/
.lede--after { font-size: 21px; color: var(--body); max-width: var(--measure, 44ch); }
/* /our-work/'s opening line: clamp(20px,1.8vw,23px) on 1.55, --body, 44ch. */
.lede--quiet { font-size: clamp(20px, 1.8vw, 23px); line-height: 1.55; color: var(--body); max-width: var(--measure, 44ch); }

/* ===========================================================================
   CLASS 4 · GROUP 2 · THE LEGAL LINKS ARE MUTED, NOT CARAMEL — 9 rows.

   Privacy · Cookies · Terms, on /contact/ and on all three legal pages. The
   design writes `color:#6E6157` on each of the nine, hovering to coffee. The
   build gave them the site link colour, caramel, because `a { color:
   var(--caramel) }` is right for a link in a sentence and these are not that:
   they are a row of legal destinations at the foot of a page, and the design
   sets them the colour of everything else that is secondary.

   The hover is coffee in the design, which is the same direction the footer
   links already travel.
   =========================================================================== */
/* The design sets 17px on the nav itself and lets the three links inherit it;
   the build had them at the 20px body size. Both the legal pages' rows and
   /contact/'s, though only /contact/'s differed — the legal pages' sit inside
   `.note--aside`, which is 17px already. */
nav[aria-label='Legal'] { font-size: 17px; line-height: normal; }
nav[aria-label='Legal'] a { color: var(--muted); }
nav[aria-label='Legal'] a:hover { color: var(--coffee); }

/* ===========================================================================
   CLASS 4 · GROUP 4 · FIVE SINGLES, READ ONE AT A TIME — 8 rows.

   Not one fault five times. Each is a different element and each was checked
   against its own design file:

     .note              TRIED AND REVERTED, and it is the third class to do
                        this. `--muted` is right for the sign-off under the
                        legal pages and the 404's second line, which the design
                        writes at #6E6157 — and wrong for the editorial note
                        under a journal article, which it writes at #5A4F46.
                        One class, two populations: 4 corrected, 5 broken. It
                        needs the legal sign-off separated from the editorial
                        note before either can be coloured.

     the mailto on      COFFEE and underlined, not caramel. The design makes
     /contact/          this one 22px coffee with a 4px underline offset and
                        hovers it to caramel — it is an address being offered,
                        set as text you can press, rather than a link in a
                        sentence. 1 row.

     .eco > figcaption  --body, not --muted. The ecosystem standfirst is
                        #5A4F46 in the design; it inherited --muted from the
                        shared figcaption rule when the diagram was rebuilt.
                        1 row.

     the home page's    --muted, not --body: the line under the journal
     journal standfirst heading, 18px on 1.68. 1 row.

   The renovation-budget disclaimer is #4A3E35 (--bio-ink) on a panel, and it
   is left alone here: `.note` is the wrong hook for it, it is one paragraph
   inside an article body, and it is on the list for the per-instance pass.
   =========================================================================== */
.contact__email { font-size: 22px; line-height: normal; color: var(--coffee); text-decoration: underline; text-underline-offset: 4px; }
.contact__email:hover { color: var(--caramel); }
.journal-standfirst p { color: var(--muted); }

/*
   THE MESSAGE ON A REFUSED SUBMISSION.

   Returned inside the form by functions/api/_lib/reject-page.mjs when an
   enquiry is rejected — most often now because the Turnstile check could not
   run, which is what a browser without JavaScript looks like from the server.

   Coffee on latte rather than a red: this is not an error the person made. It
   has to be impossible to miss and it must not read as an accusation. The left
   rule is the same device the pull quotes use, and `tabindex="-1"` is there so
   the Function can move focus to it.
*/
.form-alert {
  margin: 0 0 32px;
  padding: 20px 24px;
  background: var(--latte);
  border-left: 3px solid var(--coffee);
  color: var(--coffee);
}
.form-alert p { margin: 0; font-size: 18px; line-height: 1.6; max-width: 60ch; }
.form-alert:focus { outline: 2px solid var(--coffee); outline-offset: 4px; }

/* An <ol> or <ul> that is a layout rather than a list of bullets — the three
   numbered steps under "What happens next", where the number is drawn as a
   caramel label and a marker would be a second one. */
.list-plain { margin: 0; padding: 0; list-style: none; }
/* Three across at 240px minimum on a 36/48 gap, which is what Contact draws.
   `.cols--narrow`'s 32/40 put each step 6px wider than the design. */
.steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: 36px 48px; }

/*
   `.stack` at the distance a project page puts between photographs.

   22px is the right rhythm for paragraphs and labels and much too tight for
   images: the project designs run 32 to 64px between one figure group and the
   next, because two photographs 22px apart read as one composition rather than
   as two observations.
*/

/*
   The ruled definition list under "What we need from you".

   Four rows, each a hairline above a term and its description side by side.
   `.checklist` is the closest thing here and is a <ul> of single-line items in
   two columns; this is a <dl> whose two halves must stay on one row, so they
   are different components rather than one with a modifier.
*/
.deflist { margin: 0; display: grid; gap: 0; }
.deflist__row {
  border-top: 1px solid var(--rule);
  padding: 24px 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: 12px 56px;
}
.deflist__row:last-child { border-bottom: 1px solid var(--rule); }
.deflist__row dt { font-size: 19px; line-height: 1.5; color: var(--coffee); }
.deflist__row dd {
  margin: 0;
  font-size: 18px;
  line-height: 1.7;
  color: var(--body);
  max-width: 56ch;
  text-wrap: pretty;
}

/*
   The qualification beside the Ready by Completion argument: "The approach
   depends on access, timely decisions and the purchase completing."

   A ruled left edge at 17px in --muted. `.quote` is the other left-ruled
   component in 02-site.css and is its opposite in every respect — 24 to 32px,
   italic, caramel rule — because a pull quote amplifies and this qualifies.
   The design draws the rule in `rgba(58,44,36,0.2)`, between --rule and
   --rule-strong; --rule-strong is used, being the token nearest to it that the
   audit already measures.
*/
.aside-note {
  margin: 0;
  padding-left: 20px;
  border-left: 2px solid var(--rule-strong);
  font-size: 17px;
  line-height: 1.6;
  color: var(--muted);
  text-wrap: pretty;
}

/* --------------------------------------------------- the two timelines */

/*
   Three things the Ready by Completion charts need that 10-charts.css does not
   carry. They are here rather than there for the reason given at the top of
   this block: this pass may add to 04-pages.css and to no other stylesheet.
   None of them redefines a `.chart*` class that file owns.

   `.chart__group` is the 36px between the two halves of a chart. Each chart is
   two labelled sequences — the usual one and this one — and without the gap
   the second reads as a continuation of the first rather than as its
   alternative, which is the entire comparison.
*/
.chart__group + .chart__group { margin-top: 36px; }

/*
   TWO PAGE CAPTIONS THE DESIGN MEASURES ITSELF.

   `figcaption` in 02-site.css is 17px --muted on 1.6 at 60ch, which is right
   for most of them. These two are not most of them, and the design says so on
   the element:

     /contact/       the label on one baseline with the sentence, 18px apart,
                     wrapping together, the sentence at 34ch on 1.55
     /our-approach/  a note under a sketch, 44ch on 1.55

   Both were drawing at 570px, because `.row` is 16/24px and centre aligned and
   because 60ch is not 34 or 44. Found while fixing the same fault on the six
   project pages, and fixed for the same reason: where the design states a
   value per instance, the instance wins over the blanket rule.
*/
.figcaption--inline {
  display: flex;
  gap: 18px;
  align-items: baseline;
  flex-wrap: wrap;
  max-width: none;
}
.figcaption--inline .label { white-space: nowrap; line-height: normal; }
.figcaption--inline .figcaption__text { max-width: 34ch; line-height: 1.55; }
.figcaption--note { max-width: 44ch; line-height: 1.55; }

/* The caption above a chart, which says in a sentence what the bars say in a
   picture. `figcaption` in 02-site.css is 17px --muted below its figure, which
   is right for a photograph's caption and wrong for this: it is a statement in
   the primary ink, set above, at the size of a section standfirst. */
.chart-caption {
  margin: 0 0 32px;
  color: var(--coffee);
  /* `figcaption` caps at 60ch and this was capped tighter at 44; the design
     caps it at nothing and lets the chart's own column hold it. */
  max-width: none;
  /* It carries `.h3` for its weight and colour; the design sets this one at
     21px on 1.4, a step below the `.h3` scale. */
  font-size: 21px;
  line-height: 1.4;
}

/* The ordered lists that replace the charts below 980px. Markers stay visible:
   they are a sequence, which is exactly what an ordinal is for, and they are
   the only thing left carrying the order once the bars have gone. */
.chart-fallback__list {
  margin: 14px 0 0;
  padding-left: 22px;
  font-size: 18px;
  line-height: 1.7;
  color: var(--body);
}
/* The Ready by Completion column in the primary ink, matching its bars, which
   are coffee where the usual sequence's are stone. */
.chart-fallback__list--ready { color: var(--coffee); }

/* ------------------------------------------------------- project images */

/*
   THE ONE RULE THAT MAKES `ratio` MEAN ANYTHING.

   partials/work-image.njk writes `style="--ratio: 16 / 9"` on every project
   image and has done since it was written, and until now nothing consumed the
   custom property: the fifteen stylesheet partials that read it were deleted
   with the rest of the old design system. So every project image was drawn at
   its intrinsic proportions while the design files crop all of them — 16:9,
   4:3, 21:9, 3:4 — and a portrait photograph at column width ran to 1400px on
   its own.

   `object-fit: cover` is already global, and nothing is upscaled by this: the
   file underneath is always wider than the box, so a crop only ever discards
   pixels. Where `ratio` is not set the property falls back to the intrinsic
   width and height and nothing is cropped, which is the case on the Our Work
   listing and inside the journal.

   Harmless inside `.media-4x3`, which is how the listing crops its cards: that
   frame gives the figure, the <picture> and the image an explicit `height:
   100%`, and `aspect-ratio` is inert once both dimensions are resolved.
*/
.work-figure__img { aspect-ratio: var(--ratio); }

/* ------------------------------------------------------- next project */

/*
   The whole "Next project" band is one link, so the label, the title and the
   arrow are a single target rather than three adjacent ones — the same
   reasoning as `.card` above, and the same colour override for the same
   reason: a caramel heading would read as emphasis.

   `.project__next` in 12-project.css draws the rule and the baseline row
   INSIDE it. This is the band around that, which is where the design puts the
   padding and the hover, and it is here rather than there because 12-project
   .css is a page-specific stylesheet this pass was asked not to edit.
*/
/*
   `.project__next-link` and its hover were deleted on 12 August 2026 by
   `npm run claims`. They styled a full-bleed hover band for a link the project
   template does not write — it writes `.project__next`, which 12-project.css
   owns. Two files styling the same idea, one of them for nothing.
*/
.project__next-title {
  margin: 12px 0 0;
  font-size: clamp(24px, 2.6vw, 34px);
  line-height: 1.2;
  font-weight: 300;
  letter-spacing: -0.01em;
}

/* ---------------------------------------------------------- the notes */

/*
   A journal article, from the eight Note*.dc.html files. The body itself needs
   nothing: 02-site.css already sets `.article__body p` at 20/1.78 and
   `.article__body h2` at 52px above and 22px below, which is what the designs
   draw. What follows is the head and the five-part footer.

   The footer's class names are FIXED and not free to improve. `checkArticleFooters`
   in scripts/audit.mjs reads the rendered pages for `article__practice`,
   `article__cta`, `article__next` and `article__bio` and fails the build on a
   missing or reordered one, and the founder-name rule strips the bio with a
   regex that matches `<p class="small article__bio">` exactly — rename that and
   "Pearl Ume" becomes an unexplained name in body copy on all eight pages.
*/
.article__standfirst {
  margin: 24px 0 0;
  font-size: clamp(20px, 1.8vw, 23px);
  line-height: 1.55;
  color: var(--body);
  text-wrap: pretty;
}
/* The rule under the date line runs the width of the reading column, so the
   element has to be one. 0.18 alpha, which is `--rule-line`. */
.article__date {
  display: block;
  margin: 28px 0 0;
  padding-bottom: 36px;
  border-bottom: 1px solid var(--rule-line);
  line-height: normal;
}
.article__figure { margin: 40px 0 0; }
.article__body { padding-top: 40px; }

/* One italic-free sentence about the practice, at lede size, on its own rule.
   It is the hinge between the article and the invitation, so it is set larger
   than the body rather than smaller. */
.article__practice {
  margin: 44px 0 0;
  padding-top: 36px;
  border-top: 1px solid var(--rule);
  font-size: 22px;
  line-height: 1.6;
  color: var(--coffee);
  text-wrap: pretty;
}
.article__cta { margin-top: 28px; }

.article__next {
  margin: 72px 0 0;
  padding-top: 24px;
  border-top: 1px solid var(--rule);
}
.article__next ul { margin: 14px 0 0; padding: 0; list-style: none; }
.article__next li + li { margin-top: 10px; }
.article__next a {
  /* A block, as the design writes it: the title fills the reading column
     rather than ending wherever its words do. */
  display: block;
  margin-top: 14px;
  font-size: clamp(22px, 2.2vw, 28px);
  line-height: 1.3;
  font-weight: 400;
  color: var(--coffee);
  text-decoration: none;
}
.article__next a:hover { color: var(--caramel); }

/* `.note` and `.bio` from 02-site.css draw the editorial block and the bio
   panel; only the air between the three closing blocks is missing, and the
   sibling selectors keep it correct whether or not an article carries an
   editorial note. */
.article__editorial { margin-top: 56px; }
.article__editorial p { margin: 0; }
.article__editorial + .bio,
.article__next + .bio { margin-top: 36px; }
.bio .article__bio { margin: 0; }

/* ------------------------------------------------------------ responsive */

/*
   The design files carry no media queries for the home page — the tool draws
   one width — so the responsive behaviour is the behaviour the layout implies.
   Almost all of it is already implied by the authoritative sheet: every grid
   here is `auto-fit`, and `.cols` and `.split` collapse to one column at 880px.

   The exception is the title-and-date row in the notes list. It is
   `justify-content: space-between` with a date that must not break across two
   lines, so on a phone the title is left with roughly two hundred pixels and
   sets at about twenty characters a line, which is the floor `npm run layout`
   asserts against. Stacking them is what the design would have drawn if it drew
   a phone: same order, same two pieces of information, one under the other.
*/
@media (max-width: 34rem) {
  .note-list__link {
    flex-direction: column;
    align-items: flex-start;
    gap: 8px;
  }
}

/* === 10-charts.css === */
/* ===========================================================================
   The two Ready by Completion timelines.

   Deliberately not in the authoritative stylesheet. The handoff is explicit:
   the bars are positioned by percentage and "the percentages encode the
   argument", so they are laid out per instance from the design file rather
   than generalised into a component that would invite someone to round them.

   The completion line sits at 51.9% in both charts. In the architecture track
   the planning bar deliberately crosses it — that is the claim the whole page
   makes, drawn: approvals can be in progress before the keys exist.

   Below 980px the charts are replaced by ordered step lists, which are already
   in the markup rather than generated. `.chart` / `.chart-fallback` in
   02-site.css do the swap; a horizontal bar chart on a 375px screen is not a
   smaller chart, it is an unreadable one.
   =========================================================================== */

.chart { position: relative; padding-bottom: 40px; }
.chart-fallback { display: none; }

@media (max-width: 61.25rem) {
  .chart-fallback {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: 32px 56px;
  }
}

.chart__now {
  position: absolute;
  top: 0;
  bottom: 28px;
  width: 1px;
  background: var(--caramel);
}
.chart__now-label {
  position: absolute;
  bottom: 0;
  transform: translateX(-50%);
  font-family: var(--font-sans);
  font-size: 10px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--caramel);
  white-space: nowrap;
  line-height: normal;
}

.chart__track { margin-top: 14px; position: relative; height: 34px; }
.chart__track--thin { margin-top: 8px; height: 28px; }

.chart__bar {
  position: absolute;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-sans);
  font-size: 12px;
  text-align: center;
  padding: 0 6px;
  /* Normal, as every label in the design is. A bar 28px tall with a 20px
     line box inside it has no room for a second line, which is what turned
     five one-line bar labels into two-line ones. */
  line-height: normal;
}
.chart__bar--wait { background: var(--stone); color: var(--body); }
.chart__bar--late { background: var(--stone-mid); color: var(--coffee); }
.chart__bar--early { background: var(--coffee); color: var(--on-dark); }

.chart__begins {
  margin-top: 12px;
  font-family: var(--font-sans);
  font-size: 12px;
  color: var(--caramel);
  line-height: normal;
}
.chart__begins--right { text-align: right; color: var(--coffee); }

/* === 11-ecosystem.css === */
/* ===========================================================================
   The Innovation ecosystem diagram.

   Rebuilt as HTML rather than dropped back in as the original SVG, which is
   what the handoff's open item describes: the icon glyphs are gone. That is a
   deliberate loss, not an oversight — the glyphs carried no information the
   labels do not, and an SVG of text is text a screen reader cannot read.

   ── IT IS A DIAGRAM, AND IT WAS BUILT AS A ROW OF CARDS ────────────────────
   Until 12 August this was `repeat(auto-fit, minmax(280px, 1fr))` over five
   padded boxes, with the standards layer and the practice intelligence layer
   as two more boxes stacked above and below. Every word was right and the
   picture was gone: no circles, no line joining the stages, nothing showing
   that the standards layer sits ABOVE the row and connects down into it. On a
   wide screen auto-fit put SOP Library alone on one line, three stages on the
   next and the rest below, which reads as a list that has come apart rather
   than as a system.

   The design draws five circles on one rule, a dashed circle above them for
   the standards layer with a short line dropping into the row, and the
   practice intelligence layer as a bordered band underneath. That is what this
   file now draws.

   ── THE ONE DEPARTURE FROM THE DESIGN, AND IT IS PEARL'S RULING ────────────
   The design centres the standards block on the whole row, so its drop line
   lands on the third stage, Dolemai. It links to STUDIO OS instead, which is
   where the original diagram put it: standards are what the studio runs on.
   The block is a cell in the same five-column grid, placed in column two, so
   the line is centred on Studio OS by construction at every width.

   Per instance for the same reason as the timelines: the arrangement is the
   argument. One practice at the centre, the products around it.
   =========================================================================== */

.eco { display: grid; gap: 0; }
/*
   Above the diagram rather than below it, which is where the design puts a
   caption that tells you how to read what follows. 19px rather than the 17px
   the shared `figcaption` rule gives a photograph's caption: this one is a
   standfirst, and at 17px its 60ch measure is 570px instead of 628 and the
   sentence takes two lines where the design gives it one.
*/
.eco > figcaption { margin: 0 0 48px; font-size: 19px; color: var(--body); max-width: none; }
/* The 60ch is on the SENTENCE, not the caption: the caption is a grid holding
   an eyebrow and a standfirst, and capping it would cap the eyebrow too. That
   is where the design puts it. */
.eco > figcaption > span + span { max-width: 60ch; }

/* ------------------------------------------------- the standards layer */

/*
   The same five columns as the row below it, so column two is column two in
   both. Nothing else occupies this grid: the four empty tracks are what put
   SOP Library over Studio OS rather than over the middle of the page.
*/
.eco__standards {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 24px;
  margin-bottom: 36px;
}
.eco__standard {
  grid-column: 2;
  display: grid;
  gap: 14px;
  justify-items: center;
  text-align: center;
}

/* The line from the standards layer down into the row. 28px, which is the
   design's, and it meets the top of the circle beneath it. */
.eco__drop { width: 1px; height: 28px; background: var(--caramel); }

/* ------------------------------------------------------- the five stages */

.eco__row {
  position: relative;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 24px;
  align-items: start;
}

/*
   THE RULE THROUGH THE CIRCLES.

   40px from the top is the vertical centre of an 80px circle, so it passes
   through them rather than under them. It stops at 10% and 90% so it does not
   run off the ends of the row into the page margin, and each circle carries
   the panel colour so the rule reads as passing behind the stages rather than
   crossing them out.

   It is `aria-hidden` and so are the circles: they are the drawing, and the
   five names beneath them are the content. A screen reader gets the list.
*/
.eco__line {
  position: absolute;
  top: 40px;
  left: 10%;
  right: 10%;
  height: 1px;
  background: var(--rule);
}

.eco__stage {
  position: relative;
  display: grid;
  gap: 14px;
  justify-items: center;
  text-align: center;
}

.eco__ring {
  width: 80px;
  height: 80px;
  border: 1px solid var(--caramel);
  border-radius: 50%;
  background: var(--panel);
}
/* No fill on this one: the design leaves it transparent, and it sits above the
   rule rather than on it, so it has nothing to mask. */
.eco__ring--dashed { width: 54px; height: 54px; border-style: dashed; background: none; }
.eco__ring--solid { border-color: var(--coffee); background: var(--coffee); }

.eco__name { font-size: 19px; line-height: normal; color: var(--coffee); }

/* --------------------------------------------- practice intelligence layer */

.eco__layer {
  margin-top: 40px;
  border: 1px solid var(--rule-strong);
  padding: 26px 28px;
  display: grid;
  grid-template-columns: minmax(0, 200px) minmax(0, 1fr);
  gap: 16px 40px;
  align-items: center;
}
.eco__layer-name { font-size: 21px; line-height: normal; color: var(--coffee); }
.eco__layer p { margin: 0; font-size: 18px; line-height: 1.6; color: var(--body); max-width: 60ch; }

/* ---------------------------------------------------------------- narrow */

/*
   Below 55rem the five will not divide, so the row becomes a column and the
   drawing becomes a list. The rule between the circles goes with it: a
   horizontal line through a vertical stack joins nothing. This is the
   "responsive behaviour the design implies but does not specify" clause — the
   design's own 880px rule collapses every grid in the bundle to one column and
   says nothing about what happens to a connector when it does.
*/
@media (max-width: 55rem) {
  .eco__standards,
  .eco__row { grid-template-columns: minmax(0, 1fr); }
  .eco__standard { grid-column: 1; }
  .eco__line { display: none; }
  .eco__row { gap: 40px; }
  .eco__layer { grid-template-columns: minmax(0, 1fr); }
}

/* === 12-project.css === */
/* ===========================================================================
   Project pages: the chapters, the compositions and the fact strips.

   Per instance, and the handoff is right that it has to be. These pages are
   illustrated essays rather than galleries: numbered chapters, a heading and
   an introduction each, two-part captions, and image groups that change shape
   from chapter to chapter — a pair, then a full-bleed single, then a pair.

   ── EVERY MEASUREMENT ON THIS PAGE IS STATED PER FIGURE ───────────────────
   Five values that were blanket rules here until 11 August 2026, and are now
   custom properties the data supplies per figure. The fallback in each `var()`
   below is what the design uses most often, not a house default:

     --ratio   the aspect ratio of the BOX, from the design, not from the file
     --cap     the caption's own measure, in ch, from the design
     --cols    a pair's column proportions, where the design states them
     --gap     a group's gutter, where the design states a wider one
     --offset  a figure stepped down against the one beside it

   The reason they are per figure is that the design states them per figure,
   and the reason that matters is what happened when it did not. One caption
   measure for a pair and one for a single, one gutter, one pair proportion,
   and the file's intrinsic ratio for every photograph: four rules over 65
   figures. Every stated value the fidelity check knew to compare matched, and
   51 photographs were drawn at the wrong shape, 37 captions at the wrong
   measure and one caption at half the width the design gives it.

   The consequence to accept: a pair of images with different ratios does not
   bottom out on the same line. That is what the design draws.

   The order of a project's images is also a claim. An "existing" photograph
   next to a finished one says this became that, and the interior transformation
   was once numbered the other way and told readers the existing kitchen had
   become the finished dining room. It built, and every check passed.
   =========================================================================== */

/* ------------------------------------------------------------ the opening */

.project__head { display: grid; grid-template-columns: repeat(auto-fit, minmax(360px, 1fr)); gap: 64px 80px; align-items: end; }

/*
   ── THE FACT STRIP IS A STRIP ─────────────────────────────────────────────
   The design draws ONE bordered band: a hairline above and below the whole
   thing, a hairline between each pair of cells, no gap, and 20px of padding
   inside each cell — dropped on the outer edge of the first and last so the
   band's ink lines up with the column it sits in.

   This was three separate top-ruled columns 40px apart, which is a different
   drawing of the same words: a strip reads as one fact about the project, and
   three ruled columns read as three unrelated notes. Nothing measured it —
   `render:pages` compares type and not borders — and the only reason it
   surfaced is that the cells came out 181px wide against the design's 187,
   167 and 188.

   Those three numbers are the reason the padding is asymmetric rather than
   even: with a gap and equal padding every cell is the same width, and with
   the design's arrangement they are not.
*/
.project__facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  border-top: 1px solid var(--rule-mid);
  border-bottom: 1px solid var(--rule-mid);
}
.project__fact { padding: 20px; border-right: 1px solid var(--rule-hair); }
.project__fact:first-child { padding-left: 0; }
.project__fact:last-child { padding-right: 0; border-right: 0; }
/*
   THE STRIP BECOMES A LIST BEFORE IT BECOMES UNREADABLE.

   `auto-fit minmax(160px,1fr)` fits as many 160px columns as it can, and 160px
   of 17px serif is nineteen characters a line — which is what `npm run layout`
   caught on three projects at 375px, and it caught it because the design draws
   the same thing. 160px is a sensible minimum for the two-word values (Built,
   Completed) and not for "Architecture, interior design, project leadership".

   So below 48rem it is one column and the dividers turn through ninety
   degrees, which keeps the band reading as one band. This is the "responsive
   behaviour the design implies but does not specify" clause, same as the
   ecosystem diagram: the bundle's own 880px rule collapses every grid it has
   to one column and says nothing about what happens to a border when it does.
*/
@media (max-width: 48rem) {
  .project__facts { grid-template-columns: minmax(0, 1fr); }
  .project__fact { padding: 20px 0; border-right: 0; border-top: 1px solid var(--rule-hair); }
  .project__fact:first-child { border-top: 0; }
}
.project__fact dt {
  font-family: var(--font-sans);
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
}
/* 17px on 1.45 in --bio-ink, which is what the design states here and what
   the sweep found missing on all six project pages. It was 18px on 1.5 in
   --coffee: three values rounded towards the house defaults at once. */
.project__fact dd { margin: 8px 0 0; font-size: 17px; line-height: 1.45; color: var(--bio-ink); }
/*
   The routes off the page. A flex row 28px apart under the fact strip, which
   is what the design draws on the interior transformation; the same component
   sits under a chapter's prose on the extension, where the design gives it
   22px of air and one link rather than two.
*/
/* 0.18em on these, where a link arrow is 0.2em elsewhere. Per instance,
   from the design. */
.project__links { margin: 24px 0 0; display: flex; gap: 12px 28px; flex-wrap: wrap; --label-ls: 0.18em; }
.chapter__head .project__links { margin-top: 22px; }

/* 52ch on all five, not the 44ch a caption gets. */
/* 28px under the strip and 1.7, both the design's. */
.project__fact-note { margin: 28px 0 0; font-size: 17px; line-height: 1.7; color: var(--muted); max-width: var(--measure, 52ch); text-wrap: pretty; }

/* ------------------------------------------------------------- a chapter */

/*
   The chapter number sits in the margin at wide widths and above the heading
   when the margin is gone. `.split`'s 260px sidebar is the same idea, but a
   chapter marker is not a sidebar: it is two lines of type that must not
   stretch to fill a column, which is why it has its own rule.
*/
.chapter { display: grid; gap: 40px; }
/*
   180px and a 56px gutter, which is what all six design files state, and what
   the outcome block states too. It was 200 and 64 — six pixels of column and
   eight of gutter invented, on every chapter of every project page. Nothing
   caught it because nothing compared a container: the type inside it was
   right, so every check that reads type passed.
*/
.chapter__head { display: grid; grid-template-columns: minmax(0, 180px) minmax(0, 1fr); gap: 40px 56px; align-items: start; }
/*
   A CHAPTER'S MARK IS 10px LOWER THAN AN OUTCOME'S.

   34 of the 40 marks in the bundle carry `padding-top:10px` and the six that
   do not are the outcome marks. The chapter's sits beside a 40px heading and
   the outcome's beside a 26px sentence, so the design drops one and not the
   other to put both on the same optical line as the text they label.
*/
.chapter__head .chapter__mark { padding-top: 10px; }
/*
   THE PULL-QUOTE CHAPTERS ARE A PAIR, NOT A STACK.

   Four chapters open on a pull quote, and in every one of them the design sets
   the quote and the prose that answers it SIDE BY SIDE, in the same
   `auto-fit minmax(280px,1fr)` grid the outcome block uses. Built as a stack,
   a 34px italic line ran the full 1064px of the body where the design gives it
   518, which is a quote set as a banner rather than as an aside.

   The links belong INSIDE the prose column, which is where the design puts
   them: `.chapter__text` is that column and holds both.
*/
.chapter__pair { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 32px 56px; align-items: start; }
.chapter__text .prose { margin-top: 22px; }
.chapter__pair .chapter__text .prose { margin-top: 0; }
/*
   Every chapter heading in the bundle is `clamp(28px,3vw,40px)` on 1.15 —
   thirty of them, across all six project pages, and not one exception. The
   build drew them at 30, which is `.h2`'s default and a third smaller than the
   thing a chapter opens with.

   `.h2--section` in 02-site.css is the same step and could carry these, but a
   chapter heading is not a heading somebody chooses a size for: the container
   decides it. Scoped here so the thirty-first is right without a modifier
   anyone has to remember.
*/
.chapter__head .h2 {
  font-size: clamp(28px, 3vw, 40px);
  line-height: 1.15;
  /*
     CLASS 3, SECOND ATTEMPT, AND IT WORKS THIS TIME BECAUSE THE POPULATION IS
     SMALLER.

     `max-width: var(--measure, 22ch)` was tried on `.h2` in August and
     REVERTED: it corrected 26 headings and broke 32. The reason was not the
     number. `.h2` was then one class over four design steps, and the design
     leaves two of those steps UNCAPPED — 38 journal headings and 20 legal ones,
     58 headings with no measure at all. No default can be right for a
     population where 58 members want none and 35 want 22ch.

     Class 5 split those steps into their own classes. What is left here is 30
     chapter headings, and the design caps 26 of them at exactly 22ch. The other
     four say so in their own chapter data.
  */
  max-width: var(--measure, 22ch);
}
/* The pull quote inside a chapter is clamp(26px,2.6vw,34px) on all four
   project pages that carry one. `.quote` in 02-site.css is About's, a step
   down; the same class, two populations, told apart by the chapter. */
/*
   The pull quote inside a chapter is clamp(26px,2.6vw,34px) on all four
   project pages that carry one, and it has NO RULE BESIDE IT. `.quote` in
   02-site.css is About's, which the design draws a step down with a 2px
   caramel border and 24px of indent. Same class, two treatments: the border
   was reaching four quotes the design draws plain, and its 26px of box was
   also why "Keep the frame. Change everything it says." set on two lines
   where the design gives it one.
*/
.chapter .quote { font-size: clamp(26px, 2.6vw, 34px); padding-left: 0; border-left: 0; }
/*
   THE TWO COLOURS WERE THE RIGHT WAY ROUND FOR EACH OTHER AND THE WRONG WAY
   ROUND FOR THE DESIGN.

   The design writes the mark as one element with a span inside it:

     <div style="… color:#6E6157;">01<br><span style="color:#6B563F;">The brief</span></div>

   The NUMERAL is muted and the NAME is caramel. This had it inverted — caramel
   on the wrapper, muted on the span — which is 89 of the ~90 colour rows in
   `npm run render:pages`, split 49 one way and 40 the other because they are
   the same swap counted from both ends.

   It is worth saying why it survived. Both colours are in use on the component,
   both clear contrast, and the pair reads as deliberate at a glance: a quiet
   number and a coloured name, or a coloured number and a quiet name, are both
   plausible designs. Only the design file says which, and nothing was reading
   the design file for colour until render:pages.

   0.2em, on both. The comment here used to say the design wrote 0.22em on the
   outcome mark and 0.2em on the chapters; counted out of the six files it is
   0.2em on all forty. That was a transcription, like `.lede`'s 52ch.

   ── AND IT IS A LINE BREAK, NOT A GRID ────────────────────────────────────
   The design writes `01<br /><span>The brief</span>`, so the span is INLINE
   and as wide as its words: 77px for "The brief", 153 for "Before this house".
   `display: grid` made it a grid item filling the whole 200px column, and the
   6px gap added a third line box to a two-line label. Forty marks, each one a
   block of colour where the design draws a word.
*/
.chapter__mark {
  font-family: var(--font-sans);
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
}
.chapter__mark span { color: var(--caramel); }
.chapter__intro { margin: 16px 0 0; font-size: 19px; line-height: 1.7; color: var(--body); max-width: 60ch; text-wrap: pretty; }

@media (max-width: 55rem) {
  .chapter__head { grid-template-columns: minmax(0, 1fr); gap: 20px; }
}

/* --------------------------------------------------------- compositions */

/*
   Four shapes, because the designs use four: a pair side by side, a single
   running the width of the measure, a single running the full width of the
   page, and — once, on the interior transformation — a photograph and its
   caption set beside each other with the label out in the margin.

   The gutter is 32px, which is what every image group in every design file
   states. It was `32px 40px`, so every pair on the site drew each photograph
   4px narrower than the design. One pair genuinely does take the wider
   column gap, and it says so in its own data rather than setting the rule for
   the other twenty-eight.
*/
.comp { display: grid; gap: var(--gap, 32px); }

/*
   A PAIR IS EXACTLY TWO, WHICH IS WHY IT IS NOT auto-fit.

   The handoff says two-column grids use `repeat(auto-fit, minmax(280-360px,
   1fr))`, and it is right about the grids it means: ones holding a variable
   number of cards, where a fixed two-column rule broke between 900 and 1200px.

   A pair is not one of those. It always holds two figures, and auto-fit does
   not know that — at 1328px it fitted THREE columns of 320px and left one
   empty, drawing both photographs a third narrower than the design does. The
   layout check caught it at 1440 and 768 on four of the six pages.

   So the columns are stated and the collapse point is stated: two above 55rem,
   one below, which is where every other two-column thing on this site
   collapses. Explicit beats clever when the count is known.
*/
/*
   AND EQUAL IS THE COMMON CASE, NOT THE ONLY ONE.

   Twenty-six of the twenty-nine pairs are two equal columns. Three are not,
   all on the major transformation, and they are stated rather than implied:
   `1fr 1.35fr` gives the dusk shot the larger share, `1.5fr 1fr` gives it to
   the entrance hall, and one pair takes a 40px gutter. The design writes each
   of those on the element, so they live in that project's data and arrive
   here as `--cols` and `--gap`.

   Below 55rem they all collapse to one column, which is what the design's own
   880px rule does to every grid in the bundle.
*/
.comp--pair { grid-template-columns: var(--cols, repeat(2, minmax(0, 1fr))); align-items: start; }
@media (max-width: 55rem) {
  .comp--pair { grid-template-columns: minmax(0, 1fr); }
}
/*
   FULL BLEED MEANS THE VIEWPORT, AND IT HAS TO ESCAPE THE CONTAINER TO GET
   THERE.

   `max-width: none` on its own did nothing: the group was still a child of
   `.container`, so it was still capped at the 1360px measure. Nine figures on
   five projects drew 1328px where the design draws 1440, and the one thing
   that made them full-bleed in the design — a section with no side padding —
   had no counterpart here at all.

   The template now emits these as direct children of `.section`, and the
   negative margin is exactly the padding the section pays. That is correct at
   every width, unlike `100vw`, which is a scrollbar wider than the page it is
   on.
*/
.section > .comp--full { max-width: none; margin-inline: calc(-1 * var(--section-x)); }

/* The chapter's parts are spaced here rather than by the grid gap on
   `.chapter`, because one of them now sits outside the container. */
.section > .chapter__part,
.section > .comp--full { margin-top: 40px; }

/*
   THE ONE BLOCK OF THIS SHAPE ON THE SITE — interior transformation, 08.

   The label sits in the same 180px margin column the chapter marks use, and
   the photograph and its caption run side by side in what is left. So the
   caption here carries no label of its own: it is not missing, it is out in
   the margin. This was ported as an ordinary single with the label stacked
   above the caption, which reads as the same content in the wrong shape.
*/
.comp--beside { grid-template-columns: minmax(0, 180px) minmax(0, 1fr); gap: 32px 56px; align-items: start; }
.comp__label { padding-top: 6px; }
.comp--beside .pfig { grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 32px; align-items: start; }
.comp--beside .pfig figcaption { margin-top: 0; }
/* No label above it here, so nothing to clear: the 8px belongs under a label
   and there is none inside this caption. */
.comp--beside .pfig__caption { margin-top: 0; }
@media (max-width: 55rem) {
  .comp--beside { grid-template-columns: minmax(0, 1fr); gap: 12px; }
  .comp--beside .pfig { grid-template-columns: minmax(0, 1fr); gap: 14px; }
}

/*
   NO GAP, AND THAT IS NOT A TYPO.

   The space under a photograph is the figcaption's own `margin-top` — 14px
   under a stacked caption, 16px under one set in the margin — because that is
   where the design states it. A 14px grid gap here as well added itself to
   that margin on all 65 figures, so every caption sat 14px lower than the
   design puts it and every figure was 14px taller.

   It cost nothing that any width-based check could see, which is why it
   outlived the first fix pass. render-sweep measures the vertical distance
   from the bottom of the image to the top of the caption now.

   Padding rather than margin for the offset, so the grid still aligns the
   tops of a pair and the step down belongs to the figure.
*/
.pfig { margin: 0; display: grid; gap: 0; padding-top: var(--offset, 0); }
.pfig__pic { display: block; width: 100%; }
/* The file's own ratio, written by the shortcode. Not a house ratio. */
.pfig__pic img { display: block; width: 100%; height: auto; aspect-ratio: var(--ratio); object-fit: cover; }

/*
   ── TWO CAPTION TREATMENTS, NOT ONE ───────────────────────────────────────
   This was the fault reported on 10 August, and it was three faults wearing
   one coat. The designs draw a caption two different ways and the port
   collapsed them:

     IN A PAIR — the design uses <div> children with no grid. Label above
     text, both flush left, 8px between them, 14px under the image, and the
     text capped at 46ch. A caption beside a half-width photograph has half
     the width to work with.

     BESIDE A SINGLE OR A FULL — the design uses <span> children in a grid:
     `minmax(0,180px) minmax(0,1fr)` with a 56px gap, 16px under the image,
     and the text capped at 70ch. The label sits in the margin and the
     sentence runs as one wide line under the picture.

   Collapsing them produced a stacked caption everywhere, wrapping at about 46
   characters where the design gives it 70, which is why it read as a narrow
   block rather than a line.

   The colour was the third: the text was --body where the design says
   --muted, the same token as the label above it. Both clear 4.5:1, so
   `npm run contrast` saw nothing wrong, and that is exactly how it shipped.
   02-site.css had figcaption on --muted already and this file overrode it.

   ── AND THE FOURTH, WHICH SURVIVED THAT FIX ───────────────────────────────
   `figcaption { max-width: var(--measure, 60ch) }` in 02-site.css is right
   for a figcaption that is a line of text under a photograph, which is what
   it was written for. It is wrong for one that is a two-column GRID, because
   it caps the grid rather than the sentence: 60ch is 570px, the label column
   takes 180 and the gap 56, and the sentence is left 334px to run in — half
   the 665px the design gives it, at eight lines instead of four.

   That is the caption reported on the live site on 11 August. `.comp--full`
   escaped it by accident, because that rule happens to set its own
   max-width; every `.comp--single` on the site had it. So the cap is lifted
   here explicitly, and the measure is set on the sentence, where a measure
   belongs.
*/
.pfig figcaption { display: block; margin-top: 14px; max-width: none; }
/*
   `line-height: normal` because the design states none, and none is not the
   same as the one this site inherits. Body copy here is 1.6, which on 10px
   type is a 16px line box around an 11px letter: five pixels of air the design
   does not have, on every caption on every project page.

   A label is one line of small caps. It has no leading to set.
*/
.pfig__label {
  display: block;
  font-family: var(--font-sans);
  font-size: 10px;
  line-height: normal;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
}
/*
   The measure is the figure's own, from the design. Nine different values are
   in use across the six projects — 38, 40, 44, 46, 48, 52, 56, 64 and 70ch —
   and a caption is written to its measure: 38ch under the receiving room is
   two short sentences that fill three lines, 64ch under the entrance hall is
   four that fill three. Setting both to 46 does not lose the words, it loses
   the shape they were written to.
*/
.pfig__caption {
  display: block;
  margin-top: 8px;
  font-size: 17px;
  line-height: 1.6;
  color: var(--muted);
  max-width: var(--cap, 46ch);
  text-wrap: pretty;
}

/*
   ── AND A THIRD TREATMENT, WHICH WAS MISSED TWICE ─────────────────────────
   Four figures in the bundle set the label on the SAME LINE as the sentence:
   baseline aligned, 18px apart, the label told not to wrap, and the sentence
   on 1.55 rather than 1.6. All four are "existing" photographs opening a
   project, which is the point — a before shot is captioned like a note, not
   like a plate.

   It survived the first fix pass because the sentence's measure, its drawn
   width and its line count were all already right. Nothing was comparing
   where the label sat. `npm run render:sweep` compares that now.

   Two of the four also let the sentence drop below the label when there is
   not room for both, and two do not. Both are as stated.
*/
.pfig--inline figcaption { display: flex; gap: 18px; align-items: baseline; }
.pfig--inline .pfig__label { white-space: nowrap; }
.pfig--inline .pfig__caption { margin-top: 0; line-height: 1.55; }
.pfig--inline-wrap figcaption { flex-wrap: wrap; }

/* Beside a single or a full-bleed image, the caption is a two-column grid. */
.comp--single .pfig figcaption,
.comp--full .pfig figcaption {
  display: grid;
  grid-template-columns: minmax(0, 180px) minmax(0, 1fr);
  gap: 56px;
  margin-top: 16px;
}
.comp--single .pfig__caption,
.comp--full .pfig__caption {
  margin-top: 0;
  max-width: var(--cap, 70ch);
}

/*
   A full-bleed image runs edge to edge; its caption does not. The design
   re-indents it back to the page measure so the sentence starts on the same
   line as every other line of type on the page.
*/
.comp--full .pfig figcaption {
  max-width: var(--w-page);
  margin-inline: auto;
  padding-inline: var(--section-x);
}

@media (max-width: 55rem) {
  .comp--single .pfig figcaption,
  .comp--full .pfig figcaption {
    grid-template-columns: minmax(0, 1fr);
    gap: 8px;
  }
}

/* ------------------------------------------------------------- outcome */

/*
   ── THE OUTCOME BLOCK IS A PAIR, AND IT WAS BUILT AS A STACK ──────────────
   The design draws the closing statement and the definitions SIDE BY SIDE, in
   the two-column body every chapter of this shape uses: mark in the margin,
   then an `auto-fit minmax(280px,1fr)` grid holding the sentence and the list.
   At page width that gives each of them 518px.

   Built as a stack, the sentence ran the full 1064px of the body and the list
   sat under it. Nothing measured it — the type was right, the colours were
   right, the words were right — and a 26px closing line set across the whole
   page reads as a heading rather than as the summary of a project. It is the
   most visible single difference left on these pages.

   `.outcome__body` is the pair. `.chapter__body--pair` in the chapter rule
   above is the same grid for the four chapters that open on a pull quote,
   because it is the same arrangement doing the same job.
*/
/* Ruled off from the last chapter, with the design's 48px under the rule. */
.outcome { display: grid; grid-template-columns: minmax(0, 180px) minmax(0, 1fr); gap: 40px 56px; align-items: start; border-top: 1px solid var(--rule-mid); padding-top: 48px; }
.outcome__body { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 32px 56px; align-items: start; }
/* No bottom margin: the pair's own 32px row gap separates it from the list
   when the two columns stack, and beside the list it needs none at all. */
.outcome__lede { margin: 0; font-size: clamp(21px, 2.1vw, 26px); line-height: 1.45; font-weight: 300; text-wrap: pretty; }
/*
   The list is its own two-column grid, not a stack of ruled rows. The rules
   were an invention: the design draws no border anywhere in this block, and
   the row that carried one also put the term and its detail side by side,
   where the design sets the detail UNDER its term.
*/
.outcome__list { margin: 0; display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 24px 32px; }
.outcome__list dt {
  font-family: var(--font-sans);
  font-size: 10px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--muted);
}
/* --body, which is what the design states for the outcome definitions and the
   services line beside them. It was --coffee: the same "no colour stated, so
   inherit the page" fault as the other 24 in this group, except here a colour
   WAS stated and it was the wrong one. */
/* 16px is the design's, stated on the list and inherited by every dd. */
.outcome__list dd { margin: 8px 0 0; font-size: 16px; line-height: 1.5; color: var(--body); }

@media (max-width: 55rem) {
  .outcome { grid-template-columns: minmax(0, 1fr); gap: 20px; }
}

/* ---------------------------------------------------------- next project */

.project__next {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 32px;
  flex-wrap: wrap;
  border-top: 1px solid var(--rule-mid);
  padding-top: 32px;
  color: var(--coffee);
}
/*
   The eyebrow and the headline are ONE block that shrinks to the headline's
   width, so the "View →" on the right sits against it rather than against the
   page. `.stack--tight` made them a grid two rows deep with a 14px gap; the
   design writes a plain div and gives the headline 12px of margin.
*/
.project__next-block { display: block; }
.project__next-block .label { display: block; letter-spacing: 0.24em; }
.project__next-block .project__next-title { display: block; margin-top: 12px; }
/*
   GROUP 3 · THE "VIEW →" ON THE NEXT-PROJECT LINK IS CARAMEL — 6 rows, one
   per project page.

   The design writes the whole link in coffee and then sets that one span in
   `#6B563F`: the eyebrow and the headline are the content and "View →" is the
   affordance, so it takes the colour every affordance on this site takes. The
   build inherited coffee onto it from the link.
*/
.project__next .link-arrow { color: var(--caramel); }
.project__next:hover { color: var(--caramel); }
.project__next:hover .link-arrow { color: var(--coffee); }
/* The design gives this its own step, clamp(24px,2.6vw,34px), a size above
   `.h3` — it is the last thing on the page and the only thing being offered. */
/* The design states no line-height here, and a headline of two or three
   words does not need one set for it. */
.project__next-title { font-size: clamp(24px, 2.6vw, 34px); font-weight: 300; line-height: normal; }

/* === 13-styleguide.css === */
/* ===========================================================================
/*
   The four `.sg__swatch…` rules were deleted on 12 August 2026 by
   `npm run claims`: they styled a colour-swatch grid src/styleguide.njk has
   never rendered. Complete, correct, and for nothing — the same shape as
   `.nav__close`, which is why the check that found them exists.
*/
   The styleguide. Internal, permanently noindexed, and the one page allowed to
   look like a reference document rather than like the site.
   =========================================================================== */

