/* ============================================================
   ASKDOCS — BASE
   Reset, element defaults, the four type voices, and the four
   motion primitives. Nothing here is component-specific.
   ============================================================ */

*, *::before, *::after { box-sizing: border-box; }

html {
  -webkit-text-size-adjust: 100%;
}

/* Smooth scrolling is opt-in per element rather than global. Applied to <html>
   it fights the scrubbed hero: a wheel notch inside the film becomes an
   animated scroll the rAF loop then has to chase, which is felt as lag. Anchor
   jumps still want it, and they are the only place it was ever doing work. */
@media (prefers-reduced-motion: no-preference) {
  html:not(.cine-page) { scroll-behavior: smooth; }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3, p, figure { margin: 0; }

a {
  color: var(--ink);
  text-decoration: none;
  transition: color var(--dur-hover) var(--ease);
}
a:hover { color: var(--red); }

/* The only in-paragraph emphasis in the system. */
strong { font-weight: 700; color: var(--ink); }

img, svg { display: block; max-width: 100%; }

::selection { background: var(--red); color: var(--paper); }

/* Focus follows the input spec: 2px red, square, no glow. */
:focus-visible {
  outline: var(--bd-panel) solid var(--red);
  outline-offset: 3px;
}

/* Anchored sections clear the sticky masthead. */
[id] { scroll-margin-top: 108px; }

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}


/* ---- Type voices ------------------------------------------ */

/* Chrome — eyebrow / section label */
.eyebrow {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--fs-eyebrow);
  letter-spacing: var(--ls-eyebrow);
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* Chrome — caption beneath a diagram */
.caption {
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--fs-caption);
  letter-spacing: var(--ls-caption);
  text-transform: uppercase;
  color: var(--ink-soft);
}

/* Display — one per section, everything else steps down 2×. */
.display {
  font-family: var(--font-display);
  font-weight: 800;
}

.display--hero {
  font-size: var(--fs-hero);
  line-height: var(--lh-hero);
  letter-spacing: var(--ls-hero);
  text-transform: uppercase;
}

.display--h2 {
  font-size: var(--fs-h2);
  line-height: var(--lh-h2);
  letter-spacing: var(--ls-h2);
}

.display--h2-lg {
  font-size: var(--fs-h2-lg);
  line-height: var(--lh-h2-lg);
  letter-spacing: var(--ls-h2-lg);
}

.display--h2-xl {
  font-size: var(--fs-h2-xl);
  line-height: var(--lh-h2-lg);
  letter-spacing: var(--ls-h2-lg);
}

/* Body */
.lead {
  font-size: var(--fs-lead);
  line-height: var(--lh-lead);
  color: var(--ink-soft);
}

.item-body {
  font-size: var(--fs-item);
  line-height: var(--lh-item);
  color: var(--ink-soft);
}

.item-label {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--fs-label);
  letter-spacing: var(--ls-label);
}

/* Ordinals: always red, always two digits, always Archivo 800. */
.ordinal {
  display: block;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: var(--fs-ordinal);
  letter-spacing: var(--ls-ordinal);
  color: var(--red);
}


/* ---- Motion primitives -------------------------------------
   Four variants, gated on a .in class the IntersectionObserver
   adds exactly once. Guarded behind .js so the page is fully
   legible if the script never runs. */

.js .r,
.js .r-pop,
.js .r-card { will-change: opacity, transform; }

/* reveal — fade + 34px rise. Text, labels, paragraphs. */
.js .r {
  opacity: 0;
  transform: translateY(var(--rise));
  transition:
    opacity   var(--dur-reveal) var(--ease) var(--d, 0ms),
    transform var(--dur-reveal) var(--ease) var(--d, 0ms);
}
.js .r.in { opacity: 1; transform: none; }

/* reveal-x — scaleX 0→1 from the left. Bars, ticks, rails, ruled lines. */
.js .r-x {
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-x) var(--ease) var(--d, 0ms);
}
.js .r-x.in { transform: scaleX(1); }

/* reveal-y — scaleY 0→1 from the top. Dividers and brackets drawing down. */
.js .r-y {
  transform: scaleY(0);
  transform-origin: center top;
  transition: transform var(--dur-y) var(--ease) var(--d, 0ms);
}
.js .r-y.in { transform: scaleY(1); }

