/* ============================================================
   SILICA WORKS - page loader, the curtain
   ------------------------------------------------------------
   Two accent fields close on the middle of the screen, the lockup
   settles on them in ink, and when the page is ready they retract
   the way they came - the page arriving in the widening gap
   between them - while the mark flies out of the lockup and down
   into the nav bar's waiting slot.

   THE PLATE IS TRANSPARENT AND THE FIELDS ARE THE COVER. A plate
   with a ground of its own would be a second thing to clear: the
   fields retract to show a sheet of paper that then has to go too,
   and for those frames the page is behind a layer rather than
   behind nothing. With the colour entirely in the fields, the
   retraction IS the transition.

   The wordmark is set in the site's title cut and builds letter by
   letter: each word is a clipping mask, and its letters rise up
   into it from under the line while gathering in toward the centre
   of the name - see js/loader.js.

   Everything is scoped under .sw-load so it cannot reach the page
   underneath, which is still laid out and measured normally while
   the fields cover it. The one exception is the nav slot and its
   ghost at the foot of this file, which is the far end of the
   handoff and has to live on the bar.
   ============================================================ */

/* ------------------------------------------------------------
   The plate
   ------------------------------------------------------------
   No ground of its own - see the note above. It is the positioning
   context for the fields and the centring box for the lockup, and
   nothing else.
   ------------------------------------------------------------ */
.sw-load {
  position: fixed;
  inset: 0;
  /* Over the fixed nav bar and the cursor layer both.

     THE CURSOR IS ALSO AT 9999 (css/cursor.css) AND ITS MARKUP
     COMES LATER IN THE BODY, so at an equal z-index it wins on
     source order and its label draws over the curtain - a black
     "Look around" chip floating on the accent before the page it
     refers to has been revealed. The old white plate carried the
     same 9999 and had the same fault; it was simply less obvious
     against white than it is against the colour.

     10000 rather than reordering the markup, because the plate has
     to stay first in the body for its own reason: it must paint
     before anything under it. The bar sits at 1000.

     The flying mark is lifted out of this plate and parented to the
     body, so it carries its own z-index above this one - otherwise
     the mark would pass BEHIND the retracting fields on its way
     down to the bar. */
  z-index: 10000;
  background: transparent;
  display: flex;
  align-items: center;
  justify-content: center;
  /* The plate is a cover, not a control. Without this a click
     landing in the gap before it clears reaches the page beneath. */
  pointer-events: auto;
}

/* Set by the script the moment the curtain is fully retracted.
   display:none rather than a removal so the markup stays
   inspectable, and so nothing has to be rebuilt if it is ever
   shown twice. */
.sw-load--done {
  display: none;
}

/* ------------------------------------------------------------
   Nothing in the lockup paints before the script has placed it
   ------------------------------------------------------------
   There is a gap between the first paint and GSAP's first write -
   the stylesheets are in the head, the script is at the foot of
   the body - and in that gap the lockup is laid out at its
   RESTING appearance: mark at full opacity, shadow at its 0.28,
   wordmark solid, all of it sitting in the middle of a screen
   whose curtain has not closed yet.

   Measured at 490ms into the load: the shade was painting at 0.28
   over the page, before the fields had met. The mark then vanished
   the moment GSAP set its starting values and faded back in - so
   the first thing the sequence did was un-draw itself.

   Hiding them up front means the script's `gsap.set` calls are
   restoring something that was never seen rather than undoing
   something that was. Scoped to .sw-loading so it only applies
   while the loader is armed: with scripting off the rule never
   matches and the lockup is simply not on screen at all, since
   the plate is hidden too.

   opacity rather than visibility, because the boxes still have to
   be laid out and measurable - the flight reads the mark's and the
   wordmark's rectangles to work out where they are going.
   ------------------------------------------------------------ */
html.sw-loading .sw-load-mark-solid,
html.sw-loading .sw-load-mark-shade,
html.sw-loading .sw-load-layer {
  opacity: 0;
}

