/* Strulen's own layer over the shared template.
   Per-site, so nothing here can reach another client's build. */

:root {
  /* The datum height. Struck once, holds the same screen position. */
  --datum: 26vh;
}

/* ---------------------------------------------------------------- THE MARK */
/* The official horizontal logo, drawn with fill="currentColor", so it is used
   as a mask over the current text colour. That way it is white over the film
   and black over paper without a second file or a swap, and the studio's full
   name stays in the markup for a screen reader. */
.nav-mark {
  display: inline-block;
  width: 164px;
  height: 26px;
  text-indent: -200vw;
  overflow: hidden;
  white-space: nowrap;
  background-color: currentColor;
  -webkit-mask: url("/assets/brand/logo-horizontal.svg") left center / contain
    no-repeat;
  mask: url("/assets/brand/logo-horizontal.svg") left center / contain no-repeat;
}

@media (max-width: 767px) {
  .nav-mark {
    width: 132px;
    height: 21px;
  }
}

/* --------------------------------------------------------------- THE DATUM */
/* Removed 2026-10-19 on the user's instruction: the full-width orange rule
   across the film was rejected. The accent now appears only where it is a
   small annotation, on the work index numerals and the status rule, which is
   the role the brand reserves for it. Nothing draws a line across the film. */

/* The copy sits BENEATH the datum, at the lower left, which is where the
   storyboard puts the display line. The template anchors it to the top, so at
   this size the datum cut straight through the headline. */
.is-hero .scroll-copy {
  top: auto !important;
  bottom: 11vh !important;
}

@media (max-height: 700px) {
  .is-hero .scroll-copy {
    bottom: 7vh !important;
  }
}

/* ---------------------------------------------------------------- THE GRAIN */
/* Film grain over the hero. The technique is 21st.dev's grain backgrounds,
   rebuilt as one SVG turbulence tile rather than a WebGL shader, because this
   is a static page whose frame budget is already spent on the film. */
.is-hero .scroll-stage > .grain {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  opacity: 0.055;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/></filter><rect width='160' height='160' filter='url(%23n)'/></svg>");
  background-size: 160px 160px;
}

@media (prefers-reduced-motion: reduce) {
  .is-hero .scroll-stage > .grain {
    opacity: 0.03;
  }
}

/* ------------------------------------------------------------ THE CALL TO ACTION */
/* The template draws it as a bordered panel and fills it with the accent on
   hover. Both break this brand: the identity is square and unpanelled, and
   orange may be a rule or a numeral but never a filled panel behind a label.
   It becomes a plain link with a rule, and the rule takes the accent. */
.cta {
  background: none !important;
  border: 0 !important;
  border-bottom: 1px solid currentColor !important;
  border-radius: 0 !important;
  padding: 0 0 7px 0 !important;
  backdrop-filter: none !important;
  -webkit-backdrop-filter: none !important;
}

.cta:hover,
.cta:focus-visible {
  background: none !important;
  color: var(--light) !important;
  border-bottom-color: var(--accent) !important;
}

/* Square everywhere. A rounded corner is not this identity. */
.cta,
.band,
.statement,
.nav {
  border-radius: 0 !important;
}

.nav-mark {
  letter-spacing: 0.08em;
}

/* ------------------------------------------------------------- THE MEASURE */
/* The template centres a 1320px band in the page, which leaves a wide empty
   margin on both sides at desktop width and an empty left column under every
   heading. The band now takes the page, and the left column is given something
   to hold. Prose stays near 66ch, because a line that runs the full width of a
   1600px screen is harder to read, not easier. */
.band {
  max-width: min(1640px, 92vw) !important;
}

/* Scoped twice over, and both scopes were learned the hard way.
   By width, because an unscoped !important overrode the template's own
   single-column rule for phones and cut the prose to four words a line.
   And by class, because it also overrode the statement and lead bands, which
   are deliberately one column: the statement's note was thrown into a right
   hand column with nothing under it, which is the stranded label and the big
   empty area the user photographed. */
@media (min-width: 860px) {
  .band:not(.is-statement):not(.lead) {
    grid-template-columns: minmax(0, 6fr) minmax(0, 9fr) !important;
  }
}

/* --------------------------------------------------------------- THE TYPE */
/* The template caps the hero display line at 4.4rem, about 70px, which the
   user judged small, and it is: on a 1440 screen a hero line should be set
   from the viewport, not from a timid clamp. It is capped by viewport HEIGHT
   as well so three lines always fit above the lead on a short screen. */
/* The template sets this in `ch`, which resolves against the body size rather
   than the display size, so a 62ch cap came out around 558px and forced the
   headline to four short lines however large the type was. In explicit units
   it breaks where the storyboard breaks it. */
.is-hero .scroll-copy {
  max-width: min(920px, 70%) !important;
}

.is-hero .scroll-copy h1 {
  font-size: min(clamp(3rem, 9.4vw, 10rem), 15svh) !important;
  line-height: 0.94 !important;
  /* The template sets text-wrap: balance, which at this size evens the lines
     out instead of filling them and gave "Architecture / drawn / before it is /
     built." Filling greedily inside the copy column gives three. */
  text-wrap: normal !important;
  max-width: none;
  margin-bottom: clamp(18px, 3vh, 34px) !important;
}

/* The beats are an argument, not a caption. Sizing is set once, further down
   with the rest of the film typography, so there is one place to change it. */
.scroll-beat {
  color: var(--light) !important;
}

/* One oversized moment per page. It was 3.4rem, which is a heading, not a
   statement, and the band around it was 200px of padding top and bottom. */
.statement {
  font-size: clamp(2.2rem, 6.4vw, 5.6rem) !important;
  line-height: 1.04 !important;
  max-width: 17ch !important;
}

.band.is-statement {
  padding-top: clamp(64px, 11vh, 140px) !important;
  padding-bottom: clamp(64px, 11vh, 140px) !important;
}

.statement-note {
  margin-top: clamp(20px, 3vh, 34px) !important;
}

/* Page headings and band headings, up a step each. */
.band.lead h1 {
  font-size: clamp(2.6rem, 8vw, 7rem) !important;
  line-height: 0.95 !important;
}

.band-head h2 {
  font-size: clamp(1.7rem, 3.4vw, 3rem) !important;
  line-height: 1.02 !important;
}

.band-body > p {
  max-width: 66ch;
}

.band.lead,
.band.is-statement {
  max-width: min(1640px, 92vw) !important;
}

/* The picture that fills the left column on the work page. Landscape rather
   than portrait: at 4:5 it towered over a four-line paragraph and opened a
   hole under the prose instead of closing one. */
.band-figure {
  display: block;
  grid-column: 1 / -1;
  width: 100%;
  margin-top: 2.4rem;
  aspect-ratio: 21 / 9;
  object-fit: cover;
  background: color-mix(in srgb, var(--ink) 8%, transparent);
}

@media (max-width: 767px) {
  .band-figure {
    aspect-ratio: 16 / 10;
    margin-top: 1.4rem;
  }
}

/* THE MEASURE RULE IS NOT OURS.
   The template draws a technical hairline down the empty left column of every
   band. Its own comment says why it exists: the shoe brand is built on one
   last and one set of measurements, so the rule is that brand's idea. Carrying
   another client's signature device into this one is the thing the studio
   rules forbid outright, and on this layout it also read as a stray fragment
   under the pictures. */
