/* =========================================================================
   METACLADS — TOKENS, BASE, HERO
   Stage 3. The token block below is the approved system.
   Source of truth: DESIGN-PLAN.md — do not change a token without approval.
   ========================================================================= */

:root {

  /* ---------------------------------------------------------------------
     PALETTE — the original 8 tokens
     Ratios in the comments below are measured against Mill Finish as the
     PAGE GROUND. That is history from 2026-07-31 onward: the ground is now
     true black (see the block after this one) and every one of these ratios
     is inverted or void. They are left in place because sections 2-10 still
     reference these tokens and have not been converted yet — deleting or
     repointing them here would silently re-colour nine unconverted sections.

     Fate of each, decided but NOT yet applied:
       mill-finish   value keeps, job changes  -> body copy. Now --color-ink.
       anthracite    value keeps, job changes  -> lifted panels. Now --color-panel.
       corten        RETIRED as the accent (client, 2026-07-31). Gold replaces
                     it. Survives only on light surfaces — see .btn--primary.
       corten-text   same. 2.00:1 on the new ground; cannot carry text there.
       zinc          2.61:1 -> 9.46:1 on black. Far too loud. --color-rule
                     replaces it on the ground.
       graphite      6.12:1 -> 2.61:1 on black. Fails everything.
                     --color-muted replaces it.
       backlight     still referenced nowhere outside this block. Untouched:
                     no ruling was given on it, and it is not the brand gold.
       weld          unchanged and unmoved. 4.34:1 on the new ground, so the
                     focus ring survives the move intact.
     --------------------------------------------------------------------- */
  --color-mill-finish: #E8E7E3;  /* Page ground — brushed aluminium sheet   */
  --color-anthracite:  #26241F;  /* Primary ink — RAL 7016 powder-coat      */
  --color-corten:      #9B4A2A;  /* The only accent — weathering-steel oxide */
  --color-corten-text: #853E23;  /* Accent on text/links — ≥4.5:1 on Mill   */
  --color-zinc:        #B7B3AA;  /* Borders & rules ONLY — 1.69:1, never text */
  --color-graphite:    #57544B;  /* Muted text — 6.12:1 on Mill ✅          */
  --color-backlight:   #E7B27A;  /* Decorative glow only — never text/focus  */
  --color-weld:        #B85C2E;  /* Focus rings only — 3.7:1 Mill / 3.4:1 ink */

  /* ---------------------------------------------------------------------
     DARK GROUND — approved by the client 2026-07-31 (CLAUDE.md rule 4).
     COLOUR ONLY. Archivo, Hanken Grotesk and IBM Plex Mono are untouched
     and stay untouched; nothing in the TYPE block below moves.

     Every ratio here is measured against --color-ground, and every one was
     computed rather than estimated.
     --------------------------------------------------------------------- */
  --color-ground: #0B0A09;                    /* Page ground — true black    */

  /* The same two values the page has always had, renamed for the jobs they
     now do. Aliased rather than re-stated so there is still one place to
     change each, and so the identity is impossible to miss. */
  --color-ink:    var(--color-mill-finish);   /* Body copy — 15.99:1 ✅      */
  --color-panel:  var(--color-anthracite);    /* Lifted panels — 1.28:1 vs
                                                 ground. A whisper, which is
                                                 the point: a panel, not a
                                                 band. Never put text
                                                 straight on this without
                                                 re-measuring.              */

  /* THE BRAND GOLD. Sampled from assets/metaclads-monogram.webp, opaque
     pixels only. The mark is a vertical gradient — light at the head, dark
     at the foot — so it gives three usable values, not one:
       --gold        the mean, #C2A86C. The flat value. Use this by default.
       --gold-light  the top-of-mark tone, for gradients and hover lifts.
       --gold-dark   the foot-of-mark tone, for the other end of a gradient.

     GOLD IS NOT BACKLIGHT. #E7B27A is peachy; this is olive-brass. Putting
     both on screen reads as two different metals. Backlight stays unused.

     HOLD THE LINE ON RUNNING TEXT (client, 2026-07-31): gold carries
     headings, the mark, links and accents. Body copy is --color-ink. Gold
     clears AA at 8.58:1 so it is tempting to spread it — at paragraph
     length it is tiring to read and it cheapens the effect. */
  --color-gold:       #C2A86C;   /* Accent, headings, links — 8.58:1 ✅     */
  --color-gold-light: #DBBE7D;   /* Gradient light end — 11.00:1           */
  --color-gold-dark:  #A6905D;   /* Gradient dark end — 6.38:1             */

  /* Replaces Graphite's JOB on the ground. Picked to land on the ratio the
     page already reads at, not to be as light as possible: Graphite was
     6.12:1 on Mill Finish, this is 5.97:1 on black. The muting should feel
     the same, which means matching the ratio rather than the hue. */
  --color-muted: #908D85;        /* Muted text — 5.97:1 ✅                  */

  /* Replaces Zinc's JOB on the ground, and it is a QUIETER value, not a
     louder one. Zinc reads 2.61:1 on black against 1.69:1 on Mill Finish —
     keeping it would turn every hairline into a near-white outline. 1.77:1
     puts a rule back at the weight it has always had. */
  --color-rule: #3E3B35;         /* Rules & borders — 1.77:1               */

  /* The perforation dot-rule's colour. Option A of the two that were put up:
     the weight the motif was drawn at (Zinc was 1.69:1 on Mill Finish, this
     is 1.77:1 on the ground) rather than gold studs at 8.58:1. The motif
     lands at the foot of nearly every section, so gold here would repeat a
     bright gesture eight times down the page — and "used sparingly, never as
     a fill everywhere" now applies to gold exactly as it did to Corten.
     .hero and .projects override this; both are documented where they do. */
  --dot-color: var(--color-rule);

  /* ---------------------------------------------------------------------
     TYPE — 3 font stacks
     Archivo carries the expanded axis; fallbacks keep headings heavy if the
     wdth axis fails to load (see DESIGN-PLAN.md).
     --------------------------------------------------------------------- */
  --font-display: "Archivo", "Arial Narrow", system-ui, sans-serif;
  --font-body:    "Hanken Grotesk", system-ui, -apple-system, "Segoe UI", sans-serif;
  --font-mono:    "IBM Plex Mono", ui-monospace, "Cascadia Mono", "Consolas", monospace;

  --font-display-stretch: 125%;   /* pair with font-variation-settings:"wdth" 125 */
  --font-display-weight:  800;
  --font-body-weight:     400;
  --font-body-weight-strong: 600;
  --font-mono-weight:     500;

  /* ---------------------------------------------------------------------
     FLUID TYPE SCALE
     --------------------------------------------------------------------- */
  --size-hero:  clamp(3rem, 9vw, 7rem);   /* Hero display (h1)             */
  --size-h2:    clamp(2rem, 4vw, 3rem);   /* Section headings             */
  /* APPROVED IN STAGE 9 — a permanent step in the scale. The plan's scale
     jumps straight from H2 to Body with no sub-heading rung; project names and
     the step titles in "How it works" both need one. At --size-body they go
     too quiet, at --size-h2 they outrank the section title. */
  --size-h3:    clamp(1.375rem, 2vw, 1.875rem);
  --size-body:  1.125rem;                  /* Body copy                    */
  --size-data:  0.8125rem;                 /* Mono data labels / captions  */

  --leading-tight: 1.05;   /* display                                     */
  --leading-body:  1.5;    /* body                                        */

  --tracking-data: 0.12em; /* mono labels, uppercase                      */

  /* ---------------------------------------------------------------------
     SPACING SCALE
     Modular ramp, ~1.5×. Section rhythm uses --space-2xl / --space-3xl.
     --------------------------------------------------------------------- */
  --space-3xs: 0.25rem;   /*  4px */
  --space-2xs: 0.5rem;    /*  8px */
  --space-xs:  0.75rem;   /* 12px */
  --space-sm:  1rem;      /* 16px */
  --space-md:  1.5rem;    /* 24px */
  --space-lg:  2rem;      /* 32px */
  --space-xl:  3rem;      /* 48px */
  --space-2xl: 4.5rem;    /* 72px */
  --space-3xl: 7rem;      /* 112px — vertical gap between sections         */

  /* HEIGHT OF THE FIXED TOP BAR. 44px mark + 2 x 16px padding = 76px.
     THREE THINGS DEPEND ON THIS AND WILL SILENTLY BREAK IF IT DRIFTS: the
     scroll-margin on every anchor target, and the sticky offset on the Systems
     figure in both its desktop and its touch form. All three read the token
     rather than repeating the number. If the mark or the bar padding changes,
     change it HERE and nowhere else. */
  --topbar-h: 4.75rem;    /* 76px */

  /* THE BAR IS 77px, NOT 76, AND THAT 1px WAS A REAL BUG. --topbar-h counts
     the mark and the padding; .topbar also carries `border-bottom: 1px`, which
     is part of its border box and therefore part of what it covers. Every
     sticky offset that used --topbar-h alone parked its element exactly 1px
     UNDERNEATH the bar's bottom border — and because the bar is translucent
     (rgba 0.82) with a backdrop blur, that 1px strip of image was visible
     through it rather than hidden by it.

     Anything positioning itself against the bar uses --topbar-total. Anything
     asking "how tall is the bar's content" still uses --topbar-h. */
  --topbar-total: calc(var(--topbar-h) + 1px);   /* 77px — includes the border */

  /* WHERE A STICKY PANEL COMES TO REST. One value, used by both Systems modes,
     so the desktop and touch offsets can never drift apart again — they were
     `--topbar-h + --space-lg` and a bare `--topbar-h` respectively, which is
     32px of breathing room on desktop and none at all on a phone.
     --space-md (24px) is the settled gap. */
  /* MEASURED FIRST, TOKEN SECOND. script.js publishes the bar's real rendered
     height as --topbar-measured and keeps it current on resize and on font
     load; --topbar-total is the fallback for JS-off and for the first frame
     before the measurement lands. The fallback is the same 77px this always
     computed to, so nothing moves if the script never runs.

     This exists because --topbar-h is a hand-maintained constant describing
     something the browser lays out, and it had already drifted from reality
     once — it omitted the border. A measured value cannot drift. */
  --sticky-top: calc(
    var(--topbar-measured, var(--topbar-total)) + var(--space-md)
  );

  /* ---------------------------------------------------------------------
     SUPPORTING TOKENS
     --------------------------------------------------------------------- */
  --measure: 68ch;                 /* max readable line length             */
  /* Repointed to --color-rule 2026-07-31. Zinc reads 9.46:1 on the ground and
     turned every photograph's hairline into a near-white outline. The one
     light surface left on the page — the inverted Projects band — overrides
     this back to Zinc locally, because there Zinc is still correct. */
  --border-rule: 1px solid var(--color-rule);
  --focus-ring-width: 2px;
  --focus-ring-offset: 2px;
  --duration-slow: 900ms;
  --duration-fast: 200ms;
  --ease-out: cubic-bezier(0.16, 1, 0.3, 1);

  /* The offset gap in the focus ring, rendered in the tone opposite the
     surface (DESIGN-PLAN). The ground is now dark, so the default flips:
     this is the one root-level change that had to happen in step 1 rather
     than section by section — left as Anthracite it would have put a black
     gap on a black ground and quietly voided the ring on all ten sections.

     The .topbar and .hero overrides further down now say the same thing the
     root does and are dead weight; .projects still needs its override, but
     it will need the OPPOSITE value once that section inverts to the light
     band. All three are left alone until their own steps. */
  --focus-gap: var(--color-ink);

  /* ---------------------------------------------------------------------
     ADDED IN STAGE 3 — not palette or type decisions, but page scaffolding.

     --filter-photo is the ONE global photo filter the brief asks for: tuned
     once here, applied to every photo and to the hero video so mixed phone
     and camera sources read as a single shoot.

     RE-JUDGED 2026-08-01 for the dark ground. Was saturate(0.85) contrast(1.06).

       contrast 1.06 -> 1.00. That lift existed to stop photographs washing
       out against a light page. A black ground supplies the apparent contrast
       for free, and keeping the lift crushed the shadow end of every image
       into the ground, so dark corners stopped being corners and became
       holes — worst in the plant grid, which is all shadowed interiors.

       brightness 1.00 -> 0.94. The new problem the light page never had: a
       bright photograph meeting #0B0A09 glares, and the boundary between a
       blown sky and the ground is a hard bright edge. A 6% trim pulls the
       highlights back off that edge. It is small on purpose — this is the
       difference between a photograph sitting on the page and a photograph
       cut out and stuck onto it.

       saturate 0.85 -> 0.88. Barely moved, and moved UP. Heavy desaturation
       reads as considered on a light ground and as dead on a dark one, where
       there is no surrounding light to give the eye a reference. It still
       does its unifying job across phone shots, sample panels and the gold
       renders, which is a wider spread of sources than the original value
       was ever asked to cover.

     TWO GROUNDS NOW, SO TWO VALUES. Projects inverted to the light band and
     overrides this back to the original — see the note there. That is not a
     second filter in the sense the brief forbids: the brief asks that every
     photograph read as one shoot, and holding one value across two grounds is
     what would actually break that. Same intent, measured per surface.
     --------------------------------------------------------------------- */
  --filter-photo: saturate(0.88) contrast(1) brightness(0.94);

  --container-max: 76rem;
  --gutter: clamp(1.25rem, 5vw, 4rem);

}

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

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

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