/* ------------------------------------------------------------
   The curtain
   ------------------------------------------------------------
   Two accent fields, one off the top edge and one off the bottom,
   which close on the middle and later retract the way they came.
   js/loader.js drives both on yPercent.

   EACH IS 52vh RATHER THAN 50, so they overlap by about 4% of the
   viewport height on the seam instead of meeting exactly on it. An
   exact 50/50 split is correct on paper and wrong on screen: at
   fractional device pixel ratios the two edges round to the same
   subpixel and leave a one-pixel line of page across the middle of
   an otherwise solid field - invisible on the machine it was built
   on, obvious on half of everything else. The overlap is free,
   since both fields are the same colour and neither is
   transparent.

   THE START OFFSET IS IN CSS, NOT ONLY IN THE SCRIPT. The fields
   have to be off their edges on the very first paint; if they sat
   at rest until GSAP parsed, the page would be under a solid
   accent screen for as long as that took and then be covered
   "again" by an animation of something already there. So the
   resting position here is the retracted one, and the script's
   first move is to close them.

   IT IS WRITTEN AS `translate`, NOT `transform`, AND THAT IS NOT
   COSMETIC. GSAP drives these on yPercent, which it composes into
   the element's `transform` - and it reads whatever is already in
   `transform` as the base to compose against. With the start
   offset declared there too, `yPercent: 0` did not mean "closed":
   it meant "zero, on top of the -101% still sitting underneath",
   so the fields never left their edges. Traced frame by frame, the
   top field went from -473px out to -772 and then crept back to
   -473 - the close and the retraction both running, both about an
   origin that was already off-screen, and the page never covered
   at any point.

   The individual `translate` property is a separate slot that
   `transform` does not read and GSAP does not write, so the two
   cannot compose. The script clears it on its first pass and from
   then on `transform` is GSAP's alone.

   -101% rather than -100 for the same rounding reason as the
   overlap: it guarantees each field is clear of its own edge even
   where the subpixel maths disagrees.
   ------------------------------------------------------------ */
.sw-load-field {
  position: absolute;
  left: 0;
  width: 100%;
  height: 52vh;
  background: var(--sw-accent);
  /* The close and the retraction are both transforms, so the
     compositor owns them and the page underneath is never
     re-rasterised mid-slide. */
  will-change: transform;
}

.sw-load-field--top {
  top: 0;
  translate: 0 -101%;
}

.sw-load-field--bottom {
  bottom: 0;
  translate: 0 101%;
}

/* ------------------------------------------------------------
   ARRIVING SHUT
   ------------------------------------------------------------
   A reload, a back/forward that was not restored from the cache,
   or a link whose page closed the curtain on the way out
   (js/page-exit.js). The class is set by the inline script in the
   head, before the first paint, so the very first frame of this
   document is the same flat accent the last one ended on - the
   colour does not break across the page load - and js/loader.js
   starts the lockup on it instead of closing a curtain that is
   already closed.

   This replaces a paper backdrop that covered the rebuild while
   the fields travelled in over it. That hid the rebuild, and put
   a flash of blank paper between the page being left and the
   curtain arriving - the one frame a refresh should never show.
   ------------------------------------------------------------ */
html.sw-load-shut .sw-load-field--top,
html.sw-load-shut .sw-load-field--bottom {
  translate: none;
}

/* ------------------------------------------------------------
   Centre stack
   ------------------------------------------------------------
   Mark over wordmark, both centred. The gap is small on purpose -
   the two should read as one lockup, not as an icon with a caption
   under it.
   ------------------------------------------------------------ */
.sw-load-lockup {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(1rem, 2.2vh, 1.6rem);
  /* Over the fields. They are absolutely positioned siblings, so
     without a position of its own the lockup is an in-flow box and
     paints underneath them. Relative rather than a z-index on the
     fields, so the fields stay plain and the type is the thing
     that is explicitly on top. */
  position: relative;
}

/* The ink mark, not the green one. The curtain's ground IS the
   accent, so the green cut would be the mark's own colour on its
   own colour - invisible. Ink on accent is the token's stated
   pairing and reads at full contrast the whole way through.

   The colour is a TOKEN AND NOT A CONSTANT because the mark does
   not stay ink: at the handoff js/loader.js recolours it to the
   accent as it descends to the near-black bar, where ink would
   disappear as completely as green does here. Each ground gets the
   cut that reads on it, and the swap happens in flight.

   Inlined in the markup rather than loaded through an <img>. That
   file's path is fill="currentColor", and an <img> is its own
   document - it inherits nothing from this page, so the fill would
   resolve against the SVG's own root and land on the initial black
   instead of --sw-ink. Inline, it is part of this document and the
   colour below reaches it, so the mark and the wordmark are driven
   by one token and cannot drift apart. It also saves a request on
   the one screen where a pending request is visible. */
