/* NextWell Lab — page transition ("the curtain").
   Shared by the landing (served from /) and by the six section galleries
   (/3d/, /interactive/, /games/, /apps/, /clocks/, /pomodoro/), so that moving
   between them reads as one system rather than as six separate documents.

   HOW THE TWO HALVES MEET
   The leaving page raises a sheet in the destination's hue, hands the hue and
   the destination's (already localized) name to the next document through
   sessionStorage, and only then navigates. The arriving document puts the same
   sheet up before its own first paint and lets it carry on in the same
   direction. One 300vh upward travel, cut in half by the navigation: out is the
   first 150vh, in is the second. See shared/page-transition.js for the hand-off.

   WHY THE SHEET IS A PSEUDO-ELEMENT ON <html>
   The arriving half has to be covered on the very first painted frame, and at
   that moment <body> does not exist yet — the script runs synchronously in
   <head>. Nothing can be appended to the DOM there, but ::before/::after on the
   root element are boxes the parser already has: adding a class to
   document.documentElement is enough to draw them. No injected markup, nothing
   to clean up beyond the class, and no chance of the section flashing unstyled
   behind a curtain that arrived a frame late.

   Only transform and opacity are animated; everything else is static paint, so
   the whole thing runs on the compositor. will-change is deliberately absent —
   the boxes exist only while a transition class is on the root, so promoting
   them costs more than it saves.

   Self-contained by design: it must not lean on main.css or shared/viewer.css,
   because it is the one stylesheet that has to look identical on both sides of
   a navigation. That is also why the label sets an explicit system-mono stack
   instead of var(--mono): each section repoints --mono to its own face, and a
   label that changed typeface at the seam would give the cut away. */

:root {
  /* Written on <html> by the script for the length of a transition; the value
     here is only the neutral fall-back for when nothing resolved. An inline
     style always wins over this rule, so the script stays in charge. */
  --pt-accent: #6b7280;
  --pt-out-dur: 360ms;
  --pt-in-dur: 420ms;
  /* One gesture, cut in half by the navigation, so the seam has to be crossed
     at speed. Out still slows into the covering position, but it is not allowed
     to stop dead there — it is still gliding when the document ends. In picks
     that motion straight up (no flat start, no anticipation dip: the pause
     between the halves is the navigation itself, and the sheet is not on screen
     to hold a breath) and accelerates off the top. */
  --pt-out-ease: cubic-bezier(.18, .66, .32, .9);
  --pt-in-ease: cubic-bezier(.3, .22, .74, .52);
  /* Resting opacity of the label, ramped for larger screens further down. */
  --pt-label-op: .58;
}

/* ===== the sheet =========================================================
   Two viewport-heights tall, masked to a soft band at each end. The opaque
   core is 120vh, so the covering position (translateY(-25%)) clears the
   viewport with 10vh to spare at both edges. That slack is also where the two
   accent hairlines are parked while the sheet is covering: each one sits just
   outside the screen at that moment, so the held frame is an even field rather
   than a bright band clipped across it.

       translateY( 50%)  sheet below the screen        — out starts here
       translateY(-25%)  covering                      — out ends / in starts
       translateY(-100%) sheet above the screen        — in ends

   The soft ends are what does the work: the leading feather veils the outgoing
   page before the fill reaches it, and the trailing feather uncovers the
   arriving one the same way, so neither side ever shows a hard edge. */
html.pt-leaving::before,
html.pt-entering::before {
  content: '';
  position: fixed;
  top: 0; left: 0; right: 0;
  height: 200%;
  margin: 0;
  box-sizing: border-box;
  /* Above the galleries' own chrome and modals, which stop around 120. */
  z-index: 9999;
  /* The sheet swallows input for as long as it is up — a second click on a
     link that is already on its way would otherwise queue a second navigation.
     The box only exists while a transition class is on the root, so there is
     nothing left to intercept anything once it is over. */
  pointer-events: auto;
  transform: translateY(50%);
  backface-visibility: hidden;

  --pt-ink: #07070d;
  --pt-tint-hi: color-mix(in srgb, var(--pt-accent) 30%, var(--pt-ink));
  --pt-tint-lo: color-mix(in srgb, var(--pt-accent) 11%, var(--pt-ink));
  --pt-edge-lead: color-mix(in srgb, var(--pt-accent) 62%, transparent);
  --pt-edge-trail: color-mix(in srgb, var(--pt-accent) 38%, transparent);
  --pt-sheen: color-mix(in srgb, var(--pt-accent) 14%, transparent);
  /* Fine static grain, blended over the tint. Without it a full-screen fill of
     a single hue reads as a flat plate; with it the sheet reads as a surface. */
  --pt-grain: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='132' height='132'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='132' height='132' filter='url(%23g)' opacity='.5'/%3E%3C/svg%3E");
  --pt-fade: linear-gradient(180deg,
      rgba(0, 0, 0, 0) 0%,
      rgba(0, 0, 0, .16) 7%,
      rgba(0, 0, 0, .6) 14%,
      rgba(0, 0, 0, 1) 20%,
      rgba(0, 0, 0, 1) 80%,
      rgba(0, 0, 0, .6) 86%,
      rgba(0, 0, 0, .16) 93%,
      rgba(0, 0, 0, 0) 100%);

  background-color: var(--pt-ink);
  background-image:
    var(--pt-grain),
    /* lit leading edge — rides inside the top feather and sweeps the screen
       bottom-to-top ahead of the fill during the out half. It ends at 23% of
       the sheet, i.e. 46vh, which is 4vh clear of the viewport's top edge when
       the sheet is covering (that edge sits at 50vh of the sheet): the line
       leaves the screen entirely before the hold instead of parking half-on. */
    linear-gradient(180deg, transparent 10.5%, var(--pt-edge-lead) 17.5%, transparent 23%),
    /* and its counterpart at the trailing edge, which leads the reveal on the
       way in — the same 4vh clear of the bottom edge (150vh) while covering */
    linear-gradient(180deg, transparent 77%, var(--pt-edge-trail) 82.5%, transparent 89.5%),
    radial-gradient(112% 30% at 50% 50%, var(--pt-sheen), transparent 72%),
    linear-gradient(180deg, var(--pt-tint-hi) 16%, var(--pt-tint-lo) 50%, var(--pt-tint-hi) 84%);
  background-repeat: repeat, no-repeat, no-repeat, no-repeat, no-repeat;
  background-size: 132px 132px, auto, auto, auto, auto;
  /* Grain only bites in the lit parts of the sheet, which is where a flat fill
     would have shown. */
  background-blend-mode: overlay, normal, normal, normal, normal;

  -webkit-mask-image: var(--pt-fade);
          mask-image: var(--pt-fade);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
}