/* reveal-pop — scale .45→1 on the snappier curve. Dots, nodes, frames. */
.js .r-pop {
  opacity: 0;
  transform: scale(.45);
  transition:
    opacity   var(--dur-pop) var(--pop) var(--d, 0ms),
    transform var(--dur-pop) var(--pop) var(--d, 0ms);
}
.js .r-pop.in { opacity: 1; transform: scale(1); }

/* reveal-pop, composed onto an element that carries a resting
   rotation (the scattered document scraps). */
.js .r-card {
  opacity: 0;
  transform: rotate(var(--rot, 0deg)) scale(.45);
  transition:
    opacity   var(--dur-pop) var(--pop) var(--d, 0ms),
    transform var(--dur-pop) var(--pop) var(--d, 0ms);
}
.js .r-card.in { opacity: 1; transform: rotate(var(--rot, 0deg)) scale(1); }


/* ---- The motion vocabulary ----------------------------------
   Five arrivals, assigned by ROLE rather than mixed per element.
   Variety that tracks what a thing IS reads as a system; variety
   sprinkled at random reads as a page that could not decide. So
   every eyebrow on the site moves one way, every section title
   another, and a reader learns the language without being asked
   to notice it.

     r         fade + rise .......... default, body and anything
                                      without a more specific role
     r-drop    arrives from ABOVE ... eyebrows: chrome descending
                                      onto the section it labels
     r-slide   enters from the LEFT . item labels, along the line
                                      of reading
     r-settle  1.03 -> 1 ............ cards and panels: a block
                                      coming to rest as one object
     r-wipe    clip left -> right ... anything that should be SET
                                      rather than moved
     r-x/-y    scaleX / scaleY ...... rules, rails, bars: they draw
     r-pop     .45 -> 1 ............. dots and nodes: marks, not words

   Travel is deliberately smaller than --rise for the chrome
   variants. An eyebrow that moves as far as a headline competes
   with it; at 60% it accompanies it. No bounce, no overshoot —
   the system's easing, unchanged. */

.js .r-drop,
.js .r-slide,
.js .r-settle,
.js .r-wipe { will-change: opacity, transform; }

/* drop — from above. */
.js .r-drop {
  opacity: 0;
  transform: translateY(calc(var(--rise) * -.55));
  transition:
    opacity   var(--dur-reveal) var(--ease) var(--d, 0ms),
    transform var(--dur-reveal) var(--ease) var(--d, 0ms);
}
.js .r-drop.in { opacity: 1; transform: none; }

/* slide — from the left, the direction the line is read. */
.js .r-slide {
  opacity: 0;
  transform: translateX(calc(var(--rise) * -.7));
  transition:
    opacity   var(--dur-reveal) var(--ease) var(--d, 0ms),
    transform var(--dur-reveal) var(--ease) var(--d, 0ms);
}
.js .r-slide.in { opacity: 1; transform: none; }

/* settle — a block arriving slightly large and coming to rest.
   1.03, not 1.1: at 1.1 it reads as a zoom, which is a different
   and much louder gesture. */
.js .r-settle {
  opacity: 0;
  transform: scale(1.03);
  transition:
    opacity   var(--dur-reveal) var(--ease) var(--d, 0ms),
    transform var(--dur-reveal) var(--ease) var(--d, 0ms);
}
.js .r-settle.in { opacity: 1; transform: none; }

/* wipe — no movement at all; the element is uncovered in place.
   The one arrival in the set that cannot be mistaken for a slide,
   which is why it carries the pages that are reference rather
   than argument. */
.js .r-wipe {
  clip-path: inset(0 100% 0 0);
  transition: clip-path var(--dur-x) var(--ease) var(--d, 0ms);
}
.js .r-wipe.in { clip-path: inset(0 0 0 0); }


/* ---- Motion budget -----------------------------------------
   Measured on the home page: 68 elements running the same fade
   and 34px rise, staggered out to 590ms. Every one of them was
   individually reasonable and the sum was not — when the eyebrow,
   the headline, the rule, the lead, three ordinals, three labels
   and three paragraphs all make the identical move, the move
   stops reading as craft and starts reading as a template
   assembling itself.

   Two corrections, both global, neither of which removes a
   reveal:

   1. Body copy fades in place. Travel is a structural gesture —
      it belongs to the things that DRAW (rules, rails, nodes,
      headlines). A paragraph that slides is just a paragraph
      arriving late.

   2. Authored delays run at 60%. The stagger was tuned per
      section in isolation; end to end it meant the last line of
      a section landed six tenths of a second after the first,
      which is long enough to notice waiting. The rhythm is
      preserved — every relationship between delays is intact —
      it is simply played faster. */

