/* ============================================================
   SILICA WORKS - scroll-driven arc slider
   ------------------------------------------------------------
   A horizontal, infinitely wrapping strip of images. Each slide's
   position, scale and vertical arc offset come from a cosine curve
   of its distance to the band's centre, so slides lift toward the
   middle and drop away at the edges. Page scroll drives the whole
   thing - there is no wheel handler, so the page keeps scrolling
   normally over the section.
   ============================================================ */

.sw-section--slider {
  /* The split layout owns its own spacing, so the column
     centring from .sw-section is not wanted here. */
  display: flex;
  align-items: center;
  padding: 6rem 0;
  overflow: hidden;
}

/* The showreel plate stops short of its own layout box, so it already
   leaves real whitespace below it. Any padding on top of that reads as
   a hole between the reel and "Who we are". */
.sw-reveal + .sw-section--slider {
  padding-top: 0;
}

.sw-split {
  width: 100%;
  display: grid;
  /* Media side still leads, but by less than before - the paragraph
     needs the extra width now that it sets larger. */
  grid-template-columns: 1fr 1.2fr;
  align-items: center;
  gap: 2.5rem;
  min-height: inherit;
}

/* Mirrored split - images left, copy right. The source order is left
   alone (copy first, then media) so the stacked layout below 1000px
   still reads paragraph-then-images and the tab order stays sane;
   only the grid tracks swap. The column ratio flips with them, so the
   paragraph keeps the wider 1.2fr track it needs on this side too. */
.sw-split--reverse {
  grid-template-columns: 1.2fr 1fr;
}

/* Copy left, images right, in source order - but the copy keeps the
   wider track, so the ratio flips from the base split. */
.sw-split--copy-left {
  grid-template-columns: 1.2fr 1fr;
  /* Cards exit and park past the RIGHT edge here, which would open
     a horizontal scrollbar once the pin releases. clip (not hidden)
     makes no scroll container, so the pin and the y overflow are
     untouched. */
  overflow-x: clip;
}

/* Bigger cards on the mirrored layout. The ceiling on .sw-card was
   set by a card exiting LEFT across "OUR"; here it exits right,
   and the title sits above the frame (z-index 2), so a corner
   passing under it is hidden rather than clipped. */
.sw-split--copy-left .sw-card {
  height: 94%;
  max-height: 660px;
}

/* The paragraph settles on black (data-copy-final); this covers the
   no-script case so it reads the same. */
.sw-split--copy-left .sw-section-body {
  color: #000;
}

/* Handwritten note pointing at the cards - the hero's .ap-note,
   mirrored: note on the left, arrow off its right edge. */
.sw-cap-note {
  position: relative;
  align-self: flex-end;
  /* In em so the two-line break holds as the face scales up. */
  width: min(100%, 11.5em);
  margin: 3.75rem 0 0;
  text-wrap: balance;
  font-family: "Shadows Into Light", "Inter Tight", cursive;
  font-size: clamp(1.15rem, 0.55vw + 0.85rem, 1.7rem);
  line-height: 1.22;
  letter-spacing: 0.03em;
  -webkit-text-stroke: 0.018em currentColor;
  color: #4a4a44;
  /* --cap-shift is set by js/cards.js so the arrow tip lands just
     short of the top card whatever the viewport. */
  transform: translateX(var(--cap-shift, 0px)) rotate(-4deg);
  transform-origin: 0 0;
}

