/* ---------------------------------------------------------------
   nickdemore.com — coming soon

   Everything is laid out in a fixed design space (--frame-w x
   --frame-h) and the whole frame is scaled to fit the viewport by
   JS. Glyph positions are measured once, in design pixels, so a
   resize is a change of scale and never a re-layout.
   --------------------------------------------------------------- */

@font-face {
  font-family: 'Marist';
  src: url('/assets/font/marist-400.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: block; /* no fallback flash — we gate on fonts.ready anyway */
}

/* The burst's window, as two insets from the edges of the photo box.
   50% is shut, 0% is wide open. Registered so each axis can be
   transitioned on its own clock — the whole point of the reveal is that
   height and width do not open at the same rate, and a single
   transition on clip-path could only give them one duration. */
@property --mask-x {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 50%;
}
@property --mask-y {
  syntax: '<percentage>';
  inherits: false;
  initial-value: 50%;
}

:root {
  /* Design space. Wide enough for the address plus the offset it sits
     at before the closing move nudges it back to centre. */
  --frame-w: 1060px;
  --frame-h: 660px;

  --type: 78px;
  --photo-w: 600px;
  --photo-h: 590px;
  --caret-w: 1.6px;

  --ink: #000;
  --paper: #fff;
}

/* The same composition in a portrait design space. Keyed off a class JS
   puts on <html>, not a media query, so there is exactly one place that
   decides which profile this page is — JS has to know too, because it
   measures glyphs in design pixels and fits the frame, and two
   independent breakpoints would eventually disagree.

   The numbers come off the address. "studio@nickdemore.com" is 10.669em
   wide whatever the font-size, so a 320px line wants 30px type
   (320 / 10.669). 320 inside a 390px frame leaves ~70px of slack, which
   is what the closing move spends spacing the line over to centre — the
   same relationship the desktop frame has, at a third of the size.
   390 is the width of the reference phone, so the frame lands at
   roughly 1:1 there and is never scaled up into softness.

   Nothing below this block needs a mobile counterpart: the band and the
   caret are both calc()'d off --type, and the photo box off --photo-w /
   --photo-h, so re-pointing the tokens re-proportions the whole piece. */
html.mobile {
  --frame-w: 390px;
  --frame-h: 620px;

  --type: 30px;
  --photo-w: 300px;
  --photo-h: 380px;
  /* 1.6px of caret was a hairline against 78px type; against 30px it
     would read as a rule. 1px is the same weight relative to the line. */
  --caret-w: 1px;
}

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

html, body {
  height: 100%;
  background: var(--paper);
}

body {
  font-family: 'Marist', 'Times New Roman', serif;
  font-weight: 400;
  color: var(--ink);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}

#stage {
  display: grid;
  place-items: center;
  height: 100%;
}

/* Scaled as a whole, so the composition keeps its proportions on any
   desktop and never overflows on a short window. --s is set by JS. */
#frame {
  position: relative;
  display: grid;
  place-items: center;
  width: var(--frame-w);
  height: var(--frame-h);
  transform: scale(var(--s, 1));
  transform-origin: center center;
}

/* Hold everything back until JS has measured glyphs and decoded images.
   Released by adding .ready. Without JS this never applies. */
.js #stage { visibility: hidden; }
.js #stage.ready { visibility: visible; }

/* ---------- the text line ---------- */

/* Translated as a unit for the closing move, which carries the caret
   and the band along with the glyphs — exactly as inserting spaces at
   the head of a line would. */
#line {
  position: relative;
  font-size: var(--type);
  line-height: 1;
  white-space: nowrap;
  font-feature-settings: "ordn" 1, "dlig" 1;
  /* The band blends against this, so it must be opaque. */
  background: var(--paper);
  isolation: isolate;
}

/* The band is a difference blend, so it can only render where opaque
   paper sits behind it. line-height 1 makes #line's own background
   exactly one --type tall, while the band and the caret overhang it by
   0.06 above and below to read as a text cursor rather than a block —
   and across that overhang the band composites white on white and
   vanishes, leaving the caret's rule standing proud of the fill it is
   meant to be the leading edge of. This lays paper under exactly the
   box the band occupies. Off --type, so the portrait frame is covered
   by the same rule. */
#line::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  top: calc(-0.06 * var(--type));
  height: calc(1.12 * var(--type));
  background: var(--paper);
  z-index: -1;
}

#mail {
  color: var(--ink);
  text-decoration: none;
  display: block;
}