.measure {
  display: none !important;
}

/* The bands were running tall and empty. */
.band {
  padding-block: clamp(2.6rem, 5vw, 4.4rem) !important;
}

/* ------------------------------------------------------------ THE EFFECTS */
/* REWRITTEN after "I don't like the text and effects". The line wipes and the
   word-by-word blur are gone. Held against the references the user sent, those
   sites are calm and their typography talks without performing.

   What is left is one short rise on enter. Nothing is hidden by this
   stylesheet on its own: the rules only bite under `fx-on`, which the script
   sets, so a blocked script leaves a readable page. */
.js-reveal .reveal.fx-managed {
  opacity: 1 !important;
  transform: none !important;
  filter: none !important;
}

.fx-on .fx-rise {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity 760ms cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 760ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.fx-on .fx-rise.is-up {
  opacity: 1;
  transform: none;
}

/* THE BEAT LABELS ARE GONE, removed 2026-10-20. The studio's anti-template
   list names tiny all-caps eyebrow labels and generic section numbering as the
   marks of a generated page, and the user had already rejected the busy text
   treatment. The statements over the film are the typography.

   So they are set properly instead: large enough to be read at a glance while
   the film is moving underneath, which is what the user asked for when they
   said the typography was too small.

   THE ELEMENT THAT HAS TO BE TARGETED IS THE PARAGRAPH, NOT THE BEAT.
   A beat is a <div class="scroll-beat"> wrapping a <p>. Sizing the div alone
   does nothing visible: the paragraph keeps the body rule and stays at about
   19px while the div reports 50px to anyone who inspects it. That is why the
   typography kept measuring correct and reading small. */
.scroll-beat,
.scroll-beat p {
  letter-spacing: -0.012em;
  font-size: clamp(1.6rem, 3.5vw, 3.1rem) !important;
  line-height: 1.12 !important;
  font-family: var(--display) !important;
  font-weight: 300 !important;
}

/* ------------------------------------------------------------- THE WEIGHT */
/* THIN, EVERYWHERE THE DISPLAY FACE IS SET. The user's instruction, given on
   the hero line and then "same for all": 2026-10-20.

   This only works because the build now ships bomstad-300.woff2. A browser
   will synthesise a bold but it will never synthesise a light, so asking for
   300 with only the 600 installed renders the 600 and the change appears to do
   nothing at all. If the hero ever looks heavy again, check the font file is
   there before touching this rule.

   At these sizes the light cut is the point: a 130px headline at 600 is a
   shout, and the same line at 300 has the air that the architecture does. The
   tracking opens very slightly with it, because a light face set tight closes
   up its counters. */
.is-hero .scroll-copy h1,
.statement,
.band.lead h1,
.band-head h2,
.work-row__name {
  font-weight: 300 !important;
  letter-spacing: -0.018em !important;
}

.scroll-beat p {
  max-width: 19ch !important;
  margin: 0 !important;
}

/* ----------------------------------------------------------- THE WORK INDEX */
/* TARQ's device from the reference scan, and the one idea in that set that is
   both the content and the navigation. Real anchors with the status as text on
   the row, so nothing a visitor needs depends on a hover. */
.work-index {
  border-top: 1px solid var(--ink);
}

.work-row {
  position: relative;
  display: grid;
  grid-template-columns: 3.5rem minmax(0, 1fr);
  gap: 0 1.5rem;
  align-items: baseline;
  padding: 1.6rem 0;
  border-bottom: 1px solid rgba(0, 0, 0, 0.14);
  color: inherit;
  text-decoration: none;
}

.work-row__n {
  font-family: var(--display);
  font-weight: 600;
  font-size: 1.4rem;
  color: var(--accent);
  line-height: 1;
}

.work-row__name {
  font-family: var(--display);
  font-weight: 600;
  font-size: clamp(1.5rem, 3.4vw, 2.6rem);
  line-height: 1.05;
  letter-spacing: -0.015em;
  transition: transform 420ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.work-row__meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 1.4rem;
  margin-top: 0.6rem;
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: rgba(0, 0, 0, 0.55);
}

.work-row__status {
  color: var(--ink);
  border-bottom: 2px solid var(--accent);
  padding-bottom: 2px;
}

@media (hover: hover) and (pointer: fine) {
  .work-row:hover .work-row__name,
  .work-row:focus-visible .work-row__name {
    transform: translateX(18px);
  }
}

/* The floating preview. Pointer-fine only: there is no cursor on a phone, so
   the row is simply a row there and loses nothing. */
.work-preview {
  position: fixed;
  top: 0;
  left: 0;
  width: 300px;
  height: 188px;
  z-index: 40;
  pointer-events: none;
  opacity: 0;
  overflow: hidden;
  transition: opacity 260ms ease;
  will-change: transform;
}

.work-preview.is-on {
  opacity: 1;
}

.work-preview img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

@media (prefers-reduced-motion: reduce) {
  .work-row__name {
    transition: none;
  }
  .work-preview {
    display: none;
  }
}

/* ================================================ THE COPY OVER THE FILM */
/* Three things the user asked for, which are really one thing: make the words
   readable over a moving picture.

   1. A GRADIENT UP FROM THE BOTTOM EDGE. Without it the copy is at the mercy
      of whatever the film is doing underneath: white type is fine over the
      dark room in shot one and invisible over the blueprint's pale lines in
      shot three. The template already carries an empty .scroll-scrim element
      for exactly this, so it goes there rather than into a new box.

   2. THE COPY MOVES, LEFT THEN BOTTOM CENTRE. The opening line sits at the
      lower left, where a headline belongs. Once the film is past its opening
      the copy moves to the bottom centre and stays there, so the four
      statements all land in the same place and the eye stops hunting for them.

   3. The move is a transition, not a jump. The engine already sets
      .past-intro on the copy at the end of the first beat, so there is a real
      moment to hang it on. */

.is-hero .scroll-scrim {
  z-index: 2;
  pointer-events: none;
  background: linear-gradient(
    to top,
    rgba(0, 0, 0, 0.9) 0%,
    rgba(0, 0, 0, 0.78) 14%,
    rgba(0, 0, 0, 0.52) 32%,
    rgba(0, 0, 0, 0.22) 52%,
    rgba(0, 0, 0, 0) 74%
  );
}

.is-hero .scroll-copy {
  z-index: 4;
  transition: left 1000ms cubic-bezier(0.22, 0.61, 0.36, 1),
    bottom 1000ms cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 1000ms cubic-bezier(0.22, 0.61, 0.36, 1),
    max-width 1000ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.is-hero .scroll-copy.past-intro {
  left: 50% !important;
  right: auto !important;
  transform: translateX(-50%);
  bottom: 9vh !important;
  max-width: min(1080px, 88vw) !important;
  text-align: center;
}

/* The beats are absolutely positioned inside the copy, so centring the block
   is not enough: the paragraph's own measure has to centre too, or the text
   sits centred as a block and ragged to the left inside it. */
.is-hero .scroll-copy.past-intro .scroll-beat {
  left: 0;
  right: 0;
}

.is-hero .scroll-copy.past-intro .scroll-beat p {
  max-width: 26ch !important;
  margin-left: auto !important;
  margin-right: auto !important;
}

@media (prefers-reduced-motion: reduce) {
  .is-hero .scroll-copy {
    transition: none;
  }
}

/* ------------------------------------------------------- CLEARER GLYPHS */
/* The user held the brand guide's own headings up against the site: the
   guide's type is open and unmistakable, the site's was not. The face was
   never the problem, Bomstad Display is correct and all 64 characters were
   proven to be loading from it rather than falling back. The TRACKING was the
   problem: -0.018em at display size closes the counters of a light cut and the
   letters start borrowing each other's shapes. Opened back out. */
.is-hero .scroll-copy h1,
.statement,
.band.lead h1,
.band-head h2,
.work-row__name,
.scroll-beat,
.scroll-beat p {
  letter-spacing: -0.002em !important;
}

/* ======================================================= THE FOOTER MARK */
/* The technique is 21st.dev's Light Beam Footer with Giant Wordmark: the mark
   set large at the end of the page with one slow shaft of light crossing it.
   Rebuilt rather than installed, because that component is React and this page
   is static with its frame budget already on a scrubbed film. Its navy, its
   blue, its glassy pill buttons and its accent eyebrow labels are left behind;
   Strulen is black, white and square.

   TWO CORRECTIONS the user made, both of which killed the first version:

   1. IT WAS CROPPING THE LETTERFORMS. Cut off at the baseline, "Strulen" read
      as "Strilen": the u lost its bowl and became two stems. A crop that makes
      the name unreadable is not a crop, it is a mistake. The mark is now whole.

   2. THE LETTERS DID NOT ANSWER THE CURSOR. A CSS mask is one element, so
      there was nothing to lift. It is inline SVG now, nine real paths from the
      official file, each one liftable on its own. */

.foot {
  position: relative;
  overflow: hidden;
  /* Set by brand.js. Declared here so the footer is right on first paint and
     stays right if the script never runs at all. */
  --beam: 58%;
  --glow: 0;
}

.foot-beam {
  position: relative;
  z-index: 1;
  margin-top: clamp(34px, 5.5vw, 76px);
  padding: 0 clamp(16px, 4vw, 56px);
  max-width: min(1640px, 94vw);
  margin-left: auto;
  margin-right: auto;
}

.foot-beam-mark {
  display: block;
  width: 100%;
  height: auto;
  /* WHOLE. Nothing clips this. */
  overflow: visible;
}

.foot-glyph {
  /* The lift the user asked for. Set per glyph by the script from how near the
     cursor is, in user units, so it scales with the mark instead of being a
     fixed pixel hop that looks large on a phone and invisible on a desktop. */
  transform: translateY(var(--lift, 0));
  transition: transform 420ms cubic-bezier(0.2, 0.8, 0.25, 1);
}

/* The same shaft across the whole footer, behind the type, so the light reads
   as one thing crossing the section rather than a trick played on the mark. */
.foot::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  filter: blur(30px);
  background-image: linear-gradient(
    112deg,
    transparent calc(var(--beam) - 13%),
    rgba(255, 255, 255, 0.05) var(--beam),
    transparent calc(var(--beam) + 11%)
  );
}

.foot-top,
.foot-base {
  position: relative;
  z-index: 2;
}

@media (prefers-reduced-motion: reduce) {
  .foot::before {
    display: none;
  }
  .foot-glyph {
    transition: none;
    --lift: 0;
  }
}

/* The studio's name was being said three times in one footer: the small mark
   line, the base line, and now the mark itself at full size. The small line
   goes; the base line stays, because it carries the legal name as text. */
.foot-mark {
  display: none !important;
}

/* ---------------------------------------------- THE CALL TO ACTION, PLACED */
/* It was landing on top of the copy, with its rule cutting through the last
   line. The template hides the button through the middle of the film and
   brings it back for the closing beat, which was right when the copy sat at
   the lower left and wrong the moment the copy moved to the bottom centre:
   the beats are absolutely positioned and the button is in normal flow, so
   they stopped knowing about each other.

   Both are pinned to the bottom of the copy box now, the button on the floor
   and the beats on a shelf above it, so they cannot meet whatever the copy
   says or how many lines it runs to. */
.is-hero .scroll-copy.past-intro {
  padding-bottom: 76px;
}

.is-hero .scroll-copy.past-intro .scroll-beat {
  top: auto;
  bottom: 76px;
}

.is-hero .scroll-copy.past-intro .cta {
  position: absolute;
  left: 50%;
  bottom: 0;
  transform: translateX(-50%);
  margin-top: 0 !important;
}

/* The button is a word and a rule, never a panel: one rule, under the label,
   and nothing above it. The template draws it as a filled white block and
   fills it with the accent on hover, both of which belong to another brand. */
.cta {
  border-top: 0 !important;
  border-left: 0 !important;
  border-right: 0 !important;
  box-shadow: none !important;
}

/* A stronger floor under the copy. The first gradient was measured as applying
   and still left white type over a sunlit concrete soffit in the closing shot,
   because the film gets BRIGHTER as the building arrives. It has to be built
   for the brightest frame, not the average one. */
.is-hero .scroll-scrim {
  background: linear-gradient(
    to top,
    rgba(0, 0, 0, 0.94) 0%,
    rgba(0, 0, 0, 0.88) 10%,
    rgba(0, 0, 0, 0.7) 24%,
    rgba(0, 0, 0, 0.42) 40%,
    rgba(0, 0, 0, 0.16) 58%,
    rgba(0, 0, 0, 0) 78%
  ) !important;
}

/* ------------------------------------------- LEGIBILITY, WHEREVER IT LANDS */
/* A gradient up from the bottom of the frame is not enough on its own. The
   film passes through a blueprint that is WHITE LINE ON DARK, and white type
   crossing white drawing lines disappears into them however dark the ground
   behind it is. The user caught exactly that frame.

   So the copy carries its own backing, sized in viewport units and anchored to
   the copy block, so it travels with the words instead of sitting at a fixed
   height and hoping. Soft-edged, because a visible box over a film is worse
   than the problem it solves.

   The text shadow is the last few per cent: it separates a stroke of type from
   a drawing line of the same colour and width, which is the one case a wash
   cannot fix. */
.is-hero .scroll-copy::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -10vh;
  transform: translateX(-50%);
  width: 160%;
  height: 52vh;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(
    ellipse 62% 52% at 50% 72%,
    rgba(0, 0, 0, 0.82) 0%,
    rgba(0, 0, 0, 0.6) 42%,
    rgba(0, 0, 0, 0.24) 68%,
    rgba(0, 0, 0, 0) 86%
  );
}

