/* ═══════════════════════════════════════════════════════════
   NB DEVELOPER — CORE
   Tokens, reset, layout and type. Nothing section-specific
   lives here. Every scene imports these and adds its own file.
   ═══════════════════════════════════════════════════════════ */

/* ── Reset ─────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
img, svg, video { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
ul, ol { list-style: none; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }

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


/* ── Fluid root ─────────────────────────────────────────
   1rem is a slice of the viewport, so the whole site scales
   proportionally instead of needing per-breakpoint retuning.
   Only the RATIO changes at a breakpoint, never the sizes. */
html {
  --fs-global: 0.5787037vw;        /* 1728px canvas → 1rem = 10px */
  font-size: var(--fs-global);

  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  overscroll-behavior-y: none;
  scrollbar-width: none;
}
html::-webkit-scrollbar { width: 0; }

/* A BAND NEEDS A CEILING AND A FLOOR.

   These ratios each describe one canvas: 1728 for desktop, 991
   for tablet, 393 for phones. A ratio only holds near the canvas
   it was measured on — run it to the far end of its band and it
   stops scaling and starts distorting.

   The phone ratio was the worse of the two: 2.5445293vw is 1rem =
   10px at 393, but the band runs to 767, where the same ratio
   gives 19.5px. Everything on a 600px screen rendered at half as
   big again as it was drawn. The cap holds the phone canvas once
   the viewport has outgrown it.

   The tablet ratio has the opposite failure — it shrinks toward
   the narrow end, and at 768 it reached 7.75px, which is where
   the header button ran out of room and broke onto three lines.
   The floor stops the canvas collapsing under its own contents. */
@media (min-width: 1921px) { html { --fs-global: 11.1px; } }
@media (max-width: 991px)  { html { --fs-global: max(1.0090817vw, 9px); } }
@media (max-width: 767px)  { html { --fs-global: min(2.5445293vw, 10.4px); } }


/* ── Tokens ─────────────────────────────────────────────
   A white site. The premium signal here is space, restraint and
   type — not colour — so the palette is deliberately almost
   monochrome, with ONE metal accent used sparingly enough that
   it still registers when it appears.

   The white is warm rather than pure: #ffffff on a large field
   reads clinical, and a few degrees of warmth reads as paper. */
