/* ------------------------------------------------------------------
   GWMC — scroll-cinema prototype, v3

   One sticky stage, four beats, driven by custom properties that
   script.js writes on :root every frame. Every property below that has
   a default in :root is one the script writes; the defaults are the
   rest state, so the page composes correctly before a single frame runs.

   Frame order (v2): the beach house is on top and is the frame the page
   opens on; the Bowl sits beneath it inside the scaled world and is
   revealed when the house frame launches out.

   v3: the menu is the WordPress site's six items; beat 4 is a 3D
   perspective marquee of the projects with the photographs on the
   frames; About Us and FAQ are real sections below the stage.

   v3.1 (polish): the logo plate is gone from the marquee, which is four
   frames; the header is fixed, outside the stage, and follows the page,
   taking an ink ground once the stage has passed; --muted is lifted to
   AA for the small text set in it.

   v3.2 (the owner's two requests, 2026-09-07): beat 4 has its own ground,
   a third photograph that dissolves in beneath the strip as it lands, so
   the Bowl is beat 3's alone; and the strip can be dragged either way.

   v3.4 (the projects, 2026-09-13): the Projects page is one photograph
   and a grid of tiles, and every project has a page of its own with the
   marquee as its gallery and a lightbox; the marquee's frame styles are
   shared between the home's strip and the galleries.

   v3.3 (multi-page, 2026-09-13): the site is six pages built by build.php
   from the client's copy — the stage is the home, and About Us, Services,
   Projects, FAQ and Contact are pages of their own with a hero, the same
   header and footer, and the enquiry form on Contact and FAQ. Below 640
   the marquee's frames are portrait, 9:16, the shape of the phone they
   are on; on a desktop they stay 3:2.

   Brand tokens are ADR-006's. Two faces, not three: DM Mono carried only
   the tells and is gone. Cyan is the focus ring, the centred-frame ring
   and one hairline; nothing else on the page is coloured.
   ------------------------------------------------------------------ */

:root {
  --blue:      #0072A3;
  --cyan:      #00AEFC;   /* dark grounds only, which this is */
  --ink:       #0D1220;
  --ink-2:     #333D57;
  --muted:     #8A93AB;   /* 6.08:1 on ink; the 13–14px text in the footer and the caption */
  --paper:     #FFFFFF;

  --paper-2:   rgba(255, 255, 255, .74);
  --hair:      rgba(255, 255, 255, .12);

  --display: "Bricolage Grotesque", Georgia, serif;
  --body:    "Schibsted Grotesk", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;

  --gutter: 48px;
  --scrub:  9880px;   /* total scroll distance the stage is stuck for; script.js writes it from its timeline */

  /* the marquee's frames: 3:2, the caption strip inside the frame; a
     quarter of the width since the owner asked for bigger frames on the
     desktop (2026-09-26; they were 17vw to 260px). The phone's are its own
     (640, below). */
  --sight-w:      clamp(220px, 24vw, 360px);
  --sight-h:      calc(var(--sight-w) * 2 / 3);
  --sight-gap:    clamp(18px, 2vw, 32px);
  --sight-strip:  38px;
  --sight-radius: 16px;

  /* ---- written by script.js, read below. Rest state. ---- */
  --mx: 0;
  --my: 0;

  /* the fixed header's height, measured once per breakpoint by script.js
     (the default is the desktop header, so an inner page lays out under
     it before the script runs): the bands' and the page heroes' clearance,
     and the sections' scroll margin */
  --head-h: 106px;

  /* the scaled world: the strip's ground and the strip */
  --back-x: 0px;
  --back-y: 0px;
  --back-scale: 1.02;

  /* the strip's ground, uncovered when the last category's frame lifts */
  --projects-visibility: hidden;

  /* the hero block */
  --hero-y: 0px;
  --hero-scale: 1;
  --hero-opacity: 1;

  /* Each category's frame, title and panel carries its own values (--f-*,
     --t-*, --p-*), written on the element; their rest states are the
     fallbacks in the rules below. */

  /* the marquee */
  --sights-opacity: 0;
  --sights-controls-opacity: 0;   /* the heading block and the reduced-motion buttons */
  --sights-enter-x: 420vw;
  --sights-visibility: hidden;
  --sights-shift: 0px;
  --sights-scale: 1;
  --sights-top: 0px;
  --sights-left: 0px;
  --sights-screen-top: 440px;
}

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

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

body {
  margin: 0;
  background: var(--ink);
  color: var(--paper-2);
  font-family: var(--body);
  font-size: 17px;
  line-height: 1.6;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

img { max-width: 100%; height: auto; display: block; }
a { color: inherit; }
h1, h2, h3 { font-family: var(--display); margin: 0; color: var(--paper); text-wrap: balance; }
p { margin: 0; }

:focus-visible { outline: 2px solid var(--cyan); outline-offset: 3px; border-radius: 4px; }

/* A kicker is a short line of the client's vocabulary above a heading:
   reading size, sentence case, no tracking. */
.kicker {
  font: 500 15px/1.4 var(--body);
  color: var(--paper-2);
}

.pill-btn {
  display: inline-flex;
  align-items: center;
  min-height: 42px;
  padding: 0 22px;
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: .01em;
  text-decoration: none;
  text-shadow: none;
  white-space: nowrap;
  transition: transform .35s cubic-bezier(.22, 1, .36, 1), box-shadow .35s ease;
}
.pill-btn:hover { transform: translateY(-1px); box-shadow: 0 8px 24px rgba(0, 0, 0, .28); }
.pill-btn--lg { min-height: 50px; padding: 0 28px; font-size: 15px; }
/* The portal's pill, before "Get in touch" in the header: the same size,
   and the one blue button on the site. --blue is the logo's hue at a shade
   white text reads on (5.34:1, ADR-006); the logo's cyan would not (2.48:1). */
.pill-btn--portal { background: var(--blue); color: var(--paper); }
button.pill-btn { border: 0; cursor: pointer; font-family: var(--body); }
button.pill-btn:disabled { cursor: default; opacity: .6; transform: none; box-shadow: none; }

/* The skip link: the first tab stop on every page, seen only when it has
   focus. */
.skip {
  position: fixed;
  top: 14px;
  left: 14px;
  z-index: 40;
  padding: 10px 18px;
  border-radius: 999px;
  background: var(--paper);
  color: var(--ink);
  font-size: 14px;
  font-weight: 600;
  text-decoration: none;
  transform: translateY(-300%);
}
.skip:focus { transform: none; }



/* ==================================================================
   The scroll rig
   ================================================================== */

.cinema-scroll {
  position: relative;
  height: calc(100vh + var(--scrub));
}

.beat-mark {
  position: absolute;
  left: 0;
  width: 1px;
  height: 1px;
  pointer-events: none;
}

.stage {
  position: sticky;
  top: 0;
  height: 100vh;
  min-height: 620px;
  overflow: hidden;
  isolation: isolate;
  background: var(--ink);
  color: var(--paper);
}

/* A stacking context of its own, so the categories' frames (z-index 11 and
   up) stack among themselves and over the strip's world without rising
   over the headline, the titles and the panels outside it. */
.world { position: absolute; inset: 0; isolation: isolate; }

.frame { position: absolute; pointer-events: none; }
.frame img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
}
.frame .grade, .frame .veil { position: absolute; inset: 0; pointer-events: none; }

/* The scaled coordinate frame: the strip's ground and the strip. Origin at
   the bottom centre; it rests at 1.02, so the pointer parallax never shows
   an edge, and takes a last 10% push-in as the strip lands. */
.back {
  position: absolute;
  inset: 0;
  z-index: 2;
  transform-origin: 50% 100%;
  transform: translate3d(var(--back-x), var(--back-y), 0) scale(var(--back-scale));
  will-change: transform;
}

/* The strip's ground, beneath the strip in the same scaled world: it takes
   the world's last push-in as the strip lands and the pointer parallax
   after, so the sharp, screen-true frames sit over a ground that is alive
   beneath them. The photograph is 1015px wide and is shown at up to 1.12×
   the stage; the dim, the veil and the blur are what make that acceptable:
   it is atmosphere, and the frames are the only sharp thing. Half
   brightness under a 3px blur, then the grade: heaviest at the top, where
   the heading sits (paper on it measures above 4.5:1, see the README),
   lightest behind the strip so the lit glass wall glows under the frames,
   and heavy again at the bottom edge for the ink band that follows the
   stage. It is uncovered, not dissolved: the last category's frame lifts
   off it, and until then it is hidden so it is not composited for nothing.
   The object-position keeps the glass wall behind the strip's centre and
   the roofline and sky above the heading on a portrait crop, and suits the
   alternate photograph as well. */
