/* ═══════════════════════════════════════════════════════════════════
   Loading screen — six projects, fast, on paper.

   What it was: one photograph of a room under a near-black veil
   (#2A2621) with the word "Loadıng" and a ball dropped on it. What it
   is now: the page's own paper, a frame with the studio's work running
   through it at four hundred milliseconds a plate, and the name rising
   underneath.

   The ground is --black, the same warm off-white index.html itself
   opens on, so the screen does not announce itself as a different
   place. The old dark veil made the arrival a cut from black; this is
   a sheet of the page's own paper, drawn up off the room underneath by
   the same curtain the project pages open with.

   Two numbers hold the whole thing together and they live at the top of
   the block below. The sequence is 2.4s. js/loader.js holds the screen
   for at least that long — its FLOOR was 900ms, which would have cut
   this off at the second plate on a warm cache — and lets go at 4.5s
   whatever the page is doing.

   ─────────────────────────────────────────────────────────────────
   SWAPPING THE ANIMATIONS
   The two blocks marked ANIMATION A and ANIMATION B below are the only
   moving parts. Everything else is layout and can stay. To drop in a
   different animation, replace the keyframes and the one rule that
   references them inside the marked block; the markup gives you
   .loader__plate (six of them, each carrying --i as its index) and
   .loader__ch (five of them, likewise).
   ═══════════════════════════════════════════════════════════════════ */

.loader{
  position:fixed; inset:0; z-index:var(--z-loader);
  display:grid;
  grid-auto-rows:max-content;
  align-content:center;
  justify-items:center;
  gap:clamp(1.5rem,4vh,2.5rem);
  padding:var(--gut);
  /* The page's own ground, not a colour of this screen's own. */
  background:var(--black);
  overflow:hidden;

  /* The sequence, written once. Six plates across 2.4s is .4s each,
     and 2.4s is the length js/loader.js is holding the screen for. The
     cycle is the fixed number here, not the slot: adding a project to
     the set makes every plate a little quicker rather than making
     anybody wait longer for the page. */
  --plates:6;
  --cycle:2.4s;
  --slot:calc(var(--cycle) / var(--plates));
}

/* ── the frame ────────────────────────────────────────────────────── */
/* Portrait, and the plates are cut 4/5 to match rather than be cropped
   by it, so no pixel is shipped that object-fit then throws away.

   20rem, up from the 13.5 it was first drawn at: at that size the work
   was legible but was not the subject, and on a wide screen the whole
   thing read as empty paper with a stamp in the middle. The ceiling is
   what keeps the composition — frame, name, rule — inside the fold on a
   laptop, which it fills to about 630px at this size. */
.loader__frame{
  position:relative;
  width:clamp(10.5rem,30vw,20rem);
  aspect-ratio:4 / 5;
  overflow:hidden;
  /* The raised tone under the plates, so the frame is never a white
     hole in a white page for the frame before the first one decodes. */
  background:var(--ink);
}

.loader__plate{
  position:absolute; inset:0;
  opacity:0;                    /* every one of them, until its slot */
}
.loader__plate img{
  width:100%; height:100%;
  object-fit:cover;
  display:block;
}

/* ═══ ANIMATION A — the plates ════════════════════════════════════ */
/* Each plate owns one slot of the cycle and is invisible for the rest
   of it. The delay is its index times the slot, so the six of them walk
   the cycle without any script keeping time.

   It runs infinite rather than once: the screen is held for 2.4s but
   may stay as long as 4.5s on a slow connection, and a sequence that
   played through and then sat on its last frame for two seconds would
   read as the site having stalled.

   The scale is 1.06 → 1.0, never from 0 — nothing in the world appears
   out of nothing, and a plate that pops from nowhere reads as a glitch
   rather than as a page turn. Fast in, slow settle across the slot,
   quick out with a 2% overlap onto the next so the cut is a change
   rather than a blink. */
@keyframes loader-plate{
  0%    { opacity:0; transform:scale(1.06) }
  1.5%  { opacity:1; transform:scale(1.035) }
  15%   { opacity:1; transform:scale(1) }
  18.5% { opacity:0; transform:scale(.995) }
  100%  { opacity:0; transform:scale(.995) }
}
.loader__plate{
  animation:loader-plate var(--cycle) var(--ease-out) infinite;
  animation-delay:calc(var(--i) * var(--slot));
}
/* ═══ end ANIMATION A ═════════════════════════════════════════════ */

/* ── the name ─────────────────────────────────────────────────────── */
.loader__mark{
  margin:0;
  display:grid;
  justify-items:center;
  gap:.5rem;
  text-align:center;
}
.loader__word{
  display:flex;
  /* The mask the letters rise into. */
  overflow:hidden;
  font-family:var(--display); font-weight:400;
  font-size:var(--fs-major);
  line-height:1.12;
  letter-spacing:.14em;
  /* the letterspacing pushes trailing space past the mask; pulled back
     so the word reads optically centred */
  text-indent:.14em;
  color:var(--bone);
}
.loader__sub{
  font-family:var(--sans);
  font-size:var(--fs-micro); letter-spacing:.34em; text-transform:uppercase;
  color:var(--bronze-lt);
}

