/* ============================================================
   THINGS I'VE BUILT — the vitrine.
   ------------------------------------------------------------
   The figure walks off at the end of the sentence before this
   one, so this frame is the room he left behind: a lit case with
   nine objects still drifting inside it.

   TWO LAYOUTS LIVE IN THIS FILE, AND THE STATIC ONE IS THE
   DEFAULT. Everything outside `.is-live` is what the page looks
   like with JavaScript off, with the module blocked, and under
   prefers-reduced-motion — a staggered composition of nine
   links that never moves. js/projects.js adds `.is-live` to the
   box only after it has measured it, and only when motion is
   allowed. The reduced-motion block at the bottom takes the
   static layout back by force even if something set that class,
   so there is no path to a moving card for a reader who asked
   for stillness.

   No monospace anywhere: label register comes from size, caps
   and tracking on Geist, the same as the rest of the site.
   ============================================================ */

/* The case is the widest object on the page — wider than the
   78rem the reading column is held to. That is the point: the
   frame opens out here. */
#build{
  max-width: none;
  width: 100%;
}

/* ------------------------------------------------------------
   THE CASE
   ------------------------------------------------------------ */
.pj-box{
  --pj-pad: clamp(0.9rem, 2.2vw, 2rem);
  position: relative;
  width: 100%;
  max-width: 86rem;
  /* align-self, not margin-inline:auto. The sticky .hold this
     sits in is a column flex container, and an auto inline
     margin on a flex item cancels the stretch — the case
     collapsed to 1px wide, because with the physics running
     every object inside it is absolutely positioned and there
     is no content left to shrink-wrap. */
  align-self: center;
  /* The hairline fades in behind the light that draws it;
     --open is driven by the entry clock in js/projects.js and
     is simply absent (so, 1) with JavaScript off. */
  border: 1px solid rgba(124, 136, 163, calc(var(--open, 1) * 0.22));
  border-radius: 2px;
  /* Lit from above, like everything else here. A flat black
     rectangle reads as a hole; this reads as a case. */
  background:
    radial-gradient(130% 92% at 50% -12%,
                    rgba(201, 209, 228, 0.05) 0%,
                    rgba(201, 209, 228, 0.012) 46%,
                    transparent 72%);
  padding: var(--pj-pad);
}

/* The case label, straddling the top-left corner the way a
   plaque sits on a vitrine — its own black cuts the hairline.
   It carries the same ordinal the three sections after this one
   carry, so the spine of the page survives the change of form. */
.pj-plaque{
  position: absolute;
  top: 0;
  left: clamp(1rem, 3vw, 2.75rem);
  transform: translateY(-50%);
  z-index: 4;
  display: flex;
  align-items: baseline;
  gap: 0.75em;
  margin: 0;
  padding: 0 0.9em;
  background: var(--ink);
  font-family: var(--label);
  font-size: 0.6875rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
  white-space: nowrap;
  opacity: var(--open, 1);
}
.pj-plaque span:first-child{ color: var(--amber); }

/* ------------------------------------------------------------
   THE BACK WALL

   The title sits INSIDE the case, behind the objects, because
   the objects are the hero and a title stacked above a box is
   the shape of every portfolio ever made. Set in Geist rather
   than the Michroma wordmark face: Michroma at this size is
   already spent on "Let's connect", and a second giant Michroma
   line would read as the same slide twice.
   ------------------------------------------------------------ */
.pj-title{
  margin: 0;
  font-family: var(--body);
  font-weight: 800;
  font-size: clamp(1.9rem, 7.2vw, 6.2rem);
  line-height: 0.84;
  letter-spacing: -0.03em;
  text-transform: uppercase;
  color: rgba(201, 209, 228, 0.135);
  max-width: 11ch;
}

/* ------------------------------------------------------------
   THE OBJECTS — shared between both layouts.
   ------------------------------------------------------------ */