/* THE GROUND FLIPS HERE, AND IT FLIPS FOR THE WHOLE PAGE AT ONCE — there is
   no way to change a page ground one section at a time. Sections 2-10 still
   set Anthracite and Graphite text and now sit on black; they will look
   wrong, and deliberately so, until each is converted in its own step. That
   is the cost of sequencing this properly rather than in one sweep. The hero
   is the reference section and is the only one meant to read correctly. */
body {
  margin: 0;
  background: var(--color-ground);
  color: var(--color-ink);
  font-family: var(--font-body);
  font-size: var(--size-body);
  font-weight: var(--font-body-weight);
  line-height: var(--leading-body);
}

/* NOTE — deliberately NOT set here:
   `text-rendering: optimizeLegibility` and `-webkit-font-smoothing: antialiased`.
   Both were in the Stage 3 build and both thin out strokes; antialiased in
   particular strips weight from light-text-on-dark, which is exactly where a
   hairline like the cedilla in "façades" goes first. The encoding and the
   Hanken Grotesk latin subset are both correct (U+00E7 is inside U+0000-00FF),
   so the glyph was being rendered thin, not substituted. Default rendering
   keeps the weight the typeface was drawn with. */

img, video { display: block; max-width: 100%; }

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

/* Weld ring, plus a 2px gap in the opposing tone so the ring stays legible
   even when it lands on a photo midtone (DESIGN-PLAN). */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
  outline: var(--focus-ring-width) solid var(--color-weld);
  outline-offset: var(--focus-ring-offset);
  box-shadow: 0 0 0 var(--focus-ring-offset) var(--focus-gap);
}

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

/* Where an anchor jump comes to rest.
   THE TOP BAR IS FIXED AS OF 2026-08-03, so this is now load-bearing rather
   than provision for later: without it every anchor jump would land the
   section's heading underneath the bar. --topbar-h clears the bar exactly and
   --space-md adds the air that stops a section reading as though its top were
   cut off. :where() keeps the specificity at zero so any section can override
   it. Lenis animates the jump but honours scroll-margin, so this is the only
   place the offset is expressed. */
:where(section[id], footer[id]) {
  /* Reads the measured bar height for the same reason --sticky-top does: this
     is the other place a hardcoded --topbar-h could park content underneath a
     bar that is not the height the token claims. Falls back to the static
     token with JS off. */
  scroll-margin-top: calc(
    var(--topbar-measured, var(--topbar-total)) + var(--space-md)
  );
}

/* Perforation dot-rule — the structural divider. Dots sit in pairs, like a
   perforation run: ·· ·· ··

   THE COLOUR IS SETTLED. It is --dot-color, and the decision is recorded in
   the token block at :root, not here. Two candidates were put up on #0B0A09
   and Option A won: --color-rule #3E3B35 at 1.77:1, the weight the motif was
   drawn at, over --color-gold #C2A86C at 8.58:1. The motif lands at the foot
   of nearly every section, and gold eight times down the page is a bright
   gesture repeated, not an accent.

   Three surfaces, three values, each set on the ancestor:
     :root      --color-rule   1.77:1 on the ground
     .hero      --color-zinc          over the video, under the scrim
     .projects  --color-zinc   1.69:1 on the light band
   Both overrides are documented where they are made.

   The Zinc below is a fallback inside the var() rather than a declaration on
   .dot-rule, on purpose: a declaration here would sit ON the element and beat
   any value inherited from an ancestor, so the :root choice could never reach
   it. As a fallback it fires only if nothing up the tree has set it, and
   :root always does — it is a backstop, not a value in use. */
.dot-rule {
  height: 5px;
  background-image:
    radial-gradient(circle at 3px 50%, var(--dot-color, var(--color-zinc)) 1.5px, transparent 1.75px),
    radial-gradient(circle at 9px 50%, var(--dot-color, var(--color-zinc)) 1.5px, transparent 1.75px);
  background-size: 18px 100%;
  background-repeat: repeat-x;
}

/* FOLDED INTO :root now that the conversion is finished — see the token
   block. What is left here are the two exceptions, and both are exceptions
   because their dot-rule does not sit on the page ground:

     .hero      sits over the scrimmed video. --color-rule is 1.77:1 against
                the flat ground and effectively invisible against a moving
                image, so the hero keeps Zinc.
     .projects  is the light band, where Zinc is the 1.69:1 hairline the
                motif was drawn at. Set with the section's other local
                overrides rather than here. */
.hero {
  --dot-color: var(--color-zinc);
}

/* Nothing animates when the OS says so. */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* =========================================================================
   TOP BAR
   Thin, mono, quiet. Overlays the hero; scrolls away with it — it is not
   sticky yet, that belongs with the real Menu panel.
   ========================================================================= */

/* FIXED, NOT ABSOLUTE (2026-08-03). It was absolute, which pinned it to the
   page origin and let it scroll away with the hero. Fixed keeps it against the
   viewport, which means it no longer has the hero's scrim underneath it and
   has to supply its own legibility — see the background below.

   z-index 50, not 2. Fixed chrome has to clear everything it now travels over,
   including the two sticky Systems figures at z-index 1. 50 leaves room
   underneath for a future menu panel without another renumbering. */
.topbar {
  --focus-gap: var(--color-mill-finish);   /* dark surface — gap flips tone */
  position: fixed;
  inset: 0 0 auto;
  z-index: 50;
  color: var(--color-mill-finish);

  /* THE BACKGROUND HAS TO WORK ON BOTH GROUNDS, and the page has exactly two:
     the black ground everywhere and the inverted Projects band. A translucent
     dark bar solves both at once — over black it is nearly invisible, over the
     light band it becomes a dark strip that keeps the Mill Finish text at
     10.32:1. A light bar would have done the opposite and failed on black.

     0.82 is the lowest alpha that still clears AA over the WORST case, which
     is Mill Finish #E8E6E1: the composite there is #333230 and Mill Finish
     text on it measures 10.32:1. Over the ground it is far higher. */
  background: rgba(11, 10, 9, 0.82);
  backdrop-filter: blur(10px) saturate(1.1);
  -webkit-backdrop-filter: blur(10px) saturate(1.1);
  border-bottom: 1px solid rgba(183, 179, 170, 0.14);
}

/* Without blur support the translucency alone lets high-contrast detail read
   through as noise behind the type, so the bar goes nearly solid instead. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .topbar {
    background: rgba(11, 10, 9, 0.96);
  }
}

/* --space-sm, down from --space-md. The mark grew from 32px to 44px and the
   bar is now permanently on screen, so the padding that was comfortable on a
   bar you scrolled past became 92px of viewport taken on every screen. 16px
   holds it at --topbar-h. */
.topbar__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-md);
  padding-block: var(--space-sm);
}

.topbar__link,
.topbar__menu {
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  line-height: 1;
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
  color: inherit;
  text-decoration: none;
  border-bottom: 1px solid transparent;
  transition: border-color var(--duration-fast) var(--ease-out);
}

/* The lockup. Flex rather than the default inline, so the line box around the
   image cannot add descender space under it and throw the bar's centring out —
   the img is display:block from the global reset, and an inline parent would
   still reserve room for a baseline it no longer has. */
.topbar__logo {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  text-decoration: none;
  color: inherit;
}

/* Optically sized, not matched to the mark. Setting the wordmark to the mark's
   44px cap height would tower over 13px nav mono; --size-h3's lower bound sits
   just under the mark and reads as a name rather than a second headline.
   Tracking is opened slightly because Archivo at wdth 125 sets tight, and a
   nine-letter word beside a mark wants air more than it wants density. */
.topbar__wordmark {
  font-family: var(--font-display);
  font-weight: var(--font-display-weight);
  font-stretch: var(--font-display-stretch);
  font-variation-settings: "wdth" 125;
  font-size: 1.375rem;
  line-height: 1;
  letter-spacing: 0.01em;
  text-transform: uppercase;
  color: var(--color-mill-finish);
  transition: opacity var(--duration-fast) var(--ease-out);
}

@media (max-width: 419px) {
  /* At 360px the nav still needs its WhatsApp link and Menu button. The mark
     alone carries the identity at this width; the word is the first thing to
     go, not the last. */
  .topbar__wordmark { display: none; }
}

/* HEIGHT DRIVES THIS, WIDTH FOLLOWS. The file is 83x96 and the width/height
   attributes say so, which is what reserves the right box before it loads.
   There is no global `height: auto` in this stylesheet, so the height
   attribute would otherwise win and pin the mark at 96px — both axes have to
   be named here, and `width: auto` is what lets 32px of height compute the
   27.7px of width rather than squashing the mark into 83x32.

   44px, up from 32px. Against the nav's 13px mono this is a deliberate
   imbalance: with the wordmark beside it the lockup is the only thing in the
   bar that has to read at a glance. 44px of mark plus 2x16px padding is what
   sets --topbar-h at 76px. */
.topbar__mark {
  height: 44px;
  width: auto;
  transition: opacity var(--duration-fast) var(--ease-out);
}

/* No underline on hover — a rule under a picture reads as a mistake. The whole
   lockup lifts instead, mark and word together, or they would come apart on
   hover. Focus is untouched: the global :focus-visible ring applies to the
   <a>, and .topbar already flips --focus-gap to Mill Finish for the dark
   surface, so the Weld ring keeps its legible gap here. */
.topbar__logo:hover .topbar__mark,
.topbar__logo:hover .topbar__wordmark {
  opacity: 0.72;
}

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

.topbar__menu {
  padding: 0 0 var(--space-3xs);
  background: none;
  border-top: 0;
  border-inline: 0;
  cursor: pointer;
}

.topbar__link {
  padding-bottom: var(--space-3xs);
}

.topbar__link:hover,
.topbar__menu:hover {
  border-bottom-color: var(--color-mill-finish);
}

/* =========================================================================
   MENU PANEL

   Absolute inside the fixed .topbar, so it hangs directly off the bottom of
   the bar at every width and needs no --topbar-h arithmetic of its own.

   It borrows the bar's exact surface — same rgba, same blur, same hairline —
   because it IS the bar, extended downwards while open. A different treatment
   would read as a separate floating object stuck underneath it.

   The links are .topbar__link's type treatment to the letter: mono, 13px,
   uppercase, +0.12em, transparent bottom rule that fills on hover. Mill
   Finish on the composite bar background is the same 10.32:1 measured for the
   bar itself. Focus needs nothing here — .topbar already flips --focus-gap to
   Mill Finish, so the global Weld ring inherits a legible gap.
   ========================================================================= */
.topbar__panel {
  position: absolute;
  inset: 100% 0 auto;
  background: rgba(11, 10, 9, 0.82);
  backdrop-filter: blur(10px) saturate(1.1);
  -webkit-backdrop-filter: blur(10px) saturate(1.1);
  border-bottom: 1px solid rgba(183, 179, 170, 0.14);
}

/* Same reasoning as the bar's own fallback: without blur the translucency
   alone lets detail read through as noise behind the type. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .topbar__panel {
    background: rgba(11, 10, 9, 0.96);
  }
}

.topbar__panel-inner {
  padding-block: var(--space-sm);
}

/* Wraps rather than scrolls. Five short mono items fit on one line on a
   desktop bar and take two at 360px, which is the correct failure. */
.topbar__menu-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm) var(--space-lg);
  margin: 0;
  padding: 0;
  list-style: none;
}

.topbar__menu-link {
  display: inline-block;
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  line-height: 1;
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
  color: var(--color-mill-finish);
  text-decoration: none;
  padding-bottom: var(--space-3xs);
  border-bottom: 1px solid transparent;
  transition: border-color var(--duration-fast) var(--ease-out);
}

.topbar__menu-link:hover {
  border-bottom-color: var(--color-mill-finish);
}

/* 360px: the three items plus gutters just fit once the gap tightens. The
   wordmark used to give up tracking here as well, but the monogram is ~28px
   wide against roughly 100px of tracked METACLADS, so the bar has more room
   at 360px than it did and there is nothing left to claw back. */
@media (max-width: 420px) {
  .topbar__inner { gap: var(--space-sm); }
  .topbar__nav { gap: var(--space-sm); }
}

/* =========================================================================
   HERO
   ========================================================================= */

/* The hero needed the least work of any section on the page, because it was
   always the dark one — light text over a scrimmed video. Its ground, its
   text colour and its focus gap were already what the whole page is becoming.
   That is why it is the right section to judge the direction on: what you are
   looking at is the gold against the ground, not a rebuilt layout.

   --focus-gap below is now identical to the root value. Left in place rather
   than deleted: it is correct, and removing it is tidying, not step 1. */
.hero {
  --focus-gap: var(--color-mill-finish);   /* dark surface — gap flips tone */
  position: relative;
  isolation: isolate;
  overflow: hidden;
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  align-content: end;
  padding-block: var(--space-3xl) var(--space-xl);
  color: var(--color-ink);
}

.hero__media {
  position: absolute;
  inset: 0;
  z-index: -1;
}

.hero__poster,
.hero__video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: var(--filter-photo);
}

/* The video sits on the poster and cross-fades in only once it is actually
   playing, so there is no flash of an empty black box while it buffers. */
.hero__video {
  opacity: 0;
  transition: opacity 600ms var(--ease-out);
}
.hero__video.is-playing { opacity: 1; }

/* Belt and braces with the JS gate: at these sizes / settings the element is
   not rendered at all, so even a stray src could not paint. */
@media (max-width: 768px) {
  .hero__video { display: none; }
}
@media (prefers-reduced-motion: reduce) {
  .hero__video { display: none; }
}

/* Dark overlay. Tuned so body copy clears 4.5:1 against the worst case — a
   blown-out white frame of the video — not just against the poster.
   Anthracite, so the shadow is the same ink as the rest of the page. */