.frame-projects {
  inset: 0;
  z-index: 1;
  visibility: var(--projects-visibility);
  filter: brightness(.5) blur(3px);
}
.frame-projects img { object-position: 42% 40%; }
.frame-projects .grade {
  background: linear-gradient(180deg,
    rgba(13, 18, 32, .40) 0%,
    rgba(13, 18, 32, .30) 24%,
    rgba(13, 18, 32, .22) 42%,
    rgba(13, 18, 32, .14) 60%,
    rgba(13, 18, 32, .22) 78%,
    rgba(13, 18, 32, .56) 100%);
}

/* A category's photograph (home.php), stacked over the next, the first on
   top (--z). Overshoots the stage by 3% so the parallax and the blur's
   soft edge never reach the viewport at scale 1. The script writes its
   values on the element each frame: the transform (the pointer parallax, a
   slow scale-in, the settle as it is uncovered, the launch), the blur and
   the brightness it takes for its project, the clip and the opacity of
   the launch. The launch is a lift, a grow, and a clip whose bottom edge
   rises faster than the frame and sits 12% higher on the right, so each
   reveal has an edge as well as a lift. A frame not yet uncovered, or
   gone, is hidden (--f-visibility) so it is not composited for nothing. */
.scene-frame {
  inset: -3%;
  z-index: var(--z);
  opacity: var(--f-opacity, 1);
  visibility: var(--f-visibility, visible);
  transform-origin: 50% 58%;
  transform: translate3d(var(--f-x, 0px), var(--f-y, 0px), 0) scale(var(--f-scale, 1));
  filter: blur(var(--f-blur, 0px)) brightness(var(--f-brightness, 1));
  clip-path: polygon(
    0 0,
    100% 0,
    100% calc(100% - var(--f-clip-r, 0%)),
    0 calc(100% - var(--f-clip-l, 0%)));
  will-change: transform, opacity, filter, clip-path;
}
/* The static grade makes the quiet field the titles sit in, as the
   headline's does on the first: a soft pool of ink behind the centred
   words, and the top and bottom edges eased so the header and the foot
   stay legible. */
.scene-frame .grade {
  background:
    radial-gradient(ellipse 52% 46% at 50% 50%,
      rgba(13, 18, 32, .62) 0%,
      rgba(13, 18, 32, .40) 45%,
      rgba(13, 18, 32, 0)   100%),
    linear-gradient(180deg,
      rgba(13, 18, 32, .46) 0%,
      rgba(13, 18, 32, 0)   32%,
      rgba(13, 18, 32, 0)   70%,
      rgba(13, 18, 32, .42) 100%);
}
/* The veil is the shade the script raises for a project. It lives inside
   the frame so it lifts and clips with it. */
.scene-frame .veil {
  background: linear-gradient(180deg,
    rgba(13, 18, 32, var(--f-veil-top, 0))    0%,
    rgba(13, 18, 32, var(--f-veil-mid, 0))    48%,
    rgba(13, 18, 32, var(--f-veil-bottom, 0)) 100%);
}

/* Film grain: fractal noise from an SVG filter, tiled, overlaid at low
   opacity. It carries the softness of a 1240px still at full bleed. */
.grain {
  position: absolute;
  inset: -6%;
  z-index: 9;
  pointer-events: none;
  opacity: .07;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='320' height='320'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: 320px 320px;
  animation: grain .9s steps(1) infinite;
  will-change: transform;
}
@keyframes grain {
  0%, 100% { transform: translate3d(0, 0, 0); }
  20%       { transform: translate3d(-2%, 1%, 0); }
  40%       { transform: translate3d(1%, -2%, 0); }
  60%       { transform: translate3d(-1%, -1%, 0); }
  80%       { transform: translate3d(2%, 2%, 0); }
}

/* ---- header ------------------------------------------------------ */

/* Fixed, outside the stage, so the menu is on screen at every scroll
   position; the z-index puts it above the stage's isolated stacking
   context and above the sections, and nothing inside the stage changes
   its order. The same box as before: logo left, the six links centred and
   quiet, the pill right, on a transparent ground. Once the stage has
   scrolled up past the header's bottom edge, script.js adds is-grounded
   (one observer on the stage, not a per-frame write): ink at .88 and a
   hairline beneath, so the sections read through it. Below 900 the row
   no longer fits six real labels beside the logo and the pill, so the
   links take a second row (see the breakpoints). */
.site-head {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  z-index: 20;
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  padding: 32px var(--gutter);
  color: var(--paper);
  text-shadow: 0 2px 16px rgba(0, 0, 0, .2);
  background: rgba(13, 18, 32, 0);
  transition: background-color .35s ease, box-shadow .35s ease;
}
.site-head.is-grounded {
  background: rgba(13, 18, 32, .88);
  box-shadow: 0 1px 0 var(--hair);
}
.brand { display: inline-block; justify-self: start; }
/* The wordmark.
   GWMC supplied it as a full-bleed dark plate with the type reversed out, and
   that plate is rgb(27,30,40) — twelve to fourteen points LIGHTER than this
   site's ink, so on the header it read as a grey rectangle in the corner, which
   the 2026-09-20 pre-flight named as the first thing making the site look
   unfinished. No blend mode fixes it: every one of them keeps whichever layer
   is lighter, and here that is the plate. So the matte was keyed out of the
   artwork instead and media/gwmc-logo.png now carries a real alpha channel.
   The original is beside it as gwmc-logo-original-plate.png. No radius: there
   is no plate left to round. Ask GWMC for an SVG and this all gets simpler. */
.brand img { height: 34px; width: auto; }

.site-nav { display: flex; gap: 34px; justify-content: center; }
.site-nav a {
  color: var(--paper);
  text-decoration: none;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: .01em;
  white-space: nowrap;
  opacity: .84;
  transition: opacity .25s ease;
}
.site-nav a:hover { opacity: 1; }

.head-cta { justify-self: end; display: flex; align-items: center; gap: 10px; }

/* ---- beat 1: the hero block ------------------------------------- */

/* Centred in the banner, on the owner's instruction (2026-09-05 23:40). The
   block sits at the stage's midpoint and the -50% pair in the transform keeps
   it there while the parallax and the scroll-out are added on top. */
.hero {
  position: absolute;
  left: 50%;
  top: 50%;
  width: min(84vw, 960px);
  z-index: 5;
  pointer-events: none;
  text-align: center;
  text-shadow: 0 2px 28px rgba(0, 0, 0, .45);
  transform-origin: 50% 50%;
  transform: translate3d(calc(-50% + var(--mx) * -4px), calc(-50% + var(--hero-y) + var(--my) * -3px), 0) scale(var(--hero-scale));
  opacity: var(--hero-opacity);
  will-change: transform, opacity;
}

/* A category's title (home.php), set as the headline is but with no
   eyebrow: the category large and a word to a line, and beneath it the
   line on what GWMC does for that kind of owner. The script writes
   each title's own rise, scale and opacity (--t-*): it comes up from below
   as its photograph settles and leaves the way the headline does. Opacity
   alone, never hidden, so a screen reader reads every category in order. */
.scene-title {
  position: absolute;
  left: 50%;
  top: 50%;
  width: min(84vw, 960px);
  z-index: 5;
  pointer-events: none;
  text-align: center;
  text-shadow: 0 2px 28px rgba(0, 0, 0, .45);
  transform-origin: 50% 50%;
  transform: translate3d(calc(-50% + var(--mx) * -4px), calc(-50% + var(--t-y, 60px) + var(--my) * -3px), 0) scale(var(--t-scale, 1));
  opacity: var(--t-opacity, 0);
  will-change: transform, opacity;
}

.hero .eyebrow {
  font: 500 16px/1.4 var(--body);
  color: var(--paper-2);
  margin: 0 0 22px;
}
.hero h1,
.scene-title h2 {
  font-size: clamp(3.25rem, 7vw, 7.5rem);
  font-weight: 700;
  line-height: .92;
  letter-spacing: -.03em;
  max-width: min-content;   /* one word to a line, whatever the words: as wide as the longest */
  margin: 0 auto 24px;
  color: var(--paper);
}
.standfirst {
  font-size: 1.15rem;
  line-height: 1.45;
  font-weight: 400;
  color: var(--paper);
  max-width: 30em;
  margin: 0 auto 28px;
}



/* ---- the categories' projects: story panels --------------------- */