.sw-cap-arrow {
  position: absolute;
  left: 100%;
  top: -20%;
  width: clamp(4.5rem, 6vw, 7rem);
  height: auto;
  margin-left: 0.4rem;
  overflow: visible;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Drawn in once the words are written; with no script (or reduced
   motion, where .is-writing is never set) it simply shows. */
.sw-cap-arrow path {
  stroke-dasharray: 1;
  stroke-dashoffset: 0;
}
.sw-cap-note.is-writing .sw-cap-arrow path {
  stroke-dashoffset: 1;
}
.sw-cap-note.is-written .sw-cap-arrow path {
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 0.7s cubic-bezier(0.65, 0, 0.35, 1);
}
.sw-cap-note.is-written .sw-cap-arrow path + path {
  transition-delay: 0.55s;
  transition-duration: 0.3s;
}

/* Stacked, the cards are below rather than beside it, so the
   sideways arrow would point at nothing. */
@media (max-width: 1000px) {
  .sw-cap-note {
    display: none;
  }
}

/* The sticky variant. A sticky child only sticks inside a scrollport,
   and `overflow: hidden` on an ancestor makes that ancestor the
   scrollport - the column would then stick to a box that never
   scrolls, i.e. not at all. The card stack is clipped by its own
   frame (.sw-cards), so the section does not need to clip anything
   here - and it must not: the stack is pinned, and a clipping
   ancestor would cut the cards as they push toward the viewer. */
.sw-section--sticky {
  overflow: visible;

  /* ----------------------------------------------------------
     THE PINNED SECTION MUST FIT ONE VIEWPORT, EXACTLY.

     While the stack is dealing, this section IS the screen -
     ScrollTrigger holds it at top:0 for the whole run. Anything
     that makes it taller than the viewport is scrolled past
     before the pin releases, so the bottom of the card frame
     was being cut off and the first card sat 1px below the
     fold.

     .sw-section's own box cannot do this: it sets min-height
     100vh and then adds 6rem top and 10rem bottom padding on
     top of that, so the real height was ~1289px in a 900px
     window. Height is therefore pinned to the viewport here and
     the padding brought down to something that fits inside it
     rather than adding to it.

     min-height is overridden as well as height - it is set
     twice in .sw-section (100vh then 100svh) and the later one
     would otherwise keep floor-ing this box back up. */
  height: 100vh;
  height: 100svh;
  min-height: 0;
  /* THE BAR IS AT THE BOTTOM. .sw-menu is bottom: 2rem, 60px
     tall - a floating bar, not a top nav - so the space that has
     to be reserved is under the stack, not over the title. The
     first pass had these the other way round, which is what put
     a slab of empty space above "Our full capabilities" while
     the cards ran down behind the bar.

     Top is now just enough air to keep the title off the
     section seam. Bottom clears the bar's 60px plus its 2rem
     inset, plus a little so a card's corner does not graze it
     on the way out. */
  padding-top: clamp(2.5rem, 5.5vh, 4rem);
  padding-bottom: clamp(6.5rem, 11.5vh, 8.5rem);
  /* The two children are the title and the split. Packed from
     the top so the leftover room collects UNDER the stack
     rather than being shared out above the title - centring is
     what put a slab of empty space over "Our full
     capabilities". */
  justify-content: flex-start;
  gap: 0;
}

/* The title is the section's header, and inside the pin it is
   competing with the stack for a fixed viewport. Its own
   bottom margin (clamp(3rem, 7vh, 6rem) - up to 96px) plus the
   section's padding was most of the gap the stack needed.

   Trimmed here rather than on .sw-hww-title, which work-v2.html
   also uses on an unpinned section where the generous margin is
   correct. */
.sw-section--sticky .sw-hww-title {
  margin-bottom: clamp(1rem, 2.5vh, 2rem);

  /* ABOVE THE CARDS. A card exits by lifting to -200 yPercent,
     which carries it up through the title's line - and since
     the cards are transformed they paint over a static sibling
     regardless of source order. The card was crossing in FRONT
     of "OUR FULL CAPABILITIES", which is the clipped look.

     A stacking context of its own, above the frame's, so the
     card slides behind the heading and is masked by it. The
     title is opaque ink on the paper ground, so it reads as
     the card going under the type rather than being cut. */
  position: relative;
  z-index: 2;
}

/* The split takes whatever the title left, and the card frame
   sizes against it (see .sw-cards height below). Without this
   the grid is content-sized and the frame's own height is the
   only thing setting the stack's scale, which is how it came to
   overflow the section it sits in. */
.sw-section--sticky .sw-split {
  flex: 1;
  min-height: 0;
}

/* The media column inside the pin, and the height chain that
   lets the card frame size itself.

   .sw-split-media carries two rules from the wave layout that
   are actively wrong here: align-self: start, which sizes the
   column to its content, and a margin-top of up to 26rem that
   dropped the image band below the paragraph. Together they
   left the frame's height: 100% resolving against nothing -
   it collapsed to 0 and the cards vanished.

   Stretching the column gives it the row's full height, and
   the row is the one the section sized to the viewport. That
   is the definite height the frame needs. */
.sw-section--sticky .sw-split-media {
  align-self: stretch;
  /* Drops the stack clear of the title's band.

     The card exits sideways across the top-left corner, which
     is exactly where "OUR" sits, and the two were competing
     for the same pixels: every increase in card size put the
     exit back into the heading, and every fix for that shrank
     the card until it floated in its column.

     Insetting the frame's TOP settles it. The heading keeps
     its full-measure spread, the stack gets a lane that starts
     below it, and the card can be sized to fill its column
     again without its flight path ever reaching the type. The
     space is not lost - the section's bottom padding already
     reserves the bar's height, and the frame simply uses the
     room between the two. */
  padding-top: clamp(2.25rem, 5.9vh, 4.5rem);
  /* And a little off the foot for the same reason, against the
     bar rather than the title. Short viewports need it most -
     they carry the same 60px bar in far less height - so this
     leans on the vh term. */
  padding-bottom: clamp(0.5rem, 2.75vh, 2rem);
  margin-top: 0;
  /* It is a flex column holding one child; the frame takes the
     whole of it. */
  justify-content: stretch;
  min-height: 0;
  gap: 0;
}

/* Each link in the chain has to pass a definite height down or
   the frame's 100% has nothing to resolve against. The grid
   row is definite, so this only has to stop growing past it. */
.sw-section--sticky .sw-split-media > .sw-cards {
  flex: 1;
  min-height: 0;
  /* Under the title above it - see the z-index note there. */
  position: relative;
  z-index: 1;

  /* NO MASK, AND NOTHING CLIPPED.

     An earlier pass faded the top of this frame, because the
     cards then exited upward into the title and had to be hidden
     as they crossed it. The exit is sideways now (js/cards.js),
     out over the section's own outer edge, so there is nothing
     to hide: the card plays its whole arc and leaves on its own.

     Nothing may clip here. overflow on this frame would flatten
     the 3D - it carries the perspective the cards push back
     into - and a mask would cut the card exactly as it reaches
     the edge, which is the artificial look this replaced. The
     card is simply allowed to go. */
}

/* ITS OWN GUTTER, for the same reason .sw-hww-title needs one:
   .sw-section--slider zeroes the section's side padding because
   .sw-split normally supplies it per column, so this column
   starts at the raw viewport edge.

   On the media COLUMN, not on .sw-cards: the cards are absolutely
   positioned, so they resolve against the frame's padding box and
   padding on the frame would only move the box they are centred
   in without changing how close the widest of them comes to the
   edge. Insetting the column moves the frame itself, which does.

   Inside the pinned block so it cannot reach the stacked layout
   below 1000px, where the cards are full-width by design and the
   section's own gutter already holds them off the edge. */
.sw-section--sticky .sw-split-media {
  padding-left: 2.5rem;
}

/* With the stack on the right the outer edge is the right one. */
.sw-section--sticky .sw-split--copy-left .sw-split-media {
  padding-left: 0;
  padding-right: 2.5rem;
  /* Lifts the stack a little closer to the title than the shared
     inset above - the exit is sideways here, so it does not need
     the full lane clear of the heading. */
  padding-top: clamp(0.75rem, 2.35vh, 2rem);
  /* The cards behind the top one peek out below it, so the pile
     reaches lower than the top card does. The shared inset let
     that last edge run onto the floating nav bar; this holds the
     whole stack clear of it. */
  padding-bottom: clamp(2.25rem, 6.1vh, 4rem);
}

/* The copy column drops a step, so "We design." and the paragraph
   sit lower against the stack beside them. top rather than a
   margin: the column is centred in its row, and a margin would
   only move it half as far while also changing its box. */
.sw-section--sticky .sw-split--copy-left .sw-split-copy {
  position: relative;
  top: clamp(2.5rem, 7.5vh, 5.5rem);
}

/* A slight tilt on the handwritten mark, so it reads as written in
   rather than typeset - the same hand as the note below. Pivots on
   its left edge so the line keeps the column's margin. */
.sw-split--copy-left .sw-story {
  transform: rotate(-4deg);
  transform-origin: 0 100%;
}

/* A narrower measure - the paragraph was running past 60
   characters a line at 1.25rem, which reads as a slab. */
.sw-section--sticky .sw-split--copy-left .sw-section-body {
  max-width: 27em;
}

/* The CTA closes the paragraph, so it sits on the column gap
   alone rather than adding .ap-btn's own top margin to it. */
.sw-split--copy-left .sw-cap-cta {
  margin-top: 1.5rem;
}

/* With the CTA now above it, the note sits up close under it -
   then lifted level with the CTA by an offset rather than a
   margin, so it clears the floating nav on short viewports
   without changing the column's height (which would shift the
   centred copy along with it). */
.sw-split--copy-left .sw-cap-note {
  margin-top: 1.25rem;
  top: -4rem;
}

/* ...but only while the columns are side by side. Below the
   breakpoint the stack is full-width by design and the
   section's own gutter already holds it off the edge, so this
   inset would just eat the track and leave the cards sitting
   off-centre against the copy above them. */
@media (max-width: 1000px) {
  .sw-section--sticky .sw-split-media,
  .sw-section--sticky .sw-split--copy-left .sw-split-media {
    padding-left: 0;
    padding-right: 0;
  }
}

/* Sticky needs a tall row to travel inside. .sw-split centres its
   two columns, which sizes the row to the taller of them and leaves
   the copy column no room to move - sticky would then have nothing
   to offset against and would never engage. Stretching the row lets
   the copy column's own align-self: start size it to its content
   while the row stays as tall as the wave beside it.

   This is on the grid, not on the section: the section is a flex
   container and its align-items governs .sw-split as a whole, not
   the two columns inside it. */
.sw-section--sticky .sw-split {
  align-items: stretch;
}

.sw-split--reverse .sw-split-copy {
  /* Row pinned as well as column. With grid-row left auto, the copy
     claims row 1 / col 2 first and auto-placement will not backtrack
     to fill col 1 behind it, so the media dropped to a second row and
     the two columns stacked diagonally instead of sitting side by
     side. Naming the row for both items keeps them on one line. */
  grid-row: 1;
  grid-column: 2;
  /* The padding is an outside gutter, so it moves to the other edge
     along with the column. */
  padding-left: 0;
  padding-right: 2.5rem;
  /* max-width alone would leave the column hugging the gutter and
     open a gap against the images; pushing it to the track's end keeps
     the measure capped while the text still reaches the outer edge.
     justify-self rather than margin-left:auto - an auto margin on a
     grid item opts it out of the container's align-items, which left
     this column pinned to the top of a row the tall image band had
     already stretched, instead of centred against it. */
  justify-self: end;
}

/* ------------------------------------------------------------
   Sticky copy. Inside the sticky section the text column parks
   against the viewport while the wave column keeps travelling, so
   the statement stays readable for the whole run of images rather
   than scrolling away after the first two plates.

   align-self: start is what makes this work at all - a stretched
   grid item is already as tall as its row, so it has no room to
   move within it and sticky has nothing to offset against. Sized
   to its content, the column can travel the row's full height.
   ------------------------------------------------------------ */
/* NOT sticky any more. It was sticky because the wave column
   scrolled past it - the section itself never moved, so the
   paragraph had to park itself. The stack is pinned instead,
   which holds the whole section still, and a sticky child
   inside a pinned parent has nothing left to offset against:
   it simply sat at its top inset until the pin released and
   then jumped as it unstuck, which was the visible glitch at
   the end of the run.

   Centred against the stack beside it instead, so the two
   columns read as one composition for the whole deal. */
.sw-section--sticky .sw-split-copy {
  position: static;
  align-self: center;
  margin-bottom: 0;
}

.sw-split--reverse .sw-split-media {
  grid-row: 1;
  grid-column: 1;
  /* The drop was set to seat a 56vh band against the paragraph's
     centre line. The wave column is several times that, so the same
     offset only pushes an already-tall column further down and
     leaves the copy stranded against its top. The grid centres the
     two columns on its own here. */
  margin-top: 0;
}

/* ------------------------------------------------------------
   Card stack - every card in one frame, dealt away as the
   section is pinned and scrubbed. Transforms are written every
   frame by js/cards.js; everything here is the resting state,
   so the stack still reads as a clean pile of cards with the
   script absent or motion reduced.

   This replaced the wave column and its thread rail. See the
   header of js/cards.js for why.
   ------------------------------------------------------------ */
.sw-cards {
  position: relative;
  width: 100%;
  /* The frame the cards are centred in. It has to hold its own
     height because every card inside it is absolutely
     positioned, so nothing else can size it.

     100% of the split's row, which the section has already
     sized to the viewport minus the title and padding - so the
     frame can never be taller than the pin that holds it. A
     viewport-relative height here instead (the 76vh this
     replaced) does not know about the title above it, which is
     how the stack came to overhang the fold. */
  height: 100%;
  min-height: 0;
  /* The one perspective for the whole stack, so the cards share
     a vanishing point as they push back in z. On each card
     instead, every card would get its own and the exit would
     read flat. */
  perspective: 1200px;
}

.sw-card {
  position: absolute;
  top: 50%;
  left: 50%;
  /* Sized from the HEIGHT, with the width following through the
     ratio - the frame is the scarce axis while the section is
     pinned to one viewport, so the card has to be the thing
     that fits it.

     78% rather than 100% leaves the headroom the deal needs:
     the card on top lifts to -200 yPercent on its way out and
     the cards behind it sit up to 50 yPercent lower, and both
     of those happen INSIDE this frame.

     max-width caps it on the other axis. Without it a short,
     wide frame resolves a card wider than the column it sits
     in - measured at left:-45px on a 1440x900 window, i.e. the
     stack hanging off the left edge of the page - because
     aspect-ratio will happily derive a width the parent cannot
     hold. The height then yields to the cap through the same
     ratio. */
  /* Height leads, width follows the ratio, and whichever runs
     out first wins - so the card is as large as BOTH axes of
     its column allow.

     The height is the exit budget: a card lifts to -200
     yPercent on its way out and the ones waiting sit up to 50
     yPercent lower, and both happen inside this frame, so the
     card cannot have all of it.

     Both numbers are set by the EXIT, not by how large the
     card could be. The card leaves sideways across the
     section's top-left corner, where the title sits, and the
     bar floats at the foot - so the stack needs clear air
     above and below it to fly through. At 84%/92% the card
     was 505px tall in a 601px frame, leaving ~48px at each
     end, and no exit path existed that did not clip the
     heading or the bar; every rotation and offset tried just
     moved the collision around. Sizing the card so the lane
     exists is the fix, rather than tuning a path through a
     gap that is too small. */
  height: 88%;
  max-width: 97%;
  aspect-ratio: 4 / 5;
  /* 88% IS THE CEILING, and it was measured rather than picked:
     89% puts a corner back into the heading and the bar on
     every size tested, and it degrades fast from there. The
     card is HEIGHT-bound, not width-bound - it uses barely
     half the column it sits in, and max-width never binds -
     so this percentage and the frame's own height are the
     only two levers. Growing it past this means widening the
     lane first (the section's bottom padding and the frame's
     insets, both above), then re-running the collision check.

     Short viewports have the same title and the same 60px bar
     but far less room between them, so the percentage resolves
     to too few real pixels and the card's corner reaches the
     bar on its way out. Capping the height in px as well hands
     those screens the clearance back, and keeps the very large
     ones from growing a card out of proportion to its column.
     (.sw-split--copy-left raises both - see below.) */
  max-height: 600px;
  overflow: hidden;
  /* Rewritten every frame while the stack is live. The script
     also sets xPercent/yPercent -50 to centre it; this is the
     fallback for when the script never runs, so the flat stack
     is still centred in its frame. */
  transform: translate(-50%, -50%);
  will-change: transform;
  background: #edede8;

  /* THE SAME LIFT THE FEATURED PLATES HAVE.

     The stack was reading flat and over-unified: six identical
     bevelled rectangles, evenly spaced, each under the same
     even scrim, with nothing separating one from the next but
     its photograph. The featured index's plates (.sw-pv,
     css/featured.css) solve this already and this section is
     next to them on the page, so it takes their treatment
     rather than inventing a second one.

     drop-shadow rather than box-shadow, for the reason given
     there: a filter shadow is taken from the CLIPPED
     silhouette, so it follows all eight sides of the bevel,
     while a box-shadow is drawn from the rectangular border
     box and would sit square behind the cut and undo it.

     Deeper than the featured plates' 18px/34px - those lie
     flat on the page, and these are a stack standing in
     perspective, where the top card is genuinely nearer than
     the one under it. The shadow is what says so. */
  filter: drop-shadow(0 22px 40px rgba(20, 20, 15, 0.28));

  /* The plates' bevel, kept. It is the section's silhouette -
     the same cut the showcase and work pages use - and the
     cards inherit it rather than arriving as plain rectangles.
     A border would not survive this: clip-path slices through
     the border box and leaves cut ends at the bevels, which is
     why the edge is the bevel itself. */
  --sw-card-notch: 14px;
  clip-path: polygon(
    var(--sw-card-notch) 0,
    calc(100% - var(--sw-card-notch)) 0,
    100% var(--sw-card-notch),
    100% calc(100% - var(--sw-card-notch)),
    calc(100% - var(--sw-card-notch)) 100%,
    var(--sw-card-notch) 100%,
    0 calc(100% - var(--sw-card-notch)),
    0 var(--sw-card-notch)
  );
}

/* Source order is top-of-stack first, so the paint order has to
   be reversed - the first card must sit ABOVE the rest. Six
   cards, counted down from 6. Keep this in step with the number
   of [data-card] in the markup: a card past the end of this
   list gets no z-index and paints under every card before it,
   which shows up as the last card refusing to leave the frame.

   :nth-child rather than ids - the cards are a list, and ids
   would have to be kept in step with the markup by hand for no
   gain. */
.sw-card:nth-child(1) { z-index: 6; }
.sw-card:nth-child(2) { z-index: 5; }
.sw-card:nth-child(3) { z-index: 4; }
.sw-card:nth-child(4) { z-index: 3; }
.sw-card:nth-child(5) { z-index: 2; }
.sw-card:nth-child(6) { z-index: 1; }

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

/* The cards behind the top one need holding back, or the stack
   reads as one bright pile with no order to it. A scrim over
   each card, cleared only on the card on top - which is the
   one the script leaves untransformed in the stack's depth
   sense, so it is addressed by paint order rather than by a
   class the script would have to write every frame. */
.sw-card::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* Above the image, below the caption */
  z-index: 1;

  /* ANGLED, not flat. A single even wash over every card was
     most of why the stack looked like one object rather than
     six - the same value across the whole frame gives the eye
     no edge to separate them on.

     Raked from the top-left instead, so each card is darker
     where the card in front of it would cast and lighter at
     the corner that catches the light. The cards sit at
     slightly different depths, so the same gradient lands
     differently on each and the stack reads as lit rather than
     tinted.

     It stays on every card including the top one: the deal
     rotates cards as they leave, and clearing the wash on the
     front card alone made it flash brighter at the moment it
     started moving. */
  background: linear-gradient(
    145deg,
    rgba(20, 20, 15, 0.04) 0%,
    rgba(20, 20, 15, 0.16) 45%,
    rgba(20, 20, 15, 0.34) 100%
  );
}