.is-hero .scroll-beat,
.is-hero .scroll-beat p,
.is-hero .scroll-copy h1 {
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.55), 0 2px 24px rgba(0, 0, 0, 0.7);
}

/* ------------------------------------------------ THE BUTTON, ACTUALLY CENTRED */
/* The template's closing-beat rule sets `transform: none` to undo its own
   entry nudge, and that also undid the translateX(-50%) holding the button on
   the centre line. So it sat exactly half its own width to the right of centre
   and read as a second, misaligned rule under the copy. Restated with the
   centring folded in, so nothing can drop it again. */
.is-hero .scroll-copy.past-intro .cta,
.is-hero .scroll-copy.past-intro.at-outro .cta {
  transform: translateX(-50%) !important;
}

/* ----------------------------------------------- SECONDARY FACE FOR TAGLINES */
/* The user's rule: the display face is for display, and a tagline or standfirst
   takes the secondary face. Poppins is the brand's secondary, chosen in the
   guide for body copy and supporting information, and a standfirst is exactly
   supporting information. Bomstad at that size and length was doing a headline's
   job for a sentence that is not a headline. */
.standfirst,
.band.lead .standfirst,
.project__facts,
.service__body p,
.flow-body p,
.band-body > p,
.foot-base,
.work-row__meta {
  font-family: var(--text) !important;
  font-weight: 400 !important;
  letter-spacing: 0 !important;
}