.pj{
  /* --u is a hundredth of the object's own width, set by
     js/projects.js once per layout. Everything inside scales
     from it, clamped at both ends so a small object stays
     legible and a large one never turns into a poster. */
  --u: 2;
  --pad: clamp(6px, calc(var(--u) * 5px), 15px);
  --rad: clamp(4px, calc(var(--u) * 5.5px), 14px);
  --a: 0;                       /* 0 at rest, 1 fully active */

  position: relative;
  display: flex;
  flex-direction: column;
  gap: calc(var(--pad) * 0.55);
  padding: var(--pad);
  /* One cut corner. A rectangle with four identical radii is
     the house style of every card on the internet; one corner
     that disagrees reads as an object that was made. */
  border-radius: 3px var(--rad) var(--rad) var(--rad);
  background:
    linear-gradient(180deg,
      rgba(255, 255, 255, 0.075) 0%,
      rgba(255, 255, 255, 0.012) 34%,
      rgba(255, 255, 255, 0) 62%),
    var(--ink-2);
  color: var(--text);
  text-decoration: none;
  overflow: hidden;
  /* A top edge that catches the light and a hairline all round,
     then the amber sheen that only arrives when it is active. */
  box-shadow:
    inset 0 1px 0 0 rgba(255, 255, 255, 0.13),
    inset 0 0 0 1px rgba(124, 136, 163, 0.23),
    inset 0 1px 0 0 rgba(242, 162, 44, calc(var(--a) * 0.5)),
    inset 0 0 0 1px rgba(242, 162, 44, calc(var(--a) * 0.26)),
    0 calc(3px + var(--lift, 0.5) * 13px + var(--a) * 10px)
      calc(10px + var(--lift, 0.5) * 32px + var(--a) * 22px)
      calc(-4px - var(--lift, 0.5) * 6px) rgba(0, 0, 0, 0.92);
}
a.pj:hover{ text-decoration: none; }
.pj:focus-visible{
  outline: 2px solid var(--cyan);
  outline-offset: 3px;
}

.pj-plate{
  position: relative;
  display: block;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  border-radius: 2px;
  background: #05070C;
  box-shadow: inset 0 0 0 1px rgba(124, 136, 163, 0.13);
}
/* One plate is a <picture> rather than a bare <img>, so it can hand
   a still to anyone who has asked for reduced motion. <picture> is
   inline by default, which puts a line box inside the plate and
   leaves the image a few pixels short of the bottom edge. */
.pj-plate picture{
  display: block;
  width: 100%;
  height: 100%;
}
.pj-plate img{
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* The plates are line drawings on black; they lift a little
     when the object wakes up rather than zooming, because a
     zooming thumbnail is the single most-used card hover on the
     web and it says nothing here. */
  opacity: calc(0.78 + var(--a) * 0.22);
}

.pj-name{
  flex: 0 0 auto;
  font-family: var(--body);
  font-weight: 600;
  font-size: clamp(10.5px, calc(var(--u) * 7px), 17px);
  line-height: 1.15;
  letter-spacing: -0.005em;
  color: #FFFFFF;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.pj-note{
  font-family: var(--body);
  font-weight: 400;
  font-size: clamp(9.5px, calc(var(--u) * 5.3px), 12.75px);
  line-height: 1.34;
  letter-spacing: 0.002em;
  color: var(--muted);
  margin: 0;
}

.pj-where{
  font-family: var(--label);
  font-size: clamp(8px, calc(var(--u) * 4.2px), 10.5px);
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--amber);
}
.pj--quiet .pj-where{ color: var(--muted); }

/* ------------------------------------------------------------
   STATIC LAYOUT — the default, and the whole of the section
   with JavaScript off.

   Deliberately not a grid: three widths, four different top
   offsets and a set of small rotations, so the composition is
   uneven on purpose and still reads top-to-bottom.
   ------------------------------------------------------------ */
