/* ==========================================================================
   Wuji easter egg

   Typing "wuji" anywhere on the site (public/js/site.js's own module,
   guarded the same way the header's "/" search shortcut already is: never
   while focus sits in an input, textarea, select or contenteditable) plays
   a nine beat set piece built around the Taoist wuji-to-taiji cosmology:
   the ten thousand things drain of their colour and fall back into the
   void, then a single point of light, then that point splits, then two
   curved halves built from this site's own two colour families, then this
   site's own brand mark drawing itself in (views/partials/mark.ejs), then a
   typed message and the reward, the site's existing "void" accent palette,
   applied through applyPalette(), the same mechanism and the same crossfade
   the home page's own swatch-row easter egg already uses.

   This is a rare, deliberately triggered moment rather than a UI
   transition, so the whole thing runs about twenty seconds and the site's
   normal 200 to 450ms motion band deliberately does not apply to it.
   Individual entrances inside it still use the house curves (--ease,
   --ease-entrance) so it reads as the same hand.

   Every duration in this section is paired with a number in the schedule in
   site.js's play(), and the two only work when they move together: a beat
   held open longer than the transition inside it does not read as slower,
   it reads as the same snap followed by dead air. The pass that slowed this
   sequence down lengthened both sides of every pair, which is why the
   durations below no longer look like the round numbers a first draft
   picks.

   Every visual beat below is driven by one attribute, data-phase, that
   site.js writes onto #wuji-overlay in sequence: '' (inert) -> drain ->
   collapse -> void -> star -> split -> bloom -> morph -> message -> leave
   -> '' again. A later phase's selector list always includes every phase
   after it, so nothing here reverts on its way to the next beat, it only
   ever adds.

   Nothing plays until JS says so, which is also most of this feature's
   reduced-motion story: the site-wide @media (prefers-reduced-motion:
   reduce) block far above (the one that collapses *, *::before, *::after to
   a 0.001ms animation-duration and transition-duration) already lands every
   transition below on its own finished frame the instant a reduced-motion
   visitor's browser reaches it, with no separate fallback rules to keep in
   sync by hand. On top of that: every animation that loops or replays here
   is wrapped in its own no-preference query so a reduced-motion visitor
   never starts one at all, site.js never starts the dust canvas for them,
   and the typed copy is written out complete in a single frame with the
   sequence shortened to match (see the schedule in site.js). The beats
   themselves still run in order, just as hard cuts between finished frames,
   which is what keeps the sequence legible rather than a blur.

   The palette trap this section has to keep clearing: under the void
   accent every one of --indigo/--violet/--blue/--teal collapses to a
   near-black hex a few steps apart (tokens.css's own void row), all of them
   close in luminance to this overlay's own #05050a. Void is not a
   hypothetical here, it is the palette this very sequence leaves people in,
   and the sequence is re-triggerable, so anything below that has to stay
   legible is mixed toward a fixed #fff rather than left as a raw accent
   token. The canvas does the same job in JS (see its lift() helper).

   --wuji-stage-y: the point every centred layer below (.wuji-wash,
   .wuji-iris, .wuji-flash, .wuji-overlay::before) closes on or blooms from.
   It is not 50%, because .wuji-stage is not vertically centred on the
   viewport: it is one flex item in a column that also holds .wuji-copy, and
   that column centres the pair as a group, which sits the stage above true
   centre by roughly half of .wuji-copy's own height. That is deliberate for
   .wuji-stage itself, the same reasoning as the copy block's own min-height
   comment further down (room for the typed lines without the stage jumping
   upward when they appear). But the iris closing over the viewport and the
   star it opens on were reading as two different points, because the iris
   used to close on the viewport's literal 50% 50% while the star sat wherever
   the flex layout actually put the stage. Reported as "the closing circle
   and where the yin-yang appears" looking off target. site.js's initWuji()
   measures .wuji-stage's real rendered centre and writes it here as
   --wuji-stage-y so every layer that is meant to align with the stage reads
   the same point the stage itself is actually at, instead of guessing at it
   independently the way .wuji-flash and .wuji-overlay::before used to (50%
   42% and 50% 46%, two different hand picked approximations of the same
   point, both still wrong). --wuji-overlay::after (the held vignette) is the
   one deliberate exception: it darkens the viewport's own edges rather than
   marking a point in it, so it stays on the viewport's literal centre. The
   var()'s 50% fallback is what a page renders before that first measurement
   lands, and is only ever visible for a single frame. */

/* --wuji-c1 to --wuji-c4 are the departing palette, copied off :root's live
   --indigo/--violet/--blue/--teal by site.js at the instant the sequence
   starts and then held here for the whole run, so the opening beat keeps
   painting in the palette the visitor came from even after the reward has
   already repainted the document underneath. The fallbacks are the
   Boundless defaults, used only if that copy never happened. */
.wuji-overlay {
  --wuji-c1: #241D57;
  --wuji-c2: #5B4FE0;
  --wuji-c3: #4F9BE8;
  --wuji-c4: #37D0C4;
  position: fixed;
  inset: 0;
  z-index: 600;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: clamp(18px, 4vw, 30px);
  padding: 24px;
  text-align: center;
  overflow: hidden;
  background: rgba(5, 5, 10, 0);
  opacity: 0;
  visibility: hidden;
  pointer-events: none;
  /* Identity values rather than "none" so the drain beat below has
     something to interpolate from. Only ever painted while a data-phase is
     set, so an inert overlay costs nothing for this. */
  -webkit-backdrop-filter: saturate(1) blur(0px) brightness(1);
  backdrop-filter: saturate(1) blur(0px) brightness(1);
  transition: opacity 1s var(--ease), background-color 0.7s var(--ease),
    -webkit-backdrop-filter 1.7s var(--ease), backdrop-filter 1.7s var(--ease);
}
.wuji-overlay[data-phase] { visibility: visible; pointer-events: auto; opacity: 1; }

/* Beat one, the drain: the page underneath loses its colour, its light and
   its focus, in place, through the overlay's own backdrop rather than
   through anything applied to the page itself. Nothing outside this element
   is touched at any point in the sequence, so there is nothing to undo on
   Escape beyond dropping data-phase, and this is safe on any page of the
   site whatever it happens to contain. */
.wuji-overlay[data-phase='drain'] {
  background: rgba(5, 5, 10, 0.1);
  -webkit-backdrop-filter: saturate(0.2) blur(3px) brightness(0.74);
  backdrop-filter: saturate(0.2) blur(3px) brightness(0.74);
}
.wuji-overlay[data-phase='collapse'] {
  background: rgba(5, 5, 10, 0.3);
  -webkit-backdrop-filter: saturate(0) blur(11px) brightness(0.22);
  backdrop-filter: saturate(0) blur(11px) brightness(0.22);
  transition-duration: 1.25s, 1.25s, 1.25s, 1.25s;
}
/* From the void beat on the overlay is fully opaque, so the backdrop filter
   has nothing left to do and is dropped in one frame rather than animated
   back: it is a per-frame full-viewport blur of a live page that may be
   running a WebGL shader, and paying for it behind an opaque surface for
   another seven seconds would be pure waste. The snap is invisible. */
.wuji-overlay[data-phase='void'],
.wuji-overlay[data-phase='star'],
.wuji-overlay[data-phase='split'],
.wuji-overlay[data-phase='bloom'],
.wuji-overlay[data-phase='morph'],
.wuji-overlay[data-phase='message'],
.wuji-overlay[data-phase='leave'] {
  background: rgba(5, 5, 10, 1);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  transition: opacity 1s var(--ease), background-color 0.6s var(--ease);
}
.wuji-overlay[data-phase='leave'] { opacity: 0; }

