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

/* 4 columns, 16px gutters, 16px margin. --col is derived from the same numbers
   the grid uses, so the logo is exactly one column wide by construction. */
:root {
  --margin: 16px;
  --gutter: 16px;
  --col: calc((100vw - (var(--margin) * 2) - (var(--gutter) * 3)) / 4);
  --ease: cubic-bezier(.16, 1, .3, 1);
  /* Where a rule sits above the bottom of a link box. The baseline lands about
     this far up once half-leading and the font's descent are accounted for, so
     both the resting underline and the laser use it and align exactly. */
  --rule: 0.09em;
  /* Slack below each reveal mask for descenders. Shared by the mask's padding
     and the resting offset of the line inside it, so the line always starts
     fully clear of the mask however this is tuned. */
  --descend: 0.26em;
}

html, body { height: 100%; overflow: hidden; }

body {
  background: #fff;
  color: #000;
  font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 14px;
  line-height: 1.35;
  -webkit-font-smoothing: antialiased;
}

.stage {
  position: relative;
  z-index: 1;        /* over the watermark */
  width: 100vw;
  height: 100vh;
  height: 100svh;
  padding: var(--margin);
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: var(--gutter);
  overflow: hidden;
}

/* ---------- media: columns 1–3, bleeding off three edges ----------
   Negative margins cancel the stage padding on top, left and bottom, so the
   footage runs to the edge of the viewport while its right edge stays on the
   column-3 grid line. */
.media {
  grid-column: 1 / 4;
  position: relative;
  overflow: hidden;
  background: #0a0a0a;
  margin: calc(var(--margin) * -1) 0 calc(var(--margin) * -1) calc(var(--margin) * -1);
  clip-path: inset(0 0 100% 0);
  transition: clip-path 1.1s var(--ease);
}
body.ready .media { clip-path: inset(0 0 0 0); }

.media video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(1);
}

/* The logo keeps the margin the footage gives up. One column exactly, so its
   right edge lands on the first gutter and the grid stays readable. */
.logo {
  position: absolute;
  top: var(--margin);
  left: var(--margin);
  width: var(--col);
  height: auto;
  z-index: 2;
  pointer-events: none;
  /* Guarantees the mark stays legible whatever frame is underneath. */
  mix-blend-mode: difference;
}

/* ---------- the wide text side of the about page ----------
   The wrapper spans columns 1-3; .essay below sets the actual measure (2.6
   columns) and the type scale. 3.6vw is ~69px at a 1920 viewport and scales
   down from there, with the clamp stopping it before it gets unreadably
   small. */
.essay-wrap {
  grid-column: 1 / 4;
  align-self: start;
}

/* Mark only, no wordmark, dead centre of the viewport and behind everything.
   The grey is baked into mark.svg as #EDEDED rather than faked with opacity —
   an opacity value only lands on that colour while the backdrop stays white,
   whereas the fill is exact whatever ends up behind it. */
.watermark {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* 80% of the previous 170 / 20.4vw / 357 — all three bounds scaled together
     so it shrinks by the same fifth at every viewport, not just mid-range. */
  width: clamp(136px, 16.32vw, 286px);
  height: auto;
  opacity: 0;
  z-index: 0;
  pointer-events: none;
  transition: opacity 900ms var(--ease) 200ms;
}


/* Carried over from the previous page: the mark occupies the same point on
   about and trade, so fading it up from nothing reads as it being rebuilt.
   On a repeat visit it is already there, dimmed, and just comes back up. */
html.mark-carry .watermark {
  opacity: 0.35;
  transition: opacity 320ms linear;
}

body.ready .watermark { opacity: 1; }