/* ------------------------------------------------------------
   Card caption - laid over the image, not under it. A label
   below each card would turn the stack back into a list; over
   the image it reads as a print credit, exactly as it did on
   the plates this replaced.
   ------------------------------------------------------------ */
.sw-card-cap {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  /* Above both the image and the scrim */
  z-index: 2;
  padding: 2.25rem 1.25rem 1.1rem;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: #fff;
  /* The cards are photographs, so the caption needs its own
     ground - a scrim rather than a solid bar, so it darkens the
     foot of the image without cutting a band across it. */
  background: linear-gradient(
    to top,
    rgba(20, 20, 15, 0.72),
    rgba(20, 20, 15, 0)
  );
  pointer-events: none;
}


/* ------------------------------------------------------------
   Right - the slider band
   ------------------------------------------------------------ */
.sw-split-media {
  display: flex;
  flex-direction: column;
  gap: 1.75rem;
  min-width: 0;
  /* The image side is dropped below the text column so the band sits
     under the paragraph rather than level with it.

     Anchored to the top of its grid row rather than centred: while
     the column was centred, this offset fought the band's own 56vh
     height - a taller viewport grew the band faster than the margin
     could push it down, and on a 1080p screen the images drifted back
     ABOVE the paragraph. Starting from the top makes the margin the
     only thing setting the band's vertical position. The copy column
     keeps the grid's centring; only this column opts out.

     The offset is solved against measured positions rather than
     guessed - the drop needed grows faster than the viewport does
     (~264px at 900px tall, ~368px at 1080px), which is the line
     below. Clamped so short and very tall screens stay sane. */
  align-self: start;
  margin-top: clamp(12rem, calc(57.8vh - 256px), 26rem);
  /* The heading sizes against this column rather than the viewport,
     so it holds its line whatever share of the grid the column gets */
  container-type: inline-size;
}

