/* ===================================================================
   THE HOME SHEET — "THE PRESS"

   Ported from `prototypes/a-the-press.html` in the workspace repo, which is
   where this design was argued out. That repo's `decisions.md` is the record
   of WHY, and several rules below only make sense against it — the ruling, the
   ground's brightness, and everything that used to be a day/night cycle.

   SCROLL ARCHITECTURE
   -------------------
   Every scroll-linked movement on this page is a CSS scroll-driven
   animation. There is no JavaScript anywhere in the scroll path.

   The first build drove all of it from requestAnimationFrame reading
   window.scrollY. That is unusable on iOS, where the scroll event does not
   fire and scrollPosition is not updated until the gesture has finished —
   measured on device, the page updated 1-2 times per second while scrolling
   and froze in between. Desktop suffered a milder version of the same thing
   for a different reason: wheel notches arrive as large discrete deltas, so
   every notch became a multi-layer lurch.

   Scroll-driven animations run on the compositor and stay smooth even when
   the main thread is blocked, which removes both failure modes at once.

   JavaScript is still used, but only for work that has nothing to do with
   scrolling: seeding the proof, generating placeholder florals, and the
   pointer-driven plate misregistration.

   OTHER LOAD-BEARING IDEAS
   ------------------------
   1. The portrait never moves. Floral planes drift past a person standing
      still at dusk, so parallax means the world moving around him.
   2. The pointer misregisters the press. Moving it pulls the cyan and
      magenta plates out of alignment. It is not drawn — the system cursor
      stays, and the sheet is what responds.
   3. Resolution is measured in dots. --dot is the atomic layout unit.
   4. Every load is a numbered proof, from one seeded PRNG.
   5. The portrait is a photographic scrap with a hand-cut paper margin, and
      it keeps continuous tone. Halftone belongs to the ground, not his face.
   =================================================================== */

/* --- fonts ---------------------------------------------------------
   Self-hosted, no runtime CDN, per the approved technical direction. Both
   faces are SIL Open Font License 1.1 and the licence text sits beside the
   files in assets/fonts/.

   These replace the Haettenschweiler/Impact stand-in the layout was originally
   sized against. League Gothic is a genuinely different face — narrower, with
   its own cap height — so anything tuned by eye against the stand-in has to be
   looked at again rather than assumed to still hold.

   The display face blocks rather than swaps: it carries the wordmark and the
   confession at half-screen size, and a flash of Impact at that scale is more
   disruptive than a beat of nothing. The mono is small and incidental, so it
   swaps. */