.hero__scrim {
  position: absolute;
  inset: 0;
  background:
    /* Top shade — without this the thin bar sits at ~3.3:1 over a bright
       video frame. With it, the mono text clears 6.7:1 worst case. */
    linear-gradient(to bottom,
      rgba(38, 36, 31, 0.55) 0,
      rgba(38, 36, 31, 0) 180px),
    linear-gradient(to bottom,
      rgba(38, 36, 31, 0.30) 0%,
      rgba(38, 36, 31, 0.45) 35%,
      rgba(38, 36, 31, 0.72) 70%,
      rgba(38, 36, 31, 0.80) 100%),
    rgba(38, 36, 31, 0.42);
}

/* GOLD, per the ruling that gold carries headings. This is the one visible
   change in the hero and the thing step 1 exists to let you judge.

   Measured against the worst case rather than the easy one. At the foot of
   the hero, where this sits, the scrim composites to ~88% Anthracite over
   the video; against a blown-out white frame that leaves ~#3F3D39, and gold
   reads 4.70:1 on it. That clears AA for body text, and this is display
   type, which only needs 3:1. Against the poster it is far higher.

   The typeface, weight, width axis and tracking are all untouched. Colour
   only, exactly as briefed. */
.hero__title {
  margin: 0;
  color: var(--color-gold);
  font-family: var(--font-display);
  font-weight: var(--font-display-weight);
  font-stretch: var(--font-display-stretch);
  font-variation-settings: "wdth" 125;
  font-size: var(--size-hero);
  line-height: var(--leading-tight);
  letter-spacing: -0.015em;
  max-width: 15ch;
  text-wrap: balance;
  overflow-wrap: break-word;
}

/* Body copy, so it stays --color-ink and does NOT go gold. This is the line
   that holds the rule: gold above it, ink here. */
.hero__subline {
  margin: var(--space-md) 0 0;
  max-width: 40ch;
  color: var(--color-ink);
}

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-md) var(--space-lg);
  margin-top: var(--space-xl);
}

/* METALLIC GOLD — the sheen, and the hero headline ONLY.

   The gradient is not invented. It runs --color-gold-light to
   --color-gold-dark, which are the head and foot tones sampled off the
   client's own monogram, in the same direction the mark itself falls.

   MEASURED AT THE WORST PIXEL, not the average. Over a blown-out white video
   frame the scrim composites to ~#3F3D39, and against that the light end
   reads 6.02:1 and the dark end 3.49:1. Display type needs 3:1, so both ends
   clear. Going darker does not: the darkest tone in the mark, #9E8653, drops
   to 3.09:1 there, which is why the gradient stops at --color-gold-dark and
   no further.

   The gradient maps to the ELEMENT box, not to each line, so the two lines of
   "Since 1987. Same hands." share one falloff rather than each getting its
   own highlight. That is deliberate — it reads as a single sheet of metal.
   box-decoration-break: clone would give per-line highlights and look like
   two separate objects. */
@supports ((background-clip: text) or (-webkit-background-clip: text)) {
  .hero__title {
    background-image: linear-gradient(
      to bottom,
      var(--color-gold-light) 0%,
      var(--color-gold) 55%,
      var(--color-gold-dark) 100%
    );
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-text-fill-color: transparent;
  }
}

/* THE FALLBACK IS THE BASE RULE, NOT A FALLBACK BLOCK. .hero__title above
   sets color: var(--color-gold) as a real colour, and the transparency only
   ever happens inside the @supports guard. Set transparent outside it and the
   headline disappears entirely wherever the clip is unsupported — that is the
   failure mode worth engineering against, not the 1-2% coverage gap. */

/* Forced colors strips background images and forces text colour. With a
   transparent fill and no gradient left, the headline would render as
   nothing. Hand both properties back to the UA. */
@media (forced-colors: active) {
  .hero__title {
    background-image: none;
    color: CanvasText;
    -webkit-text-fill-color: revert;
  }
}

.hero__rule {
  margin-top: var(--space-2xl);
}

/* =========================================================================
   BUTTONS
   ========================================================================= */

.btn {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2xs);
  border: 0;
  border-radius: 0;              /* stamped, not rounded */
  font-family: var(--font-body);
  font-size: var(--size-body);
  line-height: 1;
  text-decoration: none;
  cursor: pointer;
  transition:
    background-color var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out),
    border-color var(--duration-fast) var(--ease-out);
}

/* Primary reads as a milled aluminium tag on the dark hero — highest
   contrast object on screen, which is what the one action deserves.
   Corten is held back for the arrow and the hover. */
.btn--primary {
  padding: var(--space-sm) var(--space-md);
  background: var(--color-mill-finish);
  color: var(--color-anthracite);
  font-weight: var(--font-body-weight-strong);
}
/* DO NOT SWAP THIS FOR GOLD. The 2026-07-31 ruling retires Corten in favour
   of the brand gold as the accent, and that ruling is right everywhere the
   accent sits on the GROUND. It is wrong here. This arrow sits on a Mill
   Finish tag, where gold measures 1.86:1 and vanishes; Corten Text measures
   6.26:1 on the same tag. Corten survives on light surfaces for exactly this
   reason and nowhere else.

   The real answer is probably to invert the button — a gold tag on black
   instead of an aluminium one — but that is the whole button system, used by
   sections this step is not allowed to touch. Left as it stands, flagged. */
.btn--primary .btn__arrow {
  color: var(--color-corten-text);
  transition:
    transform var(--duration-fast) var(--ease-out),
    color var(--duration-fast) var(--ease-out);
}
/* The hover fills with gold, so the TEXT has to flip dark — Mill Finish on
   gold is 1.86:1 and the label would vanish at the exact moment somebody is
   pointing at it. Panel on gold is 6.73:1. This is the trap in "replace
   Corten with gold": Corten is dark and gold is light, so every rule that
   used Corten as a fill has to invert its foreground too. */
.btn--primary:hover {
  background: var(--color-gold);
  color: var(--color-panel);        /* 6.73:1 ✅ */
}
.btn--primary:hover .btn__arrow {
  color: var(--color-panel);        /* 6.73:1 ✅ */
  transform: translateX(3px);
}

/* THIS BUTTON HAD TO BE REBUILT, not recoloured. It was Anthracite-filled
   because it sat on a light page ground and needed to be the dark object. The
   ground is now #0B0A09 and Anthracite is 1.28:1 against it — the button had
   become an invisible rectangle with floating text, which is the single worst
   thing the palette change did to any component on the page.

   It is now the gold button, and it is the only large gold fill in the
   document. That is defensible: it marks the form's one submit action, which
   is the single thing the whole page is asking a visitor to do. Panel text on
   gold reads 6.73:1.

   The two buttons now read as a pair rather than as light/dark twins:
   --primary is the milled aluminium tag, --ink is the gold tag. */
.btn--ink {
  padding: var(--space-sm) var(--space-md);
  background: var(--color-gold);
  color: var(--color-panel);        /* 6.73:1 ✅ */
  font-weight: var(--font-body-weight-strong);
}
.btn--ink .btn__arrow {
  color: inherit;
  transition: transform var(--duration-fast) var(--ease-out);
}
.btn--ink:hover {
  background: var(--color-gold-light);  /* 8.62:1 against Panel text ✅ */
}
.btn--ink:hover .btn__arrow {
  transform: translateX(3px);
}
.btn--ink[disabled] {
  opacity: 0.55;
  cursor: default;
}

/* Quiet secondary — text on a rule. --color-muted rather than --color-rule:
   this underline is the button's only boundary, which makes it a UI component
   under WCAG 1.4.11 and owes 3:1. --color-rule is 1.77:1 and would leave the
   affordance invisible; --color-muted is 5.97:1. Zinc, the old value, is
   9.46:1 here and would shout louder than the primary button's fill. */
.btn--quiet {
  padding: var(--space-2xs) 0;
  background: none;
  color: var(--color-ink);
  border-bottom: 1px solid var(--color-muted);
}
.btn--quiet:hover {
  border-bottom-color: var(--color-mill-finish);
}

/* .btn is display:inline-flex with text-decoration:none, so --quiet reads
   identically on an <a> and on a <button>. The hero's second action is an
   anchor to #send-drawings — nothing about the type treatment changes. */

/* =========================================================================
   MARQUEE BAND — the page's one moving element besides the compare slider

   Dark text on gold: --color-panel on --color-gold is 6.73:1. Panel rather
   than Ground so the strip reads as ink printed on metal rather than as a
   hole cut through to the page behind it.

   NOTHING MOVES WITHOUT JAVASCRIPT. The animation only runs under .is-live,
   which script.js adds at the same moment it reveals the pause button. With
   JS off the band is a static gold strip of names — which still works as a
   divider, and which is conformant, where an unpausable CSS animation would
   not be.
   ========================================================================= */

.marquee {
  --focus-gap: var(--color-panel);   /* light surface — gap flips dark */
  position: relative;
  overflow: hidden;
  margin-block: var(--space-3xl);
  padding-block: var(--space-sm);
  background: var(--color-gold);
  color: var(--color-panel);
}

.marquee__track {
  display: flex;
  width: max-content;
}

.marquee.is-live .marquee__track {
  animation: marquee-scroll 62s linear infinite;
}

/* Slow. Deliberately below the speed at which the eye starts tracking
   individual words — it should read as drift, not as a ticker demanding to be
   followed.

   THE DURATION IS DERIVED, NOT PICKED, AND IT MUST BE RE-DERIVED WHENEVER A
   NAME IS ADDED OR REMOVED. The keyframe travels -50% of the track, which is
   exactly one set, so the speed on screen is (width of one set) / duration.
   Leave the duration alone while the list grows and the band silently speeds
   up — the same 90s pushing more text past the eye.

   One set, measured from the tokens rather than the rendered page:
     per character  0.6em advance + 0.12em tracking = 0.72em at 13px = 9.36px
     per item       --space-lg gap 32px + the ::after dot (4px + 32px) = 68px
     set width      chars x 9.36 + items x 36 + (items-1) x 32 + 32 padding

   20 names / 261 chars  ->  ~3803px  ->  90s   = 42.3 px/s
   26 names / 335 chars  ->  ~4904px  -> 116s   = 42.3 px/s
   15 names / 169 chars  ->  ~2602px  ->  62s   = 42.0 px/s   <- current

   The set got shorter, so the duration had to come down with it: left at 116s
   the band would have drifted at 22 px/s, roughly half the speed that was
   judged, and a marquee that slow reads as broken rather than as calm.

   62s keeps the drift at the speed that was judged, rather than holding the
   number that produced it.

   ONE SET IS NOW 2602px, AND THAT IS THE FLOOR TO WATCH. The loop is seamless
   only while one set is wider than the viewport; below that a gap opens at the
   wrap. 2602px clears every common width including 2560, but it no longer
   clears it by much — if names are ever removed from this band, check the
   width before checking the speed. */
@keyframes marquee-scroll {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

.marquee.is-paused .marquee__track { animation-play-state: paused; }

/* Manners, not conformance — these help a pointer user and nobody else, which
   is exactly why they are not a substitute for the button. */
.marquee:hover .marquee__track,
.marquee:focus-within .marquee__track { animation-play-state: paused; }

.marquee__set {
  display: flex;
  align-items: center;
  gap: var(--space-lg);
  margin: 0;
  padding: 0 var(--space-lg) 0 0;
  list-style: none;
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
  white-space: nowrap;
}

/* The separator between names — a single gold-dark dot, echoing the
   perforation motif without repeating the dot-rule itself. */
.marquee__set li::after {
  content: "";
  display: inline-block;
  width: 4px;
  height: 4px;
  margin-left: var(--space-lg);
  vertical-align: middle;
  border-radius: 50%;
  background: var(--color-panel);
  opacity: 0.55;
}

.marquee__toggle {
  position: absolute;
  top: 50%;
  right: var(--space-sm);
  transform: translateY(-50%);
  padding: var(--space-3xs) var(--space-2xs);
  background: var(--color-panel);
  color: var(--color-gold);
  border: 0;
  border-radius: 0;
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
  cursor: pointer;
}

/* REDUCED MOTION — the band stops being a marquee and becomes a static strip.
   The duplicate set goes, so a sighted reader and a screen reader get the same
   single list; the names wrap instead of scrolling, so nothing is clipped
   mid-word; and the pause button goes with the movement it existed to stop. */
@media (prefers-reduced-motion: reduce) {
  .marquee__track      { width: auto; }
  .marquee__set        { flex-wrap: wrap; white-space: normal; }
  .marquee__set + .marquee__set { display: none; }
  .marquee__toggle     { display: none; }
  .marquee.is-live .marquee__track { animation: none; }
}

/* =========================================================================
   SECTION FURNITURE
   ========================================================================= */

.section-title {
  margin: 0 0 var(--space-xl);
  font-family: var(--font-display);
  font-weight: var(--font-display-weight);
  font-stretch: var(--font-display-stretch);
  font-variation-settings: "wdth" 125;
  font-size: var(--size-h2);
  line-height: var(--leading-tight);
  letter-spacing: -0.01em;
}

/* =========================================================================
   NUMBERS

   Four proof points, not a stat wall. No cards, no borders, no centring, no
   count-up — left-aligned like entries on a spec sheet, with the dot-rule
   closing the section. The only rule in here is the perforation divider;
   a full-width hairline above the row would be the generic section divider
   DESIGN-PLAN replaced.
   ========================================================================= */

.numbers {
  padding-block: var(--space-3xl) 0;
}

.stats {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--space-2xl) var(--space-lg);
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 900px) {
  .stats { grid-template-columns: repeat(4, 1fr); }
}