/* EVERY RULE HERE IS SCOPED :not(.is-live), and that is load
   bearing rather than tidy. The stagger below is written with
   :nth-child, which outranks the plain `.is-live .pj` reset — so
   while these were unscoped, a live card kept its 2.6rem top
   margin and rendered up to 41px below the body the solver was
   keeping inside the case. The audit still read zero escapes,
   because the model was right and only the paint was wrong. */
.pj-box:not(.is-live) .pj-field{
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: clamp(0.9rem, 2vw, 1.75rem);
  margin-top: calc(var(--u, 1rem) * 2);
}
.pj-box:not(.is-live) .pj{ flex: 0 1 clamp(15rem, 27%, 22rem); --u: 2.6; }
.pj-box:not(.is-live) .pj[data-size="m"]{ flex-basis: clamp(13rem, 23%, 19rem); }
.pj-box:not(.is-live) .pj[data-size="s"]{ flex-basis: clamp(11rem, 19%, 16rem); }
.pj-box:not(.is-live) .pj-plate{ aspect-ratio: 8 / 5; flex: 0 0 auto; }
.pj-box:not(.is-live) .pj:nth-child(3n + 2){ margin-top: 2.6rem; }
.pj-box:not(.is-live) .pj:nth-child(4n + 1){ margin-top: 1.1rem; }
.pj-box:not(.is-live) .pj:nth-child(5n + 3){ margin-top: 3.4rem; }
.pj-box:not(.is-live) .pj:nth-child(odd){ rotate: -0.5deg; }
.pj-box:not(.is-live) .pj:nth-child(4n){ rotate: 0.8deg; }

.pj-foot{
  max-width: var(--measure);
  margin: calc(var(--u) * 1.75) auto 0;
  color: var(--muted);
  text-align: center;
}

/* ------------------------------------------------------------
   LIVE LAYOUT — physics. Only once js/projects.js has measured
   the case, and only when motion is allowed.
   ------------------------------------------------------------ */
.pj-box.is-live{
  /* The case is the frame of this whole station, so it takes the
     screen rather than sitting in a band across the middle of it.
     Height is the one that matters: the objects need somewhere to
     fall through, and at 62svh they were shuffling sideways in a
     letterbox. */
  height: clamp(600px, 94svh, 1400px);
  padding: 0;
  overflow: hidden;
}

.is-live .pj-title{
  position: static;
  z-index: 1;
  /* RESTING ON the bottom hairline, flush inside it. The first pass
     pushed the type 38% past the edge so it would be cut by it — but
     the case runs overflow:hidden to keep the objects in, so that
     simply chopped the bottom off the word and read as a bug rather
     than as a crop. Flush is the version that survives the clip. */
  bottom: 0;
  max-width: 92%;
  pointer-events: none;
  user-select: none;
}
/* Split by js/projects.js. The whole heading keeps its text as
   an aria-label and every one of these is aria-hidden, so a
   screen reader is never handed a title one letter at a time. */
.pj-ch{
  display: inline-block;
  transform-origin: 50% 92%;
  color: var(--tc, inherit);
  will-change: transform;
}
.pj-word{ white-space: nowrap; }

.is-live .pj-field{
  position: absolute;
  inset: 0;
  display: block;
  margin: 0;
  gap: 0;
}
.is-live .pj{
  position: absolute;
  left: 0;
  top: 0;
  width: var(--w);
  height: var(--h);
  margin: 0 !important;
  rotate: none !important;
  flex: none;
  will-change: transform;
  contain: layout paint style;
  opacity: calc(((0.55 + var(--lift, 0.5) * 0.45) * (1 - var(--a)) + var(--a))
                * var(--enter, 1));
}
.is-live .pj-plate{
  aspect-ratio: auto;
  /* The static layout above pins the plate to an 8:5 box. Here the
     card has a measured height and the plate takes what is left of
     it, so this has to put the flex back — resetting only
     aspect-ratio left every card overflowing its own height. */
  flex: 1 1 0%;
  min-height: 0;
}

/* The note rides over the plate rather than beside it, so
   waking an object never changes the rectangle the physics is
   solving — an object that grew on hover would either overlap
   its neighbours or need a second collision shape. */