/* Out: rises from under the screen and lands covering. `forwards` matters more
   than the motion does — the sheet has to stay put after the animation ends,
   because the next document may take a while to commit. */
html.pt-leaving::before {
  animation: pt-sheet-out var(--pt-out-dur, 360ms) var(--pt-out-ease, cubic-bezier(.18, .66, .32, .9)) both;
}

/* In: held covering from the first frame, before <body> exists. */
html.pt-entering::before {
  transform: translateY(-25%);
}

/* …and released once the arriving page is ready to be seen. */
html.pt-entering.pt-lift::before {
  animation: pt-sheet-in var(--pt-in-dur, 420ms) var(--pt-in-ease, cubic-bezier(.3, .22, .74, .52)) both;
}

@keyframes pt-sheet-out {
  from { transform: translateY(50%); }
  to   { transform: translateY(-25%); }
}

/* Straight on from where the out stopped. There is no held beat and no backward
   step at the top: the sheet is one travel across two documents, and anything
   that stalls at the seam turns it back into two animations that happen to
   share a colour. The pacing is entirely in --pt-in-ease. */
@keyframes pt-sheet-in {
  from { transform: translateY(-25%); }
  to   { transform: translateY(-100%); }
}

/* ===== the destination's name ============================================
   Rendered from the attribute the script writes, so the string is whatever the
   visitor saw on the link — already in their language, never hardcoded here.
   Drawn only when there is something to draw. */
html.pt-leaving[data-pt-label]::after,
html.pt-entering[data-pt-label]::after {
  content: attr(data-pt-label);
  position: fixed;
  inset: 0;
  margin: 0;
  padding: 0 32px;
  box-sizing: border-box;
  z-index: 10000;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
  pointer-events: none;
  -webkit-user-select: none;
          user-select: none;

  /* System mono on purpose — see the note at the top of the file. */
  font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
  /* One word alone on an empty field is the whole composition, so it has to
     hold its own at the size the field is: 11px at the small end, 15px by the
     time the screen is 1440 wide. The tracking is in em and follows by itself. */
  font-size: clamp(11px, 0.48vw + 8.1px, 15px);
  font-weight: 500;
  line-height: 1.4;
  letter-spacing: .28em;
  /* letter-spacing trails the last glyph, which pulls centred text half a step
     left; this puts it back. */
  text-indent: .14em;
  text-transform: uppercase;
  color: #f4f2ff;
  text-shadow: 0 0 26px color-mix(in srgb, var(--pt-accent) 55%, transparent);
  -webkit-font-smoothing: antialiased;
  opacity: 0;
}

/* Fades in late, once the sheet is already under it — never over the page. */
html.pt-leaving[data-pt-label]::after {
  animation: pt-label-out var(--pt-out-dur, 360ms) var(--pt-out-ease, cubic-bezier(.18, .66, .32, .9)) both;
}

html.pt-entering[data-pt-label]::after {
  opacity: var(--pt-label-op, .58);
}

/* …and leaves ahead of the sheet, drifting the same way it travels. */
html.pt-entering.pt-lift[data-pt-label]::after {
  animation: pt-label-in var(--pt-in-dur, 420ms) ease both;
}

@keyframes pt-label-out {
  0%, 34% { opacity: 0; transform: translateY(10px); }
  100%    { opacity: var(--pt-label-op, .58); transform: translateY(0); }
}

@keyframes pt-label-in {
  0%        { opacity: var(--pt-label-op, .58); transform: translateY(0); }
  46%, 100% { opacity: 0; transform: translateY(-14px); }
}

/* A wide screen puts far more field around the word, and .58 of white on it
   reads as a smudge rather than a mark; the same value at phone size is right,
   so the ramp only ever goes up. */
@media (min-width: 1000px) {
  :root { --pt-label-op: .68; }
}

@media (min-width: 1400px) {
  :root { --pt-label-op: .74; }
}

@media (max-width: 520px) {
  html.pt-leaving[data-pt-label]::after,
  html.pt-entering[data-pt-label]::after {
    font-size: 10px;
    letter-spacing: .24em;
    text-indent: .12em;
  }
}

/* ===== motion off ========================================================
   Either the visitor's system setting or the landing's own switch. The script
   checks both as well and simply navigates, but the curtain is taken out here
   too: the class can still land on the root (the switch is read from storage
   in <head>, and on the arriving page it may be applied a beat after this
   script runs), and when it does, nothing should be drawn. */
@media (prefers-reduced-motion: reduce) {
  html.pt-leaving::before,
  html.pt-leaving::after,
  html.pt-entering::before,
  html.pt-entering::after { display: none !important; }
}

html.no-anim.pt-leaving::before,
html.no-anim.pt-leaving::after,
html.no-anim.pt-entering::before,
html.no-anim.pt-entering::after { display: none !important; }