.panel {
  position: absolute;
  left: 50%;
  width: min(1080px, 100vw - 2 * var(--gutter));
  z-index: 6;
  pointer-events: none;
  display: grid;
  grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
  gap: 24px 64px;
  align-items: start;
  will-change: transform, opacity;
}
/* Each category's project (home.php), its own values written by the script
   (--p-*): it rises 58px into place and leaves upwards. */
.scene-panel {
  top: 52%;
  opacity: var(--p-opacity, 0);
  transform: translate3d(-50%, var(--p-y, calc(-50% + 58px)), 0);
}
.panel .kicker { margin: 0 0 14px; }
.panel h3 {
  font-size: 4.4rem;
  font-weight: 700;
  line-height: .95;
  letter-spacing: -.03em;
  color: var(--paper);
  text-shadow: 0 16px 38px rgba(0, 0, 0, .32);
  margin: 0 0 22px;
}
.panel-copy {
  width: min(520px, 100%);
  font-size: 1.14rem;
  line-height: 1.38;
  color: var(--paper);
  text-shadow: 0 2px 18px rgba(0, 0, 0, .35);
}
.panel-side { padding-top: 34px; }

.facts {
  margin: 0;
  display: grid;
  gap: 26px;
  padding-top: 18px;
  border-top: 1px solid rgba(0, 174, 252, .45);   /* the one hairline */
}
.facts dt {
  font-family: var(--display);
  font-size: clamp(2.2rem, 3vw, 3rem);
  font-weight: 700;
  line-height: .95;
  letter-spacing: -.03em;
  color: var(--paper);
  text-shadow: 0 10px 30px rgba(0, 0, 0, .3);
}
.facts dd {
  margin: 10px 0 0;
  font-size: 1rem;
  line-height: 1.35;
  color: var(--paper-2);
  max-width: 34ch;
}
.panel-cta {
  min-height: 50px;
  padding: 0 28px;
  margin-top: 30px;
}
.scene-panel.is-live .panel-cta { pointer-events: auto; }
/* Not a tab stop while its panel is faded out: pointer-events does not
   gate the keyboard, so the link is hidden the way the frames and the
   strip are, on the same is-live threshold. */
.scene-panel:not(.is-live) .panel-cta { visibility: hidden; }

/* ---- beat 4: the projects marquee ------------------------------- */

/* The scene. It lives in the scaled world and is counter-scaled by
   1/backScale about its top-left, so screen px == marquee px and the
   perspective below is in screen terms. The mask is the reference's
   vignette: frames are born out of and dissolve into the dark rather
   than being clipped. Nothing on this element transitions; the track is
   moved every frame by the script.

   The strip can be taken hold of and dragged either way (script.js,
   the marquee's pointer handlers): grab at rest, grabbing while held,
   on the links too, since the whole strip is the handle. pan-y leaves
   vertical page scrolling to the browser on a phone and hands us the
   horizontal moves. While a drag is on, nothing in the strip can be
   selected; the photographs and links never start a native drag. */
.sights-slider {
  position: absolute;
  left: var(--sights-left);
  top: var(--sights-top);
  width: 100vw;
  z-index: 3;
  visibility: var(--sights-visibility);
  opacity: var(--sights-opacity);
  transform-origin: 0 0;
  transform: translate3d(var(--sights-enter-x), 0, 0) scale(var(--sights-scale));
  perspective: 1200px;
  perspective-origin: 50% 50%;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  will-change: transform, opacity;
  touch-action: pan-y;
  cursor: grab;
}
.sights-slider a { cursor: grab; }
.sights-slider.is-dragging,
.sights-slider.is-dragging a { cursor: grabbing; }
.sights-slider.is-dragging {
  -webkit-user-select: none;
          user-select: none;
}
.sight img, .sight a {
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
}
.sights-track {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  gap: var(--sight-gap);
  transform: translate3d(var(--sights-shift), 0, 0);
  transform-style: preserve-3d;
  will-change: transform;
  /* Out of hit-testing. In a preserve-3d context the track and its frames
     are depth-sorted together, and every frame but the one at dead centre
     is sunk behind the track's own plane by its translateZ; the track's
     transparent box then takes every press, and no link can be clicked.
     With the track stood aside the frames are hit, and a press in a gap
     lands on the slider, which holds the drag listeners either way.
     Measured with elementFromPoint along the strip, 2026-09-07. */
  pointer-events: none;
}
.sight { pointer-events: auto; }

/* One frame. 3:2, a hairline, no drop shadow: the depth is the turn, the
   blur and the vignette, as in the reference. The script writes the
   transform, the filter and the opacity of each frame every frame. */
.sight {
  position: relative;
  flex: 0 0 var(--sight-w);
  width: var(--sight-w);
  height: var(--sight-h);
  border: 1px solid var(--hair);
  border-radius: var(--sight-radius);
  overflow: hidden;
  background: var(--ink);
  color: var(--paper);
  transform-origin: 50% 50%;
  backface-visibility: hidden;
  will-change: transform, filter, opacity;
}
/* The ring is on every copy of the focused frame, so it is on the one that
   is visible; the link's own outline would be on a copy that may be off
   the screen. */
.sight.is-centred { box-shadow: 0 0 0 2px var(--cyan); }

