/* ============================================================================
   BRAND — the faces, the graticule, and the typographic craft.

   MARKETING ONLY. Nothing in the app imports this. The boundary from docs/22 §1
   holds: the marketing surface may be louder than the instrument.

   ## The field is gone

   What used to be here: three viewport-sized radial-gradient blobs in blue and
   violet, drifting on 44s/58s/52s loops. It was well engineered — compositor-only
   transforms, coprime periods, reduced-motion aware — and it was the single most
   generic thing in the product. Soft drifting gradient blobs are the house style
   of every AI-generated landing page of the last three years. Being well built
   did not make it ours.

   ## What replaced it, and why this one is ours

   A GRATICULE: the faint ruled grid of graph paper and an oscilloscope screen,
   stepped on the spacing token, fading out downward, with a single brass hairline
   across it at the optical centre.

   Four reasons it earns the place the blobs had:

   1. It is the chart. The product's whole surface is a ruled plane with a price
      series on it; the front door now says that before a screenshot loads.
   2. It is static. The motion budget in this product is inverse to how live the
      data is, and a landing page is not live at all — but "may move" is not "must
      move", and a background that holds still is a stronger claim from a company
      whose instrument refuses to animate a price.
   3. It costs nothing. Two repeating-linear-gradients and a mask. No animation
      frames, no compositor layers, no reduced-motion special case.
   4. It scales down honestly. The step shrinks at 767px instead of the whole
      composition being cropped.
   ========================================================================= */

/* --- the graticule ------------------------------------------------------
   Applied to <body class="graticule">, as a BACKGROUND rather than an element.

   The first version was a fixed `<div>` at z-index -1, which is how the old
   gradient field did it. That only ever worked through background propagation —
   a negative-z child of <body> paints behind body's own background — and it
   broke the moment reset.css gave <body> a background. Raising it to z-index 0
   and pushing the landmarks to 1 fixed it in the browser and still failed in
   DOM-rasterising pipelines, which is what screenshots, PDF export and Canva
   import all use.

   So: no element, no z-index, no stacking context, nothing to lose. Six
   background layers on the body, viewport-locked. Content paints over a body
   background unconditionally, in every renderer there is.

   Layer order, topmost first:
     1  the brass rule, above the fade so it stays crisp
     2  the fade — transparent to surface, so the grid is gone by the time the
        page is prose (a grid behind body copy fights the text's baseline)
     3,4  major lines, every fourth cell
     5,6  minor lines
   --------------------------------------------------------------------------- */
/* MUST come before anything relies on .tape being visible.

   `reset.web.css` puts a background on <body> and leaves <html> bare. A body
   background with no html background PROPAGATES to the canvas, where it paints
   before every descendant — including a negative-z child. That is precisely the
   trap noted at the top of this file, and it is why the old fixed field had to
   be abandoned rather than fixed.

   Giving <html> a background stops the propagation. Body's background then
   paints in normal order: above .tape, below the content. The grid stays a body
   background — no element, no pseudo-element, nothing a DOM-rasterising
   screenshot pipeline can drop — and the tape shows through from behind it. */
html { background-color: var(--surface-base); }

/* NOTE THE MISSING background-color.

   It moved to <html> above, and body keeps only the image layers. Both halves
   of that are load-bearing:

   - html needs a background or body's PROPAGATES to the canvas, painting before
     every descendant including a negative-z one.
   - body must not keep an opaque colour of its own, because a block's own
     background paints AFTER negative-z descendants. An opaque body would hide
     the tape just as thoroughly as propagation did, one step later.

   The grid stays a body background — no element, no pseudo-element, nothing a
   DOM-rasterising screenshot pipeline can drop — and it paints over the tape
   because that is where a block background sits in the painting order.

   The old fade-to-surface layer is gone with it: it faded to an OPAQUE colour,
   which would have erased the tape across the bottom two thirds of every
   viewport. The grid is faint enough now not to need it. */
body.graticule {
  /* Explicit. `reset.web.css` sets `background: var(--surface-base)` on body,
     and a rule that only writes background-image leaves that colour in place —
     opaque, over the tape, exactly as invisible as before. */
  background-color: transparent;
  background-image:
    linear-gradient(to right, transparent, var(--grat-rule) 22%, var(--grat-rule) 78%, transparent),
    repeating-linear-gradient(to bottom, var(--grat-line-major) 0 1px, transparent 1px calc(var(--grat-step) * 4)),
    repeating-linear-gradient(to right,  var(--grat-line-major) 0 1px, transparent 1px calc(var(--grat-step) * 4)),
    repeating-linear-gradient(to bottom, var(--grat-line) 0 1px, transparent 1px var(--grat-step)),
    repeating-linear-gradient(to right,  var(--grat-line) 0 1px, transparent 1px var(--grat-step));
  background-attachment: fixed, fixed, fixed, fixed, fixed;
  background-repeat: no-repeat, repeat, repeat, repeat, repeat;
  background-size: 100% 1px, auto, auto, auto, auto;
  background-position: 0 41vh, 0 0, 0 0, 0 0, 0 0;
}

/* --- the tape ------------------------------------------------------------
   The defocused field the graticule now sits over. See src/tape.js for what it
   draws and why it costs nothing to run.

   Fixed, so it does not scroll: the page moves and the field does not, which is
   the parallax the composition is built on. `inert`-by-nature — no pointer
   events, hidden from assistive tech, and it carries no information.
   --------------------------------------------------------------------------- */
