/* ============================================================
   SILICA WORKS - handwritten reveal
   ------------------------------------------------------------
   The look half of js/intro-type.js. That script wraps every
   character of a [data-handwrite] element in a .sw-ink span and
   puts .is-writing on the element; from there this file owns the
   appearance and the script owns only the timing.

   Applies to everything on the page set in Shadows Into Light
   that is not already animated by something else: the two hero
   paragraphs, the static section labels, the split headings and
   the footer tagline.

   EVERY RULE HERE IS SCOPED UNDER .is-writing. That class is set
   by the script only after it has successfully wrapped the copy,
   so a blocked, failed or reduced-motion load leaves the text in
   its plain authored state rather than blank. Nothing in this
   file can hide text that is not going to be revealed again.
   ============================================================ */

/* ------------------------------------------------------------
   The characters
   ------------------------------------------------------------ */

/* THE STROKE SWEEP.
   ------------------------------------------------------------
   Each character is revealed through a gradient mask that travels
   across it in the direction of writing, so the ink is laid down
   ALONG the letter rather than appearing all at once. This is as
   close to a pen drawing a stroke as a reveal gets while keeping
   the copy as real, selectable, screen-readable text.

   Chosen over tracing the font to SVG paths, which is what the
   handwriting libraries do. Tracing this face gives letter
   OUTLINES rather than pen centrelines - measured on the real
   TTF, one word came out as 11 contours that draw as loops around
   each letter - and it gives up wrapping, selection, screen
   readers and the inline .sw-tag pills. See the note at the top
   of _handwriting-options.html for the full comparison.
   ------------------------------------------------------------ */

/* One box per word, so the line still wraps at spaces.

   The characters inside are inline-block (the mask needs a box to
   sweep across), and a run of loose inline-blocks is breakable at
   every one of them - without this a word would wrap mid-word
   wherever the line happened to end. Built by js/intro-type.js.

   Not inline-block itself: that would stop the word's own box from
   sitting on the text baseline the way an inline run does. */
.sw-word {
  white-space: nowrap;
}