/* A photograph with its caption in a strip beneath, inside the frame. */
.sight-photo a {
  display: grid;
  grid-template-rows: minmax(0, 1fr) var(--sight-strip);
  height: 100%;
  color: var(--paper);
  text-decoration: none;
  outline: none;
}
.sight-photo img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 58%;   /* the beach house: keep the two floors and the pool edge */
}
.sight-caption {
  display: flex;
  align-items: center;
  padding: 0 14px;
  font: 500 13px/1.2 var(--body);
  background: var(--ink);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* A project with no publishable photograph yet: the name set large on the
   ink plate, its kicker above. The film grain over the stage textures it. */
.sight-type a {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  height: 100%;
  padding: 14px 16px;
  color: var(--paper);
  text-decoration: none;
  outline: none;
}
.sight-kicker {
  font: 500 12px/1.3 var(--body);
  color: var(--paper-2);
}
.sight-name {
  font: 700 clamp(19px, 1.8vw, 26px)/1 var(--display);
  letter-spacing: -.025em;
  color: var(--paper);
  max-width: 8em;
  text-wrap: balance;
}

/* The heading block, in screen space, centred over the symmetric strip. */
.sights-head {
  position: absolute;
  left: 50%;
  top: 27vh;
  width: min(640px, 100vw - 2 * var(--gutter));
  z-index: 7;
  transform: translateX(-50%);
  text-align: center;
  opacity: var(--sights-controls-opacity);
  pointer-events: none;
  text-shadow: 0 2px 18px rgba(0, 0, 0, .4);
}
.sights-head h2 {
  font-size: clamp(1.9rem, 3.4vw, 2.8rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -.03em;
  margin: 0 0 16px;
}
.sights-head p {
  font-size: 1.08rem;
  line-height: 1.45;
  color: var(--paper);
  max-width: 52ch;
  margin: 0 auto;
}

/* The two buttons exist for prefers-reduced-motion, where the strip does not
   move on its own; otherwise it is ambient and has no controls. */
.sights-controls {
  position: absolute;
  left: 50%;
  top: calc(var(--sights-screen-top) + var(--sight-h) + 28px);
  z-index: 7;
  display: none;
  gap: 10px;
  transform: translateX(-50%);
  opacity: var(--sights-controls-opacity);
  pointer-events: none;
}
.sights-controls.is-ready { pointer-events: auto; }
/* Under reduced motion the buttons are display:flex from the start but
   fade in at 3780px; keep them out of the tab order until is-ready. */
.sights-controls:not(.is-ready) { visibility: hidden; }
.sights-controls button {
  width: 54px;
  height: 54px;
  border-radius: 50%;
  border: 0;
  padding: 0;
  background: var(--paper);
  color: var(--ink);
  display: grid;
  place-items: center;
  cursor: pointer;
  box-shadow: 0 10px 30px rgba(13, 18, 32, .3);
}

/* ==================================================================
   Below the stage
   ================================================================== */

.wrap {
  width: min(1180px, 100% - 2 * var(--gutter));
  margin: 0 auto;
}

.band { padding: 128px 0; position: relative; scroll-margin-top: var(--head-h); }
.band + .band { border-top: 1px solid var(--hair); }

.band-head { max-width: 40ch; margin-bottom: 64px; }
.band h2 {
  font-size: clamp(2.2rem, 4vw, 3.4rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -.03em;
}

/* About Us and FAQ: the heading in a narrow left column, the reading in
   the right — a spread, not a heading over a column of text. The heading
   stays in view while the reading scrolls past it. */
.spread {
  display: grid;
  grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr);
  gap: 48px 64px;
}
.spread-head {
  position: sticky;
  top: 128px;
  align-self: start;
}
.spread-head h2 { max-width: 14ch; }

.reading p {
  font-size: 1.14rem;
  line-height: 1.6;
  color: var(--paper-2);
  max-width: 62ch;
}
.reading p + p { margin-top: 1.2em; }

.services {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 56px 64px;
}
.service {
  padding-top: 22px;
  border-top: 1px solid var(--hair);
}
.service h3 {
  font-size: 1.4rem;
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: 1.1;
  margin-bottom: 14px;
}
.service p {
  font-size: 1.02rem;
  line-height: 1.6;
  color: var(--paper-2);
  max-width: 46ch;
}

/* Five questions, rules between them, no markers: they are not a sequence. */
.faq-list { margin: 0; }
.faq-list > div { padding: 28px 0; border-top: 1px solid var(--hair); }
.faq-list > div:first-child { padding-top: 0; border-top: 0; }
.faq-list dt {
  font-family: var(--display);
  font-size: 1.3rem;
  font-weight: 600;
  letter-spacing: -.02em;
  line-height: 1.15;
  color: var(--paper);
  max-width: 30ch;
  text-wrap: balance;
}
.faq-list dd {
  margin: 12px 0 0;
  font-size: 1.02rem;
  line-height: 1.6;
  color: var(--paper-2);
  max-width: 60ch;
}
.faq-close { margin-top: 64px; }
.faq-close h3 {
  font-size: 1.6rem;
  font-weight: 700;
  letter-spacing: -.02em;
  line-height: 1.1;
  margin-bottom: 12px;
}
.faq-close p {
  font-size: 1.08rem;
  line-height: 1.55;
  color: var(--paper-2);
  max-width: 56ch;
}

.contact {
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, .75fr);
  gap: 64px;
  align-items: center;
}
.contact .kicker { margin-bottom: 16px; }
.contact h2 { margin-bottom: 22px; }
.lede {
  font-size: 1.14rem;
  line-height: 1.5;
  max-width: 44ch;
  margin-bottom: 36px;
}
.mail-link {
  display: inline-block;
  font: 600 clamp(1.5rem, 2.6vw, 2.2rem)/1.1 var(--display);
  letter-spacing: -.02em;
  color: var(--paper);
  text-decoration: none;
  border-bottom: 1px solid rgba(255, 255, 255, .35);
  padding-bottom: 4px;
  transition: border-color .25s ease;
}
.mail-link:hover { border-color: var(--paper); }

.portrait { margin: 0; justify-self: end; max-width: 300px; }
.portrait img { border-radius: 22px; }
.portrait figcaption {
  margin-top: 14px;
  font: 400 13px/1.4 var(--body);
  color: var(--muted);
}

/* The footer is an address, set as one. */
.site-foot {
  border-top: 1px solid var(--hair);
  padding: 40px 0 48px;
  font-size: 14px;
  line-height: 1.6;
  color: var(--muted);
}
.site-foot .wrap {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: flex-start;
  gap: 16px 32px;
}
.site-foot address { font-style: normal; }
.site-foot strong { font-weight: 500; color: var(--paper-2); }
.site-foot a {
  color: var(--paper-2);
  text-decoration: none;
  border-bottom: 1px solid var(--hair);
  transition: border-color .25s ease;
}
.site-foot a:hover { border-color: var(--paper-2); }

/* ==================================================================
   Breakpoints: 1500 / 1100 (the reference's), 900 (the hero's
   composition swap, set by the photograph; the header's second row),
   640 (the phone).
   ================================================================== */

@media (max-width: 1500px) {
  .panel h3 { font-size: 3.9rem; }
}

@media (max-width: 1100px) {
  :root { --gutter: 32px; }
  /* The header takes two rows: brand and pills, then the six links. Where
     the row is wider than the screen it scrolls sideways, with no
     scrollbar; the script fades its right edge only while there is more.
     From 1100 rather than 900 since the Project Portal pill joined "Get in
     touch" (2026-09-26): one row no longer holds the logo, the six links
     and both pills below about 1100 (measured: the links met the pills at
     1024 and ran under the logo at 920). */
  .site-head {
    grid-template-columns: 1fr auto;
    grid-template-areas: "brand cta" "nav nav";
    row-gap: 14px;
  }
  .brand { grid-area: brand; }
  .head-cta { grid-area: cta; }
  .site-nav {
    grid-area: nav;
    justify-content: flex-start;
    gap: 24px;
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
    padding: 6px 32px 6px 0;
    margin: -6px 0;
  }
  .site-nav::-webkit-scrollbar { display: none; }
  .site-nav.is-overflowing:not(.is-at-end) {
    -webkit-mask-image: linear-gradient(90deg, #000 calc(100% - 56px), transparent);
            mask-image: linear-gradient(90deg, #000 calc(100% - 56px), transparent);
  }
  .panel { grid-template-columns: minmax(0, 1fr); gap: 20px; width: min(720px, 100vw - 2 * var(--gutter)); }
  .panel h3 { font-size: 3rem; }
  .panel-copy { width: min(560px, 100%); font-size: 1.05rem; }
  .panel-side { padding-top: 0; }
  .facts { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; padding-top: 16px; }
  .facts dt { font-size: 2.2rem; }
  .facts dd { font-size: .95rem; }
  .panel-cta { margin-top: 22px; }
  .scene-panel { top: 50%; }
  .site-nav { gap: 22px; }
  .site-nav a { font-size: 14px; }
  .services { gap: 44px 40px; }
  .spread { grid-template-columns: minmax(0, 1fr); gap: 32px; }
  .spread-head { position: static; }
  .spread-head h2 { max-width: 20ch; }
  .contact { grid-template-columns: minmax(0, 1fr); }
  .portrait { justify-self: start; max-width: 240px; }
}

/* Below 900px the cover-crop of a photograph no longer has a quiet field
   around its middle: the frame shows the centre of the building top to
   bottom. So the building goes above the words. The headline and every
   category's title are anchored to the bottom, the grade rises from the
   bottom, and the upper floors and sky stay clear. */
@media (max-width: 900px) {
  /* Portrait frames put the building across the whole upper half, so the
     blocks keep to the lower half here — still centred left-to-right. */
  .hero {
    top: auto;
    bottom: clamp(40px, 7vh, 72px);
    width: calc(100vw - 2 * var(--gutter));
    transform-origin: 50% 100%;
    transform: translate3d(calc(-50% + var(--mx) * -4px), calc(var(--hero-y) + var(--my) * -3px), 0) scale(var(--hero-scale));
  }
  .scene-title {
    top: auto;
    bottom: clamp(40px, 7vh, 72px);
    width: calc(100vw - 2 * var(--gutter));
    transform-origin: 50% 100%;
    transform: translate3d(calc(-50% + var(--mx) * -4px), calc(var(--t-y, 60px) + var(--my) * -3px), 0) scale(var(--t-scale, 1));
  }
  .hero .eyebrow { font-size: 14px; margin-bottom: 14px; }
  .hero h1, .scene-title h2 { margin-bottom: 18px; }
  .standfirst { font-size: 1.02rem; line-height: 1.45; max-width: 30em; margin-bottom: 24px; }

  .scene-frame .grade {
    background: linear-gradient(180deg,
      rgba(13, 18, 32, .24) 0%,
      rgba(13, 18, 32, 0)   20%,
      rgba(13, 18, 32, 0)   42%,
      rgba(13, 18, 32, .56) 62%,
      rgba(13, 18, 32, .86) 100%);
  }
}

@media (max-width: 640px) {
  :root { --gutter: 20px; --sight-strip: 34px; --sight-radius: 12px; }
  body { font-size: 16px; }
  .site-head { padding: 20px var(--gutter); row-gap: 12px; }
  /* The mark alone on a phone: the full lockup, both pills and the gaps do
     not fit one row (the owner chose the monogram, 2026-09-26). Cropped,
     not swapped: 26px of the 28px-high logo is its first 100 source px, the
     GW/MC mark (columns 6-92), short of the divider at 112. */
  .brand img { width: 26px; height: 28px; object-fit: cover; object-position: 0 50%; }
  .pill-btn { min-height: 38px; padding: 0 16px; font-size: 13px; }
  .head-cta { gap: 8px; }

  .hero h1, .scene-title h2 { font-size: 3.25rem; line-height: .94; }

  .panel h3 { font-size: 2.3rem; margin-bottom: 14px; }
  .panel .kicker { font-size: 14px; margin-bottom: 10px; }
  .panel-copy { font-size: .98rem; line-height: 1.4; }
  .facts { grid-template-columns: minmax(0, 1fr); gap: 18px; }   /* stacked */
  .facts dt { font-size: 1.85rem; }
  .facts dd { font-size: .9rem; margin-top: 6px; }
  .panel-cta { min-height: 44px; padding: 0 20px; }
  .scene-panel { top: 50%; }

  .sights-head { top: 20vh; }
  .sights-head h2 { font-size: 2rem; margin-bottom: 12px; }
  .sights-head p { font-size: .98rem; }
  .sight-caption { font-size: 12px; padding: 0 12px; }
  .sight-type a { padding: 12px 14px; }
  /* Buttons under the thumb: screen-anchored to the bottom, not to the row. */
  .sights-controls { top: auto; bottom: 36px; gap: 12px; }
  .sights-controls button { width: 52px; height: 52px; }

  .band { padding: 88px 0; }
  .band-head { margin-bottom: 40px; }
  .services { grid-template-columns: minmax(0, 1fr); gap: 36px; }
  .reading p { font-size: 1.02rem; }
  .faq-list > div { padding: 22px 0; }
  .faq-list dt { font-size: 1.15rem; }
  .faq-close { margin-top: 48px; }
  .site-foot .wrap { flex-direction: column; gap: 14px; }
}

/* ==================================================================
   Reduced motion, exactly as the reference: no smooth scrolling, no
   layer transitions. script.js bypasses the inertia and the pointer
   parallax on the same query, so the composition still scrubs, without
   momentum. The marquee does not auto-scroll; its two buttons appear.
   ================================================================== */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .stage *, .stage *::before, .stage *::after { transition: none !important; }
  .grain { animation: none; }
  .pill-btn { transition: none; }
  .site-head { transition: none; }
  .sights-controls { display: flex; }
}

/* ==================================================================
   The inner pages (v3.3). Same tokens, same register as the bands
   below the stage: on ink, real headings, no cards. Each page opens
   with its hero — the client's heading, standfirst and button — and
   the header is transparent over it and grounded once it has passed,
   as over the stage.
   ================================================================== */

.page-hero {
  position: relative;
  padding: calc(var(--head-h) + 72px) 0 72px;
}
.page-hero h1 {
  font-size: clamp(2.7rem, 5.4vw, 4.8rem);
  font-weight: 700;
  line-height: .96;
  letter-spacing: -.03em;
  max-width: 13ch;
}
.page-hero .kicker { margin-bottom: 18px; }
.page-lede {
  font-size: 1.18rem;
  line-height: 1.5;
  color: var(--paper-2);
  max-width: 46ch;
  margin-top: 28px;
}
.page-hero .pill-btn { margin-top: 36px; }

/* The first band after a hero sits close to it. */
.band-first { padding-top: 40px; }
/* About Us closes with the client portal, under the team strip. .band + .band
   does not reach it — the strip is a gallery-band — so it carries its own. */
.band-portal { border-top: 1px solid var(--hair); }

/* Services: one row per service, the title left and the paragraph right,
   a rule between rows. */
.rows { border-top: 1px solid var(--hair); }
.row {
  display: grid;
  grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr);
  gap: 18px 64px;
  padding: 56px 0;
  border-bottom: 1px solid var(--hair);
  scroll-margin-top: var(--head-h);
}
.row h2 {
  font-size: clamp(1.7rem, 2.8vw, 2.4rem);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -.025em;
  max-width: 14ch;
}
.row p {
  font-size: 1.14rem;
  line-height: 1.6;
  color: var(--paper-2);
  max-width: 58ch;
}

/* About: the portrait and the button in the held column, the reading beside. */
.spread-about .spread-head { display: grid; gap: 28px; justify-items: start; }
.spread-about .portrait { justify-self: start; max-width: 300px; }


/* Contact, and the FAQ page's contact block: the details or the
   introduction left, the form right. */
.contact-page {
  display: grid;
  grid-template-columns: minmax(0, .8fr) minmax(0, 1.2fr);
  gap: 48px 80px;
  align-items: start;
}
.contact-page .kicker { margin-bottom: 16px; }
.contact-page h2 { margin-bottom: 22px; }
.contact-details {
  display: grid;
  gap: 24px;
  justify-items: start;
  position: sticky;
  top: calc(var(--head-h) + 32px);
}
.contact-details address {
  font-style: normal;
  font-size: 1.08rem;
  line-height: 1.55;
  color: var(--paper-2);
}

/* The home's close: the button and, quieter, the address beside it. */
.contact-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 18px 28px; }
.mail-link--quiet { font: 500 1.05rem/1.4 var(--body); letter-spacing: 0; }