.band.lead .standfirst {
  font-size: clamp(1.02rem, 1.5vw, 1.24rem) !important;
  line-height: 1.55 !important;
  max-width: 54ch;
  color: rgba(0, 0, 0, 0.62);
}

/* ===================================================== THE PROJECTS, /work */
/* The page listed "Residential study" and "Interior study", which are services
   wearing a project's clothes: a work page with no buildings on it. It is six
   projects now, each with the things a project page gives, a name, a place, a
   type, an area and a year.

   Laid out as full-width plates rather than a card grid. A card grid is the
   default of every template and it also crops architecture to a thumbnail,
   which is the one thing a building cannot survive. Each project gets the full
   measure, the picture runs wide, and the facts sit under it as a real
   definition list so a screen reader reads them as pairs. */
.project-grid {
  grid-column: 1 / -1;
  display: grid;
  gap: clamp(56px, 9vw, 128px);
}

.project {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(14px, 2vw, 22px);
}

.project__img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  display: block;
  background: rgba(0, 0, 0, 0.06);
}

.project__head {
  display: flex;
  align-items: baseline;
  gap: clamp(12px, 2vw, 24px);
}

.project__n {
  font-family: var(--text);
  font-weight: 500;
  font-size: 0.8125rem;
  letter-spacing: 0.14em;
  color: var(--accent);
}

.project__name {
  margin: 0;
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(1.9rem, 4.4vw, 3.6rem);
  line-height: 1.02;
  letter-spacing: -0.002em;
}

/* The facts as a row of pairs. The label small and quiet, the value in the
   reading face: this is reference information, not a headline. */
.project__facts {
  margin: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 2px clamp(28px, 5vw, 64px);
  padding-top: 4px;
  border-top: 1px solid rgba(0, 0, 0, 0.14);
}

.project__facts dt {
  font-size: 0.6875rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(0, 0, 0, 0.45);
}

.project__facts dd {
  margin: 2px 0 0;
  font-size: 0.98rem;
}

.project > p {
  margin: 0;
  max-width: 62ch;
}

/* THE STATUS IS NOT DECORATION. None of this is built, and the word that says
   so is given the accent and a rule so it cannot be skimmed past. */
.project__status {
  font-family: var(--text) !important;
  font-size: 0.75rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
  border-bottom: 2px solid var(--accent);
  align-self: start;
  justify-self: start;
  padding-bottom: 3px;
}

@media (min-width: 900px) {
  .project {
    grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
    column-gap: clamp(32px, 4vw, 64px);
    align-items: start;
  }
  .project__img {
    grid-row: 1 / span 4;
    aspect-ratio: 4 / 3;
  }
}

/* ================================================= THE SERVICES, /services */
/* Interiors and exteriors, in the words a client uses, each with a photograph
   and its own icon from the brand's own iconography. The page had neither
   before: it named three internal stages and showed nothing. */
.service-grid {
  grid-column: 1 / -1;
  display: grid;
  gap: clamp(28px, 4vw, 48px);
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
}

.service {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.service__img {
  width: 100%;
  aspect-ratio: 5 / 4;
  object-fit: cover;
  display: block;
  background: rgba(0, 0, 0, 0.06);
}

.service__body {
  padding: clamp(18px, 2.4vw, 26px) 0 0;
  border-top: 1px solid var(--ink);
  margin-top: clamp(16px, 2vw, 22px);
}

.service__icon {
  width: 34px;
  height: 34px;
  display: block;
  color: var(--accent);
  margin-bottom: 14px;
}

.service__body h2 {
  margin: 0 0 10px;
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(1.3rem, 2.1vw, 1.75rem);
  line-height: 1.08;
}

.service__body p {
  margin: 0;
  color: rgba(0, 0, 0, 0.68);
  font-size: 0.98rem;
  line-height: 1.58;
}

/* ================================================== THE PROCESS, /process */
/* Seven steps on one rail. The technique is 21st.dev's vertical how-it-works
   timeline, a continuous rail with a node and an icon per step, rebuilt in
   plain CSS because this is a static page.

   Left behind: the dashed rail, the rounded cards and the tick marks. A tick
   says a step is complete, and nothing here is complete: these are the stages
   of a job, not the progress of one. */
.flow-rail {
  grid-column: 1 / -1;
  list-style: none;
  margin: 0;
  padding: 0;
  position: relative;
}

.flow-step {
  position: relative;
  display: grid;
  grid-template-columns: 64px minmax(0, 1fr);
  gap: clamp(20px, 3vw, 40px);
  padding-bottom: clamp(36px, 5vw, 64px);
}

/* The rail itself: one line down the page through every node, stopping at the
   last one rather than running off the end. */
.flow-step::before {
  content: "";
  position: absolute;
  left: 31px;
  top: 64px;
  bottom: 0;
  width: 1px;
  background: rgba(0, 0, 0, 0.16);
}

.flow-step:last-child {
  padding-bottom: 0;
}

.flow-step:last-child::before {
  display: none;
}

.flow-node {
  width: 64px;
  height: 64px;
  display: grid;
  place-items: center;
  border: 1px solid var(--ink);
  background: var(--paper);
  position: relative;
  z-index: 1;
}

.flow-icon {
  width: 32px;
  height: 32px;
  display: block;
  color: var(--ink);
  transition: color 300ms ease;
}

.flow-step:hover .flow-icon {
  color: var(--accent);
}

.flow-body {
  padding-top: 4px;
}

.flow-n {
  font-family: var(--text);
  font-weight: 500;
  font-size: 0.6875rem;
  letter-spacing: 0.18em;
  color: var(--accent);
}

.flow-body h2 {
  margin: 4px 0 8px;
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(1.4rem, 2.6vw, 2.1rem);
  line-height: 1.06;
}

.flow-body p {
  margin: 0;
  max-width: 58ch;
  color: rgba(0, 0, 0, 0.68);
  line-height: 1.58;
}

@media (max-width: 600px) {
  .flow-step {
    grid-template-columns: 48px minmax(0, 1fr);
    gap: 18px;
  }
  .flow-node {
    width: 48px;
    height: 48px;
  }
  .flow-icon {
    width: 25px;
    height: 25px;
  }
  .flow-step::before {
    left: 23px;
    top: 48px;
  }
}

/* Centred lines break better when they are evened out rather than filled.
   "Houses, interiors, and the planning / before them." is a bad rag: the
   second line is two words under a full one. Balance fixes exactly this, and
   it is safe here because these are short lines, which is the case the
   algorithm is for. */
.is-hero .scroll-copy.past-intro .scroll-beat p {
  text-wrap: balance;
}

/* The project facts were a flex row, so the fourth pair dropped to its own
   line and sat alone under the first. A four-track grid keeps them on one
   line, and on a narrow screen they fall to two clean columns rather than
   three-then-one. */
.project__facts {
  display: grid !important;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0 clamp(16px, 2vw, 32px);
}

@media (max-width: 760px) {
  .project__facts {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 14px clamp(16px, 2vw, 32px);
  }
}

/* ===================================================== SMOOTH SCROLLING */
/* The user asked for smooth scroll and the menu already jumps to anchors.
   This is the native property rather than a scroll-hijacking library, which
   matters on this site more than most: the hero is a scroll-scrubbed film, and
   a library that takes over the scroll position fights the scrub for control
   of the same number. Native smooth scroll only affects programmatic jumps and
   anchor clicks, so the film keeps following the real scroll exactly.

   scroll-padding-top keeps an anchored section from landing underneath the
   fixed header, which is the usual reason anchor links feel broken. */
/* SMOOTH SCROLL ON ANCHORS ONLY, not on the document.
   Set on html it also animates every programmatic scroll, including the one
   that puts a reloaded page back at the top, so the page was visibly gliding
   down to where it used to be instead of starting at the top. :target scopes
   it to a real anchor jump, which is the only place it was wanted. */
html {
  scroll-padding-top: 96px;
}

@media (prefers-reduced-motion: no-preference) {
  html:focus-within {
    scroll-behavior: smooth;
  }
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }
}