:root {
  /* THE NIGHT GROUND. Named rather than written into the is-night
     rule below, because the footer has to START at this exact value
     to merge out of the section above it - and a second hex literal
     in footer.css would be a second place to change it. */
  --page-night: #0b111c;

  --paper-rgb: 251 250 247;  /* ONE OWNER for the ground colour. The seam over the
                                invitation needs it at partial alpha, and a second
                                hex literal there would be a second place to change
                                it. Everything else keeps using --paper. */
  --paper:     rgb(var(--paper-rgb));   /* the ground — warm white            */
  --paper-2:   #f2f0eb;   /* tonal band, section separation     */
  --ivory:     #ffffff;   /* lifted surfaces only               */

  /* Sampled from NB's own artwork: the navy from their marketing
     material, the gold straight out of the supplied logo. */
  --navy:      #1b2742;   /* dark surfaces, the menu            */
  --navy-2:    #26355a;
  --ink:       #131926;   /* text — a near-black with navy in it */
  --ink-2:     #2a3244;
  --ink-soft:  #4c5566;

  /* SAMPLED FROM THE LOGO, not chosen alongside it. These are the
     median values of the solid gold in assets/brand/nb-lockup.png,
     read band by band down the artwork — so the site's gold is the
     mark's own gold rather than a near-miss beside it, which is the
     one colour mismatch a client sees instantly.

     The mark is a metallic sweep: warm and light at the top, a
     touch deeper through the middle, warm again at the foot. --gold
     is that middle value, and --gold-grad below is the sweep
     itself, for anything large enough to show it. */
  --gold:      #d9b378;
  --gold-lift: #ebc486;
  --gold-deep: #daa666;
  /* The logo's own sweep, and it is a REAL range: the artwork runs
     from #daa666 in its deepest strokes to #ebc486 in its
     highlights, lightest at the left and deepening through the
     middle. The first attempt at this took a median per row, which
     averaged the sweep away and produced three stops within four
     points of each other — technically sampled from the logo and
     visually flat, which is the worst of both.

     Angled, because the logo's own gradient runs across the mark
     rather than down it. Use it anywhere gold covers enough area to
     show it: a pill, a display word, a large numeral. */
  --gold-grad: linear-gradient(105deg,
    #ecc588  0%,
    #dcae6d 38%,
    #d6a565 58%,
    #e5ba7c 100%);

  --accent:      var(--gold);
  --accent-lift: var(--gold-lift);

  /* Text roles per ground, so a section only declares which
     ground it sits on. */
  --on-light:        rgba(21,22,26,.72);
  --on-light-mute:   rgba(21,22,26,.48);
  --on-light-faint:  rgba(21,22,26,.26);
  --line-light:      rgba(21,22,26,.11);
  --line-light-lift: rgba(21,22,26,.24);

  --on-dark:        rgba(251,250,247,.70);
  --on-dark-mute:   rgba(251,250,247,.44);
  --on-dark-faint:  rgba(251,250,247,.20);
  --line-dark:      rgba(251,250,247,.12);
  --line-dark-lift: rgba(251,250,247,.24);

  /* ONE TYPEFACE.

     The site is set entirely in Jost. The three tokens below are
     kept because they name a ROLE, not a family — display type,
     running text and micro-labels are still three distinct voices
     and code should still say which one it is asking for. What
     changed is that the distinction is now carried by weight,
     size and tracking rather than by three different faces.

     Anything reading these expecting a Didone or a monospace will
     be disappointed on purpose: the `opsz` / `SOFT` / `WONK`
     variation axes that used to appear alongside them belonged to
     Bodoni Moda and Fraunces and have been removed, because a
     variation-settings declaration naming an axis the font does
     not have is not merely inert — it still wins over font-weight
     for any axis it DOES name, which is how a stray "wght" in an
     old declaration silently outranks the weight beside it. */
  /* The claim, and only the claim. Orange Avenue ships one cut,
     so there is no weight or optical axis to reach for — which is
     also why it stays off everything that needs a range. */
  --font-title:   "Orange Avenue", "Bodoni Moda", Didot, Georgia, serif;
  --font-display: "Jost", "Helvetica Neue", Arial, sans-serif;
  --font-sans:    "Jost", "Helvetica Neue", Arial, sans-serif;
  --font-mono:    "Jost", "Helvetica Neue", Arial, sans-serif;

  --pad:   5rem;
  --gap:   2rem;
  --cols:  16;
  --max:   172.8rem;

  --hair:     max(1px, .1rem);
  --ease:     cubic-bezier(.785, .135, .15, .86);
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  /* Slow to leave, fast to settle — reads considered where a
     symmetric ease reads mechanical. */
  --ease-arch: cubic-bezier(.24, .15, .02, 1);

  --header-h: 9.6rem;
}

@media (max-width: 767px) { :root { --pad: 2.4rem; --gap: 1.6rem; --header-h: 7.6rem; } }


/* ── The page turns at the invitation ────────────────────
   FROM THE CTA DOWN, THE SITE IS BLUE. Everything above the
   invitation is paper — that is the whole register — and the last
   two screens leave it. The panel asking for the meeting is already
   navy, so the page arriving at the same colour is the ground
   catching up with the thing standing on it rather than a second
   idea about colour.

   ONE TOKEN, SWITCHED ON <html>. Every surface that has to turn
   reads --page instead of naming a colour, so the change is one
   value in one place and no section can be left behind. It is on the
   root rather than the body because the root is what paints the
   canvas beyond the last element — without it the page ends in
   paper below the final section.

   IT IS DARKER THAN THE PANEL, not equal to it. At the same navy the
   CTA box loses its edges and dissolves into the page; a few steps
   down, the box still reads as an object sitting on a ground.

   AND IT TRANSITIONS, which is the reason for the token rather than
   a stylesheet swap. A custom property cannot be transitioned, but
   the properties that READ one can: when --page changes, every
   background-color resolved from it animates to the new value on its
   own element's transition. js/sections/cta.js only adds a class. */
html {
  --page: var(--paper);
  background: var(--page);
  transition: background-color .9s var(--ease);
}

html.is-night {
  --page: var(--page-night);
}

body {
  /* NO BACKGROUND HERE - IT IS ON <html> ABOVE, AND THAT MATTERS.
     body is position: relative, which makes it a POSITIONED element:
     its background paints late, above any negative z-index descendant.
     The footer reveal puts two of those under the page (see
     css/sections/footer.css) and a background here would bury both.
     The root paints the canvas, which is what a page ground is. */
  color: var(--on-light);
  font-family: var(--font-sans);
  font-weight: 300;
  font-size: 1.7rem;
  line-height: 1.62;
  /* The partners marquee and the projects rail are both wider than
     the window on purpose. */
  overflow-x: hidden;
  position: relative;
}

/* Lenis */
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-stopped { overflow: hidden; }


/* ── TEMPORARY compatibility aliases ────────────────────
   The sections below the hero have not been reworked yet. They
   still reference the previous palette, so those names are
   mapped onto the new one here to keep the page coherent while
   we work section by section. DELETE this block once the last
   old section has been rebuilt. */
:root {
  --kiln: var(--navy);         --kiln-2: var(--navy-2);
  --soot: var(--ink-soft);     --sand: var(--paper);
  --chalk: var(--ivory);       --mortar: var(--paper-2);
  --brick: var(--gold);        --brick-lit: var(--gold-lift);
  --brick-deep: var(--gold-deep); --brass: var(--gold-lift);
}


/* ── Optical trim ───────────────────────────────────────
   A font reserves space above cap-height and below the baseline.
   At display size that dead space is visible and headings look
   as though they sit too low in their own box. This trims it so
   type aligns to its OPTICAL edge, not its metric one.

   Values are measured PER FACE. Re-measure if the display face
   changes, or every heading on the site will drift. */
.trim {
  --lh: 1;
  /* MEASURED against a line-height:1 box, which is what this
     formula is written for — NOT against the raw font metrics.
     Jost's ascent+descent is 1.445em, so the baseline sits .8475em
     below the box top; the cap top lands .144 below it and the
     descender runs .066 BELOW the box, hence a negative bottom.

     Measuring trap worth knowing: a canvas reports metrics for
     whatever font is RESOLVED, and a webfont is only fetched when
     something needs it. Measure without awaiting
     document.fonts.load() and you get plausible numbers for the
     fallback — two unrelated faces reporting identical values is
     the tell. Always assert document.fonts.check() first. */
  --tt: .144;
  --tb: -.066;
  --half-leading: calc((var(--lh) - 1) / 2);
  display: block;
}
.trim::before,
.trim::after { content: ''; display: block; width: 0; height: 0; }
.trim::before { margin-bottom: calc(-1em * (var(--half-leading) + var(--tt))); }
.trim::after  { margin-top:    calc(-1em * (var(--half-leading) + var(--tb))); }


/* ── Grid ──────────────────────────────────────────────
   16 content columns with the page gutters as real tracks, so any
   child can break to full-bleed without leaving the grid. */
.grid {
  --pad-inline: calc(var(--pad) - var(--gap));

  display: grid;
  width: 100%;
  max-width: var(--max);
  margin-inline: auto;
  gap: 0 var(--gap);
  grid-template-columns:
    [full-start]    minmax(var(--pad-inline), 1fr)
    [content-start] repeat(var(--cols), minmax(0, 1fr))
    [content-end]   minmax(var(--pad-inline), 1fr)
    [full-end];
}
/* min-width:0 is not optional — grid items default to min-width:auto,
   so any item with wide intrinsic content blows the track out and
   pushes the whole grid past the viewport. */
.grid > * { grid-column: content; min-width: 0; }


/* ── Type ──────────────────────────────────────────────── */
.display {
  font-family: var(--font-display);
  /* 300 is the display weight now. A geometric sans at 8rem and
     above wants LESS weight, not more — the counters stay open
     and the line reads as composed rather than as a headline
     shouting. Running text stays at 400. */
  font-weight: 300;
  line-height: 1;
  letter-spacing: -.012em;
  color: var(--ink);
}
.display em { font-style: italic; }

.d1 { font-size: 13rem; }
.d2 { font-size: 8.4rem; }
.d3 { font-size: 5.6rem; }
@media (max-width: 991px) { .d1 { font-size: 9rem; } .d2 { font-size: 6rem; } .d3 { font-size: 4.4rem; } }
@media (max-width: 767px) { .d1 { font-size: 5.4rem; } .d2 { font-size: 4rem; } .d3 { font-size: 3.2rem; } }

/* Micro-label voice — locations, years, indices, figures. */
.mono {
  font-family: var(--font-mono);
  font-size: 1.25rem;
  font-weight: 500;
  letter-spacing: .2em;
  text-transform: uppercase;
  color: var(--on-light-mute);
  font-variant-numeric: tabular-nums;
}
@media (max-width: 767px) { .mono { font-size: 1.05rem; letter-spacing: .12em; } }

.sec-label {
  display: flex;
  align-items: center;
  gap: 1.4rem;
  color: var(--accent);
}
.sec-label::after {
  content: '';
  flex: 1;
  height: var(--hair);
  background: var(--line-light);
}
.on-dark .sec-label::after { background: var(--line-dark); }


/* ── Shared: hairline that wipes in on hover ───────────── */
.hover-line { position: relative; display: inline-block; vertical-align: bottom; }
.hover-line span { display: inline-block; }
.hover-line::after {
  content: '';
  position: absolute;
  left: 0; bottom: -.5rem;
  width: 100%;
  height: var(--hair);
  background: currentColor;
  transform: scaleX(0);
  transform-origin: right center;
  transition: transform .5s var(--ease);
}
@media (hover: hover) {
  .hover-line:hover::after { transform: scaleX(1); transform-origin: left center; }
}


/* ── Shared: scroll cue ────────────────────────────────
   A LABEL AND A BAR THAT FILLS AS THE SCENE IS SCROLLED. It is not
   an invitation to scroll — the page has already done that by the
   time one appears — it is a progress reading: how much of THIS
   held screen is left before it hands over.

   ON currentColor, so a scene sets one colour and the label, the
   track and nothing else follow. The fill stays gold in both
   registers: it is the one part that is measuring something, and it
   should read as a different kind of mark from the rule it runs in.

   THE HERO HAS AN OLDER TWIN of this, .hero-scroll in hero.css,
   which is bound to js/sections/hero.js by class name. This is where
   the next one belongs; that one should move here when the hero is
   next opened. */
.cue {
  display: flex;
  align-items: center;
  gap: 1.2rem;
}
.cue-label {
  font-family: var(--font-sans);
  font-size: 1.25rem;
  font-weight: 500;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: inherit;
}
.cue-bar {
  display: block;
  width: 7rem;
  height: var(--hair);
  background: currentColor;
  opacity: .38;
  overflow: hidden;
}
.cue-bar i {
  display: block;
  height: 100%;
  /* THE FILL IS SET BY THE SCENE, because the gold that reads on a
     photograph is not the gold that reads on paper. --gold-lift is
     the highlight value and it all but vanishes on the white ground;
     --gold-deep is the one mixed for light. The default stays lift
     for the dark scenes that are the common case. */
  background: var(--cue-fill, var(--gold-lift));
  transform: scaleX(0);
  transform-origin: left center;
}


/* ── Shared: button ────────────────────────────────────
   A hairline pill. On a white site the resting state has to be
   quiet enough not to shout and clear enough to find — so the
   weight arrives on hover, not before. */
.btn {
  --h: 5rem;
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 1.4rem;
  height: var(--h);
  padding: 0 2.2rem;
  border: var(--hair) solid var(--line-light-lift);
  border-radius: 100vmax;
  color: var(--ink);
  /* 500 and .1em. One typeface site-wide means a button has to
     separate itself from the caption beside it by weight and
     tracking alone — at 400 it read as another label. */
  font-family: var(--font-sans);
  font-size: 1.25rem;
  font-weight: 500;
  letter-spacing: .1em;
  text-transform: uppercase;
  /* A label that wraps inside a fixed-height pill spills straight
     out of it. This is a single short phrase in every language it
     ships in — it should shrink the layout around it, never fold. */
  white-space: nowrap;
  flex: none;
  overflow: hidden;
  transition: border-color .5s var(--ease), color .5s var(--ease);
}
.btn::before {
  content: '';
  position: absolute;
  inset: 0;
  background: var(--ink);
  transform: scaleY(0);
  transform-origin: bottom center;
  transition: transform .55s var(--ease);
}
.btn > * { position: relative; z-index: 1; }

.btn-dot {
  display: grid;
  place-items: center;
  width: 1.5rem; height: 1.5rem;
  transition: transform .5s var(--ease);
}
.btn-dot svg { width: 1.25rem; height: 1.25rem; }

@media (hover: hover) {
  .btn:hover { border-color: transparent; color: var(--paper); }
  .btn:hover::before { transform: scaleY(1); }
  .btn:hover .btn-dot { transform: translateX(.35rem); }
}

.btn--solid { background: var(--ink); border-color: var(--ink); color: var(--paper); }
.btn--solid::before { background: var(--gold-grad); }
.btn--lg { --h: 5.8rem; padding: 0 2.8rem; }

.on-dark .btn { border-color: var(--line-dark-lift); color: var(--paper); }
.on-dark .btn::before { background: var(--paper); }
.on-dark .btn:hover { color: var(--ink); }


/* ── Header ────────────────────────────────────────────
   Sits over the hero from the first frame. On a white site it
   does not need a background — the hairline under it is enough
   to separate it from the page. */
.header {
  /* NOT fixed. The header belongs to the top of the page, not to
     the viewport — it scrolls away with the hero and does not
     follow you down the site. */
  position: absolute;
  inset: 0 0 auto;
  z-index: 200;
  height: var(--header-h);
  color: var(--paper);
  transition: opacity .6s var(--ease), color .5s var(--ease);
}
/* The one exception: the menu is a fixed full-screen overlay, so
   if the header had scrolled off there would be no way back out
   of it. While the panel is open the header pins to the viewport
   so its close control is always reachable. */
.header.is-menu { position: fixed; top: 0; }
.header-inner { height: 100%; align-items: center; }
.header-inner > * { grid-row: 1; align-self: center; }

.header-logo {
  grid-column: content-start / span 5;
  display: block;
  width: 8.6rem;
  /* OPTICALLY centred, not box-centred.

     The lockup is the monogram plus a rule and "Since 1946"
     beneath it — the artwork is 1327x1023 where the monogram
     alone is 1327x765, so the mark's centre sits 12.6%% of the
     lockup's height ABOVE the box centre. Centring the box
     therefore hangs the mark high of the button and the menu
     label, which are centred on their own type.

     The nudge is weighted toward the monogram rather than
     splitting the difference evenly: it carries nearly all the
     ink, so it is what the eye actually aligns to. */
  transform: translateY(.7rem);
}
.header-logo img { width: 100%; height: auto; }

.header-right {
  /* The full content width, not a five-column track. justify-self
     keeps it against the right edge, so it still occupies only
     its own intrinsic width — but it can no longer be squeezed
     narrower than its contents by the grid. */
  grid-column: content;
  justify-self: end;
  display: flex;
  align-items: center;
  gap: 2.6rem;
}

/* The header opens over a photograph and later sits on white, so
   it carries both states — and the OVER-IMAGE state is the
   default. Defaulting to the on-white state and having JS add the
   inversion means the first painted frame is wrong until a script
   runs, which is both a flash for the visitor and invisible to
   any capture that does not repaint. The page always opens on the
   hero image, so light is simply the truth at load. */
/* Solid white by default over the photograph — an outline pill
   reads as secondary, and this is the one action in the header. */
.header .btn {
  background: var(--paper);
  border-color: var(--paper);
  color: var(--ink);
}
.header .btn::before { background: var(--gold-grad); }
.header .btn:hover { color: var(--ink); border-color: var(--gold); }

/* .is-dark is added once the hero has handed over. */
.header.is-dark { color: var(--ink); }
/* On white it inverts — a white pill on a white page is nothing.
   Hover goes gold in both states, so the interaction is the same
   gesture wherever the header happens to be sitting. */
.header.is-dark .btn {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--paper);
}
.header.is-dark .btn::before { background: var(--gold-grad); }
.header.is-dark .btn:hover { color: var(--ink); border-color: var(--gold); }