/* The enquiry form. Fields on ink: a hairline box, paper type, the focus
   in cyan like every other focus on the site. 16px in the fields so a
   phone does not zoom into them. */
.form { display: grid; gap: 22px; }
.form-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 22px; }
.field { display: grid; gap: 8px; }
.field label { font: 500 15px/1.4 var(--body); color: var(--paper-2); }
.field input,
.field select,
.field textarea {
  width: 100%;
  min-height: 50px;
  padding: 12px 16px;
  font: 400 16px/1.45 var(--body);
  color: var(--paper);
  background: rgba(255, 255, 255, .04);
  border: 1px solid rgba(255, 255, 255, .2);
  border-radius: 12px;
  outline: none;
  transition: border-color .2s ease, background-color .2s ease;
}
.field input:hover, .field select:hover, .field textarea:hover { border-color: rgba(255, 255, 255, .34); }
.field input:focus, .field select:focus, .field textarea:focus {
  border-color: var(--cyan);
  background: rgba(255, 255, 255, .06);
}
.field textarea { min-height: 168px; resize: vertical; }
.field select {
  appearance: none;
  -webkit-appearance: none;
  padding-right: 44px;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 6l4.5 4.5L12.5 6' fill='none' stroke='%23FFFFFF' stroke-opacity='.74' stroke-width='1.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 16px center;
}
.field select option { color: var(--ink); background: var(--paper); }
.hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.form-status {
  font-size: 15px;
  line-height: 1.5;
  color: var(--paper);
  padding: 14px 16px;
  border: 1px solid var(--hair);
  border-radius: 12px;
}
.form-status:empty { display: none; }
.form-foot { display: flex; flex-wrap: wrap; align-items: center; gap: 16px 24px; }
.form-note { font: 400 13px/1.4 var(--body); color: var(--muted); }

@media (max-width: 1100px) {
  :root { --head-h: 144px; }   /* two rows: brand and pills, then the links; the script measures the real one */
  .page-hero { padding-bottom: 56px; }
  .row { grid-template-columns: minmax(0, 1fr); gap: 14px; padding: 44px 0; }
  .spread-about .spread-head { order: 2; position: static; }
  .spread-about .portrait { max-width: 240px; }
  .contact-page { grid-template-columns: minmax(0, 1fr); gap: 40px; }
  .contact-details { position: static; }
}

@media (max-width: 640px) {
  :root {
    --head-h: 112px;
    /* The marquee's frames are portrait, 9:16: the shape of the phone, and
       of a card that can be held. Wide enough to be the strip's one clear
       frame, capped by the height of the screen so the strip sits between
       the heading block and the bottom of the stage (script.js holds its
       top clear of the heading). svh, so a phone's address bar showing and
       hiding does not resize the frames mid-scroll. */
    --sight-w: min(clamp(196px, 57vw, 236px), calc(46svh * 9 / 16));
    --sight-h: calc(var(--sight-w) * 16 / 9);
    --sight-strip: 40px;
  }
  .sight-name { font-size: 26px; max-width: 7em; }
  .sight-caption { font-size: 13px; }

  .page-hero { padding: calc(var(--head-h) + 40px) 0 44px; }
  .page-hero h1 { font-size: 2.5rem; line-height: .98; }
  .page-lede { font-size: 1.05rem; margin-top: 20px; }
  .page-hero .pill-btn { margin-top: 28px; }
  .band-first { padding-top: 24px; }
  .row { padding: 36px 0; }
  .row p, .reading p { font-size: 1.02rem; }
  .form-row { grid-template-columns: minmax(0, 1fr); }
  .form-foot { flex-direction: column; align-items: flex-start; gap: 14px; }
  .site-foot .wrap address { margin-bottom: 4px; }
}