/* ============================================ THE PROCESS RAIL, ON SCROLL */
/* The rail draws itself as you come down the page: the line grows between the
   nodes and each step arrives just after the line reaches it. It is the one
   place on this site where an entrance animation is doing real work, because
   the subject IS a sequence, and a sequence that assembles in order says what
   the page says.

   Hidden only under .fx-on, which the script sets, so a blocked script leaves
   every step visible. */
.fx-on .flow-step::before {
  transform: scaleY(0);
  transform-origin: 50% 0;
  transition: transform 820ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.fx-on .flow-step.is-in::before {
  transform: scaleY(1);
}

.fx-on .flow-node,
.fx-on .flow-body {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 620ms cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 620ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.fx-on .flow-step.is-in .flow-node,
.fx-on .flow-step.is-in .flow-body {
  opacity: 1;
  transform: none;
}

.fx-on .flow-step.is-in .flow-body {
  transition-delay: 130ms;
}

@media (prefers-reduced-motion: reduce) {
  .fx-on .flow-step::before,
  .fx-on .flow-node,
  .fx-on .flow-body {
    transform: none !important;
    opacity: 1 !important;
    transition: none !important;
  }
}

/* ================================================== THE ENQUIRY FORM */
/* Square, unpanelled, one rule under each field. The default browser input is
   a rounded grey box, which is the opposite of this identity, and a bordered
   card around the form would be the template look the brand rules forbid. */
.contact-grid {
  grid-column: 1 / -1;
  max-width: 680px;
}

.enquiry h2 {
  margin: 0 0 clamp(26px, 4vh, 42px);
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(1.7rem, 3.4vw, 2.8rem);
  line-height: 1.04;
}

.field {
  margin: 0 0 clamp(20px, 3vh, 30px);
  display: flex;
  flex-direction: column;
  gap: 7px;
}

.field label {
  font-family: var(--text);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(0, 0, 0, 0.5);
}

.field input,
.field select,
.field textarea {
  font: inherit;
  font-family: var(--text);
  font-size: 1rem;
  color: var(--ink);
  background: none;
  border: 0;
  border-bottom: 1px solid rgba(0, 0, 0, 0.26);
  border-radius: 0;
  padding: 9px 0;
  width: 100%;
  transition: border-color 220ms ease;
  appearance: none;
  -webkit-appearance: none;
}

.field textarea {
  resize: vertical;
  min-height: 118px;
  line-height: 1.55;
}

/* A select with appearance:none loses its arrow, so one is drawn back on. */
.field select {
  background-image: linear-gradient(45deg, transparent 50%, var(--ink) 50%),
    linear-gradient(135deg, var(--ink) 50%, transparent 50%);
  background-position: calc(100% - 11px) calc(50% + 1px),
    calc(100% - 6px) calc(50% + 1px);
  background-size: 5px 5px, 5px 5px;
  background-repeat: no-repeat;
  padding-right: 26px;
}

.field input:focus,
.field select:focus,
.field textarea:focus {
  outline: none;
  border-bottom-color: var(--accent);
  border-bottom-width: 2px;
  padding-bottom: 8px;
}

/* :invalid fires on an empty required field the moment the page loads, so
   every required field came up already underlined in the error colour before
   anyone had typed a character. :user-invalid only fires once the visitor has
   actually interacted and got it wrong, which is the only point at which an
   error is information rather than an accusation. */
.field input:user-invalid,
.field textarea:user-invalid {
  border-bottom-color: var(--accent);
}

.cta--form {
  margin-top: 6px !important;
  cursor: pointer;
  font-family: var(--text);
  font-size: 0.875rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: var(--ink) !important;
  border-bottom: 2px solid var(--accent) !important;
}

.cta--form:hover,
.cta--form:focus-visible {
  color: var(--accent) !important;
}

.enquiry__note {
  margin: clamp(28px, 4vh, 44px) 0 0;
  font-family: var(--text);
  font-size: 0.9375rem;
  line-height: 1.6;
  color: rgba(0, 0, 0, 0.6);
  max-width: 54ch;
}

.enquiry__note a {
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid var(--accent);
}

.enquiry__sent {
  margin-top: 18px;
  font-family: var(--text);
  font-size: 0.9375rem;
  color: var(--ink);
  border-left: 2px solid var(--accent);
  padding-left: 12px;
}

/* The rail was leaving the right half of a wide screen empty, which is the
   same void the user objected to on the first build. On a wide screen the step
   splits: the number and the name hold the left column, the description takes
   the right, so the sequence runs down the page and the text runs across it.
   Below 1000px it falls back to one stacked column, where a second column
   would just make the measure too narrow to read. */
@media (min-width: 1000px) {
  .flow-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr);
    column-gap: clamp(36px, 5vw, 88px);
    align-items: start;
  }
  .flow-n {
    grid-column: 1;
    grid-row: 1;
  }
  .flow-body h2 {
    grid-column: 1;
    grid-row: 2;
    margin-top: 2px;
  }
  .flow-body p {
    grid-column: 2;
    grid-row: 1 / span 2;
    max-width: 48ch;
    padding-top: 2px;
  }
}

/* The right-hand column of a project was spreading its four rows down the full
   height of the picture, so the facts, the paragraph and the status drifted
   apart with gaps between them. The column packs to the top instead and the
   picture is free to be as tall as it likes beside it. */
@media (min-width: 900px) {
  .project {
    align-content: start;
    row-gap: clamp(12px, 1.6vw, 20px);
  }
  .project__img {
    align-self: stretch;
    height: 100%;
  }
}