/* While the menu is open the header stands on the navy panel, not
   on the page — so it goes back to its light state whatever the
   scroll position had made it. Declared AFTER .is-dark so it wins
   at equal specificity. */
.header.is-menu { color: var(--paper); }
.header.is-menu .btn {
  background: var(--paper);
  border-color: var(--paper);
  color: var(--ink);
}
.header.is-menu .btn::before { background: var(--gold-grad); }
.header.is-menu .btn:hover { color: var(--ink); border-color: var(--gold); }

@media (max-width: 767px) {
  .header-logo { width: 7rem; }
  .header-right .btn { display: none; }
}


/* ── Reduced motion ────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  .scene-stage { position: relative; }
  .scene-runway { display: none; }
  .has-gsap [data-reveal] { opacity: 1; }
}


/* ── Scene primitives ──────────────────────────────────
   A section WITH a runway is pinned; a section without one
   scrolls normally. That single distinction is the whole
   scrollable / non-scrollable system. */
/* A FLOOR OF 3, AND IT IS THE FOOTER THAT NEEDS IT. The hero, the
   statement and the approach were all position: relative at z-index
   AUTO, which is a layer nothing can be slid underneath: the reveal
   footer has to sit below the whole page, and the only spaces below
   an auto layer are negative ones - where an element is behind its
   own ancestors' boxes and stops being clickable. Naming a positive
   floor here opens two usable layers under the page (footer 1, its
   shade 2) while staying below the travelling picture at 5 and the
   sections at 6. Scenes that need those higher numbers still set
   them. */