.sw-split-heading {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  /* Centred under the image band - the heading is the column's
     anchor, so it sits on the band's own axis. Padding on both sides
     keeps it centred on the column rather than pushed off by a
     one-sided inset. */
  align-items: center;
  text-align: center;
  padding-left: 2.5rem;
  padding-right: 2.5rem;
}

/* The paragraph under each image band, where the "Built to be used"
   and "Made to hold up" titles used to sit. Both splits now carry one
   of these instead of a heading.

   Set in the HERO BLURB's exact voice rather than in a title's - the
   face, the size ramp, the tracking, the stroke and the grey are all
   lifted from .sw-intro-blurb / .sw-intro-note in css/intro.css. The
   page now has three handwritten passages and they have to read as
   one hand; a caption sized off the column (cqi) drifted away from
   the hero's as soon as the grid changed.

   So the size is on the same vmin ramp the hero pair uses, not on
   cqi: these should match each other across every viewport, and only
   a shared unit does that. Change one, change both. */
.sw-split-note {
  font-family: "Shadows Into Light", "Inter Tight", cursive;
  /* Ships a single weight. Naming anything heavier here would only
     invite a synthesized fake bold. */
  font-weight: 400;
  font-size: clamp(1.05rem, 1.15vmin + 0.95rem, 2.15rem);
  letter-spacing: 0.035em;
  /* Keeps this in step with the page's other display accents;
     without it the face reads visibly thinner than the rest. */
  -webkit-text-stroke: 0.018em currentColor;
  /* Tighter than this face wants, but it still clears the
     descenders it swings well below the baseline. */
  line-height: 1.38;
  text-transform: none;
  color: #55554e;
  /* Wraps, unlike the title it replaces - so no white-space: nowrap
     and no override needed once the columns stack. Measured to break
     at three or four lines under the band; the cap stops it running
     the full width of the column, where a hand-set paragraph loses
     its line as the eye returns. */
  max-width: 26em;
  /* Rag right rather than centred: this is a sentence, not a
     caption. The block itself stays centred on the column by the
     flex align-items above. */
  text-align: left;
  margin: 0;
}