.js .lead.r,
.js .item-body.r,
.js .prose.r,
.js .caption.r,
.js p.r { transform: none; }

.js .r,
.js .r-x,
.js .r-y,
.js .r-pop,
.js .r-card,
.js .r-drop,
.js .r-slide,
.js .r-settle,
.js .r-wipe { transition-delay: calc(var(--d, 0ms) * .6); }


/* ---- Reduced motion ----------------------------------------
   Collapse every duration to .2s and drop all transforms; the
   reveal survives as a plain fade. Hover stops moving. */

@media (prefers-reduced-motion: reduce) {

  html { scroll-behavior: auto; }

  .js .r,
  .js .r-x,
  .js .r-y,
  .js .r-pop,
  .js .r-card,
  .js .r-drop,
  .js .r-slide,
  .js .r-settle,
  .js .r-wipe {
    opacity: 0;
    transform: none;
    clip-path: none;
    transition: opacity var(--dur-hover) var(--ease);
    transition-delay: 0ms;
  }
  .js .r-card { transform: rotate(var(--rot, 0deg)); }

  .js .r.in,
  .js .r-x.in,
  .js .r-y.in,
  .js .r-pop.in,
  .js .r-card.in,
  .js .r-drop.in,
  .js .r-slide.in,
  .js .r-settle.in,
  .js .r-wipe.in { opacity: 1; }

  *, *::before, *::after {
    animation-duration: .01ms !important;
    transition-duration: var(--dur-hover) !important;
  }
}


/* ---- Line reveal -------------------------------------------
   js/type-reveal.js wraps each authored line of a [data-lines]
   heading in a mask. The line sits below the mask's edge and
   climbs into it when the heading's own .r reveal fires, so the
   two are one gesture rather than two.

   Only inside .js: with no script there are no wrappers, and
   these rules match nothing. */

.js .lines { display: block; }

.js .lines__mask {
  display: block;
  overflow: hidden;
  /* The descenders of Archivo 800 at hero size sit below the
     baseline box; without the pad the mask crops the tail of a
     comma or a 'y'. */
  padding-bottom: .08em;
  margin-bottom: -.08em;
}

.js .lines__line {
  display: block;
  transition: transform var(--dur-y) var(--ease)
              calc(var(--d, 0ms) * .6 + var(--ln, 0ms)),
              clip-path var(--dur-y) var(--ease)
              calc(var(--d, 0ms) * .6 + var(--ln, 0ms));
}

/* Three ways a masked heading can arrive. The page picks one via
   data-lines="up|down|wipe" and every heading on it agrees, so the
   variation is between PAGES rather than between neighbouring
   headings — which is the difference between a site with a voice
   and a site with a slideshow transition menu.

     up    climbs from below the mask ... narrative pages
     down  drops from above it ......... procedural pages
     wipe  uncovered in place .......... reference pages */
.js .lines--up   .lines__line { transform: translateY(105%); }
.js .lines--down .lines__line { transform: translateY(-105%); }
.js .lines--wipe .lines__line { clip-path: inset(0 100% 0 0); }

.js .lines--up.in   .lines__line,
.js .lines--down.in .lines__line,
.js .r.in .lines--up   .lines__line,
.js .r.in .lines--down .lines__line { transform: none; }

.js .lines--wipe.in .lines__line { clip-path: inset(0 0 0 0); }

.js .r.in .lines__line,
.js .lines.in .lines__line { transform: none; }
.js .lines.in .lines--wipe .lines__line,
.js .lines--wipe.in .lines__line { clip-path: inset(0 0 0 0); }

/* The heading element itself must not also fade, or the lines
   climb behind an opacity ramp and the mask edge is lost. The
   mask IS the reveal here. */
.js .lines.r { opacity: 1; transform: none; }

@media (prefers-reduced-motion: reduce) {
  .js .lines__mask { overflow: visible; }
  .js .lines__line,
  .js .lines--up .lines__line,
  .js .lines--down .lines__line,
  .js .lines--wipe .lines__line {
    transform: none;
    clip-path: none;
    transition: none;
  }
}