.scene { position: relative; z-index: 3; overflow-x: clip; background: var(--paper); }
.scene-stage {
  position: sticky;
  top: 0;
  height: 100svh;
  contain: paint;
  overflow: hidden;
}
.scene-runway { pointer-events: none; }

.has-gsap [data-reveal] { opacity: 0; }


/* ── The travelling picture ──────────────────────────────
   One image shared by two sections, fixed to the viewport so it
   survives one section unpinning and the next pinning. Its rect
   is written every frame from js/core/travel.js; nothing about
   its position is declared here, because both ends of the journey
   are defined by the sections' own grids.

   In core.css rather than a section file precisely because it
   belongs to neither — putting it in one section's stylesheet
   would make the other section's layout depend on a file it has
   no reason to load. */
.tv {
  position: fixed;
  top: 0; left: 0;
  z-index: 5;
  overflow: hidden;
  background: var(--ink);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  transition: opacity .5s var(--ease), visibility .5s;
  will-change: transform, width, height;
}
.tv.is-on {
  opacity: 1;
  visibility: visible;
  /* Only on while it is on screen: a fixed element that can be
     hovered would sit over every section below it. */
  pointer-events: auto;
}
.tv img {
  width: 100%;
  height: 100%;
  /* max-width:none — the reset caps every image at 100%, which is
     not what a cover-fitted picture in a resizing box wants. */
  max-width: none;
  object-fit: cover;
  display: block;
  transition: transform 1.3s var(--ease-arch);
}
/* The inset gold rule, matching the approach section's picture. */
.tv::after {
  content: '';
  position: absolute;
  inset: 1.4rem;
  z-index: 1;
  border: var(--hair) solid rgba(228,198,142,0);
  transition: border-color .7s var(--ease), inset .7s var(--ease-arch);
  pointer-events: none;
}
@media (hover: hover) {
  .tv:hover img { transform: scale(1.08); }
  .tv:hover::after { border-color: rgba(228,198,142,.6); inset: 2.2rem; }
}