/* THE CAPTION SITS UNDER THE NAME, IN FLOW.

   It used to be absolutely positioned over the plate, floated a
   measured distance up from the bottom so it would clear the name.
   That only held while it was a hover reveal; permanently on, with a
   caption free to wrap, the two ran into each other — "needed a
   better site" straight through "DUAT". In flow they cannot collide
   at any length. The plate is flex:1 so it gives up the height, and
   the card's own rectangle never changes, which is what the physics
   needs. */
.is-live .pj-note{
  position: static;
  /* a real gap, not a rounding one: the two line boxes were
     touching by a pixel on the smaller cards */
  margin-top: max(4px, calc(var(--pad) * 0.35));
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  overflow: hidden;
  opacity: 1;
  transform: none;
}

/* THE DESTINATION SITS AT THE TOP, not beside the name.

   Sharing the bottom line was the overlap: the name only yielded
   room in step with --a, so through the whole fade the URL was
   already legible while the name still filled the line, and
   "Style." ran straight through "style-lookbook.vercel.app".
   Moving it to the opposite corner means the two can never meet,
   whatever --a is doing, and the name never has to shrink. */
.is-live .pj-where{
  position: absolute;
  z-index: 3;
  right: calc(var(--pad) * 1.1);
  top: calc(var(--pad) * 0.9);
  max-width: calc(100% - var(--pad) * 2.2);
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  opacity: var(--a);
  pointer-events: none;
  text-shadow: 0 1px 6px rgba(0,0,0,0.9), 0 0 14px rgba(0,0,0,0.7);
}
.is-live .pj-name{ padding-right: 0; }

/* ------------------------------------------------------------
   IMPACT MARKS

   A rebound off an invisible wall reads as a glitch. Each edge
   lights where an object actually struck it and fades over
   about half a second, so the boundary is something you can
   see the objects hitting.
   ------------------------------------------------------------ */
.pj-edges{
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
}
.pj-edge{
  position: absolute;
  display: block;
  opacity: var(--g, 0);
}
.pj-edge--t, .pj-edge--b{
  left: 0; right: 0; height: 2px;
  background: linear-gradient(90deg,
    transparent calc(var(--p, 50%) - var(--sp, 120px)),
    var(--amber) var(--p, 50%),
    transparent calc(var(--p, 50%) + var(--sp, 120px)));
}
.pj-edge--t{ top: -1px; }
.pj-edge--b{ bottom: -1px; }
.pj-edge--l, .pj-edge--r{
  top: 0; bottom: 0; width: 2px;
  background: linear-gradient(180deg,
    transparent calc(var(--p, 50%) - var(--sp, 120px)),
    var(--amber) var(--p, 50%),
    transparent calc(var(--p, 50%) + var(--sp, 120px)));
}
.pj-edge--l{ left: -1px; }
.pj-edge--r{ right: -1px; }
/* Nothing to light until the case is open and populated. */
.pj-box:not(.is-live) .pj-edges{ display: none; }

/* ------------------------------------------------------------
   TABLET
   ------------------------------------------------------------ */
@media (max-width: 62rem){
  .pj-box.is-live{ height: clamp(560px, 90svh, 1100px); }
  .is-live .pj-title{ max-width: 94%; }
}

/* ------------------------------------------------------------
   PHONE

   Not the desktop layout made smaller. The objects are near
   square rather than landscape, the note is on show at rest
   because there is no hover to reveal it, and the case is
   portrait so there is somewhere to drift TO. Speeds and sizes
   are re-tuned in js/projects.js, not scaled down from the
   desktop numbers.
   ------------------------------------------------------------ */