.sw-load-mark {
  color: var(--sw-ink);
  /* Sized by height so the mark's own aspect ratio decides the
     width, the same way the nav bar's logo is sized. */
  height: clamp(3rem, min(7.2vh, 13vw), 4.75rem);
  width: auto;
  display: block;
}

/* ------------------------------------------------------------
   The wordmark
   ------------------------------------------------------------
   Set in the site's title cut rather than a loader face of its
   own: Söhne Extrabold, caps, -0.045em, 0.8 leading - the hero's
   "MAKING BRANDS" (.ap-w) and every inner page's title
   (css/page-type.css), value for value. It used to be Inter Tight
   600 opened up to 0.16em, which read as a label on a splash screen
   rather than the first line of the site.

   Sized well under the hero's min(9.4vw, 15.5vh) so it stays a
   lockup with the mark above it, not a headline the mark sits on.

   transform, not the typed string. The markup keeps "Silica Works"
   as readable text, so a screen reader, a search result or a page
   with this stylesheet missing all still get the name in its normal
   case.
   ------------------------------------------------------------ */
.sw-load-word {
  display: flex;
  flex-direction: column;
  align-items: stretch;
  font-family: "sohne-bold", "Inter Tight", sans-serif;
  font-size: clamp(1.9rem, min(3.9vw, 6.8vh), 3.8rem);
  line-height: 0.8;
  font-weight: 800;
  letter-spacing: -0.045em;
  text-transform: uppercase;
  color: var(--sw-ink);
  white-space: nowrap;
}

/* The layer holds the words; it does not move itself. Flex so the
   masks sit on one line and share a baseline. */
.sw-load-layer {
  display: flex;
  align-items: flex-end;
  justify-content: center;
  white-space: nowrap;
  /* The word space, as a gap: a flex row discards whitespace-only
     text nodes, so the space js/loader.js keeps inside the first
     mask is for whatever reads the markup, not for the eye.
     Tighter than a typed space, to sit with the -0.045em tracking. */
  gap: 0.24em;
  /* Negative tracking hangs the last letter's ink past its box;
     this puts the same amount back so the line centres under the
     mark. */
  padding-right: 0.045em;
}

/* ------------------------------------------------------------
   One mask per word, one moving span per letter
   ------------------------------------------------------------
   Built by js/loader.js. The mask clips; the letters inside it are
   what move - a transform on the element that clips would carry
   the clip along with the type and hide nothing.

   THE CLIP IS VERTICAL ONLY. The letters start spread outward from
   the centre of the name and gather in as they rise, so the sides
   have to stay open or the outer letters would be shaved on the
   way in. clip-path rather than overflow, because overflow cannot
   be set per-axis without the other becoming a scroll container.

   The top inset reaches above the line box the way .ap-w's padding
   does: at 0.8 leading the caps overshoot the box, and a cut at the
   box would shave the tops of them.
   ------------------------------------------------------------ */
.sw-load-word-mask {
  display: block;
  clip-path: inset(-0.14em -100vw -0.04em -100vw);
}

.sw-load-char {
  display: inline-block;
  white-space: pre;
  /* Pivots on the baseline, so the lean tips each cap like type on
     a slope rather than shearing it about its middle. */
  transform-origin: 50% 100%;
  will-change: transform;
}

/* ============================================================
   The far end of the handoff - the nav bar's slot
   ------------------------------------------------------------
   These are the only rules in this file that touch the page
   underneath, and they are here rather than in css/menu.css
   because they exist entirely for the loader: the bar's resting
   appearance is unchanged by all of them.
   ============================================================ */

/* The slot. Both marks stack inside it, so the box holds the bar's
   layout whether it is filled, waiting or mid-handoff - nothing
   beside it can shift while the mark is in the air.

   Sized by the mark itself rather than given a size here: the img
   inside carries .sw-menu-logo-mark's height from css/menu.css and
   the slot takes its box from that, so there is one figure for the
   bar's logo and this cannot drift out of step with it. */
.sw-menu .sw-menu-logo-slot {
  position: relative;
  display: block;
  line-height: 0;
  flex: none;
}