/* Once JS takes over, #mail becomes a positioning context for absolutely
   placed per-glyph spans, and its own text node is removed. */
.js #mail { position: relative; }

/* The glyph layer, held back from load. The photo box is only 600x590 of
   a 1060x660 frame, and while the mask is shut it covers nothing at all,
   so "more soon." would otherwise be legible around and under the burst
   from the first frame. Released by .lit on the same frame the mask is
   cut away. visibility, not opacity: nothing can transition, so no ramp
   can leak into a piece that only ever cuts. Without JS this never
   applies and the address stays a plain, visible, centred link. */
.js #mail { visibility: hidden; }
.js #mail.lit { visibility: visible; }

#mail .g {
  position: absolute;
  top: 0;
  display: inline-block;
  will-change: transform, opacity;
}

/* Letters not yet typed. */
#mail .g.fade { opacity: 0; }

/* ---------- the band ---------- */

/* The trail the cursor leaves behind it. A white rectangle in difference
   blend mode: over white paper it renders black, over black glyphs it
   renders white. It is anchored at --left-x and runs to the cursor, so the
   cursor is always its leading edge and the fill is always ground the
   cursor has already crossed. min/max give it the absolute distance
   between the two, so it works travelling either way. It is zero-width
   outside the beats that deliberately part them, and the letters the
   cursor is not acting on are never inverted. */
#band {
  position: absolute;
  top: calc(-0.06 * var(--type));
  height: calc(1.12 * var(--type));
  left: min(var(--left-x, 0px), var(--caret-x, 0px));
  width: max(
    calc(var(--caret-x, 0px) - var(--left-x, 0px)),
    calc(var(--left-x, 0px) - var(--caret-x, 0px))
  );
  background: var(--paper);
  mix-blend-mode: difference;
  pointer-events: none;
}

html:not(.js) #band, html:not(.js) #caret { display: none; }

/* ---------- the caret ---------- */

/* A text cursor, and the only thing on the page with agency: it walks to
   the head of the line, types the address out, scans "com" into view, and
   then spaces the whole line over to centre. */
#caret {
  position: absolute;
  top: calc(-0.06 * var(--type));
  height: calc(1.12 * var(--type));
  width: var(--caret-w);
  left: var(--caret-x, 0px);
  background: var(--ink);
  pointer-events: none;
  opacity: 0;
}

#caret.on { opacity: 1; }
#caret.blink { animation: blink 1.06s steps(1, end) infinite; }

@keyframes blink {
  0%, 49.99% { opacity: 1; }
  50%, 100%  { opacity: 0; }
}

/* ---------- the photo burst ---------- */

/* The box never changes size. What changes is how much of it is cut
   away: equal insets top/bottom and left/right, so both edges of each
   axis travel outward from the middle at the same rate and the centre
   of the frame never moves. Clipping rather than sizing is what keeps
   the photographs from scaling, squashing or re-cropping as it opens —
   they sit at full size the whole time and a window opens over them.
   Starts shut, so nothing can show before the reveal. */
#photos {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: var(--photo-w);
  height: var(--photo-h);
  z-index: 2; /* above the text line, which it covers then clears */
  --mask-x: 50%;
  --mask-y: 50%;
  clip-path: inset(var(--mask-y) var(--mask-x));
}

#photos img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  visibility: hidden;
}

#photos img.show { visibility: visible; }

/* ---------- behind the piece ---------- */

/* The tuner, shut, drawn by this file because tuner.js does not exist
   yet: nothing fetches it until the button inside this thing is pressed,
   so the stylesheet it injects cannot be the one that draws its own
   collapsed state. Built by JS and only once the piece has stopped
   moving, so nothing here exists during the burst.

   THIS BLOCK IS A DELIBERATE SECOND COPY. The shell below — fixed at
   top/right 10px, 300px wide, 11px/1.5 ui-monospace on white inside a
   1px rule and that shadow — and the head bar's padding, gap and
   letter-spacing are duplicated from the <style> tuner.js injects for
   #tuner and #tuner-head. They must be kept in step by hand: change
   either side and the panel will visibly jump the frame it mounts,
   which is the one thing this arrangement exists to prevent. Nothing
   past the head is copied; #behind-body downward is this file's own.

   Fixed to the viewport and deliberately outside #frame, which is
   transform-scaled by fit() — the real tuner is fixed too, and a panel
   that swelled with the window would neither match it nor stay a
   footnote. */