/* ── The veil ────────────────────────────────────────────
   The mediator between the photograph and the content that will be
   set over it. It arrives only at the end of the opening: before
   that the picture is a picture, and a scrim over a picture in a
   layout is just a picture with the life dimmed out of it.

   NAVY, NOT BLACK. It is the brand's own dark — the same ink the
   type is set in and the menu is built from. Black over a warm
   interior shot reads as a generic video overlay and grays the
   photograph; a navy at the same strength cools it, which is a
   deliberate-looking thing for it to do and keeps the page in one
   palette.

   A GRADIENT, NOT A FLAT SCRIM. A flat tint costs the same
   legibility everywhere, including where nothing is written, so it
   spends the photograph evenly to protect text that is only in one
   place. Weighted to the bottom and the top — where a heading and
   a footer line will sit — and lightest across the middle, where
   the picture is left to be seen. The second pass adds a little
   more on the left, because type on this page is set from the left
   and its first characters are what have to hold.

   Deliberately readable-but-not-opaque: the point is that the
   photograph is still doing work behind the words. If content ends
   up needing more than this, deepen the stop under that content
   rather than raising the whole thing. */
.tv-veil {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: 0;
  /* TWO GRADIENTS COMPOSITE, THEY DO NOT AVERAGE. Stacked layers
     multiply what gets through — .26 over .40 leaves 44% of the
     photograph, not 66% — so the first pass at these numbers read
     as a near-solid navy panel with a hint of a room in it. Each
     stop is set well below what it would need to be on its own.
     Check any change against the combined value, not the two
     numbers separately. */
  background:
    /* THE BAND VALUES ARE A CONTRAST TARGET, NOT A TASTE CALL. Over
     the bright part of this photograph, white type needs the
     composite under it near .77 navy to clear 4.5:1 — at .67 it is
     3.3:1, which passes for large display type and fails for
     anything smaller. The top and bottom bands are set to carry
     text; the middle is left at .28, which is nowhere near legible
     and is not meant to be. Content goes in the bands. */
    linear-gradient(180deg,
      rgba(19, 25, 38, .80) 0%,
      rgba(19, 25, 38, .54) 24%,
      rgba(19, 25, 38, .46) 52%,
      rgba(19, 25, 38, .66) 78%,
      rgba(19, 25, 38, .84) 100%),
    linear-gradient(90deg,
      rgba(19, 25, 38, .30) 0%,
      rgba(19, 25, 38, .12) 46%,
      rgba(19, 25, 38, 0)   72%);
}

/* Once it starts opening out it is no longer a picture in a layout,
   it is the background — so it stops answering the pointer, and the
   inset rule that framed it goes with it. Set from js/core/travel.js
   on the first frame of the opening, not at the end of it, so there
   is never a full-bleed image that reacts to a hover. */
.tv.is-open { pointer-events: none; }
.tv.is-open::after { opacity: 0; }


/* ── Page-edge rules ─────────────────────────────────────
   Vertical hairlines at the page gutter, shared by any section
   that wants them.

   SHARED SO THEY LINE UP. Each section had its own rule at its own
   x — the statement's at --pad, the approach's at a computed
   column edge — so at the join one stopped and a different one
   started somewhere else. Two sections drawing the same margin
   have to draw it in the same place, and the only way to be sure
   of that is for it to be the same rule. */
.edge-rule {
  position: absolute;
  top: 0; bottom: 0;
  width: var(--hair);
  background: var(--line-light);
  z-index: 0;
  pointer-events: none;
}
.edge-rule--l { left: var(--pad); }
.edge-rule--r { right: var(--pad); }
/* At phone widths they sit almost on the text. */
/* Hidden below 992px — see the block near .ground-rail. */