/* ------------------------------------------------------------
   The slider band
   ------------------------------------------------------------ */
.sw-slider {
  position: relative;
  width: 100%;
  /* Tall enough for the arc drop plus the centre lift */
  height: 56vh;
  min-height: 360px;
  overflow: hidden;
  /* Seats the band in its own stacking context, which contains the
     large z-indexes inside it: js/slider.js stacks the slides to
     ~14000. Without this they are not contained by anything -
     .sw-slider sets no z-index and
     .sw-section sets none either - so those values compete in the
     ROOT context and paint over anything on the page below 14000,
     including a dragged tag.

     isolation rather than a z-index: it creates the context without
     giving the band a stacking position of its own, so the band's
     own order against its neighbours is exactly what it was. */
  isolation: isolate;
}

.sw-slide {
  position: absolute;
  top: 0;
  left: 0;
  overflow: hidden;
  /* Sizes are written every frame, so hint the compositor */
  will-change: transform, width, height;
  background: #edede8;
}

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

/* ------------------------------------------------------------
   Left - the paragraph, on its own
   ------------------------------------------------------------ */
.sw-split-copy {
  display: flex;
  flex-direction: column;
  /* Spacing between the paragraphs. The label sets its own gap with
     an explicit bottom margin, so this only governs the space
     between the two paragraphs. */
  gap: 1.35rem;
  padding-left: 2.5rem;
  /* Wider measure - the paragraph was breaking early against the
     old 34rem cap while the column still had room. */
  max-width: 42rem;
}