/* .site-header (header.css) is position: sticky with its own stacking context
   (isolation: isolate, for its ::before's negative z-index, see the comment
   there), and Chromium does not reliably include sticky content in what a
   sibling's backdrop-filter samples. The drain and collapse beats above are
   the two that lean on backdrop-filter rather than an opaque fill, and the
   header sat fully lit through both of them while the rest of the page
   visibly dimmed underneath it, caught by screenshotting those two beats
   specifically rather than by reading the filter values back. Fading the
   header out directly sidesteps the sampling question rather than chasing
   it, and doing that for the sequence's whole run rather than only the two
   affected beats costs nothing: everything from the void beat on is already
   opaque and would have covered it regardless. html.wuji-playing is the one
   thing this sequence ever sets outside #wuji-overlay; play() and reset()
   in site.js are the only two places that ever touch it. */
html.wuji-playing .site-header {
  opacity: 0;
  transition: opacity 1.1s var(--ease);
  pointer-events: none;
}

/* The colour that is draining, pooled at the rim so the middle of the
   viewport stays clear and the page's own desaturation is actually visible
   happening underneath rather than hidden behind a wash. Its own saturate()
   winds to zero across the two opening beats, so the pooled colour goes
   grey in place at the same time the page behind it does. */
.wuji-wash {
  position: absolute;
  inset: -10%;
  pointer-events: none;
  background:
    radial-gradient(circle at 50% var(--wuji-stage-y, 50%),
      transparent 0%,
      transparent 26%,
      color-mix(in srgb, var(--wuji-c2) 42%, transparent) 58%,
      color-mix(in srgb, var(--wuji-c1) 78%, transparent) 100%);
  opacity: 0;
  transform: scale(1.06);
  filter: saturate(1.35);
  transition: opacity 1.3s var(--ease), transform 1.7s var(--ease-entrance), filter 1.7s var(--ease);
}
.wuji-overlay[data-phase='drain'] .wuji-wash {
  opacity: 1;
  transform: scale(1);
  filter: saturate(1.35);
}
.wuji-overlay[data-phase='collapse'] .wuji-wash {
  opacity: 0.45;
  transform: scale(0.72);
  filter: saturate(0);
  /* Matched to the iris's own 1.25s close just below, so the pooled colour
     goes grey and shrinks on exactly the clock the hole shuts on rather
     than finishing early and leaving the last third of the close to happen
     against an already-dead rim. */
  transition-duration: 1.25s;
}
/* The same trap the iris rule below fell into, for the same reason: without
   this list the wash stopped matching anything at the void beat and spent
   the next 1.7s transforming a full-viewport gradient back out to its
   resting scale behind an already opaque overlay. It has no business being
   animated after the collapse has swallowed it, so it holds where the
   collapse left it and only takes its opacity the rest of the way down. */
.wuji-overlay[data-phase='void'] .wuji-wash,
.wuji-overlay[data-phase='star'] .wuji-wash,
.wuji-overlay[data-phase='split'] .wuji-wash,
.wuji-overlay[data-phase='bloom'] .wuji-wash,
.wuji-overlay[data-phase='morph'] .wuji-wash,
.wuji-overlay[data-phase='message'] .wuji-wash,
.wuji-overlay[data-phase='leave'] .wuji-wash {
  opacity: 0;
  transform: scale(0.72);
  filter: saturate(0);
  transition-duration: 0.5s;
}

/* The iris: the hole the page is still visible through, closing until there
   is no page left.

   Every pixel outside the hole has to be opaque at every size, and that one
   requirement is what makes this element harder than it looks. A shrinking
   element paints nothing outside itself, so the first pass (a radial
   gradient on a scaled circle) let the page reappear around the edges of
   the viewport exactly as the iris got small, which is the opposite of what
   this beat means. Screenshotting the collapse beat on its own is what
   caught it.

   The fix after that was a fixed-size circle carrying an enormous box-shadow
   spread, animated by width and height, and it worked but it was by a wide
   margin the most expensive thing in the sequence. Counting rAF callbacks
   through the drain and collapse beats on the home page, three runs each,
   nothing else changed between them:

     circle resized, spread + both blurred glows   138, 148, 150 frames/3.3s
     circle resized, spread only, no glows         145, 195, 136
     the iris removed from the page entirely       196, 195, 195

   60fps over that window is 198, so the resized circle was costing about a
   quarter of every frame in the two beats it runs in. Shrinking it (230vmax
   to 160vmax, spread 140vmax to 76vmax) changed nothing measurable, and
   nor did cutting the blurred glows down: the cost is not the area and it
   is not the blur, it is a laid-out box whose width and height are a
   different number every frame with a shadow that has to be re-recorded
   around it each time.

   A caution worth leaving here, because it wasted a good while: the
   measurement that first said the shrink had fixed it was wrong, and wrong
   in a way that looks right. It was taken by injecting an override
   stylesheet with width and height marked !important, which of course also
   outranked the drain and collapse rules below, so the iris sat at one
   constant size for the whole run. It was not a faster iris, it was an iris
   that never animated. Any A/B run by injecting !important over the very
   property under test is measuring that property not animating.

   So the geometry is gone. This is one element that is exactly the size of
   the viewport and never changes size or lays out again, and what animates
   is the radius of a gradient painted inside it. Measured the same way,
   three runs, against the same control:

     this rule as it now stands                    197, 198, 196 frames/3.3s
     the iris removed from the page entirely       197

   which is to say it now costs nothing, and the collapse beat specifically
   went from 12 to 14 dropped frames out of 70 to 0 out of 84.

   The thing that has to be got right in a gradient version, and that a
   first attempt here got wrong: the hole has to be a real hole. Shrinking
   the gradient's own box with background-size and letting a
   background-color cover what falls outside it looks correct written down
   and is not, because background-color paints the whole element including
   the part under the transparent centre. The hole comes out solid black and
   the page this beat is supposed to be closing over is never visible
   through it at all. Caught by screenshotting the collapse beat, which is
   the third separate thing that habit has caught in this section.

   So the stops are absolute lengths off one radius rather than percentages
   of a shrinking box. --wuji-iris-r is that radius, registered just above
   so it is a real <length> the transition can interpolate rather than an
   opaque token that would jump straight to its end value. The gradient's
   box stays the full element at all times and its last stop runs black to
   the edges, so everything outside the hole is opaque at every radius for
   free, while the centre is genuinely transparent and the
   backdrop-filtered page really is what shows through it.

   Working in absolute lengths also keeps the rim honest: the glow is a
   fixed 46px of falloff at any radius, the way the old 90px blur was fixed,
   rather than thinning in proportion as the hole shuts the way a percentage
   band would. Screenshotted against the old construction at matched hole
   diameters (1027px, 285px and 61px, at 620ms, 800ms and 1000ms into the
   beat) it reads as the same closing ring, a little cleaner at its edge.

   80vmax at rest and 69vmax at the drain are hole radii, matching the
   geometry this beat had before, both comfortably outside a viewport whose
   half diagonal cannot exceed 70.7vmax, which is why nothing of this
   element is visible until the collapse actually starts closing it. */