/* ── THE MARGIN RAILS ────────────────────────────────────
   Shared background furniture for the statement and approach
   sections. It lives here rather than in either stylesheet for the
   same reason .edge-rule does: the two sections sit against each
   other with no visible join, so anything drawn in the margins has
   to be drawn by ONE rule in ONE place, or the seam reappears as a
   line that stops in one section and restarts somewhere else in
   the next.

   ONE DEVICE, AND IT STAYS IN THE MARGIN.

   The first attempt was a set of column lines on the page's own
   grid with registration marks on them, plus a plot grid in one
   corner and a scale bar in another. Two things were wrong with
   it, and they compounded:

     · the column lines ran BEHIND THE CONTENT. A hairline passing
       through a paragraph and down the side of a photograph is not
       background — it is a line drawn on top of the work, and it
       sat hard against the edges of things it had no relationship
       to;
     · it was four different kinds of object. Long verticals, tiny
       crosses, a horizontal bar, a patch of grid — each individually
       quiet, together a jumble with no rule to it. Variety in
       background furniture does not read as richness, it reads as
       clutter that nobody decided about.

   So: one element, repeated, and only ever outside the content.
   The ticks sit in the outer margin between the edge rule and the
   screen, which is the one part of the page that is genuinely
   empty and always will be. Nothing crosses text, nothing touches a
   box, and there is only one thing to recognise.

   PROPORTIONAL, NOT PACED IN PIXELS. The gradient's period is a
   percentage of the section, so every section carries the same
   number of ticks whatever its height, and the rhythm can never
   fall out of step between one section and the next — which a
   fixed pixel spacing would do at every join, since the sections
   are not the same height. */
.survey {
  position: absolute;
  /* INSET FROM TOP AND BOTTOM. At inset:0 the first tick of the
     pattern falls exactly on the section edge, which puts a mark
     right on the join between the two sections — the one place a
     background element must not draw attention to. Held clear of
     both ends, the rails read as running past the edge of the
     frame rather than as starting and stopping at it. */
  inset: 9% 0;
  z-index: 0;
  pointer-events: none;
}

.survey-rail {
  position: absolute;
  top: 0;
  bottom: 0;
  width: 1.4rem;
  background-image: repeating-linear-gradient(
    180deg,
    var(--line-light) 0 var(--hair),
    transparent var(--hair) 20%
  );
  opacity: .9;
}
/* Just outboard of the edge rule, extending into the margin — the
   ticks read as marks ON that rule rather than as a second line
   parallel to it. */
.survey-rail--l { left: calc(var(--pad) - 1.4rem); }
.survey-rail--r { right: calc(var(--pad) - 1.4rem); }

@media (max-width: 767px) {
  /* The edge rules are already hidden at this width and the margin
     is too narrow to hold anything without touching the text. */
  .survey { display: none; }
}

/* ── The veil on narrow screens ──────────────────────────
   FLATTER AND DEEPER, because the type moved.

   The banded version above is built for a wide layout, where the
   copy sits at the top and the bottom and the middle is left
   photographic on purpose. At these widths the argument runs down
   the whole screen — straight through the band that was set at .46
   precisely because nothing was meant to be written there. Body
   text over the bright part of the picture came out around 2:1.

   So the gradient loses most of its shape here: still deepest at
   the edges, but never light enough anywhere to drop a line of type
   below legible. The photograph gives up some presence for it,
   which is the correct trade when the alternative is text you have
   to work at. */
@media (max-width: 991px) {
  .tv-veil {
    background:
      linear-gradient(180deg,
        rgba(19, 25, 38, .86) 0%,
        rgba(19, 25, 38, .68) 26%,
        rgba(19, 25, 38, .64) 54%,
        rgba(19, 25, 38, .74) 80%,
        rgba(19, 25, 38, .88) 100%);
  }
}

/* ── The narrow-screen ground ────────────────────────────
   On a wide screen the photograph is one fixed layer that travels,
   opens out and is then shared by two sections. None of that works
   at this width, and the reason is worth stating: a fixed layer
   fills the VIEWPORT, not the section. While the approach section
   is scrolling up into place, a fixed picture is already covering
   the whole screen — including the part still showing the section
   above it. There is no way to have both "normal scrolling" and a
   fixed backdrop; they are different things.

   So below 991px the photograph stops being a layer and becomes
   what it looks like: the background of the sections that use it.
   It scrolls in with its own section, arrives with the content on
   it, and needs no fading, no triggers and no stacking order.

   These two properties are the whole ground, and both sections use
   them so the join between them is seamless — same image, same
   size, same position. */
/* NO url() IN A CUSTOM PROPERTY. A relative URL inside a custom
   property does not resolve against the stylesheet that USES it —
   so --ground-img declared here and used from css/sections/ pointed
   at a path that does not exist, and the sections rendered as the
   veil over bare paper: a flat grey panel that looks like a design
   decision rather than a missing image.

   The path is written out in each section's own stylesheet
   instead. The veil below is safe as a property because it holds no
   URL. */
:root {
  /* Flatter and deeper than the wide layout's veil: at this width
     the copy runs down the whole screen rather than sitting in
     bands at the top and bottom, so no part of it can be left light
     enough to lose a line of type. */
  --ground-veil: linear-gradient(180deg,
    rgba(19, 25, 38, .86) 0%,
    rgba(19, 25, 38, .68) 26%,
    rgba(19, 25, 38, .64) 54%,
    rgba(19, 25, 38, .74) 80%,
    rgba(19, 25, 38, .88) 100%);
}