.is-writing .sw-ink {
  /* The pen is positioned against whichever character it is
     currently riding, so every character has to be a containing
     block for it. */
  position: relative;
  /* The mask needs a box to travel across, which an inline box
     does not reliably give it.

     inline-block, NOT block or flex: the characters still have to
     sit on one line and wrap as text. The word grouping that keeps
     a word from being split mid-reveal is done in the markup by
     js/intro-type.js, not here. */
  display: inline-block;
  /* UNWRITTEN - the mask sits entirely off the glyph.
     ----------------------------------------------------------
     The character still takes its full advance width: the mask
     hides the ink, it does not remove the box. Nothing here uses
     display, visibility or font-size to hide a character, so the
     element holds its final measure and line breaks from the
     first frame. That is what keeps the plate-drift clamp in
     js/intro.js:207, and every ScrollTrigger below it, measuring
     a stable box while the copy is still coming down.

     The mask is 300% of the character's width so its soft leading
     edge can start clear of the glyph and finish clear of it,
     rather than the stroke beginning half-drawn. */
  -webkit-mask-image: linear-gradient(95deg, #000 0 38%, rgba(0, 0, 0, 0.5) 52%, transparent 66%);
  mask-image: linear-gradient(95deg, #000 0 38%, rgba(0, 0, 0, 0.5) 52%, transparent 66%);
  /* 300% wide, so the soft leading edge can start clear of the
     glyph and finish clear of it rather than the stroke beginning
     half-drawn.

     The height stays at 100% and the box is padded instead - see
     the padding below. Oversizing the mask vertically was tried
     and is wrong: at 300% height the gradient's horizontal ramp no
     longer lines up with the glyph, so partial ink bleeds through
     across the whole character and unwritten lines come up covered
     in faint marks. The mask has to track the glyph, not float
     over it. */
  -webkit-mask-size: 300% 100%;
  mask-size: 300% 100%;
  -webkit-mask-position: 100% 0;
  mask-position: 100% 0;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  /* This face swings ascenders and descenders well outside the
     character's own line box, and a mask only covers the box it is
     sized to - so the top of a d or f and the tail of a g escaped
     it and showed as faint marks on lines that had not been
     written yet. The padding grows the box to cover the overhang;
     the matching negative margins take the space straight back, so
     the line metrics and the wrapping are untouched. */
  padding: 0.5em 0;
  margin: -0.5em 0;
  /* The waver, written per character by the script and held for the
     life of the page. Handwriting does not sit on a ruled baseline
     and no two letters are set at the same angle; without this the
     finished line relaxes back into the dead regularity of set
     type, which is the thing that gives a "handwriting" font away.

     The fallbacks matter: whitespace is deliberately left without
     the custom properties, so a space must resolve to upright and
     on the baseline rather than to an invalid transform. */
  transform: translateY(var(--sw-rise, 0)) rotate(var(--sw-tilt, 0deg));
  /* The rotation is about the foot of the character, where a pen
     pivots, not about its middle. */
  transform-origin: 50% 90%;
  will-change: mask-position;
}

/* INKED. The mask sweeps off the glyph, laying the stroke down
   across it in the direction of writing. */
.is-writing .sw-ink.is-inked {
  animation: sw-ink-in 240ms cubic-bezier(0.3, 0.7, 0.4, 1) both;
}

@keyframes sw-ink-in {
  from {
    -webkit-mask-position: 100% 0;
    mask-position: 100% 0;
  }
  to {
    -webkit-mask-position: 0% 0;
    mask-position: 0% 0;
  }
}

/* Once the line is down the spans stop doing anything.

   The mask is removed outright rather than left parked at 0%. A
   mask-image is a compositing operation the browser keeps paying
   for on every paint, and a finished paragraph is several hundred
   masked spans that will never move again. `mask: none` puts the
   glyphs back to plain painted text.

   Dropping will-change likewise releases the per-character
   compositor layers - leaving several hundred spans promoted for
   the life of the page is real memory for no benefit. */
.is-written .sw-ink {
  -webkit-mask-image: none;
  mask-image: none;
  will-change: auto;
  animation: none;
  /* The padding existed only to give the mask something to cover
     the ascender and descender overhang with. With no mask there
     is nothing to cover, so the box goes back to its plain size. */
  padding: 0;
  margin: 0;
}

/* ------------------------------------------------------------
   The pen
   ------------------------------------------------------------
   Appended by the script to whichever character was written last,
   so it rides the line as it fills. A nib, not a terminal block:
   a thin stroke leaned over at the angle this face is written at,
   sitting just past the last letter on its baseline.

   Absolutely positioned against the character it is parked on, so
   it adds NOTHING to the line's width. An inline caret would
   widen the line by its own width and reflow the very box this
   whole animation is built to keep still.
   ------------------------------------------------------------ */
.sw-pen {
  position: absolute;
  left: 100%;
  bottom: 0.04em;
  width: 0.05em;
  height: 0.88em;
  margin-left: 0.045em;
  /* Leaned to sit along the slope of the handwriting face. */
  transform: rotate(14deg);
  transform-origin: bottom center;
  /* The page accent, so the pen reads as the same hand that draws
     the cursor label and the hang tags rather than as a stray
     terminal caret. --swc-accent is declared in css/cursor.css. */
  background: var(--swc-accent, #d9ff3d);
  border-radius: 0.03em;
  /* A 0.05em stroke needs the glow to be visible at all against
     the paper ground. */
  box-shadow: 0 0 0.4em rgba(217, 255, 61, 0.6);
  pointer-events: none;
  /* Never a drag target and never announced. */
  user-select: none;
  animation: sw-pen-pulse 620ms ease-in-out infinite;
}

/* Alive while it waits out a pause at a comma or a full stop.
   Without it the pen looks dropped every time the cadence rests,
   and those rests are the longest beats in the run. */
@keyframes sw-pen-pulse {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.4;
  }
}

/* ------------------------------------------------------------
   The hang pills wait for their own line
   ------------------------------------------------------------
   A pill is a finished object, not copy being written, and the
   script deliberately leaves its text alone. Left fully drawn it
   would sit at the end of a half-written sentence and give away
   the ending before the hand reaches it.

   visibility, not opacity alone: it takes the pill out of the
   accessibility tree and off the pointer until it is really
   there. js/tags.js binds every pill on load, and a pill that can
   be dragged before it is visible is a pill that can be dragged
   out of a sentence that has not been written yet.

   The pill keeps its space in the line either way, so the line
   breaks do not move when it arrives.
   ------------------------------------------------------------ */
.is-writing .sw-tag {
  opacity: 0;
  visibility: hidden;
}

.is-written .sw-tag {
  opacity: 1;
  visibility: visible;
  /* Faded up, NOT transformed.
     ------------------------------------------------------------
     This deliberately animates opacity only, and the stamp-in
     scale it used to have is gone for good.

     js/tags.js writes `el.style.transform` on EVERY gsap.ticker
     frame - it is what makes the pill drift, lean and spring home,
     and it is how a drag is applied. A running CSS animation on
     transform beats an inline style, so while the stamp played the
     pill was pinned to the keyframe instead of the drag. With
     `both` fill-mode it kept the final keyframe afterwards too, so
     the pill never moved again: measured, it sat at
     matrix(1,0,0,1,0,0) through a whole drag while js/tags.js was
     still correctly setting .is-dragging on it.

     Anything here that animates transform silently breaks the
     drag. Leave it to opacity.

     The second animation is the pill's idle bob (css/tags.css),
     restated because this declaration replaces the whole list. It
     runs on `translate`, not `transform`, so it composes with the
     drag rather than pinning it. */
  animation: sw-tag-fade 340ms ease-out both,
    sw-tag-drift 5.03s cubic-bezier(0.37, 0, 0.63, 1) infinite alternate;
  animation-delay: 0s, var(--sw-drift-delay, 0s);
}

@keyframes sw-tag-fade {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* The label dot is decoration on the end of a section label and is
   skipped by the wrap, so it needs the same hold as a pill or it
   sits there as a full stop with nothing in front of it. */
.is-writing .sw-label-dot {
  opacity: 0;
}

.is-written .sw-label-dot {
  opacity: 1;
  transition: opacity 0.3s ease 0.06s;
}

/* ------------------------------------------------------------
   Motion off
   ------------------------------------------------------------
   The script returns before wrapping anything and never sets
   .is-writing, so none of the above can apply in the first place.
   This is the belt to that braces: if an element is somehow left
   mid-write when the preference flips, it shows in full rather
   than freezing half-drawn.
   ------------------------------------------------------------ */
@media (prefers-reduced-motion: reduce) {
  .sw-ink,
  .is-writing .sw-ink {
    opacity: 1 !important;
    visibility: visible !important;
    animation: none !important;
    transform: none !important;
    /* The reveal hides characters with a mask, not with opacity, so
       clearing opacity alone would leave a half-swept character
       stuck mid-stroke. */
    -webkit-mask-image: none !important;
    mask-image: none !important;
  }

  /* The pills get their visibility back but NOT `transform: none`.
     js/tags.js owns their transform from its ticker, and overriding
     it here would freeze the drag exactly as the stamp animation
     did - see the note on .is-written .sw-tag above. js/tags.js
     already reads prefers-reduced-motion itself and drops the
     drift, so the motion preference is honoured there. */
  .is-writing .sw-tag,
  .is-writing .sw-label-dot {
    opacity: 1 !important;
    visibility: visible !important;
    animation: none !important;
  }

  .sw-pen {
    display: none !important;
  }
}
