/* Styles for assets/js/fx.js and assets/js/bg.js.
   Monochrome only: the site is black and white and these are no exception. */

/* ── the ASCII field behind everything ─────────────────────────── */
.bg-field{position:fixed;inset:0;width:100%;height:100%;z-index:0;
  pointer-events:none;opacity:.34;display:block}
.inv .bg-field{opacity:.14}

/* ── per-character rise, the bleibtgleich way ───────────────────
   Copied off the reference rather than from memory. Its markup is
   .split-line > .split-word > .split-char, and the three differences
   that decide how it reads are:

     the LINE clips and the WORD clips, the character does not. Two
     boxes, not one: the line gives the edge the letters come out from,
     the word keeps them from sliding under their neighbours;

     overflow is CLIP, not hidden. hidden makes an inline-block a scroll
     container and moves its baseline to the bottom margin edge, which
     shifts the whole line. clip does neither;

     opacity never moves. It is 1 the entire time. A letter is either
     behind the edge or out from behind it, and fading it in on the way
     is what makes the difference between this and something soft. */
.rv-l{display:block;overflow:hidden;overflow:clip;overflow-clip-margin:.18em}
.rv-w{display:inline-block;white-space:nowrap;overflow:clip;overflow-clip-margin:.18em}
.rv-c{display:inline-block;
  transition:transform 1.05s cubic-bezier(.16,1,.3,1)}
/* exactly one line down, so it sits precisely behind the edge */
html.fx-reveal [data-reveal]:not(.is-in) .rv-c{transform:translateY(100%)}
[data-reveal].is-in .rv-c{transform:translateY(0)}

/* blocks that hold markup are moved as a whole instead of split */
html [data-reveal][data-plain]{transition:opacity .7s ease-out,
  transform .8s cubic-bezier(.16,1,.3,1)}
html.fx-reveal [data-reveal][data-plain]:not(.is-in){opacity:0;transform:translateY(18px)}
[data-reveal][data-plain].is-in{opacity:1;transform:none}

@media(prefers-reduced-motion:reduce){
  .rv-c{transform:none;transition:none}
  [data-reveal][data-plain]{opacity:1;transform:none;transition:none}
}

/* ── whole blocks arrive: rows, figures, numbers, cards ─────────
   Rising alone is barely visible on a dark page. Opening the clip at
   the same time is what makes it read, and it is what the reference
   does to its images and marks. */
/* the html prefix is not decoration: components like .row carry their own
   transition, and a bare attribute selector loses to them on specificity,
   which left the reveal snapping instead of moving */
html [data-reveal-block]{transition:opacity .85s ease-out,
  transform .95s cubic-bezier(.16,1,.3,1),
  clip-path .95s cubic-bezier(.16,1,.3,1),
  background .25s, color .25s}
html.fx-reveal [data-reveal-block]:not(.is-in){
  opacity:0;transform:translateY(30px);clip-path:inset(0 0 100% 0)}
html [data-reveal-block].is-in{opacity:1;transform:none;clip-path:inset(0 0 0 0)}
@media(prefers-reduced-motion:reduce){
  [data-reveal-block]{opacity:1;transform:none;clip-path:none;transition:none}
}

/* ── the decoding heading ───────────────────────────────────────
   While the canvas draws it out of the grain the real text is
   hidden, but it is still in the DOM and still read aloud. */
[data-decode]{position:relative}
[data-decode].is-decoding{color:transparent}
.fx-decode{position:absolute;inset:0;pointer-events:none}

/* ── grain ─────────────────────────────────────────────────────── */
.fx-grain-layer{position:fixed;inset:0;z-index:150;pointer-events:none;
  opacity:.5;mix-blend-mode:overlay;background-repeat:repeat}

/* ── the marquee band between sections ─────────────────────────── */
.band{overflow:hidden;border-block:1px solid var(--hair);
  padding-block:clamp(10px,1.4vw,18px);display:flex}
.band-track{display:flex;gap:clamp(20px,3vw,44px);white-space:nowrap;
  will-change:transform;animation:band 38s linear infinite}
@keyframes band{to{transform:translateX(-50%)}}
@media(prefers-reduced-motion:reduce){.band-track{animation:none}}
.band b{font-family:var(--disp);font-weight:700;text-transform:uppercase;
  letter-spacing:-.045em;font-size:clamp(1rem,2.2vw,1.7rem);color:var(--ink)}
.band b:nth-child(even){color:transparent;-webkit-text-stroke:1px var(--ink)}
.band .dot{align-self:center;width:6px;height:6px;border-radius:50%;
  background:var(--ink);flex:none}

/* ── live embeds obey the monochrome ───────────────────────────────
   The app, the game, the catalogue and the reel are real running things
   with their own colour. Desaturated to sit inside the page, full colour
   under the cursor.

   Driven by a class, not by :hover. Once the pointer is inside an
   embedded frame the parent document stops receiving pointer events, so
   the wrapper's :hover state was anybody's guess: colour that came on
   halfway, or came on and stuck. live.js sets the class instead, and
   listens inside the frames as well, which is possible because every one
   of them is served from this same origin.

   Two durations on purpose: colour arrives quickly, because it is an
   answer to the cursor, and leaves over three seconds, because a hard
   cut back to grey the instant the pointer moves reads as a glitch.

   Photographs are NOT dithered: a screenshot has to be readable, and
   dithering one only makes it a puzzle. */
[data-tint]{filter:grayscale(1) contrast(1.05);
  transition:filter 3s cubic-bezier(.4,0,.6,1)}
[data-tint].is-tint{filter:none;transition:filter .3s ease-out}
@media(pointer:coarse){
  [data-tint]{filter:none;transition:none}
}