#behind {
  position: fixed;
  top: 10px;
  right: 10px;
  z-index: 9998; /* under the tuner it opens, over everything else */
  width: 300px;
  max-height: calc(100vh - 20px);
  overflow: auto;
  font: 11px/1.5 ui-monospace, Menlo, monospace;
  color: #111;
  background: #fff;
  border: 1px solid #111;
  box-shadow: 0 2px 14px rgba(0, 0, 0, .18);
  /* The one fade on the page. Every move in the piece is a cut, which is
     precisely why this is not: a hard cut here would read as one more
     beat of the choreography rather than as something arriving after it
     has ended. */
  opacity: 0;
  pointer-events: none;
  transition: opacity 900ms linear;
}

#behind.awake {
  opacity: 1;
  pointer-events: auto;
}

/* The rule under the head stays whether the body is open or shut,
   because the tuner's does — a bar that gained and lost its underline
   on click would be the one thing giving away that these are two
   different objects. */
#behind-head {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 5px 7px;
  border: 0;
  border-bottom: 1px solid #111;
  font: inherit;
  color: inherit;
  background: none;
  text-align: left;
  cursor: pointer;
  user-select: none;
  -webkit-appearance: none;
  appearance: none;
}

#behind-head b { flex: 1; font-weight: 400; letter-spacing: .04em; }

/* ---------- the note ---------- */

#behind-body { padding: 7px; }

/* pre, not a paragraph: the figures are set in a hand-spaced column and
   a wrap would break it. At 11px monospace the longest line is ~265px
   inside 284px of content box, so nothing here needs to wrap anyway —
   pre is what guarantees that stays true rather than hoping it does. */
#behind-note {
  margin: 0;
  font: inherit;
  white-space: pre;
}

#behind-row {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 10px;
  margin-top: 9px;
}

/* The click that costs a network request is bracketed, which in a
   monospace panel is what a button looks like. The one that costs
   nothing is a word. Neither takes the tuner's bordered button chrome:
   that belongs to its three-across action row, and one bordered box
   floating in a body of prose would read as a dialog. */
#behind-open {
  font: inherit;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  cursor: pointer;
}

#behind-open:hover { background: #111; color: #fff; }
#behind-open[disabled] { opacity: 0.4; cursor: default; }

#behind-dismiss {
  font: inherit;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  opacity: 0.45;
  cursor: pointer;
}

#behind-dismiss:hover { opacity: 1; }

#behind button:focus-visible {
  outline: 1px solid #111;
  outline-offset: 2px;
}

/* The head runs the full width of the panel, so its ring has to be
   inside the border or the overflow clips it away. */
#behind-head:focus-visible { outline-offset: -3px; }

/* ---------- scrubbing ---------- */

/* Held by playback.js while a recorded frame is being put on screen.
   Applying a snapshot has to be a cut, never a tween: the frame either
   side of it was recorded too, and anything the browser animates between
   them is a position the choreography never held.

   Left to itself it would tween, because the piece does not tidy up
   after itself — it ends with live inline transitions still hanging off
   its elements, #line holding the 95ms it jumped its last space on and
   #band the 230ms it swept ".com" with. The first snapshot written into
   that state would be glided to rather than shown, and every one after
   it would arrive late and smeared behind the playhead.

   Killing animation matters just as much as killing transition, and for
   a different reason. The caret blinks on a keyframe whose phase is set
   by the moment the class went on, five separate times over a run, and
   that clock has nothing whatever to do with where the playhead is
   standing. It is stopped here so the recorded opacity, written inline,
   is the only thing deciding whether the cursor is lit.

   !important throughout because every declaration this overrules is an
   inline style the piece wrote on itself, and nothing weaker reaches. */
html.scrubbing #photos,
html.scrubbing #caret,
html.scrubbing #band,
html.scrubbing #line,
html.scrubbing #mail .g {
  transition: none !important;
  animation: none !important;
}

/* ---------- reduced motion ---------- */

/* Skip straight to the resting state: no burst, no typing, just the email.
   The burst never runs, so the mask is never opened; taking #photos out
   of the box entirely also guarantees it cannot be left half-cut. */
@media (prefers-reduced-motion: reduce) {
  #photos { display: none; }
  #caret.blink { animation: none; }
  /* The panel still appears — a reader who asked for stillness asked
     about motion, not about being shut out of the page. It simply
     arrives the way everything else here does, at once. */
  #behind { transition: none; }
}