.essay {
  /* 2.6 columns, not 2.5. The extra tenth is purely to stop "discretion."
     falling to a line of its own: at 2.5 the last line is a single word at
     essentially every common desktop width (1280 through 1920 all widow), and
     2.6 clears the lot while staying inside columns 1-3. Measured across
     1152-2560 before it was picked, because the box and the type scale on
     different curves — --col carries a fixed 80px of margin and gutter, the
     font-size clamp does not — so the ratio between them drifts with the
     viewport and a fix that works at one width can widow at another. */
  width: calc(var(--col) * 2.6 + var(--gutter) * 2);
  font-family: "Instrument Serif", Georgia, "Times New Roman", serif;
  font-size: clamp(30px, 3.6vw, 70px);
  line-height: 1.03;
  /* No negative tracking here — that was compensating for the grotesque at
     display size; the serif sets tightly enough on its own. */
  letter-spacing: 0;
}

/* ---------- column 4 ---------- */
.col {
  grid-column: 4 / 5;
  position: relative;
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--gutter);
  white-space: nowrap;
}

.col p { margin-top: 34px; }

/* Column flow so the label and the link each get their own reveal box —
   a <br> would leave the two of them in a single line and one mask. */
.contact {
  margin-top: 24px;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}

/* Dead centre of the viewport, not of the leftover space. The column spans the
   full height between equal margins, so 50% of it is 50% of the page. */
.mid {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  transform: translateY(-50%);
}

.foot { margin-top: auto; color: #b0b0b0; text-align: right; }

/* Images are furniture, not content to be pulled out of the page. The
   attribute alone is ignored by some browsers, and the property alone still
   allows a selection-drag, so both are set. */
img {
  -webkit-user-drag: none;
  user-select: none;
  -webkit-user-select: none;
}

a { color: inherit; text-decoration: none; position: relative; display: inline-block; }

/* The resting underline is drawn as a pseudo-element rather than with
   text-decoration, so it shares an anchor with the hover laser below. Left as
   text-decoration the two are measured from different things — the baseline
   versus the element box — and the laser lands below the static rule. */
.u::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--rule);
  height: 1px;
  background: currentColor;
}

/* inline-block so cta.js can lock the width while the label scrambles —
   `width` is ignored on an inline box, which lets the link twitch. */
.cta { display: inline-block; }

/* Highlights are black across the site: the live text selection and the baked
   .hl run below are the same treatment, so a selection dragged over a
   highlighted sentence does not change its look. */
::selection { background: #000; color: #fff; }

/* A marked phrase inside reveal copy, underlined rather than boxed. reveal.js
   rebuilds each line and re-wraps the run, so a phrase crossing a line break
   comes back as one element per line and each gets its own rule — which is
   what makes the underline stop at the line end and pick up again below,
   instead of one rule stretching under the gap.

   inline-block and nowrap because decipher.js pins each fragment to its
   resting width while the cipher runs: width is ignored on an inline box, and
   without the pin the substitute glyphs would reflow the copy around them.
   Shares --rule with the link underline so the two sit at the same height. */
.hl {
  position: relative;
  display: inline-block;
  white-space: nowrap;
  /* The cipher glyphs run 16-25% wider than the real text and up to 88% in the
     worst case, so without clipping the scramble spills over the words after
     it. Clipped with clip-path rather than overflow: an inline-block whose
     overflow is not visible takes its BOTTOM MARGIN EDGE as its baseline
     (CSS 2.1 10.8.1), which lifts the phrase off the line it sits on.
     clip-path is a paint operation and leaves layout and baseline untouched.
     Open vertically so ascenders and descenders are never cut. */
  clip-path: inset(-0.4em 0 -0.4em 0);
}

.hl::after {
  content: '';
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--rule);
  height: 1px;
  background: currentColor;
}

/* ---------- hover laser ----------
   Draws in from the left, holds while the pointer is on the link, then exits
   to the right. Each hover appends its own element, so a second hover starts a
   second line instead of restarting the first — the passes overlap rather than
   cutting each other off, and each removes itself when it leaves.

   Driven by clip-path, not scaleX: retracting a scaled line means flipping
   transform-origin from left to right mid-sweep, which re-anchors the visible
   segment and makes it jump across the link. Clip insets interpolate
   continuously, so the exit can pick up exactly where the entry got to. */