/* ============================================ THE SERVICES, ON SCROLL */
/* The technique is 21st.dev's Scroll Reveal Image: the picture is uncovered as
   it enters and settles out of a slight scale, so it arrives rather than
   appears. Rebuilt in CSS because this is a static page, and chosen over the
   catalogue's showier options (the parallax strips, the 3D unfurl) because a
   services page is a list somebody is reading to find out what they can buy,
   not a showreel.

   The picture is uncovered by a clip-path rather than faded: a fade leaves the
   image at half opacity if anything interrupts it, and a clip either has run
   or it has not. The icon, the name and the copy follow a beat later, so the
   eye lands on the photograph first and the words arrive as it settles.

   Everything is gated on .fx-on, which the script sets, so a page with no
   JavaScript shows every card complete. */
.fx-on .service__img {
  clip-path: inset(0 0 100% 0);
  transform: scale(1.08);
  transition: clip-path 900ms cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 1200ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.fx-on .service.is-in .service__img {
  clip-path: inset(0 0 0 0);
  transform: none;
}

.fx-on .service__body > * {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 620ms cubic-bezier(0.22, 0.61, 0.36, 1),
    transform 620ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.fx-on .service.is-in .service__body > * {
  opacity: 1;
  transform: none;
}

.fx-on .service.is-in .service__body > :nth-child(1) {
  transition-delay: 240ms;
}
.fx-on .service.is-in .service__body > :nth-child(2) {
  transition-delay: 330ms;
}
.fx-on .service.is-in .service__body > :nth-child(3) {
  transition-delay: 410ms;
}

/* The rule above the body draws itself across rather than switching on. */
.fx-on .service__body {
  background-image: linear-gradient(var(--ink), var(--ink));
  background-repeat: no-repeat;
  background-position: 0 0;
  background-size: 0 1px;
  border-top-color: transparent !important;
  transition: background-size 900ms cubic-bezier(0.22, 0.61, 0.36, 1) 160ms;
}

.fx-on .service.is-in .service__body {
  background-size: 100% 1px;
}

/* The icon takes the accent as its card arrives, then stays. A colour change
   on arrival is the one place the accent earns a moment of its own. */
.service__icon {
  transition: color 700ms ease 420ms;
}

.fx-on .service:not(.is-in) .service__icon {
  color: rgba(0, 0, 0, 0.22);
}

@media (hover: hover) and (pointer: fine) {
  .service__img {
    transition: transform 700ms cubic-bezier(0.22, 0.61, 0.36, 1);
  }
  .service:hover .service__img {
    transform: scale(1.03);
  }
  .service {
    overflow: hidden;
  }
  .service__img {
    will-change: transform;
  }
}

@media (prefers-reduced-motion: reduce) {
  .fx-on .service__img,
  .fx-on .service__body > *,
  .fx-on .service__body {
    clip-path: none !important;
    transform: none !important;
    opacity: 1 !important;
    background-size: 100% 1px !important;
    transition: none !important;
  }
  .fx-on .service__body {
    border-top-color: var(--ink) !important;
  }
}

/* ------------------------------------------------- THE STATEMENT, SMALLER */
/* It was running to five lines at 5.6rem on a 17ch measure, which is a poster,
   not a sentence. The user asked for it small and two or three lines. Both
   levers were pulled: the type is down about a third and the measure is nearly
   twice as wide, and the sentence itself lost a clause. */
.statement {
  font-size: clamp(1.45rem, 3.3vw, 2.9rem) !important;
  line-height: 1.1 !important;
  max-width: 30ch !important;
  text-wrap: balance;
}

/* ------------------------------------------------------- THE NAV ON A PHONE */
/* The template turns the nav into a horizontal scroller below 768px, with
   nowrap, overflow-x and the scrollbar hidden. That is a real pattern, but
   with the scrollbar hidden there is nothing on screen to say it scrolls, so
   it simply reads as "Contact" being cut in half, which is what the user saw.
   Six short words fit in two rows, so it wraps instead and nothing is hidden.
   The negative margins and the horizontal padding the scroller needed are
   unwound with it, or the second row starts off the left edge of the screen. */
@media (max-width: 767px) {
  .nav nav {
    flex-wrap: wrap;
    overflow-x: visible;
    gap: 4px 16px;
    margin: 0;
    padding: 0;
    font-size: 0.8125rem;
  }
}

/* ================================================= THE HOME PAGE BANDS */
/* Three compact bands that say what the studio does, what it has drawn and
   how a job runs, each linking through rather than repeating the page it
   points at. They sit in the template's own two-column band, so the heading
   keeps the left column and the content takes the right. */

.more {
  display: inline-block;
  margin-top: clamp(22px, 3vh, 34px);
  font-family: var(--text);
  font-size: 0.8125rem;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink);
  text-decoration: none;
  border-bottom: 1px solid var(--accent);
  padding-bottom: 4px;
  transition: color 220ms ease;
}

.more:hover,
.more:focus-visible {
  color: var(--accent);
}

/* The six services as a list of icons and names. Two columns, because six
   items in one column is a long thin ribbon down a wide band. */
.svc-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: 2px clamp(24px, 4vw, 56px);
}

.svc-item {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 13px 0;
  border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}

.svc-item__icon {
  width: 26px;
  height: 26px;
  flex: none;
  color: var(--accent);
}

.svc-item__name {
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(1.05rem, 1.6vw, 1.3rem);
  letter-spacing: -0.002em;
}

/* Three projects, picture first. A link, not a card: no border, no panel, no
   rounded corner. The name and the facts sit under the picture the way a
   caption sits under a plate. */
.mini-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
  gap: clamp(20px, 3vw, 36px);
}

.mini {
  display: block;
  text-decoration: none;
  color: inherit;
}

.mini__img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  display: block;
  background: rgba(0, 0, 0, 0.06);
  transition: filter 420ms ease;
}

.mini__name {
  display: block;
  margin-top: 13px;
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(1.1rem, 1.8vw, 1.45rem);
  line-height: 1.1;
}

.mini__meta {
  display: block;
  margin-top: 5px;
  font-family: var(--text);
  font-size: 0.8125rem;
  color: rgba(0, 0, 0, 0.55);
}

@media (hover: hover) and (pointer: fine) {
  .mini:hover .mini__img,
  .mini:focus-visible .mini__img {
    filter: brightness(1.06);
  }
}

/* The seven stages as a plain numbered run. Deliberately not the full rail:
   this is a summary, and repeating the rail here would make the process page
   pointless. */
.steps {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 0 clamp(24px, 4vw, 56px);
}

.step {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding: 11px 0;
  border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}

.step__n {
  font-family: var(--text);
  font-weight: 500;
  font-size: 0.6875rem;
  letter-spacing: 0.14em;
  color: var(--accent);
  flex: none;
}

.step__name {
  font-family: var(--text);
  font-size: 0.98rem;
}

/* ======================================= THE HOME BANDS, SECOND PASS */
/* The first version was a plain two-column list with a rule under each item
   and no response to the pointer, which the user called out on both counts.

   THE SERVICES, as rows that answer the cursor. Each one is a real link now,
   because a list of services nobody can click is a list of claims. On hover
   the row's rule fills from the left in the accent, the icon takes the accent
   with it and the name steps across: the same vocabulary as the work index,
   so the site has one way of saying "this is a link", not three. */
.svc-list {
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 0 clamp(28px, 4vw, 64px);
}