/* THE GHOST - the resting state of an unfilled slot.
   ------------------------------------------------------------
   Absolutely positioned over the same box as the real mark, so the
   arriving mark lands exactly on it and the swap is a change of
   opacity with nothing moving.

   A GREY, STATED OUTRIGHT, not paper dimmed down. Washed white is
   the one value in the bar that belongs to no part of its palette,
   so a faded-out mark reads as a rendering fault. --sw-border is
   the bar's own structural grey, the colour its outline already
   uses: a placeholder drawn in the colour of the furniture's edges
   reads as part of the furniture - an empty seat cut into it,
   which is what a waiting slot is.

   Full opacity rather than a fade, for the same reason. The colour
   is doing the work, so the shape can stay crisp; a dimmed shape
   reads as unfinished rendering, a solid shape in a recessive
   colour reads as intent.

   NO CAST SHADOW, though the box has room for one. The shadow is
   what makes the real mark sit ON the bar, and a placeholder
   should read as set INTO it - dropping the shade is most of what
   separates the two at a glance. A placeholder with a cast shadow
   would be claiming to be lit, which is exactly what it is not. */
.sw-menu .sw-menu-logo-ghost {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  color: var(--sw-border);
  pointer-events: none;
  /* Off unless the loader is running - see below. */
  opacity: 0;
}

/* WHO HIDES WHAT, AND WHEN.
   ------------------------------------------------------------
   With scripting off, or on any page that does not load the plate,
   the bar is simply the bar: the real mark is visible and the
   ghost is not. That is the resting state above, and it is the
   state the markup produces on its own.

   .sw-loading is set by the inline script in the head before the
   first paint, so while the loader is armed the pair swaps: the
   slot waits, empty, with the ghost in it.

   .sw-slot-filled is what ENDS that, and it is a second class
   rather than the absence of the first because the two states do
   not end at the same moment. The mark lands on the slot partway
   through the retraction and the slot has to be filled on that
   frame - but .sw-loading is also what keeps the plate displayed,
   so dropping it there would blank the fields while they are
   still moving and the curtain would vanish rather than part.
   Each class now ends the thing it is actually about: this one
   the empty slot, .sw-loading the cover.

   Hiding the real mark with visibility rather than opacity so it
   is out of the way of nothing - it holds its box either way, and
   the box is what keeps the bar from reflowing at the swap. */
html.sw-loading:not(.sw-slot-filled) .sw-menu .sw-menu-logo-ghost {
  opacity: 1;
}

html.sw-loading:not(.sw-slot-filled) .sw-menu .sw-menu-logo-mark {
  visibility: hidden;
}

/* ------------------------------------------------------------
   No-JS
   ------------------------------------------------------------
   The curtain is inert markup without a script to drive it, so
   with scripting off it would never close and never open. Hiding
   the plate up front and showing it only once something has said
   it will be cleared means a reader with JS off gets the page
   directly - and, by the rules above, a bar with its real logo in
   it rather than a ghost waiting on a mark that is never coming.

   .sw-loading is set by the inline script in the head, before the
   first paint, so the fields are over the page from the start
   rather than snapping across one that has already drawn.
   js/loader.js removes it once the curtain has retracted. */
.sw-load {
  display: none;
}

html.sw-loading .sw-load {
  display: flex;
}

/* Motion off: the curtain still covers the load, but nothing
   slides. The script shortens the whole sequence to a plain fade -
   including the flight, which is skipped entirely and the slot
   simply handed over - so the offsets set here are the resting
   ones. The fields are placed closed rather than off-screen, since
   with the motion gone they are a screen of colour that fades,
   not two panels that travel. */
@media (prefers-reduced-motion: reduce) {
  .sw-load-char {
    transform: none !important;
    opacity: 1 !important;
  }

  /* The pre-paint guard above is undone here. Nothing is going to
     fade these up on this path - the script returns before it
     writes a single value - so hiding them would leave a bare
     accent screen with no lockup on it for the whole hold. */
  html.sw-loading .sw-load-mark-solid,
  html.sw-loading .sw-load-layer {
    opacity: 1;
  }

  html.sw-loading .sw-load-mark-shade {
    opacity: 0.22;
  }

  /* `translate`, which is the slot the offset is actually in -
     clearing `transform` would write to a slot that was never
     holding anything and leave the fields off their edges. */
  .sw-load-field--top,
  .sw-load-field--bottom {
    translate: none;
  }
}