/* ==================================================================
   The Projects page (v3.4): one photograph, then the tiles.
   The hero is the client's copy for the page over the Bowl — the same
   composition as the home's opening, static, with one load sequence:
   the photograph settles from 1.06, the two lines of the heading rise
   out of a mask, the kicker, the sentence and the button follow.
   ================================================================== */

.photo-hero {
  position: relative;
  min-height: min(88vh, 900px);
  display: flex;
  align-items: flex-end;
  padding: calc(var(--head-h) + 120px) 0 clamp(48px, 9vh, 96px);
  overflow: hidden;
  isolation: isolate;
  background: var(--ink);
  color: var(--paper);
  text-shadow: 0 2px 22px rgba(0, 0, 0, .45);
}
.photo-hero .ground { position: absolute; inset: 0; z-index: -1; }
.photo-hero .ground img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 45%;
  animation: hero-settle 1.8s cubic-bezier(.22, 1, .36, 1) both;
}
.photo-hero .ground .grade {
  position: absolute;
  inset: 0;
  background: linear-gradient(180deg,
    rgba(13, 18, 32, .46) 0%,
    rgba(13, 18, 32, .06) 30%,
    rgba(13, 18, 32, .10) 56%,
    rgba(13, 18, 32, .82) 100%);
}
/* A project with no photograph.
   project.php has emitted this modifier since it was written and nothing
   styled it, because until 2026-09-19 the only project without a photograph
   also had no write-up and so never got a page. Malibu now has the write-up
   and still has no photograph, so the hero has to stand on type alone.
   Three changes from the photographic hero: no text-shadow, which over a flat
   ground only muddies the letterforms; less height, because 88vh of empty ink
   reads as a page that failed to load rather than as a decision; and a wash
   off the top-left corner so the ground has some depth to it. The ink and the
   paper are the ones the rest of the site uses, so it is recognisably the
   same hero with its photograph absent. */
.photo-hero--type {
  min-height: min(62vh, 620px);
  padding-top: calc(var(--head-h) + 72px);
  text-shadow: none;
  background:
    radial-gradient(120% 90% at 8% 0%, rgba(0, 174, 252, .10) 0%, rgba(0, 174, 252, 0) 58%),
    var(--ink);
  border-bottom: 1px solid var(--hair);
}
.photo-hero--type h1 { max-width: 16ch; }
.photo-hero--type .page-lede { color: var(--paper-2); }

.photo-hero .wrap { position: relative; }
.photo-hero .kicker { margin-bottom: 18px; animation: hero-rise .9s cubic-bezier(.22, 1, .36, 1) .1s both; }
.photo-hero h1 {
  font-size: clamp(2.8rem, 6.2vw, 5.8rem);
  font-weight: 700;
  line-height: .94;
  letter-spacing: -.03em;
  max-width: 13ch;
}
.photo-hero h1 .line { display: block; overflow: hidden; padding-bottom: .06em; margin-bottom: -.06em; }
.photo-hero h1 .line > span { display: block; animation: hero-line 1.1s cubic-bezier(.22, 1, .36, 1) both; }
.photo-hero h1 .line:nth-child(2) > span { animation-delay: .14s; }
.photo-hero .page-lede { color: var(--paper); margin-top: 24px; max-width: 42ch; animation: hero-rise .9s cubic-bezier(.22, 1, .36, 1) .5s both; }
.photo-hero .facts { margin-top: 30px; max-width: 340px; animation: hero-rise .9s cubic-bezier(.22, 1, .36, 1) .5s both; }
.photo-hero .pill-btn { margin-top: 30px; animation: hero-rise .9s cubic-bezier(.22, 1, .36, 1) .68s both; }

@keyframes hero-rise   { from { opacity: 0; transform: translateY(22px); } to { opacity: 1; transform: none; } }
@keyframes hero-line   { from { transform: translateY(108%); } to { transform: none; } }
@keyframes hero-settle { from { transform: scale(1.06); } to { transform: none; } }

/* The tiles: every project, the client's four first. A photograph at 3:2
   with the name and the place beneath; the photograph eases in a little
   under the pointer. A project with no publishable photograph is its name
   on ink, as on the home's strip. */
.tiles {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 44px 32px;
}
.tile a { display: block; color: var(--paper); text-decoration: none; outline-offset: 6px; }
.tile-media {
  position: relative;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  border-radius: 16px;
  border: 1px solid var(--hair);
  background: var(--ink);
}
.tile-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transform: scale(1.001);
  transition: transform 1.1s cubic-bezier(.22, 1, .36, 1);
}
.tile a:hover .tile-media img, .tile a:focus-visible .tile-media img { transform: scale(1.045); }
.tile-media .plate-name {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-end;
  padding: 18px 20px;
  font: 700 clamp(1.5rem, 2.2vw, 2rem)/1 var(--display);
  letter-spacing: -.025em;
  color: var(--paper);
}
/* The small line is the project's place, and the client's projects have
   none, so the line is always there and holds its height, empty or not:
   in a row that mixes the two, the names stay level. */
.tile-kicker { display: block; min-height: 1.4em; margin: 14px 0 6px; font: 500 14px/1.4 var(--body); color: var(--muted); }
.tile h3 { font-size: 1.35rem; font-weight: 700; letter-spacing: -.02em; line-height: 1.1; }

/* The Projects page's categories. A bar under the header jumps to each
   (no counts since 2026-10-03, the owner's call), and marks the one in view (script.js, is-current); on a
   narrow screen it scrolls sideways, with no scrollbar. Each category is a
   section: its name and its line, then its tiles. The name and the line sit
   on the tiles' own grid — the name in the first column, the line from the
   second, starting where the second tile does — and are centred on each
   other, as the owner asked, so the head and the tiles share their edges
   (2026-09-26: set apart on a grid of their own, they did not). */
.category-bar {
  position: sticky;
  top: var(--head-h);
  z-index: 15;
  background: rgba(13, 18, 32, .9);
  -webkit-backdrop-filter: blur(10px);
          backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--hair);
}
.category-bar .wrap {
  display: flex;
  gap: 10px;
  padding: 14px 0;
  overflow-x: auto;
  scrollbar-width: none;
}
.category-bar .wrap::-webkit-scrollbar { display: none; }
.category-bar a {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  padding: 8px 16px;
  border: 1px solid var(--hair);
  border-radius: 999px;
  font: 500 14px/1.3 var(--body);
  color: var(--paper-2);
  text-decoration: none;
  white-space: nowrap;
  transition: color .25s ease, border-color .25s ease, background-color .25s ease;
}
.category-bar a:hover,
.category-bar a.is-current { color: var(--paper); border-color: rgba(255, 255, 255, .45); background: rgba(255, 255, 255, .06); }
.category { padding: 80px 0 32px; scroll-margin-top: calc(var(--head-h) + 60px); }
.category + .category { border-top: 1px solid var(--hair); }
.category-head {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));   /* the tiles' columns */
  column-gap: 32px;                                    /* and their gap */
  row-gap: 12px;
  align-items: center;
  margin-bottom: 40px;
}
.category-head h2 { grid-column: 1; font-size: clamp(2.2rem, 4vw, 3.4rem); font-weight: 700; line-height: 1; letter-spacing: -.03em; }
.category-head p { grid-column: 2 / -1; margin: 0; max-width: 52ch; font-size: 1.1rem; line-height: 1.5; color: var(--paper-2); }
@media (max-width: 1100px) {
  /* Two tiles across here (gap 24px), so two columns: the name, the line. */
  .category-head { grid-template-columns: repeat(2, minmax(0, 1fr)); column-gap: 24px; }
  .category-head p { grid-column: 2; }
}
@media (max-width: 640px) {
  .category-bar .wrap { padding: 10px 0; gap: 8px; }
  .category { padding: 52px 0 16px; }
  .category-head { grid-template-columns: minmax(0, 1fr); margin-bottom: 26px; }
  .category-head p { grid-column: 1; font-size: 1rem; }
}
@media (prefers-reduced-motion: reduce) {
  .category-bar a { transition: none; }
}

/* ==================================================================
   A project's page: the photograph, the write-up, the gallery.
   ================================================================== */

/* The write-up as a spread: the place and the figure held in the left
   column, the paragraphs in the right. */
.project-body { padding: 88px 0 56px; }
.spread-project .spread-head { display: grid; gap: 22px; justify-items: start; }
.project-place {
  font: 700 clamp(1.6rem, 2.6vw, 2.2rem)/1.05 var(--display);
  letter-spacing: -.025em;
  color: var(--paper);
}
.spread-project .facts { max-width: 320px; }
.project-photos-link {
  font: 500 15px/1.4 var(--body);
  color: var(--paper-2);
  text-decoration: none;
  border-bottom: 1px solid var(--hair);
  padding-bottom: 3px;
  transition: color .25s ease, border-color .25s ease;
}
.project-photos-link:hover { color: var(--paper); border-color: var(--paper); }