/* Shadows Into Light - a handwriting face. It carries the section's
   one display accent, so the label reads as a mark rather than a
   caption. Overrides .sw-section-label, set in Inter. */
.sw-label-display {
  font-family: "Shadows Into Light", "Inter Tight", cursive;
  font-weight: 400;
  /* Up from 1.75rem - a light handwriting face reads smaller at a
     given size, so it needs more to hold as a mark. */
  font-size: 2.1rem;
  /* Drops the caps and 0.1em tracking inherited from
     .sw-section-label. The face ships a single weight, so the stroke
     is what gives the mark its weight - font-weight: 700 would only
     get a synthesized fake bold. The tracking opens a little to pay
     back the gap the stroke closes between letters. */
  text-transform: none;
  letter-spacing: 0.035em;
  -webkit-text-stroke: 0.028em currentColor;
  color: #14140f;
  /* Room for the descenders the face swings below the baseline */
  line-height: 1.3;
  /* Pulls the mark up off the paragraph so it sits clear of the
     block rather than crowding the first line. The bottom margin
     works with the column gap to open real air under the mark, so
     it reads as a heading over the copy rather than a first line of
     it. The face swings descenders low, so some of this is spent
     clearing them. */
  margin-top: -0.75rem;
  margin-bottom: 0.4rem;
}

/* ------------------------------------------------------------
   The dot opening the mark
   ------------------------------------------------------------
   A full stop set before "Who we are", drawn in the accent so it
   reads as part of the mark rather than as punctuation somebody
   left in. It is a real character in the markup, so it is set in
   the same handwriting face as the words and inherits its
   irregular, hand-drawn shape from the face itself.
   ------------------------------------------------------------ */
.sw-label-dot {
  color: #14140f;
  /* The period sets small, so it is opened up a little to register as
     a mark. It closes the line, so the air goes on its left - the
     right side needs nothing, there is no glyph after it. */
  font-size: 1.15em;
  margin-left: 0.04em;
}

/* The copy under the "Who we are" mark reads as the studio's own
   statement, so it sets in full black rather than the softened grey
   .sw-section-body carries elsewhere on the page. Scoped to the
   split column - the one-line sections keep the quieter grey.

   The decode reveal paints each character inline, so on a page with
   JS the settle color comes from data-copy-final in the markup. This
   is what the paragraph reads as before the reveal runs and if it
   never does; the two are kept at the same black on purpose. */
.sw-split-copy .sw-section-body {
  color: #000;
}

/* ------------------------------------------------------------
   Copy CTA - bare uppercase label with a wiping underline and a
   small round icon badge that spins on hover. No box, so the CTA
   sits in the text column as a line of type rather than a slab.
   ------------------------------------------------------------ */
.sw-btn {
  position: relative;
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  margin-top: 1.25rem;
  color: #14140f;
  text-decoration: none;
  text-transform: uppercase;
}

/* Type metrics lifted from the reference button verbatim - 1rem / 900 /
   -0.025rem, with the 0.1rem optical nudge it uses to sit the label on
   the icon's centreline. */
.sw-btn-label {
  position: relative;
  display: inline-block;
  width: max-content;
  transform: translateY(0.1rem);
  font-size: 1rem;
  font-weight: 900;
  letter-spacing: -0.025rem;
  -webkit-font-smoothing: antialiased;
}

/* Underline grows from the right on the way out and from the left on
   the way in, so the hover reads as a wipe rather than a fade. */
.sw-btn-label::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -0.35rem;
  width: 100%;
  height: 0.1rem;
  background: var(--sw-ink);
  transform: scaleX(0);
  transform-origin: bottom right;
  transition: transform 0.25s ease-out;
  will-change: transform;
}

.sw-btn:hover .sw-btn-label::after,
.sw-btn:focus-visible .sw-btn-label::after {
  transform: scaleX(1);
  transform-origin: bottom left;
}

/* The outer span carries the spin so the badge's own layout stays put. */
.sw-btn-icon {
  display: flex;
  align-items: center;
  justify-content: center;
  perspective: 1000px;
  transform-style: preserve-3d;
  will-change: transform;
}

/* Square badge with two opposite corners sliced, the same chamfer the
   cursor and nav bar carry. The notch is scaled to the badge - the
   11px the cursor uses would eat most of a 1.5rem square. */
.sw-btn-icon-inner {
  --sw-btn-notch: 6px;

  display: flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  background: var(--sw-ink);
  color: var(--sw-paper);
  clip-path: polygon(
    var(--sw-btn-notch) 0,
    100% 0,
    100% calc(100% - var(--sw-btn-notch)),
    calc(100% - var(--sw-btn-notch)) 100%,
    0 100%,
    0 var(--sw-btn-notch)
  );
  transition: background 0.3s ease, color 0.3s ease;
}

.sw-btn:hover .sw-btn-icon-inner,
.sw-btn:focus-visible .sw-btn-icon-inner {
  background: var(--sw-accent);
  color: var(--sw-ink);
}

/* Mark variant: the logo replaces the arrow, so the badge behind it
   goes away. No black square, no chamfer - the mark stands on the
   paper and carries the colour itself, while the flip on the outer
   .sw-btn-icon still runs untouched. */
.sw-btn--mark .sw-btn-icon-inner {
  width: auto;
  height: auto;
  background: none;
  color: var(--sw-ink);
  clip-path: none;
}

