/* =====================================================================
   Platform pages — numbered sections that load in as they arrive.

   Two effects, both scroll-triggered, both fire-once:

     DECODE  the mono index and the heading churn through code glyphs
             and land on their real values, left to right.
     SCAN    a single rust rule travels down a paragraph and the text is
             inked in behind it.

   NOTHING HERE HIDES ANYTHING BY ITSELF. Every rule that starts an
   element invisible is scoped to .js-pf, which include/platform.js adds
   only once it has decided it is really going to run. With JavaScript
   off, or with reduced motion on, the class never lands and the page is
   simply present. A decorative reveal must never be able to blank a
   page: that failure has already happened once on this site.
   ===================================================================== */

.pf-list{
  --pf-ease: cubic-bezier(.2,.8,.25,1);
  margin: 0;
}

/* ---- section shell ---------------------------------------------------
   Reading column indented from the left with a mono gutter beside it.
   Hairline between sections, never a filled band. */
.pf{
  display: grid;
  grid-template-columns: 3.5rem minmax(0, 1fr);
  gap: 0 24px;
  padding: 52px 0 56px;
  border-top: 1px solid var(--line);
  margin-left: 9rem;
}
.pf:first-child{ border-top: 0; padding-top: 8px; }

.pf__n{
  font-family: var(--f-mono);
  font-size: 11px;
  letter-spacing: .14em;
  font-variant-numeric: tabular-nums;
  color: var(--text-3);
  padding-top: 9px;
}
.pf__title{
  margin: 0 0 22px;
  font-family: var(--f-sans);
  font-size: clamp(21px, 2.2vw, 27px);
  font-weight: 400;
  letter-spacing: -.022em;
  line-height: 1.25;
  color: var(--text);
  /* No tight measure here. Every character is its own inline-block with
     a pinned width, and squeezing that into a 24ch box left headings
     clipped mid-word instead of wrapping. The reading column below is
     what sets the rhythm; the heading just needs room to finish. */
  max-width: 46ch;
}
.pf__body > p{
  margin: 0 0 18px;
  font-size: 15px;
  line-height: 1.78;
  color: var(--text-2);
  max-width: 66ch;
}
.pf__body a{
  color: var(--accent);
  border-bottom: 1px solid var(--accent-line);
  transition: border-color var(--e-fast);
}
.pf__body a:hover{ border-color: var(--accent); }

@media (max-width: 1100px){
  .pf{ margin-left: 0; }
}
@media (max-width: 640px){
  .pf{ grid-template-columns: 1fr; gap: 10px; padding: 36px 0 38px; }
  .pf__n{ padding-top: 0; }
}

/* ---- EFFECT 1: decode ------------------------------------------------

   Rust here is a STATE, not decoration: it marks a character that has
   not yet resolved, and it drains out of the heading as the heading
   completes. That is the whole reason the accent is allowed in this
   block at all, so it is used nowhere else on these sections.

   Widths are pinned per character by platform.js before any glyph is
   swapped in. A wide W standing in for a thin i would otherwise shove
   the line around, and a heading that reflows while it churns reads as
   broken rather than as decoding. */
.pf-ch{
  display: inline-block;
  white-space: pre;
  text-align: center;
  color: var(--text);
}
.pf-ch[data-churn="true"]{
  color: var(--accent);
}
/* The settle runs on the character itself once it lands, so the drain
   to ink is per character rather than a single fade of the whole line. */
.pf-ch[data-settling="true"]{
  transition: color 620ms var(--pf-ease, ease);
  color: var(--text);
}

/* ---- EFFECT 2: scan --------------------------------------------------

   The rule is not decoration passing over finished text. It is what is
   doing the writing, so the mask and the rule share one duration and
   one delay, both supplied per paragraph as --pf-dur / --pf-delay.
   If those two ever drift apart the line stops writing the text and
   becomes a stripe sliding over it. */
.pf-scan{
  position: relative;
}
.pf-scan__rule{
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 1px;
  background: var(--accent);
  opacity: 0;
  pointer-events: none;
}

.js-pf .pf-scan > p{
  -webkit-mask-image: linear-gradient(to bottom, #000 0 50%, transparent 50%);
  mask-image: linear-gradient(to bottom, #000 0 50%, transparent 50%);
  -webkit-mask-size: 100% 200%;
  mask-size: 100% 200%;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: 0% 100%;
  mask-position: 0% 100%;
}
.js-pf .pf.is-in .pf-scan > p{
  animation: pf-write var(--pf-dur, 600ms) linear var(--pf-delay, 0ms) forwards;
}
/* Linear, not stepped: it has to move continuously with the rule. */
@keyframes pf-write{
  from{ -webkit-mask-position: 0% 100%; mask-position: 0% 100%; }
  to  { -webkit-mask-position: 0% 0%;   mask-position: 0% 0%; }
}

.js-pf .pf.is-in .pf-scan__rule{
  animation: pf-rule var(--pf-dur, 600ms) linear var(--pf-delay, 0ms) forwards;
}
@keyframes pf-rule{
  0%  { opacity: 0; transform: translate3d(0, 0, 0); }
  6%  { opacity: 1; }
  99% { opacity: 1; }
  100%{ opacity: 0; transform: translate3d(0, var(--pf-h, 0px), 0); }
}

/* Belt and braces. platform.js declines to add .js-pf under reduced
   motion, so these should never be needed — but if the class ever lands
   another way, everything is present and static. */
@media (prefers-reduced-motion: reduce){
  .js-pf .pf-scan > p,
  .js-pf .pf.is-in .pf-scan > p{
    -webkit-mask-image: none;
    mask-image: none;
    animation: none;
  }
  .pf-scan__rule{ display: none; }
  .pf-ch{ color: var(--text); }
}
