/* ---------------------------------------------------------------
   Nube Szapinszky — portfolio

   Recreation of the Cargo site (nubeszapinszky.cargo.site).

   Structure, matching Cargo's:
   - the chrome is one pinned layer, exactly a viewport tall, above
     the content (Cargo pins it at z-index 498/499) and never moves
   - the stream on the right is the only thing that scrolls, running
     continuously through all 21 projects
   - project numbers pass over the pinned text as they scroll. That
     overlap is intentional — nothing is masked or given a backdrop.

   Layout model, also Cargo's — the whole composition is driven by
   viewport HEIGHT:
   - type size is 1.584vh, line-height 1.15x it
   - vertical positions are fixed fractions of viewport height
   - horizontal positions are fixed percentages of viewport width
   - rhythm that belongs to the text (index indent and gutter, the
     stagger on the numerals) is in em, so it tracks the type
   --------------------------------------------------------------- */

:root {
  --bg: #fff;
  --fg: rgba(0, 0, 0, 0.85);   /* Cargo's text colour — not pure black */
  --fg-meta: rgba(0, 0, 0, 0.4);
  --fg-caption: rgba(0, 0, 0, 0.75);
  --slot: rgba(0, 0, 0, 0.06);

  /* Type scales with viewport HEIGHT, not width. Measured off Cargo at
     six viewport sizes (768 / 800 / 900 / 1080 / 1215 / 1400 tall):
     font-size / viewport-height is 0.01584 every time, and line-height
     is always 1.15x font-size.

     1.584vh gives 19.2456px at 1215 tall and 17.1072px at 1080 —
     both exactly what Cargo renders. The clamp only guards viewports
     shorter than ~700px, where the raw value stops being readable. */
  --fs: clamp(11px, 1.584vh, 26px);
  --lh: 1.15em;
  --fs-caption: clamp(8px, 1.008vh, 17px);   /* 12.2472 / 1215 */

  /* horizontal anchors, as % of viewport width */
  --x-edge:    0.82%;   /* portrait, first numeral */
  --x-marks-3: 4.45%;   /* third numeral + contact list */
  --x-name:   17.23%;
  --x-bio:    16.90%;
  --x-index:  49.73%;   /* section headers */
  --x-stream: 60.30%;   /* stream starts here — the project numbers */
  --x-proj:   68.68%;   /* project detail column */
  --w-proj:   29.92%;   /* ...and its width */
  /* The hover still is placed against its own title in site.js, so it
     follows the ragged right edge of the list rather than a common line.
     This is only where it waits before the first hover. */
  --x-preview: 62%;
  /* How far it lies over the last characters of that title — the same
     kind of overlap the stream's numbers make with the pinned text.
     Raise it to reach further in; a title is never covered by more than
     half, however high this goes. */
  --x-preview-overlap: 1.2%;
  /* How far below its title the still hangs, counted in lines of the
     index. 1 clears the hovered title exactly, so its name stays
     readable; raise it to let more of the list show above the image. */
  --y-preview-drop: 1;

  --index-indent: 2.394em;   /* list rows sit at 51.53% */
  --index-gutter: 4.523em;   /* number column -> title */

  /* Vertical anchors, as % of viewport height — the same fractions
     Cargo holds constant across sizes. (The name sits at 0.4527 of the
     viewport height at every size measured.) The px values are what
     these resolve to at the 1215-tall reference. */
  --y-top:      3.539vh;   /*  43px */
  --y-index:    3.374vh;   /*  41px */
  --y-marks:   27.160vh;   /* 330px */
  --y-contact: 39.840vh;   /* 484px */
  /* The name rides two lines below the top of the contact list, which puts
     "Nube" on the same line as "instagram", its third and last entry. Tied
     to --y-contact rather than set as its own figure, so the two stay level
     at every size. (Cargo has it at 45.270vh, a line and a half lower.) */
  --y-name:    calc(var(--y-contact) + 2 * var(--lh));
  --y-bio:     70.700vh;   /* 859px */
  /* Where the stream begins. Tied to the line the index starts on, so the
     first project stands at the same height as every project the page
     later comes to rest on — a landing has to look the same wherever in
     the stream it happens. (Cargo has it at 1.564vh, a touch higher.) */
  --y-proj:    var(--y-index);
  /* The cv sheet hangs from the same line as the portrait so it reads as
     part of the grid, and runs as tall as the viewport allows — the sheet
     is dense, so every pixel of height is legibility. */
  --y-cv: var(--y-top);

  --portrait-w: 7.62%;   /* the image is 195px at 2560, not the 210px column */
  --w-cv-max: 32%;       /* keeps the sheet clear of the index at 49.73% */
  /* The hover still, as a 4:3 frame. Every still is sized to this frame's
     AREA rather than its width, so standing and lying images carry equal
     weight; see sizePreview() in site.js, which also keeps the still
     inside --w-proj and inside the screen. */
  --w-preview: 17%;

  /* How far the index steps aside for the cv sheet. The list goes all the
     way to the right edge, so the real distance is measured in site.js and
     written back over this value — this is only the fallback for the
     moment before that runs. */
  --cv-index-shift: 10.57vw;
}