.sw-btn--mark:hover .sw-btn-icon-inner,
.sw-btn--mark:focus-visible .sw-btn-icon-inner {
  background: none;
  color: var(--sw-accent);
}

/* The mark is masked rather than embedded so it tints with the button
   instead of baking its own fill. Sized a touch above the 1.5rem badge
   it replaces, since the glyph reads smaller than a filled square. */
.sw-btn-mark {
  display: block;
  width: 1.65rem;
  height: 1.65rem;
  background: currentColor;
  /* The polygon mark. A mask takes the source's ALPHA and ignores its fill, so
     this points at the plain file rather than a coloured copy - the black comes
     from the background above, which is currentColor inheriting --sw-ink from
     .sw-btn--mark .sw-btn-icon-inner. That is also what lets it turn accent on
     hover without a second asset. */
  -webkit-mask: url("../img/polygon-hole-o.svg") center / contain no-repeat;
  mask: url("../img/polygon-hole-o.svg") center / contain no-repeat;
  transition: background 0.3s ease;
}

/* Stroke weight and cap style match the social icons elsewhere on the
   page so the arrow reads as part of the same set. */
.sw-btn svg {
  width: 0.75rem;
  height: 0.75rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Two full turns in, unwinding back on leave - the reference's motion.
   Keyframes rather than a transition so the rotation always completes
   in the same direction. 720 degrees returns the notched square to its
   exact starting orientation, so the chamfer lands back where it began. */
.sw-btn:hover .sw-btn-icon {
  animation: swBtnFlipTwice 0.8s ease-out forwards;
}

.sw-btn:not(:hover) .sw-btn-icon {
  animation: swBtnFlipBack 0.8s ease-out forwards;
}

@keyframes swBtnFlipTwice {
  0% { transform: rotateY(0deg); }
  100% { transform: rotateY(720deg); }
}

@keyframes swBtnFlipBack {
  0% { transform: rotateY(720deg); }
  100% { transform: rotateY(0deg); }
}

.sw-btn:focus-visible {
  outline: 2px solid #14140f;
  outline-offset: 6px;
}

/* Touch pointers get no hover state to leave, so the spin would just
   flash on tap - drop both effects there. */
@media (max-width: 1000px) {
  .sw-btn-label::after {
    display: none;
  }

  .sw-btn:hover .sw-btn-icon,
  .sw-btn:not(:hover) .sw-btn-icon {
    animation: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sw-btn-label::after {
    transition: none;
  }

  /* Colour still changes on hover - only the spin is dropped. */
  .sw-btn:hover .sw-btn-icon,
  .sw-btn:not(:hover) .sw-btn-icon {
    animation: none;
  }
}

.sw-split-copy .sw-section-body {
  max-width: none;
  /* Up from the 1.0625rem base - the column is wider now, and the
     paragraph is the section's main read. */
  font-size: 1.25rem;
  line-height: 1.55;
}

/* ------------------------------------------------------------
   Narrow screens - stack. Source order already reads
   paragraph, then images.
   ------------------------------------------------------------ */
@media (max-width: 1000px) {
  .sw-split {
    grid-template-columns: 1fr;
    gap: 2.5rem;
  }

  /* One column, so there is nothing left to mirror - the explicit
     placements would only fight the stack and leave a hole in row 1. */
  .sw-split--reverse .sw-split-copy,
  .sw-split--reverse .sw-split-media {
    grid-row: auto;
    grid-column: auto;
  }

  .sw-split--reverse .sw-split-copy {
    justify-self: stretch;
  }

  /* Stacked, the copy sits above the images rather than beside them,
     so there is nothing for it to hold against - sticky would just
     pin the paragraph over the cards as they scrolled under it. */
  .sw-section--sticky .sw-split-copy,
  .sw-section--sticky .sw-split--copy-left .sw-split-copy {
    position: static;
    margin-bottom: 0;
  }

  /* The pinned block fixes this section to one viewport, because
     while the stack is dealing the section IS the screen. Nothing
     is pinned here, so that cap has to come off or the column of
     cards is squeezed into a single screen's height. Back to the
     ordinary flow rules: as tall as its content needs. */
  .sw-section--sticky {
    height: auto;
    min-height: 100vh;
    min-height: 100svh;
    justify-content: center;
    padding-top: 6rem;
    padding-bottom: 10rem;
  }

  .sw-section--sticky .sw-split,
  .sw-section--sticky .sw-split-media {
    flex: none;
    min-height: 0;
  }

  /* The inset that kept the stack out of the title's band belongs
     to the side-by-side layout; stacked, the grid gap already
     separates them. */
  .sw-section--sticky .sw-split-media {
    padding-top: 0;
    padding-bottom: 0;
    align-self: auto;
  }

  /* js/cards.js stops pinning below this width and clears the
     transforms it wrote, so the stack falls back to the rules
     here: the cards come out of the single frame and run down the
     page as a plain column of images.

     Static rather than absolute is the whole fallback - absolute
     cards would all pile on the frame's centre with only the
     topmost visible, which is what the deal animation was doing
     on purpose and is useless without it. */
  /* The pinned rules are written as .sw-section--sticky
     descendants, so overriding them from a bare .sw-cards here
     loses on specificity however late it appears. These carry the
     same ancestors deliberately - without that the frame kept
     flex: 1 against a row that no longer exists, resolved to zero
     height, and took every card down with it. */
  .sw-section--sticky .sw-split-media > .sw-cards {
    height: auto;
    perspective: none;
    display: flex;
    flex-direction: column;
    gap: 1.25rem;
    /* No viewport-sized row to fill here - the section scrolls
       normally below the breakpoint - so the frame is sized by
       the cards inside it rather than the other way round. */
    flex: none;
  }

  .sw-section--sticky .sw-card {
    position: static;
    /* The centring transform goes with the absolute positioning;
       left at -50%,-50% the cards would sit half off the page. */
    transform: none;
    /* Width leads here and the ratio supplies the height, which is
       the reverse of the pinned layout. Both caps have to be
       cleared for that: max-width would hold the card off the
       track's edge, and the px max-height - which exists only to
       protect the exit lane on short desktop windows - would crop
       the ratio's result on a tall phone. */
    height: auto;
    width: 100%;
    max-width: none;
    max-height: none;
  }

  /* Nothing is stacked here, so nothing needs holding back - every
     card is read on its own terms. */
  .sw-section--sticky .sw-card::after {
    display: none;
  }

  /* The offset only makes sense while the columns sit side by side.
     Once they stack it would just pad the gap the grid already
     sets, pushing the band further from the paragraph. */
  .sw-split-media {
    align-self: auto;
    margin-top: 0;
  }

  .sw-split-copy,
  .sw-split-heading {
    padding: 0 1.5rem;
  }

  /* The paragraphs that replaced the two titles need no size override
     here - they wrap by default and their ramp is on vmin, so a
     narrow window brings them down on its own. What they do need is a
     measure that cannot outrun the stacked column, which is now the
     full page width rather than one grid track. */
  .sw-split-note {
    max-width: 34em;
  }

  .sw-slider {
    height: 46vh;
    min-height: 300px;
  }

  /* The section's own padding was set for a side-by-side layout. Once
     the columns stack it doubles up against the gap, so the band ends
     up marooned in dead space. */
  .sw-section--slider {
    padding: 4rem 0;
  }

  .sw-reveal + .sw-section--slider {
    padding-top: 0;
  }

  .sw-split-copy .sw-section-body {
    /* 1.25rem is a desktop measure; at phone width it forces short,
       ragged lines. */
    font-size: 1.0625rem;
  }
}

/* Phones proper. The arc needs a shorter band here or the plates
   float in the middle of a tall, mostly empty box. */
@media (max-width: 640px) {
  .sw-slider {
    height: 38vh;
    min-height: 240px;
  }

}

/* ------------------------------------------------------------
   Touch target - the button draws at its type height (~24px), which
   is under the ~44px a finger needs. The label carries an animated
   underline and the icon badge flips on its own centre, so padding
   the anchor would move both. Instead an invisible pseudo-element
   stretches the hit area to 44px around the artwork, leaving every
   drawn part exactly where it was.
   ------------------------------------------------------------ */
@media (hover: none), (pointer: coarse) {
  .sw-btn::after {
    content: "";
    position: absolute;
    left: 0;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    height: max(100%, 44px);
  }
}

/* ------------------------------------------------------------
   Touch devices
   ------------------------------------------------------------
   will-change on width/height is a poor trade on a phone: the box
   dimensions are rewritten every frame, so each slide relayouts
   regardless, and the hint just pins extra compositor layers on a
   device with far less memory to spare. Keeping it on transform
   alone is what actually helps. The images are also composited
   with the plate rather than as separate layers.
   ------------------------------------------------------------ */
@media (hover: none) {
  .sw-slide {
    will-change: transform;
  }

  .sw-slide img {
    /* Stops iOS promoting each image to its own layer inside an
       already-transformed parent, which showed up as tearing along
       the plate edges while the strip moved. */
    transform: translateZ(0);
    backface-visibility: hidden;
  }
}

/* Motion-sensitive visitors get the strip held at its start */
@media (prefers-reduced-motion: reduce) {
  .sw-slide {
    will-change: auto;
  }
}

/* ============================================================
   "Our full capabilities" title
   ------------------------------------------------------------
   The section's heading, in the featured index's treatment:
   .sw-featured-title carries the face, the size and the
   space-between spread, so nothing typographic is restated here
   and the two headings cannot drift apart.

   It is a direct child of the section, above .sw-split. The
   section is a flex column, so the title takes its own row and
   the split follows underneath.
   ============================================================ */
.sw-hww-title {
  /* The split below is the section's content; this is its header.
     .sw-section's own 1.5rem gap is too tight between a heading at
     this size and the columns under it. */
  margin-bottom: clamp(3rem, 7vh, 6rem);
  /* The title has to span the full measure for space-between to
     spread the three words to the edges, which is the whole
     treatment. */
  width: 100%;

  /* The ink is softened a step in css/featured.css, alongside the
     .sw-featured-title rule it overrides - that file loads after
     this one, so the colour would lose to it if it were set here.
     The face, weight and size stay exactly as .sw-featured-title
     sets them. */

  /* ITS OWN GUTTER, and it needs one. .sw-section sets 2.5rem of
     side padding, but .sw-section--slider zeroes it (padding: 6rem
     0) because .sw-split supplies its own gutters per column - so a
     direct child of this section sits against the raw viewport
     edge. Left at width: 100% alone the three words spread 0..1440
     on a 1440 window, where the featured index's identical heading
     spreads 40..1400, and the two treatments did not line up.

     Same 2.5rem the section would have given it, so "How" starts on
     the same left margin as "Stuff" and "work" ends on the same
     right margin as "built". */
  padding-left: 2.5rem;
  padding-right: 2.5rem;
}


/* Below 1000px .sw-featured-title stacks its three words into a
   left-aligned column on its own (css/featured.css). The gap under
   it comes down to match the tighter stack. */
@media (max-width: 1000px) {
  .sw-hww-title {
    margin-bottom: clamp(2rem, 5vh, 3.5rem);
    /* The split's columns drop to a 1.5rem gutter at this width
       (.sw-split-copy / .sw-split-media below), so the title
       matches them rather than holding the wider desktop margin. */
    padding-left: 1.5rem;
    padding-right: 1.5rem;
  }
}