@media (max-width: 46rem){
  .pj-box{ --pj-pad: 0.85rem; }
  .pj-box.is-live{ height: clamp(500px, 86svh, 900px); }

  .pj-title{ font-size: clamp(1.75rem, 11vw, 3.2rem); }
  .is-live .pj-title{ max-width: 96%; }

  /* Shown at rest: a phone has no hover, and a caption you can
     only reach by tapping is a caption nobody reads. */
  .is-live .pj-note{
    position: static;
    padding: 0;
    background: none;
    opacity: 0.82;
    transform: none;
    line-height: 1.28;
    -webkit-line-clamp: 2;
    flex: 0 0 auto;
    /* this breakpoint re-declares the caption, so it needs the gap too
       or the name and the caption close up again here */
    margin-top: max(4px, calc(var(--pad) * 0.35));
  }
  .is-live .pj[data-size="s"] .pj-note{
    -webkit-line-clamp: 1;
  }
  /* There is no room for the destination on a phone, but "Not
     public" is a fact about the two objects that are not links,
     so it is taken off the screen rather than out of the page. */
  .is-live .pj-where{
    position: absolute;
    clip-path: inset(50%);
    width: 1px;
    height: 1px;
    overflow: hidden;
  }
  .is-live .pj-name{
    padding-right: 0;
    /* Two of nine names do not fit one line at this size, and a
       project truncated to "The job-market ..." is a project
       nobody can look up. */
    white-space: normal;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-height: 1.1;
  }
  .pj-box:not(.is-live) .pj:nth-child(n){ margin-top: 0; rotate: none; }
  .pj-box:not(.is-live) .pj-field{ justify-content: center; }
}

/* ------------------------------------------------------------
   REDUCED MOTION

   js/projects.js never starts under this, so `.is-live` should
   never appear — but the rules below take the static layout
   back by force if it ever does. Nothing in this section can
   move for a reader who asked for stillness.
   ------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce){
  .pj-box.is-live{
    height: auto;
    padding: var(--pj-pad);
    overflow: visible;
    clip-path: none;
  }
  .is-live .pj-title{
    position: static;
    max-width: 11ch;
  }
  .is-live .pj-field{
    position: static;
    display: flex;
    flex-wrap: wrap;
    gap: clamp(0.9rem, 2vw, 1.75rem);
    margin-top: calc(var(--u, 1rem) * 2);
  }
  .is-live .pj{
    position: relative;
    width: auto;
    height: auto;
    flex: 0 1 clamp(15rem, 27%, 22rem);
    transform: none !important;
    will-change: auto;
    --a: 0;
  }
  .is-live .pj[data-size="m"]{ flex-basis: clamp(13rem, 23%, 19rem); }
  .is-live .pj[data-size="s"]{ flex-basis: clamp(11rem, 19%, 16rem); }
  .is-live .pj-plate{ aspect-ratio: 8 / 5; flex: 0 0 auto; }
  .is-live .pj-note{
    position: static;
    padding: 0;
    background: none;
    opacity: 1;
    transform: none;
    -webkit-line-clamp: 4;
  }
  .is-live .pj-where{ position: static; opacity: 1; }
  .is-live .pj-name{ padding-right: 0; }
  .pj-ch{ transform: none !important; }
  .pj-edges{ display: none; }
}


/* THE SECTION HAS TO GIVE UP ITS COLUMN.
   Every .ch is capped at 78rem, so the case could never reach the
   86rem it asks for — the cap won and the box stayed narrower than
   the design. Releasing it here lets the case take the width it was
   drawn for, and the .hold inside keeps it centred. */
@media (min-width: 62rem){
  #build{
    max-width: none;
    width: 100%;
  }
}


/* The case is the frame of this station, so the station stops padding
   around it. On a 25-inch laptop the .ch's own vertical padding plus
   the .hold's was leaving a screen of black above the box before the
   objects even started. */
#build .hold{ padding-top: 0; padding-bottom: 0; min-height: 100svh; }
#build{ padding-top: calc(var(--u) * 0.75); padding-bottom: calc(var(--u) * 0.75); }