.stat__figure {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--font-display-weight);
  font-stretch: var(--font-display-stretch);
  font-variation-settings: "wdth" 125;
  font-size: var(--size-h2);
  line-height: var(--leading-tight);
  letter-spacing: -0.02em;
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--color-ink);          /* 15.99:1 on the ground ✅ */
}

/* The one warm mark in an otherwise monochrome section. Now the brand gold:
   this is a glyph and an accent, which is exactly the job the 2026-07-31
   ruling gives gold. Corten Text would read 2.00:1 here and is retired from
   the dark ground entirely — it survives only on the light .btn--primary tag. */
.stat__plus {
  color: var(--color-gold);         /* 8.58:1 on the ground ✅ */
}

/* Mill Finish, per the step 2 ruling — the same tone as the figures.

   FLAGGED: this drops the section's two-tone hierarchy. On the light ground
   the labels were Graphite under Anthracite figures, so colour reinforced
   what size and weight were already saying. Both are now the same tone and
   the separation rests entirely on 13px uppercase mono against display-weight
   Archivo — which is a large gap, and arguably all the hierarchy this needs.
   If it flattens too much, --color-muted (#908D85, 5.97:1) restores the two
   levels and is a one-word change. Not doing it unasked. */
.stat__label {
  margin: var(--space-xs) 0 0;
  max-width: 18ch;
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  line-height: 1.45;
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
  color: var(--color-ink);          /* 15.99:1 on the ground ✅ */
}

.numbers__divider {
  margin-top: var(--space-3xl);
}

/* =========================================================================
   SYSTEMS

   Written linearized-first: the base rules below are what touch, hover:none,
   ≤768px and JS-off all get — a two-column mono list, then one static texture
   underneath. The side-by-side layout and the hover swap are added only inside
   the (hover: hover) and (min-width: 769px) query at the end of this block, so
   nothing on a phone depends on a pointer that isn't there.
   ========================================================================= */

.systems {
  padding-block: var(--space-3xl) 0;
}

/* ONE COLUMN, NOT TWO, AS OF 2026-08-11. This ran as two mono columns and had
   a further override dropping to one below 420px. Both are gone: every row now
   carries a descriptor sentence under its name, and a sentence in a 150px
   column at 360px is not a list, it is a stack of two-word lines. The rows are
   blocks now, so they get the full measure and the desktop query below still
   resets `columns` for the side-by-side layout. */
.systems__list {
  columns: 1;
  margin: 0 0 var(--space-2xl);
  padding: 0;
  list-style: none;
}

/* The mono treatment now lives on the NAME, not on the row, because the row
   also holds a descriptor sentence and mono caps at 0.12em tracking is a label
   face, not a reading face. The row keeps the colour so the recede below still
   works by inheritance on both lines at once. */