/* The gallery: a screen of its own. The home's marquee, tuned for
   frames the size of a cinema screen (script.js, createMarquee reads the
   --sight-* properties below at build): the front frame flat and sharp
   at up to 1100px wide, its neighbours — one pitch away, so fully turned
   — leaning away either side, sunk and dimmed, the edges dissolving into
   the ink. Same drift, drag and fling as the home's strip; the two
   buttons shown always, since a gallery is the reader's to browse. */
.gallery-band { padding: 0 0 48px; scroll-margin-top: var(--head-h); }
.gallery-stage {
  min-height: 100vh;
  display: grid;
  align-content: center;
  gap: 28px;
  padding: calc(var(--head-h) + 24px) 0 48px;
}
.gallery {
  --sight-w: min(66vw, 1100px);
  --sight-h: calc(var(--sight-w) * 2 / 3);
  --sight-gap: clamp(20px, 2.6vw, 44px);
  --sight-radius: 20px;
  --sight-turn: 26;
  --sight-sink: 260;
  --sight-blur: 4;
  --sight-dim: .5;
  --sight-reach: pitch;
  position: relative;
  height: var(--sight-h);
  width: 100vw;
  overflow: hidden;
  perspective: 1800px;
  perspective-origin: 50% 50%;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
  touch-action: pan-y;
  cursor: grab;
}
.gallery a { cursor: grab; }
.gallery.is-dragging, .gallery.is-dragging a { cursor: grabbing; }
.gallery.is-dragging { -webkit-user-select: none; user-select: none; }
.gallery .sights-track { position: absolute; top: 0; left: 0; }
/* No caption strip inside these frames: the name is under the strip. A
   screen, not a card: no hairline, the corners barely turned. */
.gallery .sight-photo a { grid-template-rows: minmax(0, 1fr); }
.gallery .sight { border: 0; border-radius: 8px; box-shadow: 0 30px 80px rgba(0, 0, 0, .5); }

/* Under the strip: the name, the counter, the buttons. */
.gallery-foot {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 24px;
}
.gallery-caption { font: 500 15px/1.4 var(--body); color: var(--paper-2); }
.gallery-name { color: var(--paper); }
.gallery-count { margin-left: 14px; color: var(--muted); font-variant-numeric: tabular-nums; }
.gallery-controls { display: flex; gap: 12px; }
.gallery-controls button {
  width: 54px;
  height: 54px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .22);
  padding: 0;
  background: transparent;
  color: var(--paper);
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: background-color .25s ease, border-color .25s ease;
}
.gallery-controls button:hover { background: rgba(255, 255, 255, .08); border-color: rgba(255, 255, 255, .4); }

/* About: Meet the Team. The gallery above with people for photographs:
   portrait frames at the portrait's 4:5, smaller than a gallery's cinema
   screen so three or four stand in the strip at once, the front one flat
   and sharp and its neighbours turned away. Under it the person in front,
   set as the home's panels set a project — the role as the kicker, the
   name large, their line at the panel's measure — with the counter and
   the buttons beside. A screen of its own: one event, as the quality bar
   asks. The person comes in with a short rise when the front changes,
   which is the strip answering the reader, not decoration. */
.team-band { padding: 0; border-top: 1px solid var(--hair); }
.team-stage { padding: calc(var(--head-h) + 16px) 0 40px; gap: 30px; }
.team-heading {
  font-size: clamp(2.2rem, 4vw, 3.4rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -.03em;
}
.gallery-team {
  /* Capped by the screen's height too, so the heading, the strip and the
     person all fit in one screen below the header. */
  --sight-w: min(clamp(240px, 24vw, 340px), calc(40svh * 4 / 5));
  --sight-h: calc(var(--sight-w) * 5 / 4);
  --sight-gap: clamp(18px, 2vw, 32px);
  --sight-turn: 30;
  --sight-sink: 220;
  --sight-blur: 3;
  --sight-dim: .55;
  --sight-pace: .33;   /* a new person every ~25s, so each can be read */
  perspective: 1400px;
}
.gallery-team .sight { border-radius: 20px; }
.gallery-team .sight-photo img { display: block; object-position: 50% 24%; }
.team-foot { align-items: flex-end; }
.team-person { max-width: 560px; }
.team-person .kicker { margin: 0 0 12px; }
.team-name {
  font-size: clamp(2.4rem, 4.2vw, 3.6rem);
  font-weight: 700;
  line-height: .95;
  letter-spacing: -.03em;
  margin: 0 0 18px;
}
/* A person's line is paragraphs, as the rest of the site's prose is: the
   script rebuilds them as the strip moves. */
.team-bio { margin: 0; max-width: 34em; }
.team-bio p { font-size: 1.1rem; line-height: 1.42; color: var(--paper-2); margin: 0; }
.team-bio p + p { margin-top: .85em; }
.team-person.is-new { animation: person-in .6s cubic-bezier(.22, 1, .36, 1) both; }
@keyframes person-in { from { opacity: 0; transform: translate3d(0, 10px, 0); } }
.team-side { display: grid; justify-items: end; gap: 16px; }
.team-side .gallery-count { margin: 0; }
@media (prefers-reduced-motion: reduce) {
  .team-person.is-new { animation: none; }
}

/* The opening of the write-up, set large: one statement, not a column. */
.project-lede {
  font-size: clamp(1.25rem, 1.7vw, 1.55rem);
  line-height: 1.45;
  color: var(--paper);
  max-width: 34em;
}

/* The paragraphs after the lede, on a project with no photograph to break
   the text up. Set smaller and quieter than the lede so the opening still
   leads, and measured at 38em: the lede's 34em is a standfirst's width and
   runs long over four or five paragraphs of continuous reading. */
.project-prose {
  margin-top: 1.1em;
  font-size: clamp(1rem, 1.15vw, 1.1rem);
  line-height: 1.62;
  color: var(--paper-2);
  max-width: 38em;
}
.project-prose + .pill-btn { margin-top: 36px; }

/* The plates: the photographs after the lead as an editorial sequence.
   No borders, no cards — a photograph is a surface. Four compositions
   (templates/project.php decides which takes which): a full-bleed spread
   with a slow parallax (script.js, the chrome block, writes --py); a pair
   at their own shapes, the second dropped; one portrait alone with room
   around it; a large one beside two small. Each opens the lightbox. */
.plates { padding: 24px 0 0; }
.plate + .plate { margin-top: clamp(48px, 9vh, 112px); }
.plate a { display: block; position: relative; overflow: hidden; background: var(--ink); outline-offset: 4px; }
.plate img { display: block; width: 100%; height: auto; }

.plate-full a { height: min(84vh, 920px); }
.plate-full img {
  position: absolute;
  left: 0;
  top: -8%;
  width: 100%;
  height: 116%;
  object-fit: cover;
  transform: translate3d(0, var(--py, 0%), 0);
  will-change: transform;
}

.plate-pair .wrap {
  display: grid;
  grid-template-columns: 7fr 5fr;
  gap: clamp(24px, 4vw, 64px);
  align-items: start;
}
.plate-pair a:nth-child(2) { margin-top: clamp(48px, 12vh, 150px); }

.plate-solo .wrap { display: grid; justify-content: center; }
.plate-solo a { width: min(46vw, 640px); }

.plate-trio .wrap { display: grid; grid-template-columns: 2fr 1fr; gap: clamp(20px, 2vw, 28px); align-items: stretch; }
.plate-trio a { height: 100%; }
.plate-trio > .wrap > a img { height: 100%; object-fit: cover; aspect-ratio: 3 / 2; }
.plate-trio .stack { display: grid; grid-template-rows: 1fr 1fr; gap: clamp(20px, 2vw, 28px); }
.plate-trio .stack img { height: 100%; object-fit: cover; }

/* A photograph eases in a little under the pointer, and nothing else. */
.plate img { transition: transform 1.2s cubic-bezier(.22, 1, .36, 1); }
.plate a:hover img, .plate a:focus-visible img { transform: scale(1.03); }
.plate-full a:hover img, .plate-full a:focus-visible img { transform: translate3d(0, var(--py, 0%), 0) scale(1.03); }

/* The lightbox: the photograph as large as the screen allows, alone on
   ink. The backdrop is near solid and blurs what little shows through, and
   the page beneath stops scrolling while it is open (html.is-lightbox,
   script.js), so neither the header nor a scrollbar sits under it. The
   project's name and the count beneath the photograph; the previous and
   next in the side gutters, clear of it; the close in the corner. The box
   fades in and each photograph fades in as it arrives. */
html.is-lightbox { overflow: hidden; }
.lightbox {
  border: 0;
  padding: 0;
  margin: 0;
  inset: 0;
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  background: transparent;
  color: var(--paper);
}
.lightbox[open] { animation: lightbox-in .3s ease both; }
.lightbox::backdrop {
  background: rgba(13, 18, 32, .97);
  -webkit-backdrop-filter: blur(12px);
          backdrop-filter: blur(12px);
}
@keyframes lightbox-in { from { opacity: 0; } }
.lightbox-frame {
  position: relative;
  height: 100%;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  padding: 76px 104px 28px;
  gap: 18px;
}
.lightbox img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  border-radius: 10px;
  touch-action: pan-y pinch-zoom;   /* a sideways swipe is the script's */
  transition: opacity .35s ease;
}
.lightbox img.is-loading { opacity: 0; }
.lightbox-caption {
  display: flex;
  justify-content: center;
  align-items: baseline;
  gap: 16px;
  margin: 0;
  min-height: 1.5em;
  font: 500 15px/1.5 var(--body);
  color: var(--paper);
}
.lightbox-count { color: var(--muted); font-variant-numeric: tabular-nums; }
.lightbox-close,
.lightbox-arrow {
  position: absolute;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, .22);
  background: rgba(255, 255, 255, .04);
  color: var(--paper);
  display: grid;
  place-items: center;
  cursor: pointer;
  padding: 0;
  transition: background-color .25s ease, border-color .25s ease;
}
.lightbox-close:hover,
.lightbox-arrow:hover { background: rgba(255, 255, 255, .1); border-color: rgba(255, 255, 255, .45); }
.lightbox-close { top: 16px; right: 24px; }
.lightbox-arrow { top: 50%; transform: translateY(-50%); }
.lightbox-arrow[data-dir="prev"] { left: 28px; }
.lightbox-arrow[data-dir="next"] { right: 28px; }
@media (prefers-reduced-motion: reduce) {
  .lightbox[open] { animation: none; }
  .lightbox img, .lightbox-close, .lightbox-arrow { transition: none; }
}