/* ------------------------------------------------------------
   THE HEADING AND THE SIGN-OFF now sit OUTSIDE the case: the
   title above its top-left corner, "More to come" under its
   bottom-right. The case is the object; these two are the label
   on the wall beside it, so they are no longer dimmed to a ghost
   behind the objects.
   ------------------------------------------------------------ */
#build .pj-title{
  position: static;
  align-self: flex-start;
  max-width: none;
  margin: 0 0 clamp(0.5rem, 1.1vw, 0.9rem);
  color: var(--text);
  opacity: 0.92;
  font-size: clamp(1.15rem, 2.1vw, 2rem);
  line-height: 1.05;
  letter-spacing: -0.015em;
}
/* The quiet half of the line — same baseline, much less weight. */
.pj-hint{
  font-weight: 400;
  /* Relative to the heading, but never smaller than legible — at 0.5em
     it fell to 8px on a narrow window. */
  font-size: max(0.72rem, 0.5em);
  letter-spacing: 0.02em;
  text-transform: none;
  color: var(--muted);
  white-space: nowrap;
}
.pj-more{
  align-self: flex-end;
  margin: clamp(0.5rem, 1.1vw, 0.9rem) 0 0;
  font-family: var(--label);
  font-size: clamp(0.8rem, 1.15vw, 1rem);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
}

/* The heading and its hint share one baseline row. */
.pj-head{
  align-self: flex-start;
  display: flex;
  align-items: baseline;
  gap: 0.6em;
  flex-wrap: wrap;
  margin: 0 0 clamp(0.5rem, 1.1vw, 0.9rem);
}
#build .pj-head .pj-title{ margin: 0; }


/* ------------------------------------------------------------
   THE HEADING LINES UP WITH THE CASE.

   It was flush to the section while the case is a centred 86rem
   block, so on a wide screen the words started a long way left of
   the frame they belong to. Giving the row the case's own measure
   puts its first letter exactly on the case's left edge, and the
   sign-off's last letter on the right one.

   Colour is set on the CHARACTERS as well: js/projects.js splits the
   heading into per-character spans that read a --tc custom property,
   so colouring only the <h2> left them on the old ghost grey.
   ------------------------------------------------------------ */
#build .pj-head,
#build .pj-more{
  width: 100%;
  max-width: 86rem;      /* the same measure as .pj-box */
  align-self: center;
}
#build .pj-head{ justify-content: flex-start; }
#build .pj-more{ text-align: right; }

#build .pj-title{
  font-size: clamp(1.6rem, 3.8vw, 3.75rem);
  color: #FFFFFF;
  opacity: 1;
}
#build .pj-title .pj-ch{ --tc: #FFFFFF; color: #FFFFFF; }

#build .pj-hint{ color: var(--muted); }


/* The label block never compresses. The card has a measured height and
   the plate is flex:1, so when a card is short the flex algorithm was
   shrinking the NAME and the CAPTION instead of the picture — which is
   how the gap between them collapsed to nothing on the smaller
   objects. The picture is the only thing here that may give. */
.is-live .pj-name,
.is-live .pj-note{ flex: 0 0 auto; }
.is-live .pj-plate{ flex: 1 1 auto; min-height: 0; }


/* His sizes: the sign-off was a footnote and the hint was too quiet to
   read as an instruction. Both up, neither loud enough to compete with
   the heading. */
#build .pj-more{
  font-size: clamp(1rem, 1.75vw, 1.7rem);
  letter-spacing: 0.05em;
  color: var(--text);
  opacity: 0.72;
}
#build .pj-hint{
  font-size: clamp(0.9rem, 1.2vw, 1.2rem);
  letter-spacing: 0.015em;
}


/* "More to come" is the closing half of the heading, so it is set like
   the heading: white and large. Slightly under it in size, because it
   is the answer to the title rather than a second title. */
#build .pj-more{
  font-size: clamp(1.3rem, 3.1vw, 3rem);
  font-family: var(--body);
  font-weight: 800;
  letter-spacing: -0.015em;
  text-transform: uppercase;
  color: #FFFFFF;
  opacity: 1;
}