* { margin: 0; padding: 0; box-sizing: border-box; }

html { background: var(--bg); }

body {
  background: var(--bg);
  color: var(--fg);
  /* Diatype Mono Variable is the licensed original (ABC Dinamo).
     It renders natively if installed; otherwise fall back to a
     neutral grotesque mono of similar proportion. */
  font-family: "Diatype Mono Variable", "ABC Diatype Mono", ui-monospace,
               "JetBrains Mono", "IBM Plex Mono", "SF Mono", SFMono-Regular,
               Menlo, Consolas, monospace;
  font-size: var(--fs);
  line-height: var(--lh);
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

a { color: inherit; text-decoration: none; }
a:hover,
a[data-active] { text-decoration: underline; }

/* --- pinned chrome ---------------------------------------------
   Exactly one viewport, fixed, above the stream. `pointer-events`
   is off on the layer so the wheel and clicks reach the stream
   underneath, and back on for the blocks that are actually
   interactive.
   ---------------------------------------------------------------- */

.chrome {
  position: fixed;
  inset: 0;
  height: 100vh;
  z-index: 100;
  pointer-events: none;
}

.chrome .block { pointer-events: auto; }

.block { position: absolute; }
.block[hidden] { display: none; }

/* --- portrait + clock ------------------------------------------ */

.portrait {
  left: var(--x-edge);
  top: var(--y-top);
  width: var(--portrait-w);
}

.portrait a { display: block; }
.portrait img { display: block; width: 100%; height: auto; }

.clock {
  left: 8.98%;
  top: var(--y-top);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* --- decorative numerals --------------------------------------- */

.marks { left: var(--x-edge); top: var(--y-marks); }

.marks span { display: block; }
.marks span:nth-child(2) { margin-left: 2.66em; }
.marks span:nth-child(3) { margin-left: 4.843em; }

/* --- contact --------------------------------------------------- */

.contact { left: var(--x-marks-3); top: var(--y-contact); }

.contact a,
.contact span { display: block; }

/* --- name ------------------------------------------------------ */

.name { left: var(--x-name); top: var(--y-name); }
.name a { display: block; }

/* --- project index ---------------------------------------------
   The index steps aside while the cv sheet is open. It moves by
   transform rather than by changing `left`, so nothing re-flows and
   the glide costs nothing; when the sheet closes it simply falls back
   to its own position. The shift is the distance between the index
   and the stream's number column, so the list lands exactly on that
   anchor instead of on an arbitrary spot.
   ---------------------------------------------------------------- */

.index {
  left: var(--x-index);
  top: var(--y-index);
  transition: transform 420ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

body[data-cv-open] .index { transform: translateX(var(--cv-index-shift)); }

@media (prefers-reduced-motion: reduce) {
  .index { transition: none; }
}

.index h2 {
  font-size: inherit;
  font-weight: 400;
  font-style: italic;
  line-height: var(--lh);
}

.index ol {
  list-style: none;
  margin-left: var(--index-indent);
  margin-top: var(--lh);   /* one blank line between header and list */
}

.index li a {
  display: grid;
  grid-template-columns: var(--index-gutter) auto;
  line-height: var(--lh);
}

.index section + section { margin-top: calc(var(--lh) * 2); }

/* --- index preview ----------------------------------------------
   Hovering a title in the index raises a small still from that
   project, so the list can be read by eye as well as by name.

   It stands on the project column — the same place the project's own
   images land once it is opened — so it reads as a glimpse of that
   page rather than as a tooltip pinned to the cursor. Its height is
   set in site.js: the still meets the line of the title that raised
   it, so the two are read together. The value here is only where it
   waits before the first hover.

   pointer-events stay off: the still must never come between the
   cursor and the title that raised it. It fades rather than snaps,
   and travels between lines rather than jumping, so running down the
   list doesn't strobe.

   No plate behind it. A photograph is opaque and covers the box on its
   own, so a background would only ever show through a still that was
   cut out — a logo, a print — and there it would put back the white
   square the cutout was made to lose, and blank the titles the still
   lies over. The empty frame brings its own grey.
   ---------------------------------------------------------------- */

.preview {
  left: var(--x-preview);
  top: var(--y-index);
  width: var(--w-preview);
  z-index: 20;            /* over the stream; the cv sheet hides it outright */
  opacity: 0;
  transition: opacity 180ms ease,
              top 180ms cubic-bezier(0.22, 0.61, 0.36, 1),
              left 180ms cubic-bezier(0.22, 0.61, 0.36, 1),
              width 180ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.preview[data-on] { opacity: 1; }

/* The still must never come between the cursor and the title that raised
   it — now that it lies over the tail of the longest titles, catching the
   pointer would pull the hover off the title, hide the still, hand the
   hover back, and set the two flickering against each other. Written to
   out-weigh `.chrome .block { pointer-events: auto }` above: a bare
   .preview loses to it on specificity, whatever the order. */
.chrome .block.preview { pointer-events: none; }

.preview img { display: block; width: 100%; height: auto; }

/* the same empty frame the stream uses, for projects with no still yet */
.preview .slot {
  width: 100%;
  aspect-ratio: 4 / 3;
  background: var(--slot);
}

/* the cv sheet sends the index across the page, right under where the
   still would stand — the sheet has the screen to itself instead */
body[data-cv-open] .preview { display: none; }

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

/* --- bio -------------------------------------------------------- */

.bio {
  left: var(--x-bio);
  top: var(--y-bio);
  width: 15.6%;   /* ~400px at 2560 — sets the original wrap points */
}

/* A break that belongs to the narrow column only — the wide one has room
   to carry the sentence on. Its opposite numbers, the breaks set for the
   wide column, are the ones lifted out down there. */
.bio br.op-mobiel { display: none; }

.bio em { font-style: italic; }
.bio p + p { margin-top: var(--lh); }

/* --- cv ---------------------------------------------------------
   Toggled by the CV link. Lies over the name and the bio — it is the
   one block that covers other chrome, so it carries its own opaque
   background and sits above them.

   Sized by HEIGHT, like everything else here: it runs from --y-cv to
   the same margin off the bottom as the top of the page has, and the
   width simply follows the sheet's proportion. An A4 at that height
   lands well clear of the index column; --w-cv-max is the guard for
   very wide viewports.
   ---------------------------------------------------------------- */

.cv {
  left: var(--x-bio);
  top: var(--y-cv);
  height: calc(100vh - var(--y-cv) - var(--y-top));
  max-width: var(--w-cv-max);
  z-index: 10;            /* over the name and the bio, still under nothing else */
  background: var(--bg);
}

.cv a {
  display: block;
  height: 100%;
}

/* The sheet is set in handwriting and packed tight, so at column width it
   reads as a composition rather than as text. The link opens the file at
   full size for anyone who wants to actually read it. */
.cv img {
  display: block;
  height: 100%;
  width: auto;
  max-width: 100%;
  object-fit: contain;
  object-position: left top;
}

.cv a:hover { text-decoration: none; }

/* --- marquee ----------------------------------------------------
   "graphic design" and "photography." drift right-to-left across the
   bio column. Cargo does this with <marquee-set behavior="scroll"
   direction="horizontal" speed="-10">, holding two copies of the text
   — the negative speed is the leftward direction, and the second copy
   is what makes the loop seamless. Same idea here, in CSS.

   The words wrap around the column: the moment a letter slides off the
   left edge, that same letter is already coming back in on the right.
   Every letter is on screen exactly once — the word is only ever split
   across the two edges, never doubled up.

   The track is exactly one column wide. The text sits in flow at x=0;
   the ::after pseudo-element is the same text again at x=100%, i.e.
   one column further along. Translating the track -100% (one column)
   lands the second copy where the first one started, so the reset at
   the end of the cycle is invisible.

   Holds as long as the text is narrower than the column — wider, and
   the two copies would overlap and you'd see letters twice.

   Tune the drift with --marquee-duration (higher = slower).
   ---------------------------------------------------------------- */

.marquee {
  display: block;      /* each marquee is its own line in the bio */
  overflow: hidden;
  white-space: nowrap;
}

.marquee-track {
  display: block;
  position: relative;
  width: 100%;          /* one column width — the loop's circumference */
  animation: marquee var(--marquee-duration, 6.5s) linear infinite;
  will-change: transform;
}

/* the copy that comes in on the right as the original leaves on the left */
.marquee-track::after {
  content: attr(data-text);
  position: absolute;
  top: 0;
  left: 100%;
  font-style: italic;   /* .bio em doesn't reach in here */
}

.marquee-track--offset {
  /* "photography." sets off a moment after "graphic design" does. The
     loop begins with the word flush against the left edge, so it simply
     waits there — no jump when it starts, and the two lines stay out of
     step for good afterwards. */
  animation-delay: calc(var(--marquee-duration, 6.5s) / 5);
}

@keyframes marquee {
  from { transform: translateX(0); }
  to   { transform: translateX(-100%); }
}

@media (prefers-reduced-motion: reduce) {
  .marquee-track { animation: none; }
}

/* --- the way home, on a phone -----------------------------------
   On the desktop the name is pinned and never leaves: the way home is
   always under the cursor. Stacked, it stands at the head of a column
   that a project is read far below, so it has to be brought along.

   The name again, then, fixed in the corner. It carries a plate of the
   page's own white — the pinned composition lets its text and the
   photographs overlap on purpose, but that reading depends on the text
   staying where it is; a word travelling over an image is only ever in
   the way, so this one is given ground to stand on.

   Held back until it is needed: only on a small screen, only while a
   project is open, and only once the whole column above the stream has
   scrolled off — a name over the name, or over the list of titles under
   it, is one word too many on the screen.
   ---------------------------------------------------------------- */

.home-vast { display: none; }

/* --- the stream -------------------------------------------------
   In normal flow, so it gives the document its height and the wheel
   works anywhere. Offset from the left so it starts under the
   project-number column.
   ---------------------------------------------------------------- */

.stream[hidden] { display: none; }

.stream {
  margin-left: var(--x-stream);
  width: calc(100% - var(--x-stream));
  padding-top: var(--y-proj);
}

.project {
  display: grid;
  /* Number column, then the detail column. The stream is 39.7% of the
     viewport, so these are the Cargo anchors expressed against it:
       number  = (68.68 - 60.30) / 39.7 = 21.11%
       detail  =  29.92          / 39.7 = 75.36%
     leaving the same ~1.4% right margin the original has. */
  grid-template-columns: 21.11% 75.36%;
  /* The air a project keeps above it when the page comes to rest on it:
     its number and title come down onto the line the index starts on,
     rather than against the very top of the screen. The scroll is set in
     script, which reads this back — the measure stays here, in the CSS,
     with every other measure of the composition. */
  scroll-margin-top: var(--y-index);
  /* The gap that closes one project and opens the next. It has to out-weigh
     every space inside a project — between the copy and the images, between
     two images — or the reader cannot tell where one work ends. */
  padding-bottom: calc(var(--lh) * 6);
}

.project-n { color: var(--fg); }

.project h1 {
  font-size: inherit;
  font-weight: 700;
  line-height: var(--lh);
}

.project-meta {
  display: grid;
  grid-template-columns: 45.4% auto;
  color: var(--fg-meta);
  font-weight: 410;
  margin-top: var(--lh);
}

.project-copy { margin-top: calc(var(--lh) * 2); }

/* A second paragraph sits closer than the copy sits to the meta above it —
   a break within one text, not a new block. */
.project-copy + .project-copy { margin-top: var(--lh); }

.project-media { margin-top: calc(var(--lh) * 2); }

/* Images run the full width of the detail column, flush — no frame,
   no border. (Cargo's placeholders ship with a black border baked in.) */
.project-media img {
  display: block;
  width: 100%;
  height: auto;
  border: 0;
  margin-bottom: var(--lh);
}

.project-media figcaption {
  font-size: var(--fs-caption);
  font-weight: 480;
  color: var(--fg-caption);
  margin-bottom: var(--lh);
}

/* empty frame shown until real images are dropped in */
.project-media .slot {
  width: 100%;
  aspect-ratio: 4 / 3;
  background: var(--slot);
  margin-bottom: var(--lh);
}

/* --- small screens ---------------------------------------------
   The pinned composition is a desktop idea. Below 900px the chrome
   stops being pinned and everything stacks into one column.
   ---------------------------------------------------------------- */

@media (max-width: 900px) {
  :root { --fs: 14px; --lh: 17px; --fs-caption: 11px; }

  .chrome {
    /* not pinned any more, but still the frame the clock is placed against */
    position: relative;
    height: auto;
    pointer-events: auto;
    padding: 24px 20px 0;
    /* Stacked, the blocks would fall in the order the markup happens to
       hold them, and the bio would end up below twenty-three titles —
       read last, when it is the thing that introduces the rest. Flex
       lifts it up to the name without moving anything on the desktop,
       where the blocks are placed by hand anyway. */
    display: flex;
    flex-direction: column;
  }

  /* The sheet is set right under the links it is opened from, rather than
     at the foot of the column: the CV link stays on screen above it, so
     the same tap that opened it is still there to put it away. */
  .contact { order: 1; }
  .cv      { order: 2; }
  .name    { order: 3; }
  .bio     { order: 4; }
  .index   { order: 5; }

  .block { position: static; }

  /* The clock keeps its place beside the portrait, the way it stands on
     the desktop: same top line, just past the picture's right edge. It
     is the one pairing in the composition, and stacking it underneath
     turned it into a caption. Lifted out of the column to sit there —
     the portrait carries the space the clock leaves behind. */
  .portrait { width: 140px; margin-bottom: var(--lh); }

  .clock {
    position: absolute;
    left: 170px;   /* 20 padding + 140 portrait + the gap the desktop keeps */
    top: 24px;     /* the chrome's own top padding: level with the picture */
    margin: 0;
  }

  /* The staircase kept its shape but not its stride: it was set in pixels
     here, at less than half the step the desktop takes. Left to the em
     values above it steps the same distance, measured in the type. */

  /* Stacked, every block starts on the same left margin and the column
     reads as one long ladder. The links and the name step in together —
     the one place the eye is asked to move sideways, and the two of them
     hold the same line so it reads as one move, not two. */
  .contact,
  .name,
  .bio { padding-left: 40%; }

  /* The links and the name share a line, so they need air between them —
     otherwise they read as one block of six lines instead of a way out
     and a way home. */
  .contact { margin: 28px 0 calc(var(--lh) * 5); }
  .name { margin-bottom: calc(var(--lh) * 3); }

  /* With the sheet open the links are no longer a way out of the page but
     the header of the thing right under them, so they close the distance. */
  body[data-cv-open] .contact { margin-bottom: var(--lh); }

  /* stacked in one column here, so there is nothing to step aside from */
  body[data-cv-open] .index { transform: none; }

  /* The list steps in under its header the way it does on the desktop —
     the same --index-indent, so the titles hang off "photography" and
     "graphics" instead of standing on the same left edge as them. Kept in
     em, which tracks the type, rather than the flat 12px it was set at. */
  .index ol { margin-left: var(--index-indent); }
  /* The number column follows, but not the full desktop --index-gutter:
     at this width that would push the longest title onto a second line. */
  .index li a { grid-template-columns: 3.4em auto; }
  .index section + section { margin-top: 28px; }

  /* hover is a pointer idea — there is none to speak of down here */
  .preview { display: none; }

  /* The corner home. Bare text on the page, the way the pinned name is on
     the desktop — no plate behind it. It stands on the left margin of the
     column and on the top padding of the chrome, so it holds exactly the
     place the name would if the page were scrolled back up. */
  body[data-stream-open][data-chrome-away] .home-vast {
    display: block;
    position: fixed;
    left: 20px;
    top: 24px;
    z-index: 200;
    color: var(--fg);
    line-height: var(--lh);
    /* A halo instead of a plate: the page's own white, blurred out around
       the letters, so there is no box and no edge — on the white of the
       page it cannot be seen at all. It only does something where the name
       crosses a line of a project or a dark photograph, and there it clears
       enough room around the letters to read them.

       Stacked rather than single: one blur at this radius is far too thin
       to stand against body text in the same size and the same ink. The
       tight ones do the clearing, the wide ones carry it out to nothing,
       so there is no edge anywhere. */
    text-shadow:
      0 0 2px var(--bg), 0 0 3px var(--bg), 0 0 4px var(--bg),
      0 0 6px var(--bg), 0 0 8px var(--bg), 0 0 11px var(--bg),
      0 0 14px var(--bg);
  }

  .home-vast:hover { text-decoration: none; }

  /* It follows the name now, which already keeps its own distance below
     it — the space belongs under the bio instead, before the index. */
  .bio { width: auto; max-width: 34em; margin: 0 0 calc(var(--lh) * 3.5); }

  /* The breaks in the second paragraph were set by hand against a column
     twice this wide. Held to here they leave a word stranded on a line of
     its own, so the text is let go and allowed to find its own turns —
     except the one break that was written for this width in the first
     place, which is the only one that stands. */
  .bio br { display: none; }
  .bio br.op-mobiel { display: inline; }

  /* no pinned layer to cover — the cv just takes its place in the column */
  .cv { height: auto; max-width: none; margin: 0 0 calc(var(--lh) * 4); }
  .cv img { height: auto; width: 100%; }

  /* The list of titles and the first project stand under each other in one
     column here, so the space between them is the only thing saying that
     the reading has changed — from a page of names to the work itself. It
     is set wider than the gap between two projects for that reason. */
  .stream {
    margin-left: 0;
    width: auto;
    padding: calc(var(--lh) * 6) 20px 60px;
  }

  /* A project comes to rest lower than it does on the desktop: the corner
     home stands where its number and title would otherwise land, and a
     project whose name is written over the way out of it has arrived
     badly. The figure is where the corner name ends — its top margin and
     its two lines — and a line of air under that. */
  .project {
    display: block;
    padding-bottom: calc(var(--lh) * 4);
    scroll-margin-top: calc(24px + var(--lh) * 3);
  }
  .project-n { margin-bottom: var(--lh); }
  /* The two halves of the meta stand beside each other here as well, on
     the same proportion the desktop sets them in — where the work came
     from on the left, what it is on the right. Set a step smaller than
     the page: at body size the left half runs out of column and breaks a
     line in two, and the pair stops reading as one line. */
  .project-meta {
    /* a hair wider than the desktop's 45.4%: the longest left half is
       "Final project in drawing", and this is what carries it whole */
    grid-template-columns: 48% auto;
    column-gap: 0.6em;
    font-size: 12px;
  }
}