/* The close: the previous project and the next as two buttons, each with
   its category and its name; Previous on the left, Next on the right and
   set to it. A hairline frame at the portrait's radius, lit on hover, the
   arrow stepping the way it points. */
.project-cta { margin-top: 36px; }
.project-steps { margin: clamp(64px, 10vh, 120px) 0 clamp(56px, 8vh, 96px); }
.project-steps-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 20px;
}
.project-step {
  display: grid;
  align-content: start;
  gap: 8px;
  padding: 24px 28px 26px;
  border: 1px solid var(--hair);
  border-radius: 20px;
  color: var(--paper);
  text-decoration: none;
  transition: border-color .25s ease, background-color .25s ease;
}
.project-step:hover { border-color: rgba(255, 255, 255, .4); background: rgba(255, 255, 255, .04); }
.project-step:focus-visible { border-radius: 20px; }   /* the global ring's 4px would square the corners */
.project-step--next { text-align: right; justify-items: end; }
.step-dir {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font: 500 14px/1.4 var(--body);
  color: var(--muted);
}
.step-dir svg { transition: transform .35s cubic-bezier(.22, 1, .36, 1); }
.project-step--prev:hover .step-dir svg { transform: translateX(-4px); }
.project-step--next:hover .step-dir svg { transform: translateX(4px); }
.step-cat { margin-top: 10px; font: 500 15px/1.4 var(--body); color: var(--paper-2); }
.step-name { font: 700 clamp(1.5rem, 2.4vw, 2.2rem)/1 var(--display); letter-spacing: -.02em; }

@media (max-width: 1100px) {
  .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 36px 24px; }
  .spread-project .spread-head { order: 0; }
  .plate-solo a { width: min(64vw, 640px); }
}

@media (max-width: 900px) {
  .photo-hero .ground .grade {
    background: linear-gradient(180deg,
      rgba(13, 18, 32, .30) 0%,
      rgba(13, 18, 32, 0)   22%,
      rgba(13, 18, 32, 0)   44%,
      rgba(13, 18, 32, .54) 66%,
      rgba(13, 18, 32, .88) 100%);
  }
}

@media (max-width: 640px) {
  .photo-hero { min-height: min(92vh, 820px); padding-top: calc(var(--head-h) + 160px); }
  /* The modifier is the same specificity as the rule above and declared
     earlier in the file, so without this it loses here and a project with no
     photograph gets the full photographic height on a phone: most of a screen
     of empty ink above the title. */
  .photo-hero--type { min-height: min(70vh, 560px); padding-top: calc(var(--head-h) + 88px); }
  .photo-hero h1 { font-size: 2.7rem; }
  .photo-hero .page-lede { font-size: 1.02rem; margin-top: 18px; }
  .photo-hero .facts { margin-top: 22px; }
  .photo-hero .facts dt { font-size: 1.85rem; }
  .photo-hero .pill-btn { margin-top: 24px; }
  /* One column of 16:9 tiles, the shape the owner asked for on a phone. */
  .tiles { grid-template-columns: minmax(0, 1fr); gap: 32px; }
  .tile-media { aspect-ratio: 16 / 9; }
  .project-body { padding: 56px 0 32px; }
  .spread-project .spread-head { gap: 16px; }
  /* Portrait frames, as on the home's strip, as large as the screen allows. */
  .gallery-stage { min-height: 0; padding: 24px 0 40px; gap: 20px; }
  .gallery {
    --sight-w: min(80vw, calc(66svh * 9 / 16));
    --sight-h: calc(var(--sight-w) * 16 / 9);
    --sight-gap: 14px;
    --sight-turn: 22;
    --sight-sink: 160;
    perspective: 1200px;
  }
  .gallery-foot { flex-direction: column; align-items: flex-start; gap: 16px; }
  .gallery-count { display: block; margin: 4px 0 0; }
  /* The team: a portrait as large as the phone allows with a glimpse of
     the next either side; the person under it, the buttons under them. */
  .team-stage { min-height: 0; padding: 72px 0 56px; gap: 26px; }
  .gallery-team {
    --sight-w: min(62vw, calc(50svh * 4 / 5));
    --sight-h: calc(var(--sight-w) * 5 / 4);
    --sight-gap: 14px;
    --sight-turn: 24;
    --sight-sink: 150;
    perspective: 1100px;
  }
  .team-name { font-size: 2.3rem; margin-bottom: 14px; }
  .team-bio p { font-size: 1rem; }
  .team-side { justify-items: start; }
  .project-lede { font-size: 1.15rem; }
  .plate + .plate { margin-top: 28px; }
  .plate-full a { height: 68vh; }
  .plate-pair .wrap { grid-template-columns: minmax(0, 1fr); gap: 28px; }
  .plate-pair a:nth-child(2) { margin-top: 0; }
  .plate-solo a { width: 100%; }
  .plate-trio .wrap { grid-template-columns: minmax(0, 1fr); gap: 20px; }
  .plate-trio .stack { grid-template-rows: none; grid-template-columns: 1fr 1fr; }
  .project-steps-row { grid-template-columns: minmax(0, 1fr); gap: 12px; }
  .project-step-empty { display: none; }
  .project-step { padding: 20px 22px 22px; }
  /* On a phone the photograph takes the width; the arrows go to the foot,
     either side of the caption, where a thumb is (a swipe works as well). */
  .lightbox-frame { padding: 68px 12px 22px; gap: 14px; }
  .lightbox-caption { flex-direction: column; align-items: center; gap: 2px; min-height: 52px; padding: 0 64px; text-align: center; font-size: 14px; }
  .lightbox-arrow { top: auto; bottom: 22px; transform: none; width: 46px; height: 46px; }
  .lightbox-arrow[data-dir="prev"] { left: 12px; }
  .lightbox-arrow[data-dir="next"] { right: 12px; }
  .lightbox-close { top: 12px; right: 12px; width: 46px; height: 46px; }
}

@media (prefers-reduced-motion: reduce) {
  .photo-hero .ground img, .photo-hero .kicker, .photo-hero h1 .line > span, .photo-hero .page-lede, .photo-hero .facts, .photo-hero .pill-btn { animation: none; }
  .tile-media img, .gallery-controls button, .plate img, .project-photos-link, .project-step, .step-dir svg { transition: none; }
  .plate-full img { transform: none; }
}