.laser {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--rule);
  height: 1px;
  background: currentColor;
  clip-path: inset(0 100% 0 0);
  pointer-events: none;
}

/* ---------- line reveal ----------
   Each line gets its own clipping box and slides up into it from below. The
   padding/margin pair keeps descenders from being shaved by overflow:hidden. */
/* The padding/margin pair gives the box room for descenders that a tight
   line-height would otherwise put outside it, without adding any layout
   height. It has to exceed the gap between the line box and the font's
   descender, which is why it grew when the leading was tightened. */
.line {
  display: block;
  overflow: hidden;
  padding-bottom: var(--descend);
  margin-bottom: calc(var(--descend) * -1);
}

/* The resting offset must clear the mask's FULL visible height — the line box
   plus that descender padding — not just 100% of the line. A flat 110% left
   the top of the displaced text showing through the padding strip, which is
   the sliver that was peeking before the animation ran. */
.line-in {
  display: block;
  /* +2px on top of the exact figure: landing flush with the mask edge leaves
     a sub-pixel row of antialiasing showing. */
  transform: translateY(calc(100% + var(--descend) + 2px));
  transition: transform 900ms var(--ease);
}

.shown .line-in { transform: translateY(0); }

/* Placed, not animated — used for chrome that carried over from the last page. */
.line.instant .line-in { transition: none; }

/* Held invisible until reveal.js has built the masks, otherwise the copy is
   painted in full for the moment before the script runs — a wait that got
   longer once it started holding for webfonts. Gated on .js so the text stays
   readable if the script never runs at all. */
.js [data-reveal],
.js [data-reveal-rows] { visibility: hidden; }

.js.reveal-set [data-reveal],
.js.reveal-set [data-reveal-rows] { visibility: visible; }

@media (prefers-reduced-motion: reduce) {
  .line-in { transform: none; transition: none; }
  .media { clip-path: none; transition: none; }
  .laser { animation-duration: 1ms; }
}

/* ---------- trade ----------
   The only page that scrolls, so it opts out of the global overflow lock. */
html.scrolls, html.scrolls body { height: auto; overflow-y: auto; overflow-x: hidden; }

/* Everything but the plates is fixed. Each of these is a direct child of body
   so that the difference blends can still see the plates scrolling underneath
   — any wrapper that formed a stacking context would cut them off. */
.trade-nav {
  position: fixed;
  top: var(--margin);
  right: var(--margin);
  width: var(--col);          /* the 4th column, as on the hero */
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--gutter);
  white-space: nowrap;
  color: #fff;                /* white + difference = black on the page,
                                 inverted where a plate passes behind it */
  mix-blend-mode: difference;
  z-index: 5;
}

/* Logotype only — the mark already sits in the middle of this page, so
   repeating it in the corner would say the same thing twice.
   Plain black, no difference blend: the plates stop at the half-way line and
   never come within ~680px of this corner, so a blend would buy nothing and
   cost a composited layer — which is what made the hero logo rasterise soft on
   HiDPI back when it was an inverted filter rather than a shipped asset. */
.trade-logotype {
  position: fixed;
  top: var(--margin);
  left: var(--margin);
  width: clamp(65px, 4.59vw, 88px);
  z-index: 5;
  pointer-events: none;
}

.trade-logotype img { display: block; width: 100%; height: auto; }

.trade-meta {
  position: fixed;
  left: var(--margin);
  top: 50%;
  transform: translateY(-50%);
  display: grid;
  grid-template-columns: var(--col) var(--col);
  gap: var(--gutter);
  width: calc(var(--col) * 2 + var(--gutter));
  z-index: 4;
}

.trade-link { margin-top: 18px; }

.trade-back {
  position: fixed;
  left: var(--margin);
  bottom: var(--margin);
  z-index: 4;
}