@font-face {
  font-family: 'League Gothic';
  src: url(/assets/fonts/league-gothic-latin-400-normal.woff2) format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: block;
}
@font-face {
  font-family: 'IBM Plex Mono';
  src: url(/assets/fonts/ibm-plex-mono-latin-400-normal.woff2) format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

:root {
  --mustard:  #F3A31E;
  --hotpink:  #ED3D78;
  --coral:    #FF7138;
  --softpink: #F5C4CA;
  --twilight: #174C49;
  --cyan:     #38D9D0;
  --pinkreg:  #FF4F85;
  --midnight: #091D2B;
  --ink-warm: #17140F;
  --paper:    #FFF0CD;

  /* Sampled from the outfit so the ground shares his register rather than
     fighting it: denim jacket and lilac shirt. */
  --denim:    #2E4A6B;
  --lilac:    #B9A3D9;

  /* The screen ruling. Everything else is a multiple of it. */
  --dot: 6px;
}

* { box-sizing: border-box; margin: 0; padding: 0; }

body {
  background: var(--ink-warm);
  color: var(--paper);
  font-family: 'IBM Plex Mono', ui-monospace, 'SFMono-Regular', monospace;
  overflow-x: hidden;
  /* Five chapters plus run-out. This height is the entire scroll timeline. */
  height: 560vh;
}

/* Every scroll-driven rule below hangs off this one timeline.

   Deliberately NOT wrapped in `prefers-reduced-motion: no-preference`. It was,
   and that was wrong: with no timeline the whole page collapsed to one frozen
   frame and a reduced-motion visitor never saw the confession, the song's name
   or COMING SOON. The page stopped saying what it is for.

   WCAG 2.3.3 scopes *motion* animation and explicitly excludes colour and
   opacity change. Nearly everything on this timeline is opacity — the wordmark,
   the lines arriving, GOODNIGHT. That is the content, not decoration, and
   deleting it leaves a page that looks broken rather than calm.

   So the timeline always runs, and the reduce block at the bottom of this file
   removes the parts that actually TRAVEL: the parallax planes, the portrait
   push, the line lift, the cursor. */
.scroll-driven {
  animation-timing-function: linear;
  animation-fill-mode: both;
  animation-duration: auto;
  animation-timeline: scroll(root block);
}

/* --- the sheet -------------------------------------------------- */

.sheet {
  position: fixed;
  inset: 0;
  overflow: hidden;
  /* Matched to the DARKENED stock, not the raw one. This shows only while the
     texture is still loading or if it fails, and a bright teal flashing to a
     deep one is worse than never seeing the flash. */
  background-color: #123A37;
  isolation: isolate;
}

/* The five cross-fading ground colours are GONE. They were five full-viewport
   layers running five scroll-driven animations behind an opaque paper stock —
   none of it had been visible since the sheet became one constant paper, and
   the comment kept them as "the load-failure fallback". The sheet's own
   `background-color` above already is that fallback, for free.

   The technique was right and is still in use one layer up: the dusk is an
   opacity cross-fade, not an animated `background-color`, because opacity is
   compositor-friendly and background-color repaints the full viewport. */

/* Paper fibre, baked to a tileable PNG. This was a live feTurbulence, which
   forced a full-viewport filter re-render whenever the page repainted. */
.fibre {
  position: absolute;
  inset: 0;
  background-image: url(/assets/tex-fibre.webp);
  background-size: 512px 512px;
  opacity: .18;
  mix-blend-mode: overlay;
  pointer-events: none;
}

/* --- paper stock ------------------------------------------------- */
/* `.stock` is the paper: one photographed sheet of teal cardstock, tileable,
   constant for the whole scroll.

   Still a baked placeholder from the workspace's `tools/bake-textures.sh`:
   three scales of noise, because real paper is uneven at three scales and noise
   at one reads as television static. It has tooth and mottling but no foxing,
   no corner wear, none of the things that make the reference sleeve look like
   an object that has been somewhere. Replace it with generated stock; the
   prompts are in the workspace's `docs/background-prompts.txt`.

   Do NOT reach for the old damage layer to add that character. It was tried and
   removed: the source was a folded poster and its fold landed on the viewport's
   centreline as a hard band. See decisions.md, 2026-07-27.                 */

.stock {
  position: absolute;
  inset: 0;
  background-size: 720px 720px;
  pointer-events: none;
}
/* ONE paper, constant for the whole scroll. It was three cross-fading with the
   chapter descent, which is not what a sleeve does: an album cover is a single
   printed object and the reader is moving down it, not swapping it for another.

   Consequence worth being explicit about: the teal-to-midnight ground descent is
   now invisible behind an opaque stock. The `.ground` layers stay as the
   load-failure fallback, but the chapter colour change no longer reads. If the
   descent is still wanted it has to come from something the paper does not
   cover — the ink, the type, or the light on it — not from the stock. */
/* Darkened at the LAYER, not in the asset. The stock is an opaque photograph
   carrying its own colour, so `.sheet`'s background-color underneath it does
   nothing and re-baking the texture would burn one brightness into a file that
   several pages share. A filter on the layer is one knob, rasterizes once, and
   never animates.

   .44 was picked by looking at .74 / .58 / .44 side by side. The sheet at full
   brightness was reading as daylight, which flattened everything printed on it:
   the pressed plates, the rim light and the cream type all need a ground that
   sits well below them. The saturate keeps it a deep TEAL — brightness alone
   walks the paper toward grey-green. */
.stock--sheet {
  background-image: url(/assets/tex-stock-sheet.webp);
  filter: brightness(.44) saturate(1.28);
}

/* --- the ruling ----------------------------------------------------
   Fine cyan ruling across the whole sheet. It was cut once, when the paper
   stock arrived, because a grid running through every chapter read as a UI
   overlay rather than as print. That was true against a BRIGHT sheet. Against
   the dark one it reads as printed ruling, and it is back for good.

   Two things keep it on the print side of that line:

   .24 opacity. Rendered at .13 / .24 / .36 / .50 side by side — .13 vanishes
   (it was the original number, tuned against the bright sheet, and measured
   about 1/255 of a level here), .36 starts reading as graph paper and .50 is
   unmistakably UI. Do not raise it.

   And it DRIFTS, with the collage. A ruling pinned to the viewport is furniture
   sitting on the screen; one that moves with the sheet is printed on it. That,
   more than the opacity, is what the original got wrong.

   Screen, not multiply: a printed line has to sit ON TOP of a dark ground. */
.ruling {
  position: absolute;
  inset: -20%;
  pointer-events: none;
  background-image:
    repeating-linear-gradient(0deg,  var(--cyan) 0 1px, transparent 1px calc(var(--dot) * 3)),
    repeating-linear-gradient(90deg, var(--cyan) 0 1px, transparent 1px calc(var(--dot) * 3));
  opacity: .24;
  mix-blend-mode: screen;
  animation-name: drift-collage;
}

/* --- damage: REMOVED -------------------------------------------------
   There was a `.damage` layer here — a neutral-grey displacement map soft-lit
   over the sheet, billed as "creases, foxing and a fold".

   It was one fold. `assets/sources/damage.png` is a poster folded in half both
   ways with a diagonal crease, and the horizontal fold dominates everything
   else in the frame. With `background-position: center` that fold landed on the
   viewport's centreline — measured at y=350 of 700, and it disappeared from the
   measurement the moment the layer was hidden. Darkening the ground is what made
   it obvious: soft-light lifts more visibly against a dark sheet, so the fold
   went from a texture to a hard band across the middle of the page.

   Deleted rather than repositioned. Moving the fold elsewhere still leaves a
   straight line across a scrolling sheet, and a straight horizontal line on a
   web page reads as a UI seam no matter what it is.

   If aging is wanted back — and the art direction does want it — it has to come
   from a source that is foxed, stained and corner-worn rather than FOLDED, and
   it should be baked with `tools/bake-stock.sh --damage`, which still works.
   The tooling is fine; the photograph was wrong.                          */

/* --- pasted collage furniture ------------------------------------ */
/* Built by Press.collage(). See the reference sleeve in Downloads and the
   collage entry in decisions.md.                                            */

.collage { position: absolute; inset: 0; pointer-events: none;
           animation-name: drift-collage; }
.pasted { position: absolute; }

.pasted--grid {
  background-image:
    repeating-linear-gradient(0deg,  var(--gridink) 0 1px, transparent 1px var(--rule)),
    repeating-linear-gradient(90deg, var(--gridink) 0 1px, transparent 1px var(--rule));
  /* Multiply, because the stock is now light. The project's rule is that the
     blend follows the GROUND and not the layer: ink multiplies into light stock
     and is opaque on dark. This was screen while the sheet was midnight, and
     inverting the ground without inverting this made the ruling vanish.
     The block's own edge stays hard — a pasted scrap has a cut edge. */
  mix-blend-mode: multiply;
}

/* The one element on the sheet with no texture at all. That is the point: the
   flat bars are what make everything around them read as aged. */
.pasted--bar { mix-blend-mode: normal; }


/* --- floral planes ---------------------------------------------- */
/* Real pressed-flower plates now, not placeholders. Vision-matted photographs
   built by tools/flower-plates.sh; see decisions.md.

   The filters are doing real work rather than decorating. The portrait is
   continuous-tone photography, and the florals were meant to be a DIFFERENT
   print generation — flat ink on the sheet. Now that they are photographs too,
   that distinction has to be reasserted here: each plane pushes saturation and
   contrast past what a photograph does, so the florals read as printed while he
   reads as photographed. The far plane also darkens, because ink laid down
   further from the eye on a descending sheet loses light.                     */

.plane {
  position: absolute;
  inset: -25%;
  pointer-events: none;
}
/* The plates are <img> now. Keeping the old `.plane svg` selector would have
   left every plate in normal flow, stacked down the page. */
.plane img { position: absolute; display: block; }
.plane--far  {
  filter: blur(1.6px) saturate(1.12) contrast(1.08) brightness(.86);
  opacity: .8; animation-name: drift-far;
}
.plane--mid  {
  filter: saturate(1.28) contrast(1.12) brightness(.96);
  animation-name: drift-mid;
}
.plane--fore {
  filter: saturate(1.4) contrast(1.18);
  animation-name: drift-fore;
}

/* The scroll animation uses `translate`; the pointer parallax written by JS
   uses `transform`. They are separate properties and compose, so neither has
   to know about the other. */
@keyframes drift-far  { to { translate: 0 -240px; } }
@keyframes drift-collage { to { translate: 0 -390px; } }
@keyframes drift-mid  { to { translate: 0 -620px; } }
@keyframes drift-fore { to { translate: 0 -1180px; } }

/* --- portrait ---------------------------------------------------- */
/* Anchored to bleed off the bottom edge. Centring him vertically left the
   photograph's own straight bottom edge visible, which reads as a pasted
   rectangle; running him off the sheet removes the frame entirely.
   Deliberately off-centre: the reject list rules out centred symmetry. */

.portrait {
  position: absolute;
  bottom: -7%;
  left: 62%;
  /* Wider than a tight cutout would need: the paper margin adds canvas around
     him, so the subject occupies a smaller fraction of the file. */
  width: min(74vw, 900px);
  transform: translateX(-50%);
  transform-origin: 50% 100%;
  animation-name: portrait-push;
}
/* He does not travel with the planes. He only grows very slightly, as if the
   sheet is being brought closer. */
@keyframes portrait-push { to { scale: 1.06; } }


.portrait img {
  display: block;
  width: 100%;
  height: auto;
  user-select: none;
  -webkit-user-drag: none;
}
.portrait .layer { position: absolute; inset: 0; }
.portrait .layer img { position: absolute; inset: 0; }

/* --- the two takes ------------------------------------------------ */
/* v4 sits in flow and therefore sets the portrait's height. v3 is laid over
   it, bottom-aligned so both photographs meet the same edge of the sheet.

   The two frames are not interchangeable: v3 is a wider crop, so at equal
   width his head comes out smaller and further left. The numbers below match
   the two takes on the afro — the head is what the eye tracks, not the
   bounding box — and then align the eye line. They were measured off the
   rendered plates, so treat them as a calibration knob: nudge them, do not
   recompute them from image dimensions. */

.take--v4 { position: relative; }

.take--v3 {
  position: absolute;
  inset: 0;
  transform-origin: 50% 100%;
  translate: 13.1% 3.8%;
  scale: 1.148;
}
.take--v3 img { position: absolute; bottom: 0; left: 0; }

/* v3 is scaled up past its own resolution, on top of a source that was already
   a screen export. It reads at this size under the print treatment; it will not
   survive a camera original arriving and this being left as-is. */

.take--v4 { animation-name: take-hero; }
.take--v3 { animation-name: take-alt; }

/* One-way. The hand-over starts as the confession's first line arrives (36%)
   and does not reverse: he looks up until he says it, then down for the rest
   of the night. Going back to v4 at the end would undo that. */
@keyframes take-hero { 0%,38% { opacity: 1 } 50%,100% { opacity: 0 } }
@keyframes take-alt  { 0%,38% { opacity: 0 } 50%,100% { opacity: 1 } }

/* Review pins: hold one take for the whole scroll. */
body[data-pin] .take { animation-name: none; }
body[data-pin="v4"] .take--v4, body[data-pin="v3"] .take--v3 { opacity: 1; }
body[data-pin="v4"] .take--v3, body[data-pin="v3"] .take--v4 { opacity: 0; }

/* The scrap itself. Graded, not screened — the face keeps continuous tone.
   Dark skin carries its detail in a narrow luminance band, so a coarse dot
   cell collapses it to a flat mass. */
.scrap { position: relative; filter: url(#skin-grade); }

/* Misregistration applies to the whole scrap rather than to separations of his
   face, so it shows as colour fringing along the cut edge — what a
   misregistered pasted-up element actually looks like. Weak on purpose: at
   high opacity it doubles the face and destroys the features. */
.scrap-cyan,
.scrap-mag {
  mix-blend-mode: multiply;
  opacity: .17;
  will-change: transform;
}
.scrap-cyan { filter: url(#plate-cyan); }
.scrap-mag  { filter: url(#plate-mag); }

/* The rim light needs no plate of its own — the paper margin carries the edge.
   A warm glow sits behind the scrap instead, so he is lit by the poster. His
   golden-hour rim is the only warm element on a cool sheet, which is what makes
   it read as a light source rather than as background noise. */
.scrap-glow {
  position: absolute;
  inset: -14%;
  background: radial-gradient(ellipse at 50% 42%,
    var(--coral) 0%, rgba(255,113,56,.35) 38%, transparent 68%);
  mix-blend-mode: screen;
  opacity: .28;
  filter: blur(26px);
  pointer-events: none;
  animation-name: glow-rise;
}
@keyframes glow-rise { to { opacity: .78; } }

/* --- type -------------------------------------------------------- */
/* Type sits on a dark ground, so it prints light. */

/* Top of the sheet's stack, above every plane. Only a z-index is set here: the
   `position: absolute` is what places the type over the sheet, and appending a
   `position: relative` under it silently unpinned the whole block. */
.type { position: absolute; inset: 0; pointer-events: none; z-index: 30; }

/* --- wordmark ------------------------------------------------------
   The first thing on the sheet. A visitor arriving from a social profile has
   to know whose page this is immediately, so it is present at scroll zero
   rather than revealed in chapter two.

   Set as two lines, not three. The break is the whole design decision: line
   one carries the phrase and line two lands BLOOM as the payoff, in the
   contrasting ink.

   It is a printed element like everything else, so it misregisters with the
   press — two offset ink copies sit under the crisp impression and pull apart
   with the pointer. */

/* At rest it is the title screen: two lines occupying roughly half the sheet,
   vertically centred, so arriving at the page is arriving at a cover.

   One size knob. `min(22vh, 15vw)` keeps it half the height of a wide screen
   and stops it running off the side of a narrow one — a tall phone would
   otherwise size it from its height and print BLOOM past both edges. The lines
   are set in em from here, so the whole lockup scales from this one value. */
.wordmark {
  position: absolute;
  left: calc(var(--dot) * 5);
  top: 50%;
  translate: 0 -50%;
  font-family: 'League Gothic', 'Haettenschweiler', 'Impact', system-ui, sans-serif;
  text-transform: uppercase;
  line-height: .78;
  font-size: min(17.1vh, 11.7vw);
  /* ponytail: one knob for every screen. A phone wants its own curve — it is
     taller, narrower, and held closer, so the size that reads as a title
     screen on a monitor is not the size that reads as one in a hand. Deferred
     to the production build, where a real type scale makes it a token rather
     than a second magic number here. */
  opacity: 0;
  animation-name: reveal-wordmark;
}

/* Stacked ink impressions. The crisp one is last so it reads on top. */
.wordmark .ink {
  display: block;
  grid-area: 1 / 1;
}
.wordmark {
  display: grid;
}
.wordmark .ink--cyan { color: var(--cyan);    opacity: .55; mix-blend-mode: screen; }
.wordmark .ink--mag  { color: var(--pinkreg); opacity: .5;  mix-blend-mode: screen; }
.wordmark .ink--key  { color: var(--paper); }

/* GUNWA sits a touch under the knob rather than on it, which widens the step
   down to BLOOM. The name is the approach and the flower is the payoff; at
   parity they competed for the same beat. */
.wordmark .l1 {
  display: block;
  font-size: .9em;
  letter-spacing: -.005em;
}
/* BLOOM is the payoff: larger, and pulled tight under the line above. */
.wordmark .l2 {
  display: block;
  font-size: 1.71em;
  letter-spacing: -.02em;
  margin-top: -.06em;
}
/* Only the key impression carries the colour break; the offset ink copies stay
   monochrome or the overlap turns muddy. */
/* The colour break stays on BLOOM alone. The phrase reads whole because the
   sizes match, but the flower is the word that changes ink. */
.wordmark .ink--key .l2 { color: var(--cyan); }
.wordmark .ink--key .l2 .hinge { color: var(--paper); }

/* "in" is the hinge between the two words. Smaller and set apart so the eye
   reads GUNWA and BLOOM as the two beats. */
/* "in Bloom" is a state, not a pun — he is in bloom. The preposition is
   load-bearing, so it is only slightly reduced and stays at full strength.
   Shrinking it hard read as two separate words with a connector between them,
   which loses the phrase. */
.wordmark .hinge {
  font-size: .74em;
  letter-spacing: .01em;
  padding-inline: 0 .1em;
  opacity: 1;
}

/* The lyric, set on a printed rule rather than floating in space. */
/* The confession sits in the middle of the screen, at display size.

   It was set small along the bottom edge, which is where a caption goes — and
   a caption is the one thing this is not. It is the reason the page exists, so
   it is the largest thing on the sheet while it is being said, and it holds the
   centre of the screen where somebody scrolling is already looking.

   Set in the display face rather than the mono, so that when GOODNIGHT arrives
   it is visibly the next line of the same sentence in the same hand, not a
   title card that interrupted one. */

.confession {
  position: absolute;
  left: calc(var(--dot) * 6);
  top: 50%;
  translate: 0 -50%;
  max-width: 92vw;
  font-family: 'League Gothic', 'Haettenschweiler', 'Impact', system-ui, sans-serif;
  text-transform: uppercase;
  color: var(--paper);
}

/* Each line arrives on its own, lifting slightly as it comes up, and none of
   them leave until the sentence has been said. A line that replaced the one
   before it would make the reader race the scroll; accumulating lets them
   read at whatever speed they are moving. */
.confession .line {
  opacity: 0;
  font-size: clamp(30px, 5.4vw, 66px);
  line-height: .96;
  letter-spacing: -.01em;
  margin-bottom: .12em;
  will-change: opacity, translate;
}
/* Staggered indents. Three flush lines would be a block of text on a sheet
   whose whole composition is off-axis. */
.confession .line--2 { margin-left: calc(var(--dot) * 5); }
.confession .line--3 { margin-left: calc(var(--dot) * 11); }

.confession .line--1 { animation-name: line-1; }
.confession .line--2 { animation-name: line-2; }
.confession .line--3 { animation-name: line-3; }

/* The fourth line. In flow under the confession, indented past the line above
   it like the sentence was always going to end here, and larger — it is the
   word the line hands off to, and the record's name. */
.release {
  margin-left: calc(var(--dot) * 11);
  margin-top: .04em;
  opacity: 0;
  animation-name: reveal-release;
}
.release h1 {
  font-size: clamp(52px, 10vw, 150px);
  line-height: .86;
  text-transform: uppercase;
  color: var(--paper);
  letter-spacing: -.02em;
  font-weight: 400;
}
/* Back to the mono: this is the status of the record, not part of the
   sentence, and the display face at this size with this much letterspacing
   reads as a shrunken headline. */
.release p {
  margin-top: calc(var(--dot) * 2);
  font-family: 'IBM Plex Mono', ui-monospace, 'SFMono-Regular', monospace;
  font-size: calc(var(--dot) * 2);
  letter-spacing: .42em;
  text-transform: uppercase;
  color: var(--cyan);
}

/* Chapter 2 Gunwa in Bloom / 3 The Confession / 4 Goodnight. */
/* Present on arrival, then cedes the sheet once the journey starts. */
/* Title screen, then masthead, then gone.

   0-8    held at half the sheet, centred. The cover.
   8-20   travels up until it sits under the top edge.
   20-30  sticks there. The pause is the point: it stops being a title and
          becomes the masthead of the sheet you are now reading, and it has to
          hold long enough to be read as one rather than as a thing passing
          through on its way off screen.
   30-36  clears, just as the confession's first line arrives at 36.

   The parked position is `-44vh` rather than an offset from the centred `-50%`,
   so the top edge lands 6vh below the top of the screen whatever the lockup
   ends up measuring. Offsetting from centre only holds while the block is
   exactly half the sheet, and it is not on a short wide window, where the
   width clamp takes over and the type gets taller. */
@keyframes reveal-wordmark {
  0%,8%    { opacity: 1; translate: 0 -50%; }
  20%,30%  { opacity: 1; translate: 0 -44vh; }
  36%,100% { opacity: 0; translate: 0 -44vh; }
}
/* The confession, line by line. The first one starts the instant the wordmark
   has finished clearing at 36%, so the sheet is never empty of type.
   Line three holds past the GOODNIGHT reveal at 74% and only leaves once the
   word it hands off to is fully on the sheet. */
@keyframes line-1 {
  0%,36%  { opacity: 0; translate: 0 14px }
  43%,62% { opacity: 1; translate: 0 0 }
  68%,100%{ opacity: 0; translate: 0 -8px }
}
@keyframes line-2 {
  0%,45%  { opacity: 0; translate: 0 14px }
  52%,64% { opacity: 1; translate: 0 0 }
  70%,100%{ opacity: 0; translate: 0 -8px }
}
@keyframes line-3 {
  0%,55%  { opacity: 0; translate: 0 14px }
  62%,80% { opacity: 1; translate: 0 0 }
  87%,100%{ opacity: 0; translate: 0 -8px }
}
@keyframes reveal-release  { 0%,64% { opacity: 0 } 71%,100% { opacity: 1 } }

/* --- trim margin ------------------------------------------------- */
/* Printer's furniture. Proof number, ink bars, crop marks. */

.trim {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 40;
  font-size: calc(var(--dot) * 1.7);
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--ink-warm);
  mix-blend-mode: difference;
}
.trim .proof { position: absolute; left: calc(var(--dot) * 2); bottom: calc(var(--dot) * 2); }
.trim .reg   { position: absolute; right: calc(var(--dot) * 2); bottom: calc(var(--dot) * 2); }
.trim .bars  { position: absolute; right: calc(var(--dot) * 2); top: calc(var(--dot) * 2); display: flex; gap: 2px; }
.trim .bars i { width: calc(var(--dot) * 2.4); height: calc(var(--dot) * 1.4); display: block; }

.crop { position: absolute; width: calc(var(--dot) * 4); height: calc(var(--dot) * 4); }
.crop::before, .crop::after { content: ''; position: absolute; background: var(--ink-warm); }
.crop::before { width: 100%; height: 1px; top: 50%; }
.crop::after  { height: 100%; width: 1px; left: 50%; }
.crop.tl { top: var(--dot); left: var(--dot); }
.crop.tr { top: var(--dot); right: var(--dot); }
.crop.bl { bottom: var(--dot); left: var(--dot); }
.crop.br { bottom: var(--dot); right: var(--dot); }

/* --- the cursor: REMOVED ------------------------------------------
   There was a flower here, drawn over a hidden system cursor and blended with
   `difference`. It was the project's own mark doing a registration mark's job —
   petals for the two axes, centre ring for the target — and it read as cheesy.
   Cut on Gunwa's call.

   The pointer still does the thing that mattered: moving it pulls the cyan and
   magenta plates out of register. That lives in `Press.registration()` and never
   needed a drawn cursor; the cursor was a second, louder announcement of an
   effect the sheet was already showing.                                    */

/* --- LINKS sticker ------------------------------------------------ */
/* Recedes during the lyric chapter, per the approved interaction spec,
   but stays usable throughout. */

.sticker {
  position: fixed;
  right: calc(var(--dot) * -1);
  top: 50%;
  z-index: 50;
  transform: translateY(-50%) rotate(90deg);
  padding: calc(var(--dot) * 1.4) calc(var(--dot) * 3);
  background: var(--pinkreg);
  color: var(--paper);
  font-size: calc(var(--dot) * 2);
  letter-spacing: .38em;
  text-transform: uppercase;
  text-decoration: none;
  box-shadow: 2px 2px 0 var(--cyan);
  animation-name: sticker-recede;
}
@keyframes sticker-recede { 0%,40% { opacity: 1 } 50%,58% { opacity: .3 } 68%,100% { opacity: 1 } }

/* --- nightfall: REMOVED ---------------------------------------------
   There was a dusk here: one multiply gradient over the sheet, stepped to rest
   where the type rests, with a star field rising under it. It was built, looked
   at, and cut — Gunwa's call, on the render. The sleeve is one printed object in
   one light; a scroll that dims it was reading as a filter sliding over the page
   rather than as evening. Do not rebuild it without a new reason.

   What the removal frees up, if the night is ever wanted again: it should come
   from the ARTWORK — a nocturnal plate set, a darker paper stock — not from a
   layer laid over the top of a daylight sheet. The full reasoning, including the
   dwell-zone rhythm that made it least-bad, is in decisions.md.            */

/* --- distress ------------------------------------------------------ */

.distress {
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 30;
  mix-blend-mode: overlay;
  /* Kept low. At higher opacity the streaks cover the sheet evenly and read as
     a screen texture rather than as damage to a printed object. */
  opacity: .16;
  background-image: url(/assets/tex-distress.webp);
  background-size: 1024px 1024px;
}


/* --- fallbacks ------------------------------------------------------
   Firefox still has scroll-driven animations behind a flag, and older iOS
   predates Safari 26. Those visitors get a fixed, composed mid-journey state
   rather than an empty sheet or a broken one. The spec already required a
   static fallback. */

@supports not (animation-timeline: scroll()) {
  /* Kill the animations outright rather than trying to out-specify them. An
     engine without scroll timelines drops `animation-timeline` as unknown AND
     rejects `animation-duration: auto`, which computes to 0s — so every
     animation snaps to its 100% keyframe with `fill: both`. A plain rule loses
     to that. This is what hid the wordmark and the whole confession here. */
  .scroll-driven { animation: none !important; }

  body { height: 100vh; overflow: hidden; }

  /* One printed state with everything on it: masthead, the full sentence, the
     record's name. Not a mid-scroll frame — a poster. The dusk sits mid-evening
     so the sheet is neither noon nor midnight. */
  /* The masthead's scroll position won't do here: parked at -44vh it clears a
     confession that is revealing ONE line at a time, and this state prints all
     four at once. So the wordmark goes smaller and hard to the top, and the
     sentence drops below it — a poster lockup rather than a frozen frame. */
  .wordmark  { opacity: 1; top: calc(var(--dot) * 3); translate: none;
               font-size: min(9vh, 7vw); }
  .confession { top: 58%; }
  .confession .line { opacity: 1; translate: none; }
  .release   { opacity: 1; }
  .take--v4  { opacity: 0; }
  .take--v3  { opacity: 1; }
  .scrap-glow { opacity: .5; }
}

@media (prefers-reduced-motion: reduce) {
  /* The page still scrolls and the light still falls — see the note on
     `.scroll-driven`. What goes is travel.

     The planes are the reason: `drift-fore` moves 1180px, and large-translation
     parallax is the worst case for vestibular symptoms. Stopping them leaves a
     still sheet that a changing light still crosses, which is the whole idea of
     this page anyway. */
  .plane--far, .plane--mid, .plane--fore,
  .collage, .portrait { animation-name: none; }

  /* The 14px lift on each line is pure decoration; opacity alone reads it. The
     wordmark's park is not — without it the masthead sits on the confession —
     so that one travel stays, and it is under the reader's own thumb. */
  .confession .line { translate: none !important; }

}

@media (max-width: 760px) {
  :root { --dot: 5px; }
  /* The paper margin means the subject is only part of the file, so the scrap
     has to run past the viewport width for his face to read at all. */
  .portrait { left: 52%; width: 150vw; bottom: -4%; }
  /* One knob, and the base rule's min() already keeps it inside a narrow
     screen. Only the left margin follows the tighter phone gutter. */
  .wordmark { left: calc(var(--dot) * 4); }
  /* On a phone the scrap runs 150vw wide to keep his face legible, which puts
     his lit afro exactly where the last line of the confession lands. Cream
     type on a cream rim light is unreadable, so two things change here and
     only here.

     One: the block sits higher, above the crown of his head rather than
     across it. */
  .confession {
    left: calc(var(--dot) * 4);
    max-width: 88vw;
    top: 32%;
  }
  .confession .line--2 { margin-left: calc(var(--dot) * 3); }
  .confession .line--3 { margin-left: calc(var(--dot) * 7); }
  .release { margin-left: calc(var(--dot) * 7); }

  /* Nothing else is needed for legibility here: at this height the confession
     clears the crown of his head, so the words never cross the lit rim. A slab
     behind the type and a dimmed photograph were both tried first — the slab
     looked pasted on, the dim cost the picture more than the collision was
     worth, and neither was necessary once the block moved up. */
}