.tape {
  position: fixed;
  inset: 0;
  z-index: -1;
  overflow: hidden;
  pointer-events: none;
  /* The body's own background paints the grid ABOVE this, because a negative
     z-index child paints behind its parent's background. That is the whole
     stacking trick: one element, no wrapper, grid stays crisp on top. */
}

.tape-layer {
  position: absolute;
  top: 0;
  left: 0;
  will-change: transform;
  animation-name: tape-drift;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  /* animation-duration is set per layer in tape.js — the two periods are
     coprime so they never fall into step and expose the loop. */
}

/* Exactly one viewport. The canvas holds the same content twice, one viewport
   apart, so the end of the cycle is pixel-identical to its start. */
@keyframes tape-drift {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(0, calc(var(--tape-shift, 100vh) * -1), 0); }
}

/* Someone who asked for less motion gets the field without the drift — the
   texture and the depth survive, the movement does not. Not `display: none`:
   the composition is built on having something behind the grid, and removing it
   entirely would leave a different page rather than a calmer one. */
@media (prefers-reduced-motion: reduce) {
  .tape-layer { animation: none; }
}

/* The grid was tuned to be the only thing back there. Over the tape it was
   twice as much structure as the composition needs, and on a phone — where the
   step is smaller and the screen is closer — it read as noise.

   Minor lines go on mobile entirely; majors carry the structure alone. On
   desktop both stay, pulled back so the tape reads through them. */
@media (max-width: 767px) {
  body.graticule {
    background-image:
      linear-gradient(to right, transparent, var(--grat-rule) 22%, var(--grat-rule) 78%, transparent),
      repeating-linear-gradient(to bottom, var(--grat-line-major) 0 1px, transparent 1px calc(var(--grat-step) * 4)),
      repeating-linear-gradient(to right,  var(--grat-line-major) 0 1px, transparent 1px calc(var(--grat-step) * 4));
    background-attachment: fixed, fixed, fixed;
    background-repeat: no-repeat, repeat, repeat;
    background-size: 100% 1px, auto, auto;
    background-position: 0 41vh, 0 0, 0 0;
  }
}

/* --- typographic craft ---------------------------------------------------
   The things that separate set type from placed text. */

/* Headings wrap on meaning rather than on whatever fits. */
h1, h2, h3, .lead { text-wrap: balance; }
p, li { text-wrap: pretty; }

/* The display face carries every heading on a marketing page. Tracking is much
   gentler than a grotesque would need: -0.035em on a serif at 4rem closes the
   counters. Nudged from -0.014em to -0.019em with the move to Source Serif 4,
   which sets a little wider than Instrument Serif did at the same size.
   font-weight 400 is deliberate — this face has real weight at Regular, and the
   optical-size axis handles large sizes without reaching for Semibold. */
h1 { font-family: var(--font-display); font-weight: 400; letter-spacing: -0.019em; line-height: 1.02; }
h2 { font-family: var(--font-display); font-weight: 400; letter-spacing: -0.01em; line-height: 1.06; }
h3 { font-family: var(--font-sans); font-weight: 600; letter-spacing: -0.005em; }

/* Every figure on the page is tabular, so a column of numbers lines up and a
   price that changes does not reflow the text around it. */
.price, .cmp-val, .step, .specimen, .logos li, .plan .price {
  font-family: var(--font-mono); font-variant-numeric: var(--figure-variant);
}

/* --- the wordmark ------------------------------------------------------
   THE MARK IS THE LOCKUP. There is no drawn symbol, by decision: the Æ ligature
   set in the display face against the name tracked open is doing the work a
   symbol would, and it cannot be mistaken for the shield-plus-rising-line that
   is the category default and that this product's compliance posture forbids.

   Three things make it a mark rather than a word:
     - the ligature is the display serif while the name is the grotesque, so the
       two halves are different voices rather than different sizes;
     - the name is tracked to 0.19em, which is what stops it reading as a word;
     - the pair is kerned by hand, because no pair-kerning table anticipates a
       ligature used as a logo. */
.wordmark {
  display: inline-flex; align-items: baseline; gap: 0.16em;
  text-decoration: none; flex: 0 0 auto;
  font-family: var(--font-sans); font-weight: var(--weight-semibold);
  font-size: 1.06rem; letter-spacing: var(--tracking-brand);
  color: var(--text-primary);
}
.wordmark span {
  font-family: var(--font-display); font-weight: 400;
  font-size: 1.72em; line-height: 1; letter-spacing: 0;
  color: var(--brand); margin-inline-end: -0.02em;
}
.wordmark.sm { font-size: 0.9rem; }
.wordmark:hover span { color: var(--brand-strong); }

/* --- brand vs interactive, on the front door ----------------------------
   Inside the instrument, blue means "this responds to you" and the brand colour
   stays out of the way. On a marketing page the relationship inverts: the page
   IS the brand asking, so the primary call to action is brass and blue is not
   used at all. One rule, and it is the whole reason moving the brand off blue
   was worth doing. */
.btn.primary { background: var(--brand); color: var(--text-inverse); border-color: var(--brand); }
.btn.primary:hover { background: var(--brand-strong); border-color: var(--brand-strong); filter: none; }
.eyebrow::before { background: var(--brand); }
.ticks li::before { color: var(--brand); }
.step b { color: var(--brand); }
.cmp-val.feat { background: var(--brand-soft); }
.plan.feat { border-color: var(--brand); }
.plan .flag { background: var(--brand); color: var(--text-inverse); }
.skip { background: var(--brand); color: var(--text-inverse); }