/* The scrolling column. Every plate is the same slot — reaching the page's
   middle and bleeding off the right edge — and the one crossing the viewport
   centre is the one at full size. Stacked flush: the only separation between
   them is what the scaling opens up.

   --plate-h is the figure the padding and the scroll wrap both derive from, so
   a plate sits dead centre whenever scrollY is a whole multiple of it. */
:root {
  --plate-w: 50vw;
  --plate-h: calc(var(--plate-w) * 4 / 5);   /* 5:4 landscape */
}

.plates {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0;
  padding-top: calc((100svh - var(--plate-h)) / 2);
  padding-bottom: calc((100svh - var(--plate-h)) / 2);
}

/* Size lives in the layout width, not in a transform. Height follows from the
   ratio, so normal flow keeps every plate touching the one above it however
   much their heights differ — a transform would shrink a plate away from its
   neighbours and open a gap. Right alignment comes from the flex container, so
   only the left edge ever moves. */
.plate {
  margin: 0;
  width: var(--plate-w);
  aspect-ratio: 5 / 4;
  overflow: hidden;
  background: #ededed;        /* holds the frame while the photo loads */
}

/* Colour is focus. Only the plate crossing the centre line carries it; every
   other one is drained to monochrome. plates.js writes --gray per plate on the
   same pass that sizes it, so the drain tracks the scroll exactly — a
   transition here would lag a frame-by-frame value and smear it. Defaults to
   1 so the stack is monochrome before the script runs, and stays that way
   under reduced motion, where nothing is ever centred. */
.plate img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: grayscale(var(--gray, 1));
}

/* Wipes the plate column in from the top, same motion as the hero. It covers
   the column rather than clipping .plates itself — that element is three
   viewport-heights tall, so a clip on it would be measured against the whole
   scrolling stack instead of what is on screen. */
.plates-veil {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: 50vw;              /* the widest a plate ever gets */
  background: #fff;
  z-index: 1;               /* over the plates, under the mark */
  pointer-events: none;
  clip-path: inset(0 0 0 0);
  transition: clip-path 1.1s var(--ease);
}
body.ready .plates-veil { clip-path: inset(100% 0 0 0); }

/* White fill + difference: reads black against the white page and inverts
   whatever plate passes beneath it. A literal black fill would difference to
   nothing on both. */
.watermark--invert {
  mix-blend-mode: difference;
  z-index: 2;
}

/* ---------- custom cursor ---------- */
html, body, a { cursor: none; }

.cursor {
  position: fixed;
  top: 0;
  left: 0;
  width: 26px;
  height: 26px;
  margin: -13px 0 0 -13px; /* centres the mark on the pointer */
  z-index: 20;
  pointer-events: none;
  mix-blend-mode: difference; /* visible over both the white page and the footage */
  opacity: 0;
}
.cursor::before,
.cursor::after {
  content: '';
  position: absolute;
  background: #fff;
}
.cursor::before { left: 0; top: 50%; width: 100%; height: 1.5px; margin-top: -0.75px; }
.cursor::after { top: 0; left: 50%; height: 100%; width: 1.5px; margin-left: -0.75px; }

@media (pointer: coarse) {
  html, body, a { cursor: auto; }
  .cursor { display: none; }
}

/* Below this the four columns are too narrow for 14px copy, so the media takes
   the full width and column 4 stacks under it. */
@media (max-width: 900px) {
  html, body { overflow: auto; }
  .stage { grid-template-columns: 1fr; grid-template-rows: 52svh auto; height: auto; min-height: 100svh; }
  .media { grid-column: 1 / -1; margin: calc(var(--margin) * -1) calc(var(--margin) * -1) 0; }
  .essay-wrap { grid-column: 1 / -1; }
  .essay { width: auto; font-size: 26px; }
  .mark { width: 64px; }
  .col { grid-column: 1 / -1; padding-top: 26px; }
  /* One column now spans the page, so "one column wide" would put the logo
     across the whole frame — size it to the stack instead. */
  .logo { width: 56vw; }
  .mid { position: static; transform: none; margin: 26px 0; }
  .foot { margin-top: 26px; }
}