/* ═══ ANIMATION B — the name ══════════════════════════════════════ */
/* Five letters up through the mask, 45ms apart, then the discipline
   underneath once the word has landed. Transform and opacity only, so
   none of it touches layout.

   forwards, not infinite: the name arrives once and stays. A wordmark
   re-animating every 2.4s alongside the plates would be two things
   competing for one eye, and the plates are the ones with something to
   say. */
@keyframes loader-rise{
  from{ opacity:0; transform:translateY(105%) }
  to  { opacity:1; transform:translateY(0) }
}
@keyframes loader-fade{
  from{ opacity:0 }
  to  { opacity:1 }
}
.loader__ch{
  display:inline-block;
  opacity:0;
  animation:loader-rise .62s var(--ease-out) forwards;
  animation-delay:calc(var(--i) * 45ms);
}
.loader__sub{
  opacity:0;
  animation:loader-fade .7s var(--ease-out) forwards;
  animation-delay:340ms;      /* as the last letter lands */
}
/* ═══ end ANIMATION B ═════════════════════════════════════════════ */

/* ── the rule, and the word ───────────────────────────────────────── */
/* The only honest progress on the screen. It closes over exactly the
   length of the sequence, so it finishes when the plates do — and then
   both start again, which is the truthful thing for a screen that does
   not know how long it has left. A bar that creeps to 90% and waits is
   a lie told slowly. */
.loader__foot{
  display:grid; justify-items:center; gap:.75rem;
}
.loader__rule{
  display:block;
  width:clamp(6rem,14vw,9rem);
  height:1px;
  background:var(--hair);
  overflow:hidden;
}
.loader__rule-i{
  display:block;
  width:100%; height:100%;
  background:var(--bronze-lt);
  transform-origin:left center;
  animation:loader-fill var(--cycle) linear infinite;
}
@keyframes loader-fill{
  from{ transform:scaleX(0) }
  to  { transform:scaleX(1) }
}
.loader__note{
  font-family:var(--sans);
  font-size:var(--fs-micro); letter-spacing:.3em; text-transform:uppercase;
  color:var(--bone-faint);
}

/* ── the exit ─────────────────────────────────────────────────────── */
/* js/loader.js adds .is-done, waits EXIT (1000ms) and removes the
   element. The 140ms lead and the 800ms curtain below are the two halves
   of that number — one decision written in two files, so neither may
   move alone.

   The contents leave first and the ground follows, and the order is
   what makes the whole thing read as one gesture: the screen puts its
   own things away, and then the screen itself is drawn off. Reverse it
   and the wipe would be cutting through a photograph.

   They no longer leave together. Three things are on this screen and
   they are not of equal weight, so they go in the order of what the
   reader has finished with: the rule and the word "Loading" first,
   because they were never the subject and their job ends the moment the
   wait does; the name next; the frame last, because it is the only
   thing here that has a counterpart on the other side.

   --ease-exit rather than --ease-out. The page's curve is a strong
   decelerate, which is right for everything arriving and wrong for
   everything going: a thing that eases out as it leaves hangs at the
   end, half-there, and reads as reluctance. This one accelerates away.
   The frame is the exception and keeps the page's own curve, for the
   reason in the block below it.

   All three are gone by 460ms, and that number is not free: the ground
   below leaves as a rising wipe, and its edge crosses the middle of the
   screen — where the frame is — at about 540ms. Anything still on the
   screen then is not dissolved by the wipe, it is sliced by it. The
   screen clears itself first and only then rises. */
.loader{
  --ease-exit:cubic-bezier(.55,0,.85,.45);
}

.loader__frame,
.loader__mark,
.loader__foot{
  transition:opacity .3s var(--ease-exit), transform .3s var(--ease-exit);
}
.loader.is-done .loader__mark,
.loader.is-done .loader__foot{
  opacity:0;
  transform:translateY(-10px);
}
.loader.is-done .loader__foot{ transition-delay:0ms }
.loader.is-done .loader__mark{ transition-delay:60ms }

/* The frame does not lift and fade like the other two. It grows very
   slightly — 1 to 1.035 — and dissolves out through the front while it
   does, because the thing behind it is the same photograph at the size
   of the whole screen. The plates have spent 2.4 seconds being the
   studio's work seen small and quickly; the hero is one room seen whole
   and slowly. Scaling up on the way out makes the second read as the
   first arrived at, rather than as a different picture that replaced it.

   Nothing morphs and nothing is measured — it is 35 thousandths of
   scale, far too little to be seen as a movement and just enough to be
   felt as a direction. That is the whole trick, and it costs one
   transform.

   Longer than the other two (380ms) and on the page's decelerate curve,
   because it is the one element here that is arriving somewhere rather
   than leaving. It finishes at 460ms, clear of the wipe's edge and well
   inside the 1000ms before js/loader.js takes the element out. */