@property --wuji-iris-r {
  syntax: '<length>';
  inherits: false;
  /* Registered properties may not have a viewport-relative initial value,
     so the real resting radius is set on the rule below and this is only
     the value a browser falls back to if that rule never applies. */
  initial-value: 0px;
}
.wuji-iris {
  position: absolute;
  inset: 0;
  pointer-events: none;
  --wuji-iris-r: 80vmax;
  background: radial-gradient(circle at 50% var(--wuji-stage-y, 50%),
    transparent 0 calc(var(--wuji-iris-r) - 10px),
    color-mix(in srgb, var(--wuji-c2) 30%, transparent) calc(var(--wuji-iris-r) + 2px),
    color-mix(in srgb, var(--wuji-c2) 58%, transparent) calc(var(--wuji-iris-r) + 12px),
    color-mix(in srgb, var(--wuji-c2) 42%, #05050a) calc(var(--wuji-iris-r) + 30px),
    #05050a calc(var(--wuji-iris-r) + 46px));
  opacity: 0;
  transition: opacity 0.7s var(--ease), --wuji-iris-r 1.7s var(--ease);
}
.wuji-overlay[data-phase='drain'] .wuji-iris { opacity: 1; --wuji-iris-r: 69vmax; }
.wuji-overlay[data-phase='collapse'] .wuji-iris {
  opacity: 1;
  --wuji-iris-r: 0px;
  /* This curve has now been wrong in both directions, so it is worth
     writing down what each one actually looked like.

     cubic-bezier(0.7, 0, 0.84, 0) accelerates the whole way and stops dead
     at full speed. The reasoning was that the last of the world does not
     drift shut, it falls in, which is right about the middle of the motion
     and wrong about its end: a curve with no settle at its own finish
     reads as a slam, and the frame it slams into is a black one, so the
     eye has nothing to land on.

     var(--ease) was the correction and it overshot. Its tail (0.2, 1) is
     the long soft glide that suits a panel opening or a colour crossfade,
     and on a hole closing under its own weight it is exactly the mush the
     word suggests: the last third of the close crawls, and because the
     hole is by then only a few pixels across, what you watch is a bright
     dot taking its time to go out.

     cubic-bezier(0.76, 0, 0.24, 1) is a symmetric ease-in-out, the quartic
     one. It holds still at the top, which the beat wants (the world has
     just stopped draining and has not yet begun to fall), gathers real
     speed through the middle, and still has a genuine deceleration at the
     end rather than either a wall or a glide. Watched frame by frame at
     1440x757: the hole is still 1027px across at 620ms, 285px at 800ms and
     61px at 1000ms, so half the close happens in the middle fifth of the
     beat and the last quarter second is one small tinted point going out.
     Weighted at the finish, not soft, and not a crash. */
  transition: opacity 0.7s var(--ease),
    --wuji-iris-r 1.25s cubic-bezier(0.76, 0, 0.24, 1);
}
/* Every phase from the void on, not just the void, and this list is the
   whole point of the rule. The section note at the top of this file says a
   later phase's selector list always includes every phase after it so that
   nothing reverts on its way to the next beat; this rule was the one place
   that was not true. Naming only 'void' meant that the instant the star
   beat opened, the iris stopped matching anything but its base rule and
   began transitioning back open again, invisibly, underneath the beat where
   a single point of light is supposed to be the only thing happening. It
   showed up exactly where the arithmetic says it should: the star beat ran
   a median 32.9ms a frame with 15 of its 37 frames over 33ms, while every
   other beat in the same run sat on 16.7ms. Holding it shut for the rest of
   the sequence puts the star beat back on 16.7ms. */
.wuji-overlay[data-phase='void'] .wuji-iris,
.wuji-overlay[data-phase='star'] .wuji-iris,
.wuji-overlay[data-phase='split'] .wuji-iris,
.wuji-overlay[data-phase='bloom'] .wuji-iris,
.wuji-overlay[data-phase='morph'] .wuji-iris,
.wuji-overlay[data-phase='message'] .wuji-iris,
.wuji-overlay[data-phase='leave'] .wuji-iris { opacity: 0; --wuji-iris-r: 0px; }

/* Dust, drawn by the canvas loop in site.js and by nothing else here. Sits
   above the drain layers and below the stage. */
.wuji-dust {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* The reward's own bloom of light. The palette flips to void under the mark
   at this exact moment, which without this reads as the mark quietly going
   dark rather than as the thing the message is about to name. One slow
   pulse, peaking below half opacity and centred rather than full-bleed, so
   it lands as light arriving and never as a strobe. Its own no-preference
   gate, since a reduced-motion visitor should not get a flash at all. */
.wuji-flash {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(circle at 50% var(--wuji-stage-y, 50%), rgba(236, 240, 255, 0.75), rgba(200, 212, 255, 0.22) 26%, transparent 58%);
  opacity: 0;
}
@media (prefers-reduced-motion: no-preference) {
  .wuji-overlay[data-phase='message'] .wuji-flash { animation: wuji-flash 1.4s var(--ease) both; }
  @keyframes wuji-flash {
    0% { opacity: 0; transform: scale(0.72); }
    16% { opacity: 0.42; transform: scale(1); }
    100% { opacity: 0; transform: scale(1.5); }
  }
}

/* A soft glow seated behind the stage, the ambient light the point in beat
   four is the source of, held under everything else at a low enough share
   of a fixed white that it reads as depth rather than a second thing to
   look at. Fixed white and not an accent token on purpose, see the palette
   trap note at the top of this section. */
.wuji-overlay::before {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(circle at 50% var(--wuji-stage-y, 50%), rgba(196, 204, 255, 0.16), transparent 58%);
  opacity: 0;
  transform: scale(0.7);
  transition: opacity 1.5s var(--ease), transform 3.2s var(--ease-entrance);
}
.wuji-overlay[data-phase='star']::before,
.wuji-overlay[data-phase='split']::before,
.wuji-overlay[data-phase='bloom']::before,
.wuji-overlay[data-phase='morph']::before,
.wuji-overlay[data-phase='message']::before,
.wuji-overlay[data-phase='leave']::before { opacity: 1; transform: scale(1); }

/* A vignette that breathes under the held message, the one continuous loop
   left running at the end of the sequence and the slowest thing in it.
   Its own no-preference gate, so a reduced-motion visitor gets the static
   vignette and no loop at all. */
.wuji-overlay::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: radial-gradient(circle at 50% 50%, transparent 42%, rgba(0, 0, 0, 0.55) 100%);
  opacity: 0;
  transition: opacity 1.6s var(--ease);
}
.wuji-overlay[data-phase='morph']::after,
.wuji-overlay[data-phase='message']::after,
.wuji-overlay[data-phase='leave']::after { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  .wuji-overlay[data-phase='message']::after { animation: wuji-breathe 7.5s var(--ease) infinite; }
  @keyframes wuji-breathe {
    0%, 100% { transform: scale(1); opacity: 0.85; }
    50% { transform: scale(1.12); opacity: 1; }
  }
}

/* Star, taiji and the mark share this one box so all of them sit on exactly
   the same centre point without each guessing at the same size on its own.
   The slow drift across the whole sequence is the only parallax here: the
   stage starts a little further away than it ends, so the beats feel like
   one continuous approach rather than a stack of separate fades. */
.wuji-stage {
  position: relative;
  width: clamp(120px, 30vw, 200px);
  height: clamp(120px, 30vw, 200px);
  flex-shrink: 0;
  transform: scale(1.14);
  transition: transform 4.8s var(--ease-entrance);
}
.wuji-overlay[data-phase='bloom'] .wuji-stage,
.wuji-overlay[data-phase='morph'] .wuji-stage,
.wuji-overlay[data-phase='message'] .wuji-stage,
.wuji-overlay[data-phase='leave'] .wuji-stage { transform: scale(1); }

/* The two shockwaves. Both are one-shot expanding rings, each fired by the
   phase it belongs to and left to finish (their selector lists include the
   phases that follow, so advancing a beat never cuts one off mid flight).
   Fixed white rather than an accent token, again for the void case. Both
   live inside a no-preference query: a reduced-motion visitor never starts
   them, rather than starting them and having the site-wide override snap
   them to a finished frame. */
.wuji-ring {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 100%;
  height: 100%;
  border-radius: 50%;
  border: 1px solid rgba(255, 255, 255, 0.85);
  opacity: 0;
  pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
  .wuji-overlay[data-phase='star'] .wuji-ring-star,
  .wuji-overlay[data-phase='split'] .wuji-ring-star {
    animation: wuji-shock 1.35s var(--ease-entrance) both;
  }
  /* Delayed to land when the two halves have finished rotating round to
     meet, not when the beat opens. The bloom beat runs 1500ms and the
     rotation inside it 1.35s; this fires at 1s in, as the halves close. */
  .wuji-overlay[data-phase='bloom'] .wuji-ring-bloom,
  .wuji-overlay[data-phase='morph'] .wuji-ring-bloom {
    animation: wuji-shock-wide 1.75s 1s var(--ease-entrance) both;
  }
  @keyframes wuji-shock {
    0% { opacity: 0.9; transform: scale(0.12); border-width: 2px; }
    100% { opacity: 0; transform: scale(2.2); border-width: 0.5px; }
  }
  @keyframes wuji-shock-wide {
    0% { opacity: 0; transform: scale(0.7); border-width: 3px; }
    12% { opacity: 0.75; }
    100% { opacity: 0; transform: scale(4.2); border-width: 0.5px; }
  }
}

/* Beat four: a single soft point of light, igniting then held and pulsing.
   The core is a small solid disc with its own layered box-shadow doing the
   near glow, so it reads as one continuous point of light rather than a
   ring with a gap around a dark centre (the first pass at this really did
   have exactly that gap, a box-shadow on a transparent ::after paints only
   outside that pseudo-element's own box, so a ring sitting a few pixels
   clear of the dot left the dot itself looking like a small dark hole by
   contrast, a real bug caught by screenshotting this beat on its own, not
   a guess). The pulse lives on a separate ::after behind the core, a soft
   filled radial glow rather than a box-shadow ring, so the one continuous
   loop here never fights the entrance transition that brings the dot in,
   the two animate independent properties on independent boxes. */
.wuji-star {
  position: absolute;
  inset: 0;
  margin: auto;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: radial-gradient(circle, #fff 0%, #fff 38%, color-mix(in srgb, var(--teal) 45%, #fff 55%) 78%, transparent 100%);
  box-shadow: 0 0 16px 3px rgba(255, 255, 255, 0.6), 0 0 42px 12px rgba(190, 205, 255, 0.28);
  opacity: 0;
  transform: scale(0.08);
  transition: opacity 0.62s var(--ease), transform 1s var(--ease-entrance);
}
.wuji-overlay[data-phase='star'] .wuji-star,
.wuji-overlay[data-phase='split'] .wuji-star { opacity: 1; transform: scale(1); }
/* The point does not simply fade when the two halves take over, it stretches
   and hands itself to them: one becoming two, which is the split beat's
   entire job. */
/* Held here for the rest of the sequence rather than named on 'bloom'
   alone, the same invariant the iris rule further down spells out: left to
   revert, the point spends the morph beat easing invisibly back to its
   entrance transform for no reason at all. */
.wuji-overlay[data-phase='bloom'] .wuji-star,
.wuji-overlay[data-phase='morph'] .wuji-star,
.wuji-overlay[data-phase='message'] .wuji-star,
.wuji-overlay[data-phase='leave'] .wuji-star {
  opacity: 0;
  transform: scale(2.4, 0.25);
  transition-duration: 0.7s;
}
.wuji-star::after {
  content: '';
  position: absolute;
  inset: -26px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(190, 215, 255, 0.42) 0%, transparent 72%);
  opacity: 0.6;
}
@media (prefers-reduced-motion: no-preference) {
  .wuji-overlay[data-phase='star'] .wuji-star::after,
  .wuji-overlay[data-phase='split'] .wuji-star::after {
    animation: wuji-pulse 2.1s var(--ease) infinite;
  }
  @keyframes wuji-pulse {
    0%, 100% { opacity: 0.45; transform: scale(1); }
    50% { opacity: 0.85; transform: scale(1.3); }
  }
}

/* Beats five and six, the split and the bloom: the point of light becomes
   two, and the two close into a taiji.

   --wuji-yang and --wuji-yin are the two sides. Both carry the current
   accent's own hue, but each is anchored to a fixed light and a fixed
   mid-dark rather than left as a raw token, because this shape only works
   if its two halves are unmistakably different from each other and that is
   exactly what the void palette destroys: all four accent tokens there are
   near-blacks a few steps apart, so a yin and a yang built from two of them
   are the same colour. Anchoring gives a guaranteed light and a guaranteed
   mid-dark on every palette, both still tinted by it, which is also closer
   to what the symbol has always meant than two coloured blobs were.

   The first version of this beat was two teardrop halves offset from a
   shared centre. Screenshotted on its own it read as two overlapping
   circles with a straight cut across them, not as a shape anyone would
   recognise, so this pass rebuilt it as a real taiji: one disc split down
   the middle, two half-size circles on that seam carrying the S curve, and
   an eye of the opposite side inside each. Four elements, no images. */
.wuji-taiji {
  --wuji-yang: color-mix(in srgb, var(--teal) 34%, #eef2ff 66%);
  --wuji-yin: color-mix(in srgb, var(--violet) 46%, #363c55 54%);
  position: absolute;
  inset: 0;
  margin: auto;
  width: 100%;
  height: 100%;
  opacity: 0;
  transform: rotate(-165deg) scale(0.06);
  filter: drop-shadow(0 0 26px rgba(150, 170, 255, 0.3));
  transition: opacity 0.7s var(--ease-entrance), transform 0.7s var(--ease-entrance);
}
/* The disc itself, held back until the bloom so the split beat before it is
   genuinely just two points of light coming apart rather than a small copy
   of the finished symbol. */
.wuji-taiji::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: linear-gradient(90deg, var(--wuji-yin) 0 50%, var(--wuji-yang) 50% 100%);
  opacity: 0;
  transition: opacity 0.95s var(--ease);
}
.wuji-overlay[data-phase='bloom'] .wuji-taiji::before,
.wuji-overlay[data-phase='morph'] .wuji-taiji::before { opacity: 1; }
.wuji-overlay[data-phase='split'] .wuji-taiji {
  opacity: 1;
  transform: rotate(-104deg) scale(0.26);
}
.wuji-overlay[data-phase='bloom'] .wuji-taiji {
  opacity: 1;
  transform: rotate(0deg) scale(1);
  transition-duration: 1.35s;
}
.wuji-overlay[data-phase='morph'] .wuji-taiji,
.wuji-overlay[data-phase='message'] .wuji-taiji,
.wuji-overlay[data-phase='leave'] .wuji-taiji {
  opacity: 0;
  transform: rotate(72deg) scale(0.55);
  transition-duration: 0.65s;
}
/* The two circles that carry the S. They sit on the vertical seam, one at
   the top and one at the bottom, each filled with the opposite side's
   colour, which is what turns a straight split into the curve. Because they
   are the only thing visible during the split beat, they are also the two
   points the one point of light divides into. */
.wuji-taiji-half {
  position: absolute;
  left: 25%;
  width: 50%;
  height: 50%;
  border-radius: 50%;
}
.wuji-taiji-half::after {
  content: '';
  position: absolute;
  inset: 0;
  margin: auto;
  width: 28%;
  height: 28%;
  border-radius: 50%;
}
.wuji-taiji-half-a {
  top: 0;
  background: var(--wuji-yang);
  box-shadow: 0 0 22px 2px rgba(220, 232, 255, 0.35);
}
.wuji-taiji-half-a::after { background: var(--wuji-yin); }
.wuji-taiji-half-b {
  bottom: 0;
  background: var(--wuji-yin);
  box-shadow: 0 0 18px 2px rgba(110, 120, 190, 0.4);
}
.wuji-taiji-half-b::after { background: var(--wuji-yang); }

/* Beat seven: the taiji shape dissolves and this site's own brand mark
   draws itself in, already the same infinity-loop idea in miniature, which
   is the whole reason this sequence reaches for it. .wuji-mark only
   overrides the size .mark carries everywhere else; the gradient fill and
   the white stroke are the same rules every other mark on the page reads,
   so it retints with the rest of the page the instant applyPalette() runs
   a beat later. */
.wuji-mark-wrap {
  --wuji-mark-size: clamp(74px, 17vw, 116px);
  position: absolute;
  inset: 0;
  margin: auto;
  width: fit-content;
  height: fit-content;
  opacity: 0;
  transform: scale(0.6) translateY(6px);
  /* A one-off curve rather than the house --ease-entrance for this one
     transform: a small back-out overshoot (the mark lands a little past
     full size, then eases back), the settle this beat's name promises,
     which --ease-entrance's own smooth deceleration does not carry. */
  transition: opacity 0.75s var(--ease-entrance), transform 0.75s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.wuji-overlay[data-phase='morph'] .wuji-mark-wrap,
.wuji-overlay[data-phase='message'] .wuji-mark-wrap,
.wuji-overlay[data-phase='leave'] .wuji-mark-wrap {
  opacity: 1;
  transform: scale(1) translateY(0);
}
.wuji-mark { width: var(--wuji-mark-size); filter: drop-shadow(0 0 30px rgba(255, 255, 255, 0.22)); }

/* The glyph draws itself before the badge behind it arrives, so the mark is
   written rather than dropped in. --wuji-mark-len is the real total length
   of that path, measured once with getTotalLength() by site.js and written
   onto the overlay; the literal fallback is the measured value rounded up,
   used only if that measurement never happened. The badge fades in a beat
   behind the stroke, which is why the two have different delays. */
.wuji-mark .mark-path {
  stroke-dasharray: var(--wuji-mark-len, 88);
  stroke-dashoffset: var(--wuji-mark-len, 88);
  transition: stroke-dashoffset 1.25s var(--ease);
}
.wuji-mark .mark-bg {
  opacity: 0;
  transition: opacity 0.85s 0.45s var(--ease);
}
.wuji-overlay[data-phase='morph'] .wuji-mark .mark-path,
.wuji-overlay[data-phase='message'] .wuji-mark .mark-path,
.wuji-overlay[data-phase='leave'] .wuji-mark .mark-path { stroke-dashoffset: 0; }
.wuji-overlay[data-phase='morph'] .wuji-mark .mark-bg,
.wuji-overlay[data-phase='message'] .wuji-mark .mark-bg,
.wuji-overlay[data-phase='leave'] .wuji-mark .mark-bg { opacity: 1; }

/* The creator variant's one flourish, and the one thing in this beat that
   plays for the creator alone: two streaks of light that whip once round
   the mark as the reward lands, and are gone. Never shown unless site.js
   has already set data-variant='creator', which it only does when
   document.body carried data-wuji-creator='1' to begin with, see head.ejs.

   This used to be a still ring, a 1px box-shadow circle that faded in and
   sat there, and screenshotted at the moment it arrives it read as a
   loading spinner's track drawn around the app icon: a flat blue outline
   on a black field with nothing about it that said light or motion. The
   sequence has just spent two beats on a taiji rotating clockwise into
   being (split -165deg to bloom 0deg to morph 72deg, see .wuji-taiji), so
   the flourish that replaces it keeps that momentum: the two halves that
   merged into the mark come back round it once as two comets, one from
   each colour family, chasing clockwise on opposite sides, then dissolve.

   Construction. Each comet is one pseudo-element, a square box a little
   larger than the mark, painted with a conic-gradient that runs from
   transparent at the tail through the family colour to a white-hot head,
   then masked to a comet shape. The mask is an inline SVG (the same
   data-URI idiom the label chevrons further up already use, see
   .doc-card-direction), a tapered arc generated by a small script rather
   than drawn by hand: a centreline at 37/50 of the box, a 170deg sweep
   trailing counter-clockwise behind a head at 12 o'clock so it trails a
   clockwise-moving head, half-thickness 2.6 at the head falling as
   (1 - s)^1.9 to a point at the tail, a round cap on the head, and a
   second copy of the same shape at 2.2x the thickness blurred to a soft
   halo at 40% so the streak carries its own glow inside the mask rather
   than needing a filter on the animated element. The gradient is rotated
   with from: 190deg so its one hard seam (conic gradients do not blend
   across 360 back to 0) lands exactly at the tail's tip, where both sides
   are transparent, and the white head band is wide enough (160 to 176deg)
   to cover the round cap's full angular extent, which a stop ending dead
   on the head's centre would have cut in half. The shape and the gradient
   are painted onto the same box and the whole box is what rotates, so the
   two turn as one rigid thing and nothing is recomputed per frame:
   transform and opacity only, composited, the same budget as the two
   shockwave rings above.

   Sizing follows the mark rather than a fixed pixel inset, through
   --wuji-mark-size, so the orbit sits the same distance outside the badge
   at 74px as at 116px: the streak's inner edge clears the badge's rounded
   corners by a few pixels at both ends of the clamp, which is the closest
   it can orbit without the head ever crossing in front of the icon.

   The two colours are the taiji's own two families, teal and violet, but
   read from --wuji-c4 and --wuji-c2, the departing palette site.js copied
   onto the overlay when the sequence started, not from the live --teal and
   --violet tokens the taiji itself uses. The taiji can read live tokens
   because it plays before the reward; these fire 100ms after
   applyPalette('void') has already repainted the document, so at that
   moment every live accent token is one of void's near-blacks and a streak
   mixed from one came out grey, both of them the same grey, which is
   exactly what the first cut of this looked like on screen. The captured
   values still carry the hue the visitor arrived in, so the comets are the
   last thing on screen in that palette, its final echo round the mark
   before the page comes back void. Each is mixed toward its own fixed
   tinted light (icy for yang, lavender for yin) rather than one shared
   white, the same anchoring --wuji-yang does and for the same palette-trap
   reason: a visitor who re-triggers this from inside void arrives with
   near-black captured values too, and the anchors are what keep the two
   comets both light and still distinguishable in that case. The second
   comet starts 180deg round from the first and a beat later, which is what
   keeps the pair reading as two things passing rather than one symmetrical
   ring.

   Motion: 420deg in 1.8s on a hard ease-out, so the first frames are the
   fastest, the head jumps whole tens of degrees between them and the tail
   is what makes that read as one streak rather than a stutter, then it
   settles to a drift as it fades. The scale runs 1.2 to 1 at the same
   time, so the streak tightens onto the mark as it slows, like something
   pulled into orbit rather than switched on in place. The rotate() and the
   fade are separate keyframe sets on purpose: opacity has its own
   in-between stops, transform does not, so the whole turn is one eased
   segment rather than four. Inside a no-preference query like the other
   one-shot animations here: a reduced-motion visitor never starts it and
   sees the mark arrive with no flourish, which is the correct still frame
   for it. */
.wuji-mark-wrap::before,
.wuji-mark-wrap::after {
  content: '';
  position: absolute;
  inset: calc(var(--wuji-mark-size) * -0.45);
  pointer-events: none;
  opacity: 0;
  background: conic-gradient(from 190deg,
    transparent 0deg,
    color-mix(in srgb, var(--wuji-swoosh) 38%, transparent) 70deg,
    color-mix(in srgb, var(--wuji-swoosh) 94%, transparent) 146deg,
    #fff 160deg 176deg,
    transparent 176deg 360deg);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cfilter id='g' x='-20%' y='-20%' width='140%' height='140%'%3E%3CfeGaussianBlur stdDeviation='2.0'/%3E%3C/filter%3E%3Cfilter id='c' x='-20%' y='-20%' width='140%' height='140%'%3E%3CfeGaussianBlur stdDeviation='0.32'/%3E%3C/filter%3E%3Cg fill='%23fff' fill-opacity='0.4' filter='url(%23g)'%3E%3Cpath d='M50.00,7.28L48.68,7.41L47.37,7.59L46.08,7.80L44.79,8.05L43.51,8.34L42.24,8.66L40.99,9.02L39.76,9.42L38.54,9.86L37.33,10.33L36.15,10.84L34.98,11.38L33.83,11.95L32.71,12.56L31.60,13.20L30.52,13.87L29.46,14.57L28.43,15.31L27.42,16.07L26.43,16.86L25.47,17.67L24.54,18.52L23.64,19.39L22.77,20.28L21.92,21.20L21.11,22.14L20.32,23.10L19.57,24.09L18.85,25.09L18.16,26.11L17.50,27.15L16.88,28.21L16.28,29.29L15.73,30.38L15.20,31.48L14.72,32.60L14.26,33.73L13.84,34.87L13.46,36.02L13.11,37.18L12.80,38.34L12.52,39.52L12.28,40.70L12.07,41.88L11.91,43.07L11.77,44.26L11.68,45.45L11.61,46.64L11.59,47.83L11.60,49.02L11.64,50.21L11.73,51.39L11.84,52.57L11.99,53.74L12.18,54.91L12.40,56.07L12.66,57.22L12.95,58.36L13.27,59.48L13.63,60.60L14.02,61.71L14.44,62.80L14.89,63.87L15.37,64.94L15.89,65.98L16.44,67.01L17.01,68.02L17.62,69.01L18.25,69.98L18.91,70.93L19.60,71.87L20.32,72.77L21.06,73.66L21.83,74.52L22.62,75.36L23.44,76.18L24.28,76.96L25.14,77.73L26.03,78.46L26.93,79.17L27.86,79.85L28.80,80.51L29.77,81.13L30.75,81.73L31.75,82.29L32.76,82.83L33.79,83.33L34.83,83.80L35.89,84.25L36.96,84.66L38.04,85.03L39.13,85.38L40.23,85.69L41.34,85.97L42.45,86.22L43.58,86.44L43.58,86.44L42.45,86.22L41.34,85.97L40.23,85.68L39.13,85.35L38.05,85.00L36.98,84.60L35.92,84.17L34.87,83.71L33.84,83.22L32.83,82.69L31.84,82.13L30.86,81.54L29.91,80.92L28.97,80.26L28.06,79.58L27.17,78.87L26.30,78.14L25.46,77.37L24.64,76.58L23.85,75.77L23.09,74.93L22.36,74.07L21.65,73.18L20.97,72.27L20.32,71.35L19.71,70.40L19.12,69.44L18.57,68.45L18.05,67.46L17.56,66.44L17.10,65.41L16.68,64.37L16.29,63.32L15.93,62.26L15.61,61.19L15.33,60.11L15.08,59.02L14.87,57.92L14.69,56.82L14.54,55.72L14.43,54.62L14.36,53.51L14.32,52.40L14.32,51.30L14.36,50.19L14.43,49.09L14.53,48.00L14.67,46.91L14.84,45.83L15.05,44.75L15.29,43.68L15.56,42.63L15.87,41.58L16.21,40.55L16.59,39.53L16.99,38.53L17.43,37.54L17.89,36.56L18.39,35.61L18.92,34.67L19.47,33.75L20.05,32.85L20.66,31.98L21.30,31.12L21.96,30.29L22.65,29.48L23.36,28.70L24.09,27.94L24.85,27.20L25.63,26.50L26.42,25.82L27.24,25.16L28.08,24.54L28.93,23.94L29.80,23.37L30.68,22.84L31.58,22.33L32.50,21.85L33.42,21.41L34.36,20.99L35.31,20.61L36.26,20.26L37.23,19.94L38.20,19.65L39.18,19.40L40.16,19.17L41.14,18.98L42.13,18.83L43.12,18.70L44.11,18.61L45.10,18.55L46.09,18.52L47.07,18.52L48.05,18.56L49.03,18.62L50.00,18.72Z'/%3E%3Ccircle cx='50' cy='13.00' r='5.72'/%3E%3C/g%3E%3Cg fill='%23fff' filter='url(%23c)'%3E%3Cpath d='M50.00,10.40L48.78,10.47L47.56,10.58L46.35,10.72L45.14,10.90L43.94,11.12L42.75,11.37L41.57,11.66L40.41,11.99L39.25,12.35L38.10,12.74L36.97,13.17L35.86,13.64L34.76,14.13L33.68,14.66L32.61,15.22L31.57,15.81L30.54,16.44L29.54,17.09L28.55,17.77L27.59,18.49L26.65,19.23L25.74,20.00L24.85,20.79L23.99,21.61L23.15,22.46L22.34,23.33L21.56,24.22L20.80,25.14L20.08,26.07L19.38,27.03L18.72,28.01L18.08,29.01L17.48,30.02L16.91,31.05L16.37,32.10L15.86,33.16L15.39,34.24L14.95,35.33L14.54,36.43L14.17,37.55L13.83,38.67L13.53,39.80L13.26,40.94L13.03,42.08L12.83,43.24L12.67,44.39L12.54,45.55L12.45,46.71L12.39,47.88L12.37,49.04L12.38,50.21L12.43,51.37L12.52,52.53L12.64,53.68L12.80,54.83L12.99,55.97L13.21,57.11L13.47,58.24L13.76,59.36L14.09,60.47L14.45,61.56L14.85,62.65L15.27,63.72L15.73,64.78L16.22,65.83L16.74,66.85L17.29,67.87L17.88,68.86L18.49,69.83L19.13,70.79L19.80,71.72L20.50,72.64L21.22,73.53L21.97,74.40L22.75,75.24L23.55,76.06L24.38,76.86L25.23,77.63L26.10,78.37L27.00,79.09L27.91,79.78L28.85,80.44L29.81,81.07L30.78,81.67L31.77,82.25L32.78,82.79L33.80,83.30L34.84,83.78L35.90,84.23L36.96,84.64L38.04,85.02L39.13,85.37L40.23,85.69L41.34,85.97L42.45,86.22L43.58,86.44L43.58,86.44L42.45,86.22L41.34,85.97L40.23,85.68L39.13,85.36L38.05,85.01L36.97,84.62L35.91,84.19L34.86,83.74L33.83,83.25L32.81,82.73L31.81,82.17L30.83,81.59L29.87,80.97L28.93,80.33L28.00,79.66L27.10,78.96L26.23,78.23L25.37,77.47L24.55,76.69L23.74,75.88L22.96,75.05L22.21,74.19L21.49,73.31L20.79,72.41L20.13,71.49L19.49,70.55L18.88,69.59L18.31,68.61L17.76,67.61L17.25,66.60L16.77,65.57L16.32,64.53L15.91,63.47L15.53,62.41L15.18,61.33L14.87,60.24L14.59,59.14L14.34,58.04L14.13,56.93L13.96,55.82L13.82,54.70L13.72,53.57L13.65,52.45L13.61,51.32L13.62,50.20L13.65,49.07L13.73,47.95L13.83,46.84L13.98,45.72L14.15,44.62L14.37,43.52L14.61,42.42L14.89,41.34L15.21,40.27L15.55,39.21L15.93,38.16L16.34,37.12L16.79,36.10L17.26,35.10L17.77,34.11L18.31,33.13L18.87,32.18L19.47,31.24L20.09,30.33L20.74,29.44L21.42,28.56L22.13,27.71L22.86,26.89L23.61,26.08L24.39,25.31L25.20,24.56L26.02,23.83L26.87,23.13L27.73,22.46L28.62,21.82L29.52,21.21L30.45,20.62L31.39,20.07L32.34,19.54L33.31,19.05L34.30,18.59L35.29,18.16L36.30,17.76L37.32,17.40L38.35,17.06L39.39,16.76L40.43,16.50L41.48,16.26L42.54,16.06L43.60,15.89L44.67,15.76L45.73,15.66L46.80,15.60L47.87,15.56L48.94,15.57L50.00,15.60Z'/%3E%3Ccircle cx='50' cy='13.00' r='2.6'/%3E%3C/g%3E%3C/svg%3E") center / 100% 100% no-repeat;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cfilter id='g' x='-20%' y='-20%' width='140%' height='140%'%3E%3CfeGaussianBlur stdDeviation='2.0'/%3E%3C/filter%3E%3Cfilter id='c' x='-20%' y='-20%' width='140%' height='140%'%3E%3CfeGaussianBlur stdDeviation='0.32'/%3E%3C/filter%3E%3Cg fill='%23fff' fill-opacity='0.4' filter='url(%23g)'%3E%3Cpath d='M50.00,7.28L48.68,7.41L47.37,7.59L46.08,7.80L44.79,8.05L43.51,8.34L42.24,8.66L40.99,9.02L39.76,9.42L38.54,9.86L37.33,10.33L36.15,10.84L34.98,11.38L33.83,11.95L32.71,12.56L31.60,13.20L30.52,13.87L29.46,14.57L28.43,15.31L27.42,16.07L26.43,16.86L25.47,17.67L24.54,18.52L23.64,19.39L22.77,20.28L21.92,21.20L21.11,22.14L20.32,23.10L19.57,24.09L18.85,25.09L18.16,26.11L17.50,27.15L16.88,28.21L16.28,29.29L15.73,30.38L15.20,31.48L14.72,32.60L14.26,33.73L13.84,34.87L13.46,36.02L13.11,37.18L12.80,38.34L12.52,39.52L12.28,40.70L12.07,41.88L11.91,43.07L11.77,44.26L11.68,45.45L11.61,46.64L11.59,47.83L11.60,49.02L11.64,50.21L11.73,51.39L11.84,52.57L11.99,53.74L12.18,54.91L12.40,56.07L12.66,57.22L12.95,58.36L13.27,59.48L13.63,60.60L14.02,61.71L14.44,62.80L14.89,63.87L15.37,64.94L15.89,65.98L16.44,67.01L17.01,68.02L17.62,69.01L18.25,69.98L18.91,70.93L19.60,71.87L20.32,72.77L21.06,73.66L21.83,74.52L22.62,75.36L23.44,76.18L24.28,76.96L25.14,77.73L26.03,78.46L26.93,79.17L27.86,79.85L28.80,80.51L29.77,81.13L30.75,81.73L31.75,82.29L32.76,82.83L33.79,83.33L34.83,83.80L35.89,84.25L36.96,84.66L38.04,85.03L39.13,85.38L40.23,85.69L41.34,85.97L42.45,86.22L43.58,86.44L43.58,86.44L42.45,86.22L41.34,85.97L40.23,85.68L39.13,85.35L38.05,85.00L36.98,84.60L35.92,84.17L34.87,83.71L33.84,83.22L32.83,82.69L31.84,82.13L30.86,81.54L29.91,80.92L28.97,80.26L28.06,79.58L27.17,78.87L26.30,78.14L25.46,77.37L24.64,76.58L23.85,75.77L23.09,74.93L22.36,74.07L21.65,73.18L20.97,72.27L20.32,71.35L19.71,70.40L19.12,69.44L18.57,68.45L18.05,67.46L17.56,66.44L17.10,65.41L16.68,64.37L16.29,63.32L15.93,62.26L15.61,61.19L15.33,60.11L15.08,59.02L14.87,57.92L14.69,56.82L14.54,55.72L14.43,54.62L14.36,53.51L14.32,52.40L14.32,51.30L14.36,50.19L14.43,49.09L14.53,48.00L14.67,46.91L14.84,45.83L15.05,44.75L15.29,43.68L15.56,42.63L15.87,41.58L16.21,40.55L16.59,39.53L16.99,38.53L17.43,37.54L17.89,36.56L18.39,35.61L18.92,34.67L19.47,33.75L20.05,32.85L20.66,31.98L21.30,31.12L21.96,30.29L22.65,29.48L23.36,28.70L24.09,27.94L24.85,27.20L25.63,26.50L26.42,25.82L27.24,25.16L28.08,24.54L28.93,23.94L29.80,23.37L30.68,22.84L31.58,22.33L32.50,21.85L33.42,21.41L34.36,20.99L35.31,20.61L36.26,20.26L37.23,19.94L38.20,19.65L39.18,19.40L40.16,19.17L41.14,18.98L42.13,18.83L43.12,18.70L44.11,18.61L45.10,18.55L46.09,18.52L47.07,18.52L48.05,18.56L49.03,18.62L50.00,18.72Z'/%3E%3Ccircle cx='50' cy='13.00' r='5.72'/%3E%3C/g%3E%3Cg fill='%23fff' filter='url(%23c)'%3E%3Cpath d='M50.00,10.40L48.78,10.47L47.56,10.58L46.35,10.72L45.14,10.90L43.94,11.12L42.75,11.37L41.57,11.66L40.41,11.99L39.25,12.35L38.10,12.74L36.97,13.17L35.86,13.64L34.76,14.13L33.68,14.66L32.61,15.22L31.57,15.81L30.54,16.44L29.54,17.09L28.55,17.77L27.59,18.49L26.65,19.23L25.74,20.00L24.85,20.79L23.99,21.61L23.15,22.46L22.34,23.33L21.56,24.22L20.80,25.14L20.08,26.07L19.38,27.03L18.72,28.01L18.08,29.01L17.48,30.02L16.91,31.05L16.37,32.10L15.86,33.16L15.39,34.24L14.95,35.33L14.54,36.43L14.17,37.55L13.83,38.67L13.53,39.80L13.26,40.94L13.03,42.08L12.83,43.24L12.67,44.39L12.54,45.55L12.45,46.71L12.39,47.88L12.37,49.04L12.38,50.21L12.43,51.37L12.52,52.53L12.64,53.68L12.80,54.83L12.99,55.97L13.21,57.11L13.47,58.24L13.76,59.36L14.09,60.47L14.45,61.56L14.85,62.65L15.27,63.72L15.73,64.78L16.22,65.83L16.74,66.85L17.29,67.87L17.88,68.86L18.49,69.83L19.13,70.79L19.80,71.72L20.50,72.64L21.22,73.53L21.97,74.40L22.75,75.24L23.55,76.06L24.38,76.86L25.23,77.63L26.10,78.37L27.00,79.09L27.91,79.78L28.85,80.44L29.81,81.07L30.78,81.67L31.77,82.25L32.78,82.79L33.80,83.30L34.84,83.78L35.90,84.23L36.96,84.64L38.04,85.02L39.13,85.37L40.23,85.69L41.34,85.97L42.45,86.22L43.58,86.44L43.58,86.44L42.45,86.22L41.34,85.97L40.23,85.68L39.13,85.36L38.05,85.01L36.97,84.62L35.91,84.19L34.86,83.74L33.83,83.25L32.81,82.73L31.81,82.17L30.83,81.59L29.87,80.97L28.93,80.33L28.00,79.66L27.10,78.96L26.23,78.23L25.37,77.47L24.55,76.69L23.74,75.88L22.96,75.05L22.21,74.19L21.49,73.31L20.79,72.41L20.13,71.49L19.49,70.55L18.88,69.59L18.31,68.61L17.76,67.61L17.25,66.60L16.77,65.57L16.32,64.53L15.91,63.47L15.53,62.41L15.18,61.33L14.87,60.24L14.59,59.14L14.34,58.04L14.13,56.93L13.96,55.82L13.82,54.70L13.72,53.57L13.65,52.45L13.61,51.32L13.62,50.20L13.65,49.07L13.73,47.95L13.83,46.84L13.98,45.72L14.15,44.62L14.37,43.52L14.61,42.42L14.89,41.34L15.21,40.27L15.55,39.21L15.93,38.16L16.34,37.12L16.79,36.10L17.26,35.10L17.77,34.11L18.31,33.13L18.87,32.18L19.47,31.24L20.09,30.33L20.74,29.44L21.42,28.56L22.13,27.71L22.86,26.89L23.61,26.08L24.39,25.31L25.20,24.56L26.02,23.83L26.87,23.13L27.73,22.46L28.62,21.82L29.52,21.21L30.45,20.62L31.39,20.07L32.34,19.54L33.31,19.05L34.30,18.59L35.29,18.16L36.30,17.76L37.32,17.40L38.35,17.06L39.39,16.76L40.43,16.50L41.48,16.26L42.54,16.06L43.60,15.89L44.67,15.76L45.73,15.66L46.80,15.60L47.87,15.56L48.94,15.57L50.00,15.60Z'/%3E%3Ccircle cx='50' cy='13.00' r='2.6'/%3E%3C/g%3E%3C/svg%3E") center / 100% 100% no-repeat;
}
.wuji-mark-wrap::before {
  --wuji-swoosh: color-mix(in srgb, var(--wuji-c4) 42%, #dcf6ff 58%);
  --wuji-swoosh-from: 0deg;
}
.wuji-mark-wrap::after {
  --wuji-swoosh: color-mix(in srgb, var(--wuji-c2) 50%, #e4dbff 50%);
  --wuji-swoosh-from: 180deg;
}
@media (prefers-reduced-motion: no-preference) {
  .wuji-overlay[data-variant='creator'][data-phase='message'] .wuji-mark-wrap::before,
  .wuji-overlay[data-variant='creator'][data-phase='leave'] .wuji-mark-wrap::before {
    animation: wuji-swoosh 1.8s cubic-bezier(0.12, 0.8, 0.2, 1) both;
  }
  .wuji-overlay[data-variant='creator'][data-phase='message'] .wuji-mark-wrap::after,
  .wuji-overlay[data-variant='creator'][data-phase='leave'] .wuji-mark-wrap::after {
    animation: wuji-swoosh 1.8s 0.16s cubic-bezier(0.12, 0.8, 0.2, 1) both;
  }
  @keyframes wuji-swoosh {
    0% { opacity: 0; transform: rotate(var(--wuji-swoosh-from)) scale(1.2); }
    8% { opacity: 1; }
    55% { opacity: 1; animation-timing-function: ease-in-out; }
    100% { opacity: 0; transform: rotate(calc(var(--wuji-swoosh-from) + 420deg)) scale(1); }
  }
}

/* Beat eight: the message types itself out, and the reward lands. Void
   applies through the site's own applyPalette() a beat before this text
   starts arriving (see site.js), so the crossfade and the first line land
   together rather than one waiting on the other.

   The copy block reserves its own height from the start (min-height sized
   off the three lines plus the headline) so the stage above it never
   shifts upward as lines appear. Nothing here animates opacity per line:
   the reveal is the typing itself, and a fade on top of it would only blur
   the characters as they land. */
.wuji-copy {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  min-height: calc(4.2em + var(--step-3) * 1.5 + 4.6em);
  opacity: 0;
  transition: opacity 0.55s var(--ease);
}
.wuji-overlay[data-phase='message'] .wuji-copy,
.wuji-overlay[data-phase='leave'] .wuji-copy { opacity: 1; }
.wuji-line {
  margin: 0;
  min-height: 1.4em;
  font-size: var(--step--1);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: var(--track-caps);
  color: rgba(255, 255, 255, 0.55);
}
/* The line the cosmology lands on carries a little more light than the two
   that set it up. */
.wuji-line:nth-child(3) { color: rgba(255, 255, 255, 0.82); }
.wuji-headline {
  margin: 6px 0 0;
  max-width: 26em;
  min-height: 1.3em;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: var(--step-3);
  letter-spacing: -0.01em;
  color: #fff;
}

/* The epilogue: three lines that land after the reward has already been
   given, so they read as what the reward means rather than as more of the
   build-up. Sentence case and the body font, not the cosmology lines'
   shouting uppercase caps, because these are said rather than intoned. The
   same .wuji-type/.wuji-caret machinery types them; they need no phase of
   their own in site.js, since data-phase stays 'message' for their whole
   run and .wuji-copy is already opacity: 1 there. */
.wuji-epilogue {
  margin: 0;
  min-height: 1.4em;
  max-width: 28em;
  font-size: var(--step-0);
  font-weight: 400;
  color: rgba(255, 255, 255, 0.68);
}
/* .wuji-copy's own flex gap (6px) is right for the cosmology lines and the
   headline, which type in back to back with no pause between them. The
   epilogue follows a real hold instead (700ms once the reward lands, see
   the schedule in site.js's play()), so the first line asks for a beat of
   its own space too, or the pause the timing already spent would not read
   on screen as one. */
.wuji-epilogue:first-of-type { margin-top: 14px; }
/* The invite is typed like the other two, not styled as a link: nothing
   here is clickable while the overlay holds input, and underlining text
   that cannot be clicked is a worse affordance than none at all. Letter-
   spacing alone is what marks it out as an address to copy rather than a
   sentence to read. */
.wuji-epilogue-link {
  margin-top: 4px;
  font-family: var(--mono);
  font-size: var(--step--1);
  letter-spacing: 0.02em;
  color: rgba(255, 255, 255, 0.86);
}

/* One caret per line, shown only on the line currently being typed and on
   the headline once it is finished (where it sits blinking under the held
   message rather than vanishing the instant the last character lands).
   The blink is gated on no-preference; without it the caret is simply a
   steady mark, which is the correct still frame for it. */
.wuji-caret {
  display: inline-block;
  width: 0.08em;
  min-width: 1px;
  height: 1em;
  margin-left: 0.16em;
  vertical-align: -0.12em;
  background: currentColor;
  opacity: 0;
}
.wuji-line.is-typing .wuji-caret,
.wuji-epilogue.is-typing .wuji-caret,
.wuji-headline.is-typing .wuji-caret,
.wuji-headline.is-done .wuji-caret,
.wuji-epilogue.is-done .wuji-caret { opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  .wuji-line.is-typing .wuji-caret,
  .wuji-epilogue.is-typing .wuji-caret,
  .wuji-headline.is-typing .wuji-caret,
  .wuji-headline.is-done .wuji-caret,
  .wuji-epilogue.is-done .wuji-caret { animation: wuji-caret 1.05s steps(1, end) infinite; }
  @keyframes wuji-caret {
    0%, 45% { opacity: 1; }
    46%, 100% { opacity: 0.06; }
  }
}

@media (max-width: 480px) {
  .wuji-headline { font-size: var(--step-2); }
  .wuji-copy { min-height: calc(4.2em + var(--step-2) * 1.5 + 4.2em); }
}