/* ── THE NARROW-SCREEN GROUND ────────────────────────────
   ONE fixed photograph for the approach and timeline sections,
   instead of a background on each of them.

   Two backgrounds could never be made to work here, and it took
   several attempts to see why. Sized to their own boxes they
   cropped differently and the join was a visible seam. Made
   viewport-aligned they matched — but the lower section still
   covers the upper one from the moment it enters the viewport, and
   with content on it that is a section arriving whether its
   background matches or not.

   One element removes the class of problem rather than the
   instance. It never moves, so there is no seam and nothing
   arrives; the sections above it are transparent and simply let it
   through.

   WHY IT IS EARLY IN THE DOCUMENT. It is fixed, so it would cover
   everything if it painted last. Placed before the hero and the
   statement — both opaque, both painting above it in document order
   at the same z-index — it is hidden until they have gone. The two
   sections that want it are lifted to z-index 6, above it, and
   paint nothing of their own. So it shows exactly where it should
   and nowhere else, with no triggers and no timing.

   This is what the travelling picture becomes at this width: not
   removed, relocated. */
.ground { display: none; }

@media (max-width: 991px) {
  .ground {
    display: block;
    position: fixed;
    inset: 0;
    z-index: 0;
    pointer-events: none;
    background-image: url("../assets/img/statement.jpg");
    background-size: cover;
    background-position: center;
  }
  /* The veil, on the ground itself — one overlay for one ground. */
  .ground::after {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 0;
    background: var(--ground-veil);
  }
}

@media (max-width: 991px) {
  /* ── The margin furniture, below 992px ─────────────────
     THE FULL-HEIGHT RULES COME OUT. They sit exactly on --pad, and
     so does the content column once the grid is tight enough that
     its outer track stops growing — measured at 882px wide, the
     rule was at 45 and the first character at 50. Five pixels is
     not a margin, it is a collision, and no amount of nudging one
     of them fixes it at every width. Below this the page has no
     spare margin to draw a line in, so it does not draw one.

     THE TICKS STAY, BUT ON THE GROUND. Inside a section they ride
     that section's stage — which is pinned, then released — so they
     held still and then scrolled away mid-section. On the fixed
     ground they cannot move at all, which is what they were always
     meant to look like. */
  .edge-rule { display: none; }
  .survey { display: none; }

  .ground-rail {
    position: absolute;
    /* ABOVE THE VEIL. ::after is the ground's last child, so it
       paints over anything else inside it — the ticks were being
       laid under a .7 navy wash and were effectively invisible. */
    z-index: 1;
    top: 9%;
    bottom: 9%;
    width: 1.4rem;
    background-image: repeating-linear-gradient(
      180deg,
      rgba(255, 255, 255, .3) 0 var(--hair),
      transparent var(--hair) 20%);
  }
  .ground-rail--l { left: calc(var(--pad) - 1.4rem); }
  .ground-rail--r { right: calc(var(--pad) - 1.4rem); }
}
@media (max-width: 767px) {
  /* The margin is too narrow to hold even a tick without it sitting
     against the text. */
  .ground-rail { display: none; }
}

/* ── The travelling picture, below 992px ─────────────────
   THERE IS ONLY EVER ONE PHOTOGRAPH ON SCREEN. At these widths that
   is .ground above; the travelling layer has no part to play, so it
   is hidden here rather than removed in script.

   The distinction matters. js/core/travel.js reads its breakpoint
   once, when the scene mounts; this one is live. Anything that
   changes the width afterwards — a rotation, a dragged window, a
   responsive preview — would leave the script believing one thing
   and the stylesheet showing the other, which put both photographs
   on screen at once with the travelling one still animating. A
   rule here cannot disagree with the rest of the stylesheet. */
@media (max-width: 991px) {
  .tv { display: none; }
}

/* ── The handover length, below 992px ────────────────────
   ONE NUMBER FOR TWO SECTIONS. At these widths the approach and
   the timeline occupy the SAME screen: both pinned, both starting
   at the same scroll position, with their content crossfaded from
   one to the other. That only works if they are exactly the same
   height and start together, so the tail they share is declared
   once here and used by both — .fx-runway sets its length, and .tl
   pulls itself up by a screen plus the same value.

   Two copies of it would be the classic unenforced pairing: change
   one and the sections drift apart by the difference, which shows
   up as the years arriving already half faded. */
@media (max-width: 991px) {
  /* Three notches of holding the finished section, then the swap
     fires (see js/core/travel.js) — and then two more so the years
     can be looked at before the section starts to leave. 78vh: 40
     before the swap, 38 after it. */
  :root { --fx-tail: 78vh; }
}
@media (max-width: 767px) {
  :root { --fx-tail: 72vh; }
}

/* ── THE POINTER ─────────────────────────────────────────
   A gold dot that IS the cursor, and a gold ring that trails it.

   THE DOT REPLACES THE ARROW, so it never lags — what is under it
   is exactly what a click will hit. The ring is free to be late
   because the dot is not; that division is what lets the follower
   have a real tail without the pointer becoming hard to aim.

   FINE POINTERS ONLY, and the native cursor is hidden by a class
   the SCRIPT adds. A stylesheet that hid it on its own would take
   it away on any page where the script failed to load, and a touch
   device would lose a pointer it cannot replace.

   ONE COLOUR FOR EVERY GROUND. The page runs from near-white paper
   to a full-bleed photograph to a navy menu, and both parts have to
   read on all of them. Gold does: it is the one hue in the palette
   that is neither the light ground nor the dark one, so it never
   disappears into either. A white pointer would vanish on paper and
   a dark one on the photograph, and inverting per section is a rule
   that has to be maintained for every section ever added. */
.cursor { display: none; }