.loader.is-done .loader__frame{
  opacity:0;
  transform:scale(1.035);
  transition:opacity .38s var(--ease-out) .08s,
             transform .38s var(--ease-out) .08s;
}

/* ── the ground: the curtain ──────────────────────────────────────── */
/* This used to be an opacity fade, and a fade is the one exit that says
   nothing. The site already owns a way of getting from one screen to
   another: opening a project runs a curtain up over the page you are
   leaving and off the page you arrive at — see .curtain in
   css/style.css and js/curtain.js. That move is the site's transition,
   and the loading screen was the last place still doing something else.

   So this is the same move, to the value: inset(0) to
   inset(0 0 100% 0), 0.8s on --ease-in-out, which is
   cubic-bezier(.77,0,.175,1). A curtain is heavy and hung. It takes a
   moment to start, gathers through the middle, and arrives without
   slamming — which is what that curve is a picture of, and why nothing
   here uses the page's ordinary decelerate.

   What is NOT taken from .curtain is the colour. That one is #2A2621,
   near-black, because it travels between two pages and has to be
   neither. This one is the page's own paper and always has been, so the
   arrival is not a cut from a dark panel — it is the sheet the reader
   has been looking at for two and a half seconds being drawn up off the
   room underneath. The edge between warm off-white and the hero's dark
   footage is hard and very visible on the way past, which is the point:
   a wipe is an edge, and an edge you cannot see is a fade.

   clip-path rather than height or transform: it takes the ground and
   everything still drawn on it in one move, needs no second element,
   and stays on the compositor.

   140ms behind the contents, so the screen is empty before it starts to
   go. Ends at 940ms — sixty inside the 1000ms EXIT in js/loader.js, at
   which point the element is taken out of the DOM. */
.loader{
  clip-path:inset(0 0 0 0);
  transition:clip-path .8s var(--ease-in-out) .14s;
}
.loader.is-done{
  clip-path:inset(0 0 100% 0);
  pointer-events:none;
}

/* ── the page underneath ──────────────────────────────────────────── */
/* Set by the inline script in <head> and removed in js/loader.js. */
.is-loading{ overflow:hidden }

/* ── no script ────────────────────────────────────────────────────── */
/* Nothing removes the screen, so there must not be one. */
.no-js .loader{ display:none }

/* ── arrived from inside the site ─────────────────────────────────── */
/* Set by the inline script in the head of index.html when js/curtain.js
   has brought the reader here from another page — see the long note
   there. The curtain is already over the viewport and about to run up
   off this page, so there is a transition happening; this screen would
   be a second one stacked on it, and a two-and-a-half second wait on a
   page whose assets are already warm.

   display:none rather than a class js/loader.js reads, because the
   decision has to hold on the first paint. js/loader.js returns on the
   same class a moment later and never runs the timers at all. */
.is-from-curtain .loader{ display:none }

/* ── short screens ────────────────────────────────────────────────── */
/* The frame is the tallest thing here and so the first that has to
   give: on a laptop in landscape, or a phone turned over, 20rem of
   frame plus the name plus the rule does not fit the fold — and a
   loading screen that scrolls is a loading screen that has failed. */
@media (max-height:44rem){
  .loader__frame{ width:clamp(8rem,20vh,12rem) }
  .loader{ gap:clamp(1rem,2.5vh,1.6rem) }
  .loader__word{ font-size:var(--fs-head) }
}

/* ── reduced motion ───────────────────────────────────────────────── */
/* The flick is the whole point of the screen and it is also six full
   changes of image in two and a half seconds, which is exactly what this
   setting exists to stop. So the sequence does not run: the first plate
   is shown, the name is already up, and the rule holds still. The screen
   still says the same thing, it just says it without moving. */
@media (prefers-reduced-motion:reduce){
  .loader__plate{ animation:none }
  .loader__plate:first-child{ opacity:1 }
  .loader__ch,
  .loader__sub{ animation:none; opacity:1 }
  .loader__rule-i{ animation:none; transform:scaleX(.35) }
  .loader__frame,
  .loader__mark,
  .loader__foot,
  .loader{ transition:none }
  /* The screen still has to go, and with the transitions off the clip
     in .is-done applies on the frame it is set — the curtain is not
     slowed, it is simply already up. A cut is exactly what this setting
     is asking for, and it is the same answer .curtain gives on the
     project pages, which take themselves out of the document entirely.
     But the frame must not be left scaled: with no transition to run
     it, the transform would apply instantly and the last thing shown
     would be a jump. */
  .loader.is-done .loader__frame{ transform:none }
}