.systems__item {
  break-inside: avoid;
  margin-bottom: var(--space-md);
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.systems__name {
  display: block;
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  line-height: 1.45;
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
}

/* The descriptor. Body face, sentence case, no tracking — deliberately the
   opposite of the name above it, because the two need to read as a label and
   its caption rather than as two lines of the same list.

   IT SITS AT --size-data, THE SAME SIZE AS THE NAME. There is no token between
   --size-data and --size-body and inventing one for this is how type scales
   rot (the same ruling .project__line records). The separation here is carried
   by family and by case, which at this size is a wide enough gap.

   Colour is INHERITED, never declared. That is what lets `.systems.has-active`
   recede a whole row — name and descriptor together — with one rule. Declaring
   --color-muted here would leave the descriptor muted on the ACTIVE row too,
   which is the row the interaction exists to pick out. */
.systems__desc {
  display: block;
  margin-top: var(--space-3xs);
  max-width: 46ch;
  font-family: var(--font-body);
  font-weight: var(--font-body-weight);
  font-size: var(--size-data);
  line-height: 1.45;
  letter-spacing: normal;
  text-transform: none;
}

/* THE ACTIVE STATE NOW LIVES IN THE BASE, NOT IN THE HOVER QUERY.

   All of this used to sit inside @media (hover: hover) and (min-width: 769px),
   which was right when the swap was hover-only: a touch device could never
   reach the state, so styling it would have been dead weight. Tap changed
   that. A phone can now activate a row, and it needs the same leader rule and
   the same recede as a pointer does — otherwise the tap does something
   invisible and the section reads as broken.

   Only LAYOUT is still gated on hover. State is universal. */
.systems__item {
  position: relative;
  padding-left: var(--space-lg);   /* room for the leader rule at left: 0 */
  transition: color var(--duration-fast) var(--ease-out);
}

/* The active mark: a short leader rule, the same annotation language as the
   elevation drawing. Gold rather than Corten — a single mark, shown one at a
   time, which is precisely the accent job the gold rule reserves for it.
   Corten would read 2.51:1 on this ground. */
/* THE OFFSET IS A LENGTH NOW, NOT 0.58em, and it had to become one. The em
   used to resolve against the row's own font-size, because the row WAS the
   name; the row now inherits body size and the name sets its own, so the same
   em would have measured against a size nothing in the row is set in.

   8px is derived: the name is 13px at line-height 1.45, so its line box is
   18.9px and its centre is 9.4px down; the rule is 2px tall, so its top sits
   at 8.4px. The desktop query re-derives it for the 18px name. */
.systems__item::before {
  content: "";
  position: absolute;
  left: 0;
  top: 8px;
  width: 20px;
  height: 2px;
  background: var(--color-gold);
  opacity: 0;
  transform: scaleX(0);
  transform-origin: left center;
  transition:
    opacity var(--duration-fast) var(--ease-out),
    transform var(--duration-fast) var(--ease-out);
}

.systems__item.is-active::before {
  opacity: 1;
  transform: scaleX(1);
}

/* Everything else recedes. THIS IS THE ONE PLACE --color-muted IS USED, and it
   is not the deferred "muted tier" — the whole interaction is the contrast
   between the active row and the rest, so the recede is functional rather than
   decorative. Graphite would read 2.61:1 here and the inactive rows would be
   unreadable, not merely quiet. */
.systems.has-active .systems__item:not(.is-active) {
  color: var(--color-muted);      /* 5.97:1 ✅ */
}

/* THE AFFORDANCE IS GATED ON [tabindex], WHICH ONLY JAVASCRIPT SETS, and only
   on rows that actually have a texture to show. With JS off, or on the four
   rows with no photograph, there is no pointer cursor and no tap target —
   nothing that promises an interaction the page cannot deliver. */
.systems__item[tabindex] {
  cursor: pointer;
}

/* The old "below 420px drop to one column" rule is gone with the two-column
   list itself — see .systems__list above. Nothing replaces it. */

/* NO overflow: hidden HERE, AND THAT IS DELIBERATE — it was added on
   2026-08-11 and removed the same day. It never clipped anything, because this
   figure is an auto-height box: it grows to fit the image and caption, so
   there is nothing outside it to clip. It was a guard against a hypothetical.

   But a clip guard on a panel that is ALSO going to be height-capped is how a
   real crop gets built by accident: the moment anything gives this box a
   max-height, overflow:hidden turns that cap into a blade and cuts the top off
   the picture. The picture is already contained — object-fit does that inside
   the img's own box — so the guard bought nothing and risked exactly the
   failure it was supposed to prevent. It stays off.

   THE PANEL IS KEPT INSIDE THE VIEWPORT BY CAPPING THE IMAGE'S WIDTH, never
   by capping this box's height. That is the same lesson the touch rules and
   the compare frame both already record: on a fixed aspect-ratio box a
   max-height does not shorten the box, it just lets the width run and brings
   the crop back. Capping width caps height while the ratio holds. */
.systems__texture {
  margin: 0;

  /* ITS OWN COMPOSITOR LAYER. This figure is sticky in both branches, so on
     every scroll frame it is the one element on the page whose position
     changes relative to everything around it. Left on the main thread that is
     a repaint of a 416px image plus its 1px border on each frame, which is
     what the stutter through this section was.

     translateZ(0) is what actually promotes the layer; will-change: transform
     tells the compositor to expect it and stops the layer being torn down and
     rebuilt. Both are needed — will-change alone is a hint a browser may
     decline, and the transform alone can be promoted lazily on first paint.

     SAFE ON A STICKY BOX. A transform on an ANCESTOR of a sticky element
     redefines its containing block and breaks the stick; a transform on the
     sticky element ITSELF is applied after positioning and does not. This must
     stay on the figure and must never move up to .systems__layout — that would
     put the bug back, in a form much harder to spot than the last one.

     No absolutely-positioned descendants live in here, so the containing block
     this also establishes has nothing to catch. */
  will-change: transform;
  transform: translateZ(0);
}

/* object-position 50% 30% biases every crop UPWARD, and it is measured, not
   guessed. The source is 1200x1600 and the building's parapet corner — where
   the cassette run meets the dark soffit — sits at y=175. Centred, the square
   crop keeps y 200..1400 and clips that corner off by 25px. At 30% it keeps
   y 120..1320: the roofline survives with 55px of sky above it, and the extra
   280px comes off the base instead, taking the plinth and the balustrade urn.
   The top of this photograph is the stronger half; the bottom is street. */
/* height: auto IS LOAD-BEARING and was missing. This is the one <img> on the
   page whose src is swapped at runtime, so it is the one most exposed to a
   height attribute arriving with a new file: a width/height pair is a
   presentational hint, and aspect-ratio is only consulted while one axis is
   indefinite. Without height:auto a swapped-in 1506px-tall source would lay
   out at 1506px and run straight out of the frame — which is exactly the
   failure this section is being fixed for. object-fit then crops within
   whatever box that leaves. */
.systems__image {
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  object-position: 50% 30%;
  border: var(--border-rule);
  filter: var(--filter-photo);
}

/* PHONES GET THE SOURCE SHAPE. The photograph is 3:4, so at this width the
   frame matches it exactly and object-fit crops nothing at all — the whole
   corner, roofline to plinth, on the screen where it has least room.

   Scoped to 768px rather than changed on the base rule, deliberately. The base
   is also what a WIDE no-hover device gets, because the desktop layout is
   gated on (hover: hover) — an iPad in landscape is 1088px of content, and at
   3:4 that would be a 1451px-tall image on a 1024px-tall screen. It keeps 3:2. */
/* -------------------------------------------------------------------------
   TOUCH — tap to swap. The layout has to earn the interaction.

   The problem tap creates that hover never had: on a phone the list stacks
   ABOVE the texture, so tapping a name changes an image that is off the
   bottom of the screen. The tap appears to do nothing, which is worse than
   having no tap at all.

   So on touch the figure moves above the list and sticks. The list scrolls
   underneath it and the texture stays on screen for every row you reach, all
   the way to WPC cladding at the foot.

   THE IMAGE IS CAPPED BY WIDTH, NOT HEIGHT — the same lesson as the compare
   frame. max-height on a fixed aspect-ratio box does not shorten it, it just
   lets the width run and the crop comes back. Capping the width caps the
   height while the ratio holds, so the frame gets smaller rather than wider
   and the 3:4 source is still never cropped. At 360px that is a 252x336
   panel, leaving roughly 360px of list on screen beneath it.

   THE CONDITION IS (hover: none) OR NARROW, not narrow alone. The JS gate is
   `(hover: hover) and (min-width: 769px)`, so an iPad in landscape — 1024px
   wide and no hover — falls into tap mode. On a width-only query it would
   have got tap behaviour with none of the layout that makes tap work: 19px
   targets and an image below a list it cannot see. It needs these rules as
   much as a phone does.

   That also retires the old worry about 3:4 on a wide no-hover device. The
   3:4 was scoped narrow because a full-width 1088px frame would have been
   1451px tall. The image is capped at 20rem now, so 3:4 is 320x427 there —
   the reason for the restriction is gone.
   ------------------------------------------------------------------------- */
@media (hover: none), (max-width: 768px) {
  /* THIRD ARM ADDED 2026-08-11 — the viewport arm.
     70vw and 20rem both cap the image against the WIDTH of the screen. Neither
     knows how tall the result is, and this figure is sticky: if it is taller
     than the gap between --sticky-top and the bottom of the window, it pins
     and its top is cut off by the header. The third arm caps it against the
     HEIGHT instead, expressed as a width so the 3:4 ratio survives.

     Derivation: the figure is 8px padding + image + 31px caption + 16px
     padding. For the whole thing to sit between --sticky-top and the foot of
     the viewport with --space-md of air under it:
         image height <= 100vh - sticky-top - 55 - 24
     and at 3:4, width = height x 0.75. The 79px of furniture is folded into
     the --space-2xl below so the sum stays readable.
     360x640:  (640 - 101 - 72) x 0.75 = 350  -> 70vw wins at 252. Not binding.
     768x1024: (1024 - 101 - 72) x 0.75 = 638 -> 20rem wins at 320. Not binding.
     It only ever bites on a short landscape phone, which is exactly the case
     the other two arms cannot see. */
  .systems__image {
    aspect-ratio: 3 / 4;
    max-width: min(
      70vw,
      20rem,
      calc((100vh - var(--sticky-top) - var(--space-2xl)) * 0.75)
    );
    margin-inline: auto;
  }

  .systems__layout {
    display: flex;
    flex-direction: column;
  }

  .systems__texture {
    order: -1;
    position: sticky;
    /* Clears the fixed bar AND its 1px border, with 24px of air under it.
       Was `var(--topbar-h)` — 76px against a 77px bar, so the top edge of the
       figure sat 1px under the bar's border with no gap at all, and the image
       read as tucked beneath the header rather than resting below it. */
    top: var(--sticky-top);
    z-index: 1;
    /* Opaque, or the list scrolls through the image as it passes underneath.
       The ground colour, so the panel reads as part of the page rather than
       as a floating card. */
    background: var(--color-ground);
    padding-block: var(--space-2xs) var(--space-sm);
  }

  /* WCAG 2.5.8 asks for 24px minimum on a target; 44px is the comfortable
     number and it is what a mono line at --size-data does not reach on its
     own (13px x 1.45 is about 19px).

     COLUMN, NOT ROW. This was a centred flex row back when the item held one
     text node. It now holds two block spans, and `row` would have stood the
     name and its descriptor side by side in a single 19px line. The min-height
     is kept as the floor it always was — with a descriptor under it every row
     already clears 44px, so it only still bites if a descriptor is ever
     removed. */
  .systems__item[tabindex] {
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: flex-start;
    min-height: 2.75rem;
  }
}

.systems__caption {
  margin-top: var(--space-xs);
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.systems__divider {
  margin-top: var(--space-3xl);
}

/* -------------------------------------------------------------------------
   DESKTOP WITH A POINTER — list left, texture right, hover/focus marks a row.
   ------------------------------------------------------------------------- */
@media (hover: hover) and (min-width: 769px) {

  .systems__layout {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
    gap: var(--space-2xl);
    align-items: start;
  }

  .systems__list {
    columns: auto;               /* back to a single column beside the image */
    margin-bottom: 0;
  }

  /* RE-DERIVED 2026-08-11 FOR FIFTEEN ROWS THAT EACH CARRY TWO LINES.
     At 1440px the container is 1088px of content; minus the 72px gap and
     split 1 : 1.15 that is a 473px list beside a 543px image. The image is
     square, so the figure is 543px plus a 31px caption — 574px.

     A row is now an 18px mono name (26px) plus a 4px gap plus a 13px body
     descriptor, which at a 473px column is one line at 19px — 49px of row.
     Fifteen of those at --space-md between them is 15 x 49 + 14 x 24 = 1071px.
     That is where twenty single-line rows were before the last pass, so the
     sticky figure below is doing exactly the job it was added for and the
     arithmetic has not moved in kind, only in composition.

     --space-md rather than --space-sm here, and it is the one number that
     went UP: a two-line block needs more air around it than a single line, or
     the descriptor of one row crowds the name of the next and the list stops
     having rows at all. */
  .systems__item {
    margin-bottom: var(--space-md);
  }

  .systems__name {
    font-size: var(--size-body);
  }

  /* Same derivation as the base rule, against the 18px name: line box 26.1px,
     centre 13.1px, less half the 2px rule = 12px. */
  .systems__item::before {
    top: 12px;
  }

  /* SQUARE, AND CAPPED AGAINST THE VIEWPORT HEIGHT.
     The image is 1:1 here, so its height IS its width, and its width is
     whatever the 1.15fr grid track gives it — a number that grows with the
     window's width while the space it has to fit into is governed by the
     window's HEIGHT. Those two are independent, so on any short-and-wide
     window the figure outgrows the gap under the header and pins.

     The cap is the gap itself: 100vh, less the offset it starts at, less the
     31px caption and --space-lg of air at the foot (folded into --space-2xl).
     Because the box is square this is a width and a height at the same time,
     so nothing is ever cropped — the frame just gets smaller.

     TWO ARMS, AND ONLY ONE OF THEM BINDS.

     The vh arm is a safety net and it is NOT binding at any supported size.
     The container caps at 76rem, so the 1.15fr track stops growing at a 543px
     image, and the gap under the header is always larger than that:
       1280x800   gap = 800 - 101 - 72 = 627   vs 543
       1920x1080  gap = 1080 - 101 - 72 = 907  vs 543
       720 floor  gap = 720 - 101 - 72 = 547   vs 543
     It is kept so that a future change to the track ratio or the container cap
     cannot silently break the fit.

     26rem IS THE ARM THAT ACTUALLY DOES SOMETHING, and it is here because
     543px was too big for this panel's job, not because it overflowed:

       figure at 543  ->  543 + 31 caption = 574;  101 + 574 = 675 of 800
       figure at 416  ->  416 + 31 caption = 447;  101 + 447 = 548 of 800

     That is 252px of clear viewport under the panel instead of 125px, and at
     this query's 720px floor it is 172px instead of 45px. The panel reads as
     sitting below the header with air around it rather than filling the window
     from the header down.

     IT ALSO LENGTHENS THE STICK, which is the other half of the complaint. A
     sticky box travels (containing-block height - its own height) before it
     releases and scrolls away, and the containing block here is the grid area,
     which is as tall as the list. With the list at ~1353px (fifteen rows, most
     of them wrapping to two lines at a 473px column):
       figure 574  ->  travel 779px, releases with row 15 at viewport y~607
       figure 447  ->  travel 906px, releases with row 15 at viewport y~480
     The last rows are now hovered while the panel is whole and well clear of
     the header, rather than at the exact moment it begins to leave. */
  .systems__image {
    aspect-ratio: 1 / 1;
    max-width: min(
      26rem,
      calc(100vh - var(--sticky-top) - var(--space-2xl))
    );
  }

  /* THE STICK LIVES HERE NOW — in the same query as the grid it belongs to.

     IT USED TO SIT IN ITS OWN QUERY WITH `and (min-height: 720px)` ON THE END,
     AND THAT WAS THE BUG. min-height is a VIEWPORT height feature, not a
     window height: a 1280x800 window is only ~620-680px of viewport once the
     tab strip, address bar and bookmarks are taken off. So on an ordinary
     laptop the grid query above matched and this one did not — the section
     laid out in two columns exactly as intended while `position` computed to
     `static`, and the figure simply scrolled away with the page. Measured on
     the live page at 1280 wide: position "static", top "auto", panel top at
     -354px. Four rounds of offset tuning went into a rule that never applied.

     Splitting layout from behaviour across two queries is what allowed that to
     go unnoticed, so they are one query now. If the grid applies, the stick
     applies. They cannot diverge again.

     THE HEIGHT GUARD IS GONE AND IS NOT REPLACED. It existed because a sticky
     box taller than the space it sticks in pins and loses its foot, and at a
     543px image the figure needed 682px of viewport. That reasoning is now
     handled by the image itself: the vh arm of the max-width above shrinks the
     frame to fit whatever viewport it is given, so the figure can never be
     taller than the gap under the header. What was a query condition is now a
     property of the box, which is why the guard can be deleted rather than
     merely lowered. The vh arm has stopped being a safety net and is doing
     real work at every viewport under ~560px tall.

     align-items: start ON THE GRID IS LOAD-BEARING and stays. A stretched grid
     item fills its area, leaving zero travel, and can never stick. Start keeps
     the figure content-sized (447px) inside a grid area as tall as the list
     (1176px measured), which is 729px of travel. */
  .systems__texture {
    position: sticky;
    top: var(--sticky-top);

    /* THE BACKGROUND IS KEPT, AND NOT FOR THE REASON IT WAS ADDED.

       Visually it is redundant HERE: on desktop the list and the figure are
       two grid columns side by side, so nothing ever scrolls behind this
       panel, and --color-ground is the body's colour anyway. On the touch
       branch it is load-bearing — there the figure sits above the list in a
       flex column and the list really does pass underneath it.

       It stays because the layer above is promoted. An opaque layer lets the
       compositor skip everything behind it; a transparent one has to be
       blended against what it covers on every frame. Removing this to save a
       paint would cost more in blending than it saved — the opposite of the
       intent. Do not delete it as dead code.

       z-index 1 is cheap and guards the day something does overlap. */
    z-index: 1;
    background: var(--color-ground);
  }
}

/* THE `and (min-height: 720px)` QUERY THAT USED TO SIT HERE IS DELETED.
   It carried the sticky declarations for the desktop branch and it never
   matched on an ordinary laptop, because min-height measures the VIEWPORT and
   a 1280x800 window is ~620-680px of viewport once browser chrome is counted.
   The rules are now inside the (hover: hover) and (min-width: 769px) query
   above, next to the grid they belong to. Do not split them apart again. */

/* =========================================================================
   WPC

   The composite range, and structurally the simplest section on the page: two
   lines of claim, then a contact sheet. It borrows the plant grid's square
   cells and its 2-then-3 column rhythm rather than inventing a third grid
   language — the two read as one sheet, which is the intent.

   THE GRID IS OPEN-ENDED. Nothing here counts the items, so photographs can be
   added to the list in the markup and they simply fill the next cell. No CSS
   change is needed to grow it.
   ========================================================================= */

.wpc {
  padding-block: var(--space-3xl) 0;
}

.wpc__lead {
  margin: 0;
  max-width: 42ch;
  font-weight: var(--font-body-weight-strong);
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.wpc__line {
  margin: var(--space-2xs) 0 0;
  max-width: 48ch;
  color: var(--color-ink);          /* 15.99:1 ✅ — weight separates it from
                                       the lead, the same pair as .plant */
}

.wpc__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-lg) var(--space-sm);
  margin: var(--space-2xl) 0 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 769px) {
  .wpc__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

.wpc__item {
  margin: 0;
  overflow: hidden;   /* clip guard, same as .project__figure */
}

/* Square, and the reasoning is written out in the markup. height:auto is
   load-bearing — without it the width/height attributes are taken literally
   and aspect-ratio is ignored entirely. */
.wpc__shot {
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border: var(--border-rule);
  filter: var(--filter-photo);
}

.wpc__caption {
  margin: var(--space-2xs) 0 0;
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  line-height: 1.45;
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.wpc__divider {
  margin-top: var(--space-3xl);
}

/* =========================================================================
   AVAILABLE PAN INDIA

   One claim and a placeholder. The section is short on purpose — it exists to
   answer "will you come to us", which is a one-line question.

   THE FRANCHISE BLOCK SHIPS `hidden` AND STYLED. Those two are not in tension:
   the styling is written now so that revealing the block later is a matter of
   deleting one attribute and dropping in the copy, rather than designing a
   component against a deadline. The bordered panel is the only boxed element
   on the black ground, which is what marks it as a distinct offer rather than
   another paragraph.
   ========================================================================= */

.reach {
  padding-block: var(--space-3xl) 0;
}

.reach__lead {
  margin: 0;
  max-width: 46ch;
  font-weight: var(--font-body-weight-strong);
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

/* Belt and braces on the UA default, the same guard .enquiry already carries:
   any later display rule on this class would otherwise beat `hidden` and put
   an empty panel on screen. */
.reach__franchise[hidden] { display: none; }

.reach__franchise {
  margin-top: var(--space-2xl);
  padding: var(--space-lg);
  max-width: 34rem;
  border: var(--border-rule);
}

.reach__franchise-title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--font-display-weight);
  font-stretch: var(--font-display-stretch);
  font-variation-settings: "wdth" 125;
  font-size: var(--size-h3);
  line-height: var(--leading-tight);
  letter-spacing: -0.01em;
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.reach__franchise-copy {
  margin: var(--space-md) 0 0;
  max-width: var(--measure);
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.reach__franchise-contact {
  margin: var(--space-md) 0 0;
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.reach__divider {
  margin-top: var(--space-3xl);
}

/* =========================================================================
   PROJECTS — the light band

   Seven rows, alternating sides. Orientation is declared per row rather than
   forced: --portrait rows carry a 3:4 image, the rest 3:2. Nothing is ever
   cropped from one into the other, with one documented exception —
   see .project__image--tall below.

   INVERTED 2026-07-31, and the inversion is the whole point of the section.
   On the old light page this was the dark band — the one tonal event in the
   document. When the page ground went black that band stopped existing: it
   merged into the ground, and the page lost its structural rhythm entirely.
   So it flips. This is now the one LIGHT section in a black page, and it does
   exactly the job it always did, in reverse.

   It is also the only place the original palette still runs, unchanged and
   still correct, because this ground never moved:
     Mill Finish  the ground        Anthracite   the text, 12.53:1
     Zinc         rules and borders, 1.69:1 — the weight it was drawn at
     Corten Text  would be legal here at 6.26:1, but nothing needs it
   Every token below is overridden LOCALLY rather than by touching :root, so
   the section carries its own light-ground vocabulary and the rest of the
   page is untouched by it.

   The six largest photographs on the site live here, which is the second
   reason to invert rather than abandon the band: their skies meet a light
   ground instead of hitting black at a hard edge.
   ========================================================================= */

.projects {
  /* Light surface, so the focus ring's offset gap flips back to the dark
     tone — the inverse of what this section used to declare, and the reason
     the override has to stay rather than being deleted as redundant. */
  --focus-gap: var(--color-anthracite);

  /* Zinc is right again here, and only here. The page-wide --border-rule now
     resolves to --color-rule (#3E3B35), which on Mill Finish is invisible;
     Zinc on Mill Finish is the 1.69:1 hairline these photographs were framed
     with from the start. Same for the dot-rule. */
  --border-rule: 1px solid var(--color-zinc);
  --dot-color: var(--color-zinc);

  /* The original filter, unchanged, because this is the ground it was tuned
     against. The global value now carries a brightness trim for the black
     ground; applying that here would only make six photographs muddy on a
     surface that never needed it. */
  --filter-photo: saturate(0.85) contrast(1.06);

  /* Full bleed by being a block-level section with a background — no negative
     margins, no transforms, so it meets the black sections above and below on
     an exact pixel with no seam. Nothing here sets a border-radius. */
  background: var(--color-mill-finish);
  color: var(--color-anthracite);
  padding-block: var(--space-3xl) var(--space-2xl);
}

.projects__list {
  display: grid;
  gap: var(--space-3xl);
  margin: 0;
  padding: 0;
  list-style: none;
}

/* THE HEIGHT CAP.
   A project has to fit one screen — image and caption together — or the top
   and bottom of the photograph fall off the viewport at once and nobody ever
   sees a whole one. The cap is 78vh.

   It is written as a max-WIDTH because width is what the layout measures: the
   ratio is fixed, so a 3:4 frame 78vh tall is 58.5vh wide and a 3:2 frame is
   117vh wide. Capping the width caps the height and keeps the ratio exactly —
   the image just gets smaller, nothing is ever squashed or re-cropped.

   Deliberately no percentage inside the min(): these values are read during
   grid track sizing, where a percentage would be indefinite. Both arms are
   absolute lengths so the track resolves deterministically. */
.project__figure {
  margin: 0 auto;   /* auto keeps a capped frame centred while rows are stacked */
  max-width: calc(78vh * 3 / 2);
  overflow: hidden; /* clip guard — see the note on .project__image below */
}

.projects__item--portrait .project__figure {
  max-width: calc(78vh * 3 / 4);
}

/* height: auto ADDED 2026-08-11. These seven <img> elements carry no width or
   height attributes today, so aspect-ratio governs and nothing overflows — but
   every image added to this page in the last batch DOES carry them (they are
   wanted, they reserve the box before load and stop layout shift). The moment
   one is added here without this line, the height attribute wins over
   aspect-ratio and a 1713px-tall source lays out at 1713px inside a row sized
   for 3:4. The rest of the section already assumes it cannot happen. */
.project__image {
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border: var(--border-rule);   /* Zinc, 7.41:1 on this ground — a machined edge */
  filter: var(--filter-photo);
}

/* The image is shown at its own proportions, so cover crops nothing. */
.projects__item--portrait .project__image {
  aspect-ratio: 3 / 4;
}

/* THE ONE EXCEPTION, ADDED 2026-08-11 FOR proj-fancy.jpg. That source is
   918x1713 — 0.54, where the portrait frame is 0.75 — so it is the only
   photograph in this section that cover actually crops, by 28% of its height.
   The crop is aimed rather than centred: 40% keeps the gold frame's top edge
   with sky above it and spends the loss on the forecourt below, both of which
   are outside the façade. At the default 50% the top edge of the frame lands
   exactly on the crop line.

   DO NOT GENERALISE THIS. Every other row is shown at its own proportions and
   the section's rule — never crop one shape into another — still stands; this
   is a single source that arrived in neither shape. */
.project__image--tall {
  object-position: 50% 40%;
}


/* Three short lines. Past about this measure the caption stops being a label
   and starts being a column, which is what pulled it away from the image. */
.project__meta {
  margin-top: var(--space-md);
  max-width: 24rem;
}

.project__name {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--font-display-weight);
  font-stretch: var(--font-display-stretch);
  font-variation-settings: "wdth" 125;
  font-size: var(--size-h3);
  line-height: var(--leading-tight);
  letter-spacing: -0.01em;
  color: var(--color-anthracite);   /* light band — 12.53:1 */
}

/* Was Graphite. On Anthracite that is 2.05:1, so it is Mill Finish now and
   the step down from the name is carried by size (13px against 30px), by the
   mono face and by the 0.12em tracking instead of by tone. */
.project__location {
  margin: var(--space-xs) 0 0;
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
  color: var(--color-anthracite);   /* light band — 12.53:1 */
}

/* Was the section's one Corten mark, matching the "+" in Numbers. Corten Text
   is 2.00:1 on Anthracite — the worst failure of the three — so the separator
   now takes the surrounding colour and the section carries no accent at all.
   The class is kept so the markup does not have to change; if the accent is
   wanted back it needs a token that is legible on dark. */
.project__sep {
  color: inherit;
}

.project__system {
  margin: var(--space-2xs) 0 0;
  color: var(--color-anthracite);   /* light band — 12.53:1 */
}

/* The descriptive line. Added 2026-08-03 because the meta column was a name,
   a location and a four-word system label against a photograph three times its
   height — the column read as unfinished rather than as restrained.

   IT SITS AT BODY SIZE, NOT A STEP DOWN. There is no token between --size-body
   (18px) and --size-data (13px), and inventing one for a single element is how
   type scales rot. Hierarchy here comes from the family and the rhythm — the
   name is display, the location is mono caps, the system is a short label, and
   this is the only element in the block that runs as sentences. --space-sm
   above it is doing the separating that a size change would otherwise do.

   Colour stays --color-anthracite, like everything else in the block. A muted
   tier would be the obvious move and it is deliberately not taken: the muted
   tier is off across the whole page pending a judgement in the browser, and a
   single section quietly opting in would pre-empt that. */
.project__line {
  margin: var(--space-sm) 0 0;
  line-height: var(--leading-body);
  color: var(--color-anthracite);   /* light band — 12.53:1 */
}

/* Tightened from --space-3xl. The band's own bottom edge is now doing most of
   the dividing work, so the rule sits closer to it rather than floating in
   the middle of a large dark gap. */
.projects__divider {
  margin-top: var(--space-2xl);
}

@media (min-width: 769px) {

  /* The row is a PAIR, not two columns.

     It used to be two fr tracks that between them ate the full container, so
     the image and the caption sat at opposite ends of the screen with the slack
     dumped in the middle. Now the image track is `max-content` — it takes
     exactly the width the height cap above allows and not a pixel more — the
     caption is held to a fixed measure beside it, and whatever is left over
     falls outside the pair, where justify-content puts it on one side.

     Track order matters: max-content is sized first, so when the viewport is
     tight it is the caption that gives way, never the photograph.

     Both figure caps also gain a rem ceiling here. On a tall desktop 78vh is
     large enough that an uncapped portrait frame would crowd the caption out. */
  .project__figure {
    margin-inline: 0;
    max-width: min(calc(78vh * 3 / 2), 44rem);
  }
  .projects__item--portrait .project__figure {
    max-width: min(calc(78vh * 3 / 4), 34rem);
  }

  .projects__item {
    display: grid;
    grid-template-columns: max-content minmax(0, 22rem);
    gap: var(--space-lg);
    align-items: center;
    justify-content: start;
  }

  /* Mirror every second row. The template flips too, otherwise the image would
     land in the caption track's width — and the pair moves to the other edge,
     so the alternation reads as an alternation rather than a twitch. */
  .projects__item:nth-child(even) {
    grid-template-columns: minmax(0, 22rem) max-content;
    justify-content: end;
  }
  .projects__item:nth-child(even) .project__figure {
    grid-column: 2;
    grid-row: 1;
  }
  .projects__item:nth-child(even) .project__meta {
    grid-column: 1;
    grid-row: 1;
  }

  /* Centred against the frame. Bottom-aligning put the name on the very foot
     of a 3:4 image, which on a short viewport is the part that scrolls off. */
  .project__meta {
    margin-top: 0;
  }
}

/* =========================================================================
   INSIDE THE PLANT

   The claim first, then the proof. The image is the only one on the page that
   breaks the container — every other section is framed, so going edge to edge
   here is what marks this as the differentiator rather than any extra styling.
   No Corten in this section; the photograph is the accent.
   ========================================================================= */

.plant {
  padding-block: var(--space-3xl) 0;
}

.plant__lead {
  margin: 0;
  max-width: 32ch;
  font-weight: var(--font-body-weight-strong);
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.plant__line {
  margin: var(--space-2xs) 0 0;
  max-width: 42ch;
  color: var(--color-ink);          /* 15.99:1 ✅ — weight still separates
                                       this from the lead above it */
}

/* Heading anchors left, copy sits beside it. Stacked below 769px. */
@media (min-width: 769px) {
  .plant__intro {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.2fr);
    gap: var(--space-2xl);
    align-items: start;
  }
  .plant__intro .section-title { margin-bottom: 0; }
}

/* THE GRID. Two columns by default, three from 769px. At the container's
   1216px that is ~390px a cell, which sits inside every source in the set —
   the largest is 568px wide — so no photograph is ever scaled up.

   Square cells, because the sources are three 1:1 and three ~1.13 landscape:
   a square costs the landscape ones ~11% off the sides and the square ones
   nothing, where a 4:3 frame would have taken 25% off the top and bottom of
   half the set. auto-fit is not used deliberately — six frames in a fixed
   rhythm read as a contact sheet, which is the point; auto-fit would reflow
   them into an uneven last row at some widths. */
.plant__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-sm);
  margin: var(--space-2xl) 0 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 769px) {
  .plant__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

/* The plant cells are bare <li> with the image straight inside — no figure to
   hang the clip on, so it goes here. Same guard as .project__figure. */
.plant__grid > li {
  overflow: hidden;
}

.plant__shot {
  width: 100%;
  height: auto;              /* the width/height attributes are hints; without
                                this the 501 would be taken literally and
                                aspect-ratio ignored entirely */
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border: var(--border-rule);
  filter: var(--filter-photo);
}

/* Retained: assets/factory-floor.webp and the full-bleed rules it used are
   gone from the markup, but the photograph is still on disk. If the wide shot
   comes back as a lead above the grid, .plant__figure / .plant__image below
   are what it needs. */
.plant__figure {
  margin: var(--space-2xl) 0 0;
}

/* FULL BLEED, 21:9. The width is the viewport and stays the viewport — the
   comment on the figure in the markup is the design decision and it holds.

   The height problem is solved by the BAND, not by the width. A 16:9 frame at
   full bleed is 0.5625 x the viewport width, which on a 1440x800 screen is
   810px — taller than the window on its own, so the caption could never share
   the screen with it. Widening the band to 21:9 makes the same full-width
   frame 0.4286 x the viewport width: 617px at 1440, and the caption comes back
   on screen without the photograph giving up a single pixel of width.

   The source is 2400x1350 and is NOT re-cut. object-fit trims the band out of
   it: at a true 21:9 the frame keeps 76.2% of the source height, 321.4px in
   all. object-position 50% 55% spends that unevenly — 176.8px off the TOP and
   144.6px off the bottom — because the roof was taking about 45% of the frame
   and the floor is what this section is arguing. 55% is the settled value: it
   is far enough off centre to buy real floor, and still shallow enough that
   the gantry walkway keeps just over half its span, so the workers, the
   trestles, the racked stock and the walkway are all inside the kept band.
   SETTLED — do not tune this further.

   THE BACKSTOP — 88vh. On an ultrawide, 21:9 of the viewport width is taller
   than the viewport itself (2560x1080 wants 1097px of a 1080px window), and on
   a tall narrow window the band is generous enough to be worth capping too.
   88vh reserves the remaining 12% of the viewport, which clears the caption
   block (12px margin + one 19.5px line = 31.5px) on any window over ~262px
   tall — so the caption is on screen with the image at every size, always.

   In aspect terms the backstop engages on any viewport wider than 2.053:1
   (0.88 x 21/9). Below that the band is a true 21:9 and the crop is the 160.7px
   above; past it the frame gets shorter and object-fit takes more.

   height:auto is load-bearing: it overrides the height="1350" presentational
   hint, without which aspect-ratio would be ignored entirely — it only applies
   when one axis is indefinite — and the image would lay out 1440 x 1350.

   Below 600px the rule further down flips the band to 4:3 for phones. The
   backstop does not bite at those widths, so that behaviour is unchanged. */
.plant__image {
  width: 100%;
  height: auto;
  max-height: 88vh;
  aspect-ratio: 21 / 9;
  object-fit: cover;
  object-position: 50% 55%;   /* weight the crop off the roof and onto the floor */
  filter: var(--filter-photo);
}

/* Portrait-ish on a phone, so a wide floor shot still has presence. */
@media (max-width: 600px) {
  .plant__image { aspect-ratio: 4 / 3; }
}

.plant__caption {
  margin-top: var(--space-xs);
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.plant__divider {
  margin-top: var(--space-3xl);
}

/* =========================================================================
   OUR PEOPLE

   Three group photographs on the same square cells as the plant grid directly
   above, then the owner block. Two grids, one contact sheet.

   THREE ITEMS, SO NO ORPHAN IS ALLOWED. A two-column grid would leave the
   third photograph alone on a row at every width between 420px and 769px,
   which reads as a missing fourth. It is one column until there is room for
   all three, and then it is three. Nothing in between.
   ========================================================================= */

.people {
  padding-block: var(--space-3xl) 0;
}

.people__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: var(--space-lg);
  margin: 0;
  padding: 0;
  list-style: none;
}

@media (min-width: 700px) {
  .people__grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: var(--space-lg) var(--space-sm);
  }
}

.people__item {
  margin: 0;
  overflow: hidden;   /* clip guard, same as .project__figure */
}

.people__shot {
  width: 100%;
  height: auto;
  aspect-ratio: 1 / 1;
  object-fit: cover;
  border: var(--border-rule);
  filter: var(--filter-photo);
}

.people__caption {
  margin: var(--space-2xs) 0 0;
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  line-height: 1.45;
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

/* -------------------------------------------------------------------------
   THE OWNER BLOCK

   Portrait beside the words, stacked below 769px. The portrait track is
   max-content so the frame takes exactly the width its own cap allows and the
   text column gives way first when the viewport tightens — the same pairing
   rule Projects settled on, for the same reason.
   ------------------------------------------------------------------------- */
.owner {
  display: grid;
  gap: var(--space-lg);
  margin-top: var(--space-3xl);
}

@media (min-width: 769px) {
  .owner {
    grid-template-columns: max-content minmax(0, 34rem);
    gap: var(--space-2xl);
    align-items: start;
    justify-content: start;
  }
}

.owner__figure {
  margin: 0;
  max-width: 22rem;
  overflow: hidden;   /* clip guard, same as .project__figure */
}

/* 4:5 AGAINST A 943x1146 SOURCE — see the note in the markup. That is 0.8
   against 0.823, so cover takes 3% off the width and nothing off the height,
   and the face is untouched. object-position is centred BECAUSE the crop is
   this small: at 3% there is no direction worth biasing towards, and a bias
   would only start moving a face that currently is not being moved at all. */
.owner__portrait {
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 5;
  object-fit: cover;
  object-position: 50% 50%;
  border: var(--border-rule);
  filter: var(--filter-photo);
}

.owner__name {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--font-display-weight);
  font-stretch: var(--font-display-stretch);
  font-variation-settings: "wdth" 125;
  font-size: var(--size-h3);
  line-height: var(--leading-tight);
  letter-spacing: -0.01em;
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.owner__role {
  margin: var(--space-xs) 0 0;
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

/* THE ONE GOLD MARK IN THIS SECTION, and it is a mark rather than a fill —
   a 2px rule beside a quotation, shown once on the page. That is the accent
   job the 2026-07-31 gold rule reserves for gold ("single glyphs, single
   marks"), and it is the same argument that puts gold on the Systems leader
   rule. THE QUOTED TEXT ITSELF IS INK. Gold on a three-line quotation would
   be gold carrying running text, which the rule refuses outright. */
.owner__quote {
  margin: var(--space-md) 0 0;
  padding-left: var(--space-md);
  border-left: 2px solid var(--color-gold);
}

.owner__quote p {
  margin: 0;
  max-width: var(--measure);
  font-weight: var(--font-body-weight-strong);
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.owner__line {
  margin: var(--space-md) 0 0;
  max-width: var(--measure);
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.people__divider {
  margin-top: var(--space-3xl);
}

/* =========================================================================
   SKETCH TO METAL — the signature

   Both panes are the same box at the same size with object-fit: cover, and the
   drawing is revealed by clip-path rather than by resizing. Nothing scales, so
   the two images stay in register at every position.
   ========================================================================= */

.compare-section {
  padding-block: var(--space-3xl);
}

/* THE CAP LIVES ON THE FIGURE, NOT THE FRAME.

   It used to be `max-height: 82svh` on the frame. That clamps height without
   clamping width, so the box just flattened back into a wide rectangle and the
   crop came straight back — at 1440x800 a 3:2 frame was really 1088 x 656,
   1.659:1, which took 318px off the top and 318px off the bottom of a square
   source. Capping the WIDTH caps the height too while the ratio is fixed, so
   the frame gets smaller instead of wider and nothing is ever re-cropped.

   82svh is the original number and the original reasoning: it leaves 18% of the
   viewport for the caption below, which needs about 36px. Putting the cap here
   rather than on the frame also keeps `Drag to compare` aligned to the frame's
   left edge instead of stranding it at the container edge. */
.compare {
  margin: 0 auto;
  /* 15px of mount rings paint outside the frame's border box. The padding is
     what gives them room, so they never crowd the caption or the section edge.
     max-width is border-box, so the frame itself gives up 32px — which the
     82svh reasoning above can absorb, since it was reserving 18% for a
     caption that needs about 36px. */
  padding: var(--space-sm);
  max-width: 82svh;
}

/* SQUARE, because both source images are square and both are composed edge to
   edge. The drawing's ink bounding box is (0, 0, 1600, 1584): row 0 is a
   full-width rule, there is no margin at all on the left or right, and 16px at
   the foot. Any ratio wider than 1:1 eats the head dimension chain at the top
   and the title block at the bottom. There is no crop to spend here. */
/* THE SHEET TREATMENT. The drawing stays on white paper — that was ruled on
   and it is the point of the section: a drawing should look like a drawing.
   On a black ground an unframed white square reads as a hole punched through
   the page rather than as a sheet lying on it, so it gets a mount.

   Done with box-shadow spread rather than padding or a wrapper, deliberately.
   Both panes are position:absolute with inset:0, and an absolutely positioned
   child resolves against its ancestor's PADDING box — so padding here would
   not create a visible mat at all, the images would simply cover it. Spread
   shadows paint outside the border box and cannot be covered, and they change
   no layout, so the square, the clip-path and the seam are all untouched.

   Two rings: 14px of --color-panel as the mount board, then 1px of
   --color-rule as its cut edge. The panel is only 1.28:1 against the ground,
   which is exactly right — a mount should be felt rather than seen. */
.compare__frame {
  --pos: 50%;                    /* the seam. JS drives it; 50% is the JS-off state */
  position: relative;
  overflow: hidden;
  aspect-ratio: 1 / 1;
  background: var(--color-mill-finish);
  border: var(--border-rule);
  box-shadow:
    0 0 0 14px var(--color-panel),
    0 0 0 15px var(--color-rule);
}


.compare__pane {
  position: absolute;
  inset: 0;
}

.compare__pane--built   { z-index: 1; }
.compare__pane--drawing { z-index: 2; clip-path: inset(0 calc(100% - var(--pos)) 0 0); }

.compare__image {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The photo takes the one global filter. The drawing does not — it is a
   drawing, not a photograph. */
.compare__image--photo {
  filter: var(--filter-photo);
}

/* Shade under the BUILT label only, so mono stays legible on a bright
   photograph. Same device as the top bar. */
.compare__pane--built::after {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  height: 110px;
  background: linear-gradient(to bottom,
    rgba(38, 36, 31, 0.55),
    rgba(38, 36, 31, 0));
  pointer-events: none;
}

/* Labels live inside their own pane, so each one is clipped away with the
   image it names — drag past a corner and its label is wiped with it. */
.compare__label {
  position: absolute;
  top: 0;
  z-index: 1;
  margin: 0;
  padding: var(--space-md);
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  line-height: 1;
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
  white-space: nowrap;
  pointer-events: none;
}

.compare__label--drawing { left: 0;  color: var(--color-anthracite); }
.compare__label--built   { right: 0; color: var(--color-mill-finish); }

/* -------------------------------------------------------------------------
   THE HANDLE — an aluminium extrusion, not a circle.

   Body: a cylindrical shading ramp across the face (dark edge -> zinc ->
   mill highlight off-centre -> zinc -> dark edge) with a fine vertical
   striation over it for the brushed grain. Every colour is a palette token;
   the rgba() values are Anthracite at low alpha, as used in the hero scrim.
   ------------------------------------------------------------------------- */

.compare__handle {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--pos);
  z-index: 3;
  width: 14px;
  transform: translateX(-50%);
  pointer-events: none;          /* the input above it takes every gesture */
  background-image:
    /* brushed grain */
    repeating-linear-gradient(90deg,
      rgba(38, 36, 31, 0) 0 1px,
      rgba(38, 36, 31, 0.06) 1px 2px),
    /* extrusion face */
    linear-gradient(90deg,
      var(--color-graphite) 0%,
      var(--color-zinc) 18%,
      var(--color-mill-finish) 44%,
      var(--color-zinc) 68%,
      var(--color-graphite) 100%);
  box-shadow:
    0 0 0 1px rgba(38, 36, 31, 0.30),
    0 0 22px rgba(38, 36, 31, 0.40);
}

/* The hairline Corten seam down the reveal edge — the joint where the new
   material begins. 1px, and the only warm mark in the section. */
.compare__handle::after {
  content: "";
  position: absolute;
  inset: 0 0 0 auto;
  width: 1px;
  /* The seam down the extrusion's reveal edge. Gold now — one hairline on one
     object, which is the accent job. It sits over the images rather than the
     ground, so this is a palette consistency change, not a contrast fix. */
  background: var(--color-gold);
}

/* Knurled grip — a machined cross-hatch on a section that stands slightly
   proud of the bar, the way a real grip section does. */
.compare__grip {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 20px;
  height: 76px;
  transform: translate(-50%, -50%);
  background-image:
    repeating-linear-gradient(45deg,
      rgba(38, 36, 31, 0.30) 0 1px, transparent 1px 5px),
    repeating-linear-gradient(-45deg,
      rgba(38, 36, 31, 0.30) 0 1px, transparent 1px 5px),
    linear-gradient(90deg,
      var(--color-graphite) 0%,
      var(--color-zinc) 20%,
      var(--color-mill-finish) 46%,
      var(--color-zinc) 70%,
      var(--color-graphite) 100%);
  border-block: 1px solid rgba(38, 36, 31, 0.45);
  box-shadow: 0 0 0 1px rgba(38, 36, 31, 0.22);
}

/* -------------------------------------------------------------------------
   THE CONTROL
   ------------------------------------------------------------------------- */

.compare__input {
  position: absolute;
  inset: 0;
  z-index: 4;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  appearance: none;
  -webkit-appearance: none;
  background: none;
  border: 0;
  opacity: 0;                    /* invisible: the extrusion is the visible handle */
  cursor: ew-resize;
  touch-action: pan-y;           /* horizontal is ours, vertical still scrolls */
}

.compare__input::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 44px;
  height: 44px;
}
.compare__input::-moz-range-thumb {
  width: 44px;
  height: 44px;
  border: 0;
}

/* The input is transparent, so its own focus ring would be too. script.js
   flags keyboard focus and the ring is drawn on the extrusion instead —
   Weld, with the 2px gap in Anthracite against the light metal. */
.compare.is-focused .compare__handle {
  outline: var(--focus-ring-width) solid var(--color-weld);
  outline-offset: var(--focus-ring-offset);
  box-shadow: 0 0 0 var(--focus-ring-offset) var(--focus-gap);
}

/* The same mono caption treatment as .plant__caption and .systems__caption —
   identical tokens, nothing new invented. It sits tight to the frame at
   --space-xs, the way the plant caption does, because it belongs to the image;
   the hint below keeps its looser --space-sm, so the two mono lines read as
   caption-then-instruction rather than one block of two. */
.compare__project {
  margin: var(--space-xs) 0 0;
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.compare__hint {
  margin: var(--space-sm) 0 0;
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

/* =========================================================================
   HOW IT WORKS

   A ledger, not a timeline. Six numbered rows, mono index on the left,
   everything hanging off one baseline — the way steps are listed down the
   margin of a shop drawing.

   Three things deliberately absent:
   · No connecting line, no arrows, no dots between steps. The numbers already
     say "sequence"; a spine would say it twice and would be the generic
     process-timeline component.
   · No rules between rows. DESIGN-PLAN replaced hairlines with the dot-rule,
     and the dot-rule is the section divider — using it six more times inside
     one section would spend the motif.
   · Corten on 06 and nowhere else. One warm glyph per section, the same rule
     the "+" in Numbers and the "·" in Projects follow — and here it means
     something: the sequence ends. Accenting all six numerals would be a
     colour scheme, not an accent.

   Both column widths are fixed rather than auto because every .step is its
   own grid — with auto tracks the rows would size independently and the
   numbers would not line up down the page.
   ========================================================================= */

.steps {
  padding-block: var(--space-3xl) 0;
}

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

.step {
  display: grid;
  grid-template-columns: 2.25rem minmax(0, 1fr);
  column-gap: var(--space-md);
  row-gap: var(--space-3xs);
  align-items: baseline;
}

/* Mono index. Graphite because it is text — Zinc fails at 1.69:1 and is for
   borders and rules only. Tabular figures so the column reads straight. */
.step__num {
  grid-column: 1;
  grid-row: 1;
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  letter-spacing: var(--tracking-data);
  font-variant-numeric: lining-nums tabular-nums;
  color: var(--color-ink);          /* 15.99:1 on the ground ✅ */
}

/* The last number, and the only warm mark in the section. Now the brand gold
   — a glyph and an accent, which is the job the 2026-07-31 ruling gives gold.
   Corten Text would read 2.00:1 here. Note this is the section's ONLY gold:
   accenting all six numerals would be a colour scheme, not an accent, and
   that reasoning is unchanged by the palette. */
.step__num--end {
  color: var(--color-gold);         /* 8.58:1 on the ground ✅ */
}

.step__title {
  grid-column: 2;
  grid-row: 1;
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--font-display-weight);
  font-stretch: var(--font-display-stretch);
  font-variation-settings: "wdth" 125;
  font-size: var(--size-h3);
  line-height: var(--leading-tight);
  letter-spacing: -0.01em;
  color: var(--color-ink);          /* 15.99:1 on the ground ✅ */
}

/* Both the title above and this line are now the same tone, where they were
   Anthracite over Graphite. See the note on .stat__label — same trade, same
   fallback available (--color-muted, 5.97:1) if the ledger flattens. */
.step__line {
  grid-column: 2;
  grid-row: 2;
  margin: 0;
  color: var(--color-ink);          /* 15.99:1 on the ground ✅ */
}

.steps__divider {
  margin-top: var(--space-3xl);
}

/* Desktop pulls the line up onto the title's baseline, so a step is one line
   of the ledger rather than a stacked block. */
@media (min-width: 769px) {

  .steps__list {
    gap: var(--space-xl);
  }

  .step {
    grid-template-columns: 2.25rem 16rem minmax(0, 1fr);
    column-gap: var(--space-xl);
  }

  .step__line {
    grid-column: 3;
    grid-row: 1;
  }
}

/* =========================================================================
   ANSWERS

   No accordion, no cards, no borders — see the note in index.html. Two
   columns of open question/answer pairs on desktop, one below 769px. The
   only thing distinguishing a question from an answer is weight and tone,
   which is the same pair the plant section uses (semibold Anthracite over
   Graphite) and enough at this length.

   Questions are body semibold, not the display face and not --size-h3: the
   six step titles directly above are already at that size, and matching them
   would make two adjacent sections read as one long list.
   ========================================================================= */

.answers {
  padding-block: var(--space-3xl) 0;
}

.answers__list {
  display: grid;
  gap: var(--space-xl);
  margin: 0;
}

.answer__q {
  margin: 0;
  font-weight: var(--font-body-weight-strong);
  color: var(--color-ink);          /* 15.99:1 on the ground ✅ */
}

/* This was the worst contrast failure on the page between step 1 and now:
   Graphite body copy on black is 2.61:1, and this is the longest run of
   running text in the document. Weight alone now separates question from
   answer — semibold against regular — which is the same distinction the
   plant section uses and is enough at this length. */
.answer__a {
  margin: var(--space-2xs) 0 0;   /* the zeros matter — <dd> ships with a 40px indent */
  max-width: 46ch;
  color: var(--color-ink);          /* 15.99:1 on the ground ✅ */
}

/* The section's one accent, and the only inline link on the page so far.
   Corten Text is the accent-on-text token (≥4.5:1 on Mill Finish); the
   underline stays because colour alone must never be the only cue. */
/* REGROUPED, as the split comment said to do once sections 9 and 10 converted.
   All three links are on the dark ground now, so all three take the gold and
   the fork is gone. The underline stays because colour alone must never be
   the only cue that something is a link. */
.answer__link,
.enquiry__direct a,
.footer__line a {
  color: var(--color-gold);         /* 8.58:1 ✅ */
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}

.answer__link:hover,
.enquiry__direct a:hover,
.footer__line a:hover {
  text-decoration-thickness: 2px;
}

.answers__divider {
  margin-top: var(--space-3xl);
}

@media (min-width: 769px) {
  .answers__list {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--space-2xl);
    row-gap: var(--space-xl);
  }
}

/* =========================================================================
   SEND DRAWINGS

   No boxed panel, no card, no shaded background — the form sits on the page
   ground like everything else. The only structure is the Zinc rule around
   each control, which is Zinc doing exactly the job the plan reserves for it.

   Labels are mono uppercase, the same treatment as the stat labels and the
   project captions, because a form on this page should read as fields on a
   requisition slip. They are real <label for> elements, never placeholders:
   placeholder text vanishes the moment you type, which is precisely when
   somebody filling in eight fields needs it.
   ========================================================================= */

.enquiry {
  padding-block: var(--space-3xl) 0;
}

/* The success panel and the error line are toggled with the hidden attribute.
   Author display rules outrank the UA one, so this keeps hidden meaning
   hidden no matter what is declared below. */
.enquiry [hidden] {
  display: none !important;
}

.enquiry__lead {
  margin: 0;
  max-width: 42ch;
  font-weight: var(--font-body-weight-strong);
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.enquiry__direct {
  margin: var(--space-2xs) 0 0;
  max-width: 46ch;
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.form {
  display: grid;
  gap: var(--space-lg);
  max-width: 52rem;             /* a form the full 76rem wide is a chore */
  margin-top: var(--space-2xl);
}

.field__label {
  display: block;
  margin-bottom: var(--space-2xs);
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.field__control {
  width: 100%;
  padding: var(--space-xs) var(--space-sm);
  background: transparent;
  color: var(--color-ink);          /* 15.99:1 ✅ */
  /* NOT --border-rule. A form control's boundary is a UI component under
     WCAG 1.4.11 and owes 3:1; --color-rule is 1.77:1 and would fail, which is
     fine for a decorative divider and not fine for the edge of an input a
     visitor has to find. --color-muted is 5.97:1 and clears it with room. */
  border: 1px solid var(--color-muted);
  border-radius: 0;             /* stamped, not rounded, like the buttons */
  font-family: var(--font-body);
  font-size: var(--size-body);
  line-height: 1.4;
  appearance: none;
  -webkit-appearance: none;
  transition: border-color var(--duration-fast) var(--ease-out);
}

/* 16px minimum on the controls or iOS zooms the page on focus. --size-body is
   already 18px, so this is only a guard against the scale being lowered. */
@media (max-width: 768px) {
  .field__control { font-size: max(1rem, var(--size-body)); }
}

.field__control:hover {
  border-color: var(--color-ink);
}

.field__control--area {
  min-height: 7rem;
  resize: vertical;
  line-height: var(--leading-body);
}

/* appearance:none strips the native arrow, so one is drawn back. A solid
   triangle rather than a chevron — flatter, closer to a drawing symbol.
   Ink (#E8E7E3) is url-encoded as %23E8E7E3. It was Graphite, which on the
   dark ground would have left the only affordance telling you this is a
   dropdown sitting at 2.61:1. A data URI cannot read a custom property, so
   this hex has to be maintained by hand — if --color-ink ever changes, this
   changes with it. */
.field__control--select {
  padding-right: var(--space-xl);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M0 0h10L5 6z' fill='%23E8E7E3'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right var(--space-sm) center;
  background-size: 10px 6px;
}

/* THE OPEN DROPDOWN. The closed control was always fine — it is styled above
   and inherits --color-ink on the page ground. The list that drops out of it
   was not: on Windows, Chromium paints the native popup with the OS palette
   unless the OPTION elements themselves carry explicit colours. Inheritance
   does not reach it, and neither does anything set on the <select>. The result
   was a white system popup with near-invisible grey rows.

   So every value here is set twice — once on the control, once on the options.
   The duplication is the fix, not an oversight.

   color-scheme is the other half. It tells the engine to render this control's
   native furniture (popup chrome, scrollbar inside a long list) dark, which is
   the part no author property can reach. Scoped to the selects rather than
   :root on purpose: at :root it would also repaint the page scrollbar and
   every other native control on the page, which is not what was asked for.

   Closed-state guarantee: --color-ground is the exact colour already behind
   these controls (.enquiry, .form and .field all set no background, so they
   sit on the body ground). Naming it explicitly is pixel-identical to the
   `background: transparent` it overrides — and it must be background-COLOR,
   not the shorthand, or it would wipe the arrow set immediately above. */
select.field__control {
  color-scheme: dark;
  background-color: var(--color-ground);
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

select.field__control option {
  background-color: var(--color-ground);
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

/* The placeholder row ("Choose one", "No date yet") reads as a placeholder
   without dropping out of sight: --color-muted is 5.97:1 on the ground, which
   still clears AA. Not --color-rule — that is 1.77:1 and is borders only. */
select.field__control option:disabled {
  color: var(--color-muted);        /* 5.97:1 ✅ */
}

/* Highlighted and currently-selected rows lift to --color-panel rather than
   inverting, so the row that is picked out is still Ink on a dark surface —
   14.14:1, measured, not estimated.

   How much of this lands is engine-dependent: Chromium on Windows honours
   :checked in the popup but overrides :hover with the OS highlight, and the
   popup is not a document the page can style beyond this. That is why the
   BASE option rule above carries the readability guarantee on its own — if
   every rule in this block is ignored, the list is still Ink on ground. */
select.field__control option:checked,
select.field__control option:hover,
select.field__control option:focus {
  background-color: var(--color-panel);
  color: var(--color-ink);          /* 14.14:1 on --color-panel ✅ */
}

/* AUTOFILL. The failure this fixes never appears in our own testing, because
   it needs a real visitor with saved details: Chrome and Safari paint
   autofilled controls with their own pale background and dark text, ignoring
   `background: transparent` entirely. On a black page that lands three pale
   blue-white blocks in the middle of the form — name, email, phone are three
   of the most reliably autofilled fields there are.

   You cannot override the autofill background directly; it is painted by the
   UA above the element's own background. The working technique is a very long
   inset box-shadow, which paints over that layer, plus -webkit-text-fill-color
   for the text, which likewise outranks `color` while autofilled.

   The transition delay is the other half of it. WebKit re-applies its yellow
   on some interactions; a long delay on background-color means the UA's paint
   never gets its moment on screen.

   :autofill is the standard selector, :-webkit-autofill the one that actually
   ships. Both are listed, in separate rules ON PURPOSE — a selector list is
   dropped whole by any engine that does not understand one of its parts, so
   grouping them would break this everywhere Firefox is involved. */
.field__control:-webkit-autofill,
.field__control:-webkit-autofill:hover,
.field__control:-webkit-autofill:focus {
  -webkit-box-shadow: 0 0 0 100px var(--color-ground) inset;
  box-shadow: 0 0 0 100px var(--color-ground) inset;
  -webkit-text-fill-color: var(--color-ink);
  caret-color: var(--color-ink);
  transition: background-color 100000s ease-in-out 0s;
}

.field__control:autofill,
.field__control:autofill:hover,
.field__control:autofill:focus {
  box-shadow: 0 0 0 100px var(--color-ground) inset;
  -webkit-text-fill-color: var(--color-ink);
  caret-color: var(--color-ink);
  transition: background-color 100000s ease-in-out 0s;
}

/* The inset shadow above would otherwise swallow the focus ring's offset gap,
   which is drawn with box-shadow too. Restated here so a focused autofilled
   field keeps both: the gap first, then the fill. */
.field__control:-webkit-autofill:focus-visible {
  box-shadow:
    0 0 0 var(--focus-ring-offset) var(--focus-gap),
    0 0 0 100px var(--color-ground) inset;
}

/* Native validation styling, but only after the browser has decided the field
   is genuinely wrong — :invalid alone would mark every empty required field
   red before the visitor has typed a character. */
.field__control:user-invalid {
  border-color: var(--color-weld);
}

/* Weld, not gold. This is the one place on the page where colour carries a
   state rather than a style, and gold is the accent everything else uses —
   an error line in the same colour as the links and the section's own
   flourishes would not read as an error at all. Weld is 4.34:1 on the ground,
   and it is already the page's alert tone via :user-invalid on the controls,
   so the border that turns and the line that appears now match. */
.form__error {
  margin: 0;
  color: var(--color-weld);         /* 4.34:1 ✅ */
}

/* Two actions now — the WhatsApp submit and the email link beside it — so
   this needs the same flex/gap the hero's action pair already uses. Copied
   from .hero__actions rather than invented, so the two pairs on the page sit
   the same way and wrap the same way at 360px.

   Nothing about the buttons themselves changed: with a single child this rule
   renders identically to the block layout it replaces, so the submit button
   has not moved. */
.form__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-md) var(--space-lg);
  margin-top: var(--space-2xs);
}

/* Was the one Corten rule on the page; now the one gold rule. A single 3px
   band marking the single moment worth marking — the confirmation. Same
   reasoning, new accent. */
.form__done {
  max-width: 52rem;
  margin-top: var(--space-2xl);
  padding-top: var(--space-lg);
  border-top: 3px solid var(--color-gold);
}

.form__done-title {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--font-display-weight);
  font-stretch: var(--font-display-stretch);
  font-variation-settings: "wdth" 125;
  font-size: var(--size-h3);
  line-height: var(--leading-tight);
  letter-spacing: -0.01em;
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.form__done-line {
  margin: var(--space-2xs) 0 0;
  max-width: 46ch;
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.enquiry__divider {
  margin-top: var(--space-3xl);
}

@media (min-width: 769px) {

  .form {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--space-lg);
    row-gap: var(--space-lg);
  }

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

/* =========================================================================
   FOOTER

   The question this comment used to raise has been answered. It argued that a
   dark footer needed a muted-light token before it could have any hierarchy,
   and called that a palette change it would not make unilaterally. The palette
   change happened on 2026-07-31 and --color-muted (#908D85, 5.97:1) now
   exists — but the client has deferred the muted tier to a per-section
   judgement made in the browser, so every line here is Ink for now and the
   hierarchy rests on size, weight and typeface. If any block on the page ends
   up wanting its second tone back, this footer is the strongest candidate:
   it has four levels of information stacked in a small space.

   No dot-rule here: the enquiry section already ends with one, and two
   perforation runs a few rem apart would read as an accident.

   No dot-rule here: the enquiry section already ends with one, and two
   perforation runs a few rem apart would read as an accident.
   ========================================================================= */

.footer {
  padding-block: var(--space-2xl) var(--space-3xl);
}

.footer__inner {
  display: grid;
  gap: var(--space-2xl);
}

.footer__wordmark {
  margin: 0;
  font-family: var(--font-display);
  font-weight: var(--font-display-weight);
  font-stretch: var(--font-display-stretch);
  font-variation-settings: "wdth" 125;
  font-size: var(--size-h3);
  line-height: var(--leading-tight);
  letter-spacing: -0.01em;
  color: var(--color-ink);          /* 15.99:1 ✅ — still Archivo, still the
                                       wordmark. The lockup's Didone was
                                       declined; only the palette moved. */
}

.footer__tagline {
  margin: var(--space-2xs) 0 0;
  max-width: 24ch;
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

/* <address> is the right element for contact details and the wrong default
   style — it ships italic in every browser. */
.footer__block {
  margin: 0;
  font-style: normal;
}

.footer__label {
  margin: 0;
  font-family: var(--font-mono);
  font-weight: var(--font-mono-weight);
  font-size: var(--size-data);
  letter-spacing: var(--tracking-data);
  text-transform: uppercase;
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

.footer__label--spaced {
  margin-top: var(--space-md);
}

.footer__line {
  margin: var(--space-3xs) 0 0;
  color: var(--color-ink);          /* 15.99:1 ✅ */
}

@media (min-width: 769px) {
  .footer__inner {
    grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr) minmax(0, 1fr);
    gap: var(--space-2xl);
    align-items: start;
  }
}