@media (hover: hover) and (pointer: fine) {
  html.has-cursor,
  html.has-cursor * { cursor: none; }
  /* EXCEPT WHERE THE NATIVE ONE CARRIES MEANING. A text field's
     caret says where typing will land, and a dot hovering over it
     does not. */
  html.has-cursor input,
  html.has-cursor textarea,
  html.has-cursor select { cursor: auto; }

  .cursor { display: block; }

  .cursor-dot,
  .cursor-ring {
    position: fixed;
    top: 0;
    left: 0;
    z-index: 999;
    border-radius: 50%;
    /* Neither may ever be a target: the layer sits over the whole
       page and would swallow every click. */
    pointer-events: none;
    will-change: transform;
  }

  /* ── The dot ───────────────────────────────────────────
     8px, in PIXELS rather than rem. Everything else on this page is
     sized against the canvas so the layout holds its proportions at
     any width — but a pointer is not part of the layout. It is a
     physical thing under the hand, and it should be the same size
     on a laptop as on a wall display. rem would make it grow with
     the page and shrink on a phone-width window.

     Its size is driven from the script, not from here: the script
     already writes this element's transform every frame for the
     position, and a scale set in a rule would be overwritten before
     it was ever painted. */
  .cursor-dot {
    width: 8px;
    height: 8px;
    margin: -4px 0 0 -4px;
    background: var(--gold);
    transition: opacity .3s var(--ease-out);
  }

  /* ── The follower ──────────────────────────────────────── */
  .cursor-ring {
    width: 3.2rem;
    height: 3.2rem;
    margin: -1.6rem 0 0 -1.6rem;
    /* 2px, not a hairline and not 1.5. Chrome resolves a border to a
       whole device pixel, so 1.5px computed back as 1px at DPR 1 —
       a value that reads as bold on a retina mock-up and as the
       thinnest possible line on the screen most people have. */
    border: 2px solid rgba(217, 179, 120, .9);
    /* A soft gold halo, barely there. It is what stops the ring
       reading as a drawn outline: an outline sits ON the page, a
       ring with a little light around it sits just above it. Two
       stops, tight then wide, for the same reason the card's shadow
       has two — a single blur is an even haze with an edge. */
    box-shadow:
      0 0 .6rem rgba(217, 179, 120, .22),
      0 0 2rem rgba(217, 179, 120, .12);
    /* One curve across every property, so the ring opening never
       separates into a resize and then a tint. */
    transition:
      width .45s var(--ease-out),
      height .45s var(--ease-out),
      margin .45s var(--ease-out),
      background-color .45s var(--ease-out),
      opacity .3s var(--ease-out);
  }

  /* Over something that can be pressed the ring opens a little and
     takes on some of its own colour.

     A LITTLE. This was 6.4rem against a 3.2rem resting size — a
     doubling, which stops reading as the same ring acknowledging
     something and starts reading as a different object appearing.
     3.8rem is about a fifth larger: enough to register as an
     answer, small enough that the ring is plainly still the same
     ring. A third was already reading as a jump. */
  .cursor-ring.is-live {
    width: 3.8rem;
    height: 3.8rem;
    margin: -1.9rem 0 0 -1.9rem;
    background: rgba(217, 179, 120, .1);
  }

  /* ── Over a project card ───────────────────────────────
     The ring becomes a solid gold disc with a word in it.

     THE SECTION DECLARES THIS, NOT THE CURSOR. A card carries
     data-cursor-view="View" and the script reads the label from the
     attribute — so a gallery that wanted "Open" would need no
     change here, and this file never has to know what a project
     card is called. */
  .cursor-ring.is-view {
    /* 6rem, down from 7.4. The word inside needs room and nothing
       more — at the larger size most of the disc was space around a
       four-letter word, which is what made it read as a badge
       dropped on the card rather than as the pointer. */
    width: 6rem;
    height: 6rem;
    margin: -3rem 0 0 -3rem;
    background: var(--gold);
    border-color: var(--gold);
  }

  .cursor-label {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%) scale(.7);
    white-space: nowrap;
    /* SET LIKE THE DESCRIPTIONS, not like a label. It was uppercase
       at .18em tracking — the treatment every eyebrow and small
       caption on this site uses — and inside a gold disc that reads
       as a stamp rather than as a word. The disc is already doing
       all the announcing it needs; the word inside it should just
       be a word, in the face the body copy is set in. */
    font-family: var(--font-sans);
    /* UPRIGHT. The ring it sits in is an <i>, chosen as an inert
       decorative hook — and <i> is italic by default, which the
       label inherited. Nothing in the styling asked for a slant;
       the element name gave it one. */
    font-style: normal;
    font-size: 1.4rem;
    font-weight: 500;
    letter-spacing: .01em;
    /* Ink on gold — the one place on this pointer that is not gold,
       because it is sitting on it. */
    color: var(--ink);
    opacity: 0;
    /* One curve and one duration for both properties: the label is
       one appearance, not a fade and then a grow. */
    transition:
      opacity .4s var(--ease-out),
      transform .4s var(--ease-out);
  }
  .cursor-ring.is-view .cursor-label {
    opacity: 1;
    transform: translate(-50%, -50%) scale(1);
  }

  /* THE DOT GOES while the disc is up. A gold dot on a gold disc is
     invisible when the disc catches up and a stray mark when it has
     not — and precision is not needed here: the whole card is the
     target, which is why it can afford a pointer this size. */
  .cursor-dot.is-view { opacity: 0; }

  /* Off the window entirely — a pointer parked at the last position
     of a mouse that has gone is a mark on the page. */
  .cursor-dot.is-out,
  .cursor-ring.is-out { opacity: 0; }
}
