/*
  15 — Pages · the entrance animation's styles, shared by page.html and
  case.html. Moved out of 14 — System's page.html unchanged, plus the
  screens' fade on a project's page.
*/
/* ---------------------------------------------------------------- */
/* entrance animations                                               */

.pre [data-a] { visibility: hidden; }
.pre [data-a].ready { visibility: visible; }
.pre .theme { opacity: 0; transition: opacity .48s; }
/* guide lines draw down as their project arrives */
.row.proj .cell::before { transform-origin: 0 0; transition: transform .4s cubic-bezier(.2,.7,.2,1); }
.pre .row.proj .cell:has(> .head:not(.ready))::before { transform: scaleY(0); }
.pre .theme.ready { opacity: 1; }
/* a project's screens arrive in their turn on a diagonal (Ben,
   2026-10-21; they had simply faded): a soft edge, 40% of the frame wide,
   sweeps from the top left corner to the bottom right over .9s — about
   what a long paragraph takes to resolve. The mask comes off once the
   frame settles (site.js REVEAL), so nothing is masked at rest. */
@property --rv { syntax: '<percentage>'; inherits: false; initial-value: 140%; }
.pre .frame[data-a] {
  --rv: 0%;
  -webkit-mask-image: linear-gradient(to bottom right, #000 calc(var(--rv) - 40%), transparent var(--rv));
          mask-image: linear-gradient(to bottom right, #000 calc(var(--rv) - 40%), transparent var(--rv));
  transition: --rv .9s cubic-bezier(.3,.15,.3,1);
}
.pre .frame[data-a].ready { --rv: 140%; }
.pre .frame[data-a].settled { -webkit-mask-image: none; mask-image: none; }
/* phones (Ben, 2026-10-22: the word-by-word entrance still stuttered
   there) — the whole page arrives in one diagonal sweep, the frames'
   reveal at the window's scale: a veil in the page's colour over the
   window, its soft edge 40% of the window across, drawn off from the top
   left corner to the bottom right. One gradient and nothing else moving.
   site.js starts it once the font is in and takes the veil away after. */
html.wipe::after {
  content: ""; position: fixed; inset: 0; z-index: 40; pointer-events: none;
  --rv: 0%;
  background: linear-gradient(to bottom right, transparent calc(var(--rv) - 40%), var(--bg) var(--rv));
  transition: --rv 1s cubic-bezier(.3,.15,.3,1);
}
html.wipe.wipe-go::after { --rv: 140%; }
/* the project pages' row as icons (no words to move) fades in its turn */
.pre .back.ic[data-a], .pre .step.icons[data-a] { opacity: 0; transition: opacity .48s ease, color .2s, background-color .2s, box-shadow .2s; }
.pre .back.ic[data-a].ready, .pre .step.icons[data-a].ready { opacity: 1; }

/* underlines wait until the words are back */
.anim, .anim * { text-decoration-color: transparent !important; }
/* …except an underlined link's (a.ul — the email, the App Store links):
   there each word draws its own underline, its trailing space included,
   so the line rises and fades with the word and nothing arrives late
   (Ben, 2026-10-20). The link's own underline steps aside so the line is
   never drawn twice (it is translucent, so twice would darken it). */
.anim a.ul, .settled a.ul { text-decoration: none; }
.anim a.ul .wi, .settled a.ul .wi {
  white-space: pre; text-decoration-skip-spaces: none;   /* Chrome leaves a line-end space bare by default */
  text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 4px;
  text-decoration-color: var(--line) !important;
}
.settled a.ul .wi { transition: text-decoration-color .2s; }
.settled a.ul:hover .wi { text-decoration-color: currentColor !important; }
[data-mask] .anim .em-i { overflow: hidden; }

/* Text is split into word masks (.w) holding a moving word (.wi), which
   holds its letters (.u) when letters resolve. Which parts move depends on
   the mode, set as flags on the page: data-res, data-rise, data-mask, data-blur.

   The split text is never swapped back for the original. Text drawn as
   separate pieces lands a fraction of a pixel differently from one run of
   text, and a letter with position:relative is drawn on its own and
   rounded to the pixel grid — so restoring the original, as round ten did,
   visibly nudged every line a moment after it arrived. Now the last frame
   of the animation is the page: letters are never positioned, and a
   settled block only turns its word boxes inline (which draws identically)
   so underlines can reach the words. A hidden copy of each run carries the
   words for screen readers. */
.w { display: inline-block; vertical-align: top; }
.wi { display: inline-block; transition: transform .6s cubic-bezier(.2,.75,.15,1), opacity .48s ease, filter .56s ease; }
.settled .w, .settled .wi { display: inline; }
/* an indicator (.mk box holding .mki) moves as one more word on its line */
.mki { transition-property: transform, opacity, filter, color;
       transition-duration: .6s, .48s, .56s, .16s;
       transition-timing-function: cubic-bezier(.2,.75,.15,1), ease, ease, ease; }
.settled .mki { transition: none; }
.sr { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; user-select: none; }

/* rise behind a mask — the word comes up from below its own line */
/* side room too, so the mask never trims a letter's antialiased edge
   (padding and margin cancel, so the word keeps its exact place) */
[data-mask] .anim .w { overflow: hidden; padding: 0 .12em .1em; margin: 0 -.12em -.1em; }
[data-mask] .anim .mk { overflow: hidden; }
[data-mask] .wi, [data-mask] .mki { transform: translateY(105%); }
/* soft rise — no mask, a short lift with a fade */
[data-rise]:not([data-mask]) .wi, [data-rise]:not([data-mask]) .mki { transform: translateY(.5em); opacity: 0; }
/* blur, kept light */
[data-blur] .wi, [data-blur] .mki { filter: blur(2.5px); opacity: 0; }
[data-blur]:not([data-rise]) .wi, [data-blur]:not([data-rise]) .mki { transform: translateY(1px); }
/* arrived — written after the starting states and specific enough to beat
   all of them ([data-rise]:not([data-mask]) .wi is 0-3-0) */
#page .wi.on, #page .mki.on { transform: none; opacity: 1; filter: none; }

/* resolve — a dot holds each letter's place, then the letter arrives.
   The dot is a background on the letter itself, so the letter needs no
   positioning (see above). */
@property --dk { syntax: '<color>'; inherits: false; initial-value: transparent; }
[data-res] .u {
  color: transparent; --dk: var(--faint);
  background: radial-gradient(circle at 50% 56%, var(--dk) 0 .8px, transparent 1.3px) no-repeat;
  transition: color .16s, --dk .11s;
}
/* an arrived letter drops its dot outright (2026-10-21): a dot is a
   gradient painted on every repaint, transparent or not, and the hundreds
   of them were what made the entrance stutter on phones */
[data-res] .u.on { color: inherit; --dk: transparent; background: none; }
/* on touch screens a letter arrives at once rather than fading in over
   .16s (2026-10-21, Ben: the entrance was janky on mobile). Safari repaints
   every fading letter on every frame; measured in the iOS simulator, this
   and the line above cut the dropped frames from about 30 to 4. Words
   still rise, fade and sharpen, and the letters still arrive one by one. */
@media (pointer: coarse) {
  [data-res] .u { transition: color 1ms; }
}
/* an indicator's "dot" is itself, faint, until its place in the line resolves */
[data-res] .anim .mki { color: var(--faint); }
#page .mki.lit { color: inherit; }

/* a project opened in place (?po=place) takes no part in the entrance —
   its link's arrow is simply there */
#page .more .mki { transform: none; opacity: 1; filter: none; color: inherit; }

/* Close — a project page (or the résumé) lifts away and the landing page
   is under it (Ben, 2026-10-21; it had cut straight across). The outgoing
   page drops 12px as it fades; the landing page fades in, overlapping it.
   Every other move between pages is skipped in each page's head script,
   so this is Close's alone. Browsers without cross-page transitions
   (Firefox, for now) keep the plain cut. */
@media (prefers-reduced-motion: no-preference) {
  @view-transition { navigation: auto; }
  ::view-transition-old(root) { animation: vt-lift .24s cubic-bezier(.4,0,1,1) both; }
  ::view-transition-new(root) { animation: vt-under .36s cubic-bezier(.2,.7,.2,1) .06s both; }
}
@keyframes vt-lift { to { opacity: 0; transform: translateY(12px); } }
@keyframes vt-under { from { opacity: 0; } }