.svc-item {
  position: relative;
  padding: 0;
  border-bottom: 0;
}

.svc-link {
  position: relative;
  display: flex;
  align-items: center;
  gap: 15px;
  padding: 16px 2px 16px 0;
  text-decoration: none;
  color: inherit;
  border-bottom: 1px solid rgba(0, 0, 0, 0.12);
}

/* The accent rule that fills in from the left, over the quiet one. */
.svc-link::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background: var(--accent);
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform 520ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.svc-item__icon {
  width: 26px;
  height: 26px;
  flex: none;
  color: rgba(0, 0, 0, 0.38);
  transition: color 340ms ease, transform 420ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.svc-item__name {
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(1.05rem, 1.6vw, 1.32rem);
  letter-spacing: -0.002em;
  transition: transform 420ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* An arrow that is not there until it is wanted. */
.svc-link__go {
  margin-left: auto;
  font-family: var(--text);
  font-size: 0.9rem;
  color: var(--accent);
  opacity: 0;
  transform: translateX(-8px);
  transition: opacity 300ms ease, transform 420ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

@media (hover: hover) and (pointer: fine) {
  .svc-link:hover::after,
  .svc-link:focus-visible::after {
    transform: scaleX(1);
  }
  .svc-link:hover .svc-item__icon,
  .svc-link:focus-visible .svc-item__icon {
    color: var(--accent);
    transform: translateX(3px);
  }
  .svc-link:hover .svc-item__name,
  .svc-link:focus-visible .svc-item__name {
    transform: translateX(9px);
  }
  .svc-link:hover .svc-link__go,
  .svc-link:focus-visible .svc-link__go {
    opacity: 1;
    transform: none;
  }
}

.svc-link:focus-visible {
  outline: none;
}

/* The stages get the same quiet hover, so the three bands behave alike. */
.step {
  transition: color 260ms ease;
}

@media (hover: hover) and (pointer: fine) {
  .step:hover .step__name {
    color: var(--accent);
  }
}

/* ============================= THE PROJECTS, PINNED AND MOVED BY SCROLL */
/* 21st.dev's Horizontal Scroll Gallery: a tall wrapper, a frame stuck to the
   viewport inside it, and a track translated sideways by how far the wrapper
   has been scrolled through. Vertical scroll drives the horizontal position,
   so the plates move with the page.

   The version before this was an overflow-x row with its own scrollbar, which
   is a different thing entirely and the user rejected it: they want the
   containers to MOVE as they scroll, not a second scrollbar to find and drag.

   Rebuilt in CSS and a few lines of JS rather than installed, because this is
   a static page. The component's rounded cards and borders are left behind. */
.hgallery {
  position: relative;
  background: var(--paper);
  /* The scroll room the sideways travel is spent against. */
  height: 320vh;
}

.hgallery-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-top: clamp(72px, 9vh, 110px);
  box-sizing: border-box;
}

/* IN FLOW, NOT OVER THE TRACK. It was absolutely positioned at the top of the
   pinned frame while the track was vertically centred in the same frame, so on
   a short viewport the heading simply sat on top of the plates. The frame is a
   flex column now and the two cannot overlap whatever the window height. */
.hgallery-head {
  flex: none;
  width: 100%;
  box-sizing: border-box;
  padding: 0 clamp(16px, 4vw, 56px);
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 24px;
  margin-bottom: clamp(20px, 3.4vh, 44px);
}

.hgallery-head h2 {
  margin: 0;
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(1.7rem, 3.4vw, 3rem);
  line-height: 1.02;
  letter-spacing: -0.002em;
}

.hgallery-head .more {
  margin-top: 0;
}

.hgallery-track {
  display: flex;
  gap: clamp(20px, 2.6vw, 40px);
  width: max-content;
  padding: 0 clamp(16px, 4vw, 56px);
  will-change: transform;
}

.plate {
  flex: none;
  width: clamp(230px, 27vw, 420px);
  text-decoration: none;
  color: inherit;
  display: block;
}

.plate__img {
  width: 100%;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  display: block;
  background: rgba(0, 0, 0, 0.06);
  transition: filter 420ms ease;
}

.plate__name {
  display: block;
  margin-top: 15px;
  font-family: var(--display);
  font-weight: 300;
  font-size: clamp(1.1rem, 1.9vw, 1.5rem);
  line-height: 1.08;
  transition: color 260ms ease;
}

.plate__meta {
  display: block;
  margin-top: 6px;
  font-family: var(--text);
  font-size: 0.8125rem;
  color: rgba(0, 0, 0, 0.55);
}

@media (hover: hover) and (pointer: fine) {
  .plate:hover .plate__img,
  .plate:focus-visible .plate__img {
    filter: brightness(1.06);
  }
  .plate:hover .plate__name,
  .plate:focus-visible .plate__name {
    color: var(--accent);
  }
}

/* No JavaScript, or reduced motion: the wrapper stops being tall and the track
   becomes an ordinary scrollable row, so every project is still reachable. */
@media (prefers-reduced-motion: reduce) {
  .hgallery {
    height: auto;
  }
  .hgallery-pin {
    position: static;
    height: auto;
    padding: clamp(120px, 18vh, 190px) 0 clamp(40px, 6vh, 80px);
  }
  .hgallery-track {
    overflow-x: auto;
    transform: none !important;
  }
}

@media (max-width: 760px) {
  .hgallery {
    height: 260vh;
  }
  .hgallery-head {
    flex-direction: column;
    gap: 10px;
  }
  .plate {
    width: 74vw;
  }
}

/* ------------------------------------------- THE MARK RISES INTO PLACE */
/* The other half of 21st.dev's beam footer, which the first build missed: the
   letters are sitting below the line and rise into place, one after another,
   the first time the footer is scrolled to.

   Two nested elements, because the two movements want different speeds. The
   group carries the slow rise (1.2s, staggered 70ms a glyph) and the path
   inside it carries the quick lift under the cursor (420ms). A single element
   can only hold one transform transition, so one of them would always be
   running at the wrong tempo.

   The rise is in SVG user units against a 169-unit viewBox, so it scales with
   the mark instead of being a fixed pixel drop. The wrapper clips, which is
   what makes them rise OUT of the bottom edge rather than fade in mid-air. */
/* CLIPPED AT THE BOTTOM ONLY. overflow:hidden was added so the glyphs rise
   out of the lower edge, and it also cut the tops off the S and the t, which
   is the one thing a wordmark must never have done to it. A clip-path open at
   the top does the rise and leaves the letterforms whole. */
.foot-beam {
  overflow: visible;
  clip-path: inset(-40% 0 0 0);
}

.foot-glyph-rise {
  transform: translateY(var(--rise, 0));
  transition: transform 1200ms cubic-bezier(0.16, 0.84, 0.2, 1) var(--d, 0ms);
}

/* Only hidden once the script has said it will bring them back. A blocked
   script leaves the mark sitting exactly where it belongs. */
/* px, not a bare number. translateY(150) is invalid CSS and the whole
   transform is dropped, so the letters simply never moved. Inside an SVG a
   px in a CSS transform IS a user unit, so 150 here is 150 of the 169-unit
   viewBox height: the glyphs start just below their own baseline. */
.fx-mark .foot-glyph-rise {
  --rise: 150px;
}

.fx-mark .foot-beam.is-in .foot-glyph-rise {
  --rise: 0px;
}

@media (prefers-reduced-motion: reduce) {
  .foot-glyph-rise {
    --rise: 0px !important;
    transition: none;
  }
}

/* ============================================================ THE LOADER */
/* The mark assembles on black, a bar fills under it, and the site arrives.
   The glyphs rise exactly as they do in the footer, so the first gesture a
   visitor sees and the last one are the same.

   IT CANNOT TRAP ANYONE. The overlay carries its own keyframe that takes it
   away after four seconds with no JavaScript involved at all; the script only
   ever makes it leave sooner. A loader is the one piece of a site that can
   lock somebody out completely, so it is built to fail open. */
.loader {
  position: fixed;
  inset: 0;
  z-index: 200;
  display: grid;
  place-items: center;
  background: var(--ink);
  color: var(--light);
  animation: loader-fallback 600ms cubic-bezier(0.4, 0, 0.2, 1) 4600ms forwards;
}

.loader-in {
  width: min(460px, 68vw);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(26px, 4vh, 44px);
}

.loader-mark {
  width: 100%;
  height: auto;
  display: block;
  overflow: hidden;
}

/* Same rise as the footer: below the line, then up, one glyph after another. */
.ld-glyph {
  transform: translateY(150px);
  animation: ld-rise 900ms cubic-bezier(0.16, 0.84, 0.2, 1) var(--d, 0ms)
    forwards;
}

@keyframes ld-rise {
  to {
    transform: translateY(0);
  }
}

/* One hairline that fills. No percentage, no spinner, no count: this is the
   length of the mark's own animation made visible, and saying "47%" would be
   inventing a number nobody measured. */
.loader-bar {
  display: block;
  width: 100%;
  height: 1px;
  background: rgba(255, 255, 255, 0.18);
  overflow: hidden;
}

.loader-bar i {
  display: block;
  height: 100%;
  width: 100%;
  background: var(--accent);
  transform-origin: 0 50%;
  transform: scaleX(0);
  animation: ld-bar 2300ms cubic-bezier(0.3, 0, 0.2, 1) 60ms forwards;
}

@keyframes ld-bar {
  to {
    transform: scaleX(1);
  }
}

/* Out. Set by the script once the bar has run and the document is ready. */
.loader.is-done {
  animation: none;
  opacity: 0;
  visibility: hidden;
  transition: opacity 620ms cubic-bezier(0.4, 0, 0.2, 1),
    visibility 0s linear 620ms;
}

@keyframes loader-fallback {
  to {
    opacity: 0;
    visibility: hidden;
  }
}

/* Nothing behind the loader scrolls while it is up, and the moment it goes the
   page is where it should be. */
html.is-loading,
html.is-loading body {
  overflow: hidden;
}

@media (prefers-reduced-motion: reduce) {
  .ld-glyph {
    transform: none;
    animation: none;
  }
  .loader-bar i {
    transform: scaleX(1);
    animation: none;
  }
  .loader {
    animation-delay: 700ms;
  }
}

/* The word on the leading edge of the statement's fill takes the accent and
   passes it on, so a single mark of colour travels along the sentence as it
   is read. The accent is an annotation on this brand, never a fill, and one
   travelling word is the smallest possible amount of it. */
.statement .word {
  transition: color 420ms ease;
}

.statement .word.is-hot {
  color: var(--accent);
}

/* Already seen this visit: the loader is never painted at all. The class is
   set inline in the head, before first paint, so there is no flash of it on
   the way back to the home page. */
.no-loader .loader {
  display: none !important;
}

/* ================================================== ONE HOVER VOCABULARY */
/* The site had grown four different hovers: a cursor-tracked veil on the
   services cards, a rule filling in accent on the home services rows, a
   brightness lift on the project plates, an icon colour change on the process
   steps. Each was reasonable on its own and together they read as four
   unrelated systems, which the user spotted.

   Two treatments now, chosen by what the thing IS rather than by which page
   it is on:

     anything carrying a photograph   the picture eases closer, its heading
                                      takes the accent, its icon follows
     a text-only row                  the rule fills in accent and the name
                                      steps across

   So a visitor learns the site's one gesture once. */
.service__shot {
  position: relative;
  display: block;
  overflow: hidden;
}

.service__img,
.plate__img {
  transition: transform 700ms cubic-bezier(0.22, 0.61, 0.36, 1),
    filter 500ms ease;
}

.service h2,
.plate__name {
  transition: color 300ms ease;
}

.service .service__icon {
  transition: color 300ms ease;
}

@media (hover: hover) and (pointer: fine) {
  .service:hover .service__img,
  .plate:hover .plate__img,
  .plate:focus-visible .plate__img {
    transform: scale(1.04);
    filter: brightness(1.05);
  }

  .service:hover h2,
  .plate:hover .plate__name,
  .plate:focus-visible .plate__name {
    color: var(--accent);
  }

  .service:hover .service__icon {
    color: var(--accent);
  }
}

@media (prefers-reduced-motion: reduce) {
  .service__img,
  .plate__img {
    transition: none;
    transform: none !important;
  }
}

/* ======================================== THE STATEMENT, PINNED AND FILLED */
/* RESTORED. This block was lost when an unrelated section was spliced out of
   this file by line markers, which is the third time that has cost something
   in this stylesheet. Appends are safe here; range deletions are not.

   The band is tall and its contents stick to the middle of the screen, so the
   reader scrolls THROUGH the sentence rather than past it. That is the only
   reliable answer to "people will skip it": the line is in front of them for
   the whole four or five gestures the fill takes, and the page moves on only
   once it is black. */
.band.is-statement {
  padding-top: 0 !important;
  padding-bottom: 0 !important;
  height: 300vh;
  display: block !important;
  position: relative;
}

.statement-pin {
  position: sticky;
  top: 0;
  height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 0 clamp(16px, 4vw, 56px);
  box-sizing: border-box;
}

@media (prefers-reduced-motion: reduce) {
  .band.is-statement {
    height: auto;
    padding-top: clamp(90px, 14vh, 170px) !important;
    padding-bottom: clamp(90px, 14vh, 170px) !important;
  }
  .statement-pin {
    position: static;
    height: auto;
  }
}

/* ============================= SCROLL PACING ON A PHONE */
/* The film and the rest of the page have to advance at rates that feel like
   the same page. Measured on a 844px viewport before this: the hero was 13
   viewports and took 64 per cent of the whole document, while every content
   section below it was under one. So the film crawled for ten swipes and then
   everything else flicked past, which is exactly what the user described.

   The cause is that a phone gesture is not a wheel notch. A notch moves about
   100px, roughly a tenth of a viewport; a flick moves two or three whole
   viewports. The same 13 viewports that read as deliberate on a desktop are
   three swipes of nothing happening on a phone.

   Six viewports on mobile: the film advances about twice as fast per gesture
   and the hero drops to under half the page, so the two halves match. The
   beats are keyed to FILM progress rather than to pixels, so they still land
   on their own shots at any height. */
@media (max-width: 767px) {
  :root {
    --hero-viewports: 6;
  }

  /* The statement comes down with it, for the same reason: three swipes for
     one sentence is the desktop figure, not the phone one. */
  .band.is-statement {
    height: 200vh;
  }
}
