/* ══════════════════════════════════════════════════════════════════════════
   PAPER — the ground layer.  See DESIGN.md at the repo root for the plan.

   THE GOVERNING IDEA, in one line: the ground is a sheet of drafting stock,
   and every number on this page is DRAWN onto it.

   Nothing in this file styles a component. It builds a material — three
   strata stacked in the order a real sheet has them — and it hands paper.js
   an SVG surface to plot on. Existing surfaces sit on top afterwards,
   untouched.

   ── WHY IT IS BUILT THIS WAY ─────────────────────────────────────────────

   ONE fixed element, `#pp-sheet`, at z-index:-1. Measured on the live page
   before writing a line: `body` carries an opaque background (rgb(12,10,8))
   and `body::before` is already a fixed z-index:-1 wash owned by another
   agent. A negative-z child paints ABOVE its stacking context's background
   and BELOW every other child, which is exactly and only what a ground needs
   — so the sheet sits over the warm ground and the wash, and under every
   word on the page, without a single z-index fight and without claiming a
   pseudo-element anyone else might want. `body::after` is deliberately left
   free.

   Because #pp-sheet is itself a stacking context, the three strata inside it
   are ordered by nothing more exotic than ::before / child / ::after:

     1. ::before  THE STOCK  — very low-frequency density variation, the
                  unevenness of the sheet itself. Under the ink.
     2. #pp-plot  THE INK    — paper.js plots here (arcs, marks, the leader).
     3. ::after   THE TOOTH  — fine speckle and laid fibre. OVER the ink, so
                  the paper's surface breaks the ink up. Ink settles into
                  fibre; it does not float above it. This ordering is the
                  whole reason the marks read as drawn rather than as CSS
                  borders, and it is the one thing here worth protecting.

   ── THE TEXTURE IS GENERATED, NOT AN IMAGE ───────────────────────────────

   Three `feTurbulence` data URIs, ~500 bytes each, no external resource, no
   binary. A noise PNG was the obvious alternative and it is a trap: random
   pixels do not compress, so a 256x256 RGBA tile lands around 250KB of
   base64 — for a texture the browser can synthesise for free.

   Each one is turbulence run through an feColorMatrix that throws the noise's
   own colour away, pins RGB to a constant warm white, and puts a THRESHOLDED
   copy of the luminance into alpha (`k*lum - c`). That last step is what
   makes it work on a near-black ground: the naive versions (turbulence at low
   opacity, or blended `overlay` / `soft-light`) were rendered, screenshotted
   and measured first, and every one of them was invisible — mid-grey noise
   has nothing to give a #0c0a08 ground. Thresholded warm-white specks are
   grains of stock catching light, which is the thing being drawn.

   Measured on the rendered page, not on declared values: the tooth lifts the
   ground about 3 luminance points (out of 255) and adds ~3.4 of standard
   deviation. That is the entire budget. It is small because it has to be —
   see the contrast note at the bottom of this file.

   ── COST ─────────────────────────────────────────────────────────────────

   `position:fixed` + `contain:strict` + `will-change` on one element. The
   layer is painted once and composited; it does not repaint on scroll, which
   matters because the tape renders 2,000 rows. Nothing here is per-row,
   per-cell, or per-poll, and there is no backdrop-filter anywhere in this
   file.

   Do not put this texture on a scrolling element. The moment it repaints per
   frame, feTurbulence stops being free.
   ══════════════════════════════════════════════════════════════════════════ */

:root {
  /* ── THE INK ────────────────────────────────────────────────────────────
     Four weights, and they are the LINE TYPES of a drafting sheet, not four
     opacities picked to look nice. A draughtsman's line weight is a
     statement about what kind of line it is; that is the discipline being
     borrowed, and it is why there are four and not twelve.

       construction  the compass work. Pencil the draughtsman never erased.
                     It must be at the very edge of visible: present when you
                     look for it, gone when you are reading a number.
       drawn         a line that is asserting something.
       lead          the ACTIVE line — the one connection on screen. It is
                     the only ink allowed to carry the accent hue.
       margin        the gutter furniture: registration crosses, foot ticks.

     Warm white, because the ground is warm near-black and the app's own --ink
     is #f8f4f0. A neutral grey line on a warm ground reads as a mistake, and
     these are read at 4% alpha where a hue error is all you can see. */
  --pp-ink-construction: rgba(248, 238, 226, .075);
  --pp-ink-drawn:        rgba(248, 238, 226, .105);
  --pp-ink-margin:       rgba(248, 238, 226, .155);
  --pp-ink-lead:         rgba(248, 238, 226, .340);

  /* The fifth value is not a line weight and is not chosen by eye. The sheet
     draws exactly one piece of TEXT — the balloon's item number — and text has
     to clear 4.5:1 whatever it is decorating.

     Worked on rendered pixels, on the brightest ground the balloon can land
     on (26/255, the top-left where the app's own bloom sits): to reach 4.5:1
     the figure has to composite to about 127/255, so it needs alpha of at
     least (127-26)/(248-26) = .455. .50 lands it at 137 and clears with room.

     The first attempt used the lead ink at .78 opacity, which composited to
     92 — a ratio of 2.4:1, i.e. a smudge, and it looked like one. That is the
     whole argument for measuring composited pixels rather than reading the
     alpha off the declaration and calling it done. */
  --pp-ink-figure:       rgba(248, 238, 226, .500);

  /* ── ONE PEN, AND IT IS NOT THE DATA'S COLOUR ───────────────────────────
     The lead was the accent orange for one render, and that render is why it
     is not. On the Compare section the inked arc ran straight through the
     at-the-money volatility chart — whose line is ALSO --accent — and for a
     moment the two were indistinguishable: a drawn guide and a recorded
     measurement in the same hue, the same weight, crossing each other.

     So the rule is now explicit and it is the most load-bearing line in this
     file: THE PLOTTER HAS ONE PEN AND IT IS WARM WHITE. Orange is reserved,
     absolutely, for things that are true about the market. Nothing
     decorative may borrow it. That is also what a real plotter is like —
     there is one pen in the carriage and every line it draws that session is
     the same ink — so the constraint is not a compromise, it is the source. */

  /* ── LINE WEIGHT ────────────────────────────────────────────────────────
     One pen. A plotter has a physical pen in it and every line it draws that
     session is the same width — variation comes from ink density and from
     drawing a line twice, never from the pen changing thickness mid-sheet.
     That constraint is doing more work here than any colour choice. */
  --pp-pen: 1px;

  /* ── THE GEOMETRY ───────────────────────────────────────────────────────
     Read by paper.js. Exposed as custom properties so the construction can
     be re-proportioned from a stylesheet without touching the plotter.

     --pp-throw is the compass radius, as a multiple of viewport height: how
     far below the rail the pivot is driven. Small values curl the arcs
     tightly into the corner; large values flatten them until the fan reads
     as six parallel lines and the circular idea is lost. 1.35 was chosen by
     rendering and looking, not by taste. */
  --pp-throw: 1.35;
  --pp-margin: 28px;   /* gutter inset for registration marks and foot ticks */

  /* ── THE FEATHER ────────────────────────────────────────────────────────
     How far from a surface the ink reaches full weight. paper.js grows every
     measured surface by this much before blurring it into the field mask at
     sigma = half of it, which puts the mask at zero exactly on the surface's
     true edge and spends the entire ramp outside it, in the margin.

     This is the number that answers "the line is entering badly the
     sections", and it is bounded on both sides by arithmetic rather than by
     taste.

     A gutter is bounded by a surface on BOTH sides, so it pays for the fade
     TWICE. The ramp is 2F wide, so the run at full weight is G - 4F, where G
     is the gutter. At 1440 the gutter between the rail and the panel is 56px:

         F = 13  ->  56 - 52 =  4px of line.  Measured, and it was
                     effectively an erased sheet: the fan vanished.
         F = 8   ->  56 - 32 = 24px of line, with a 16px fade at each end.
         F = 4   ->  56 - 16 = 40px of line, but an 8px fade on a 1px
                     hairline is not read as a fade, it is read as a cut,
                     which is the defect this exists to remove.

     8 is where the line is a line and the ends are still ends. Anything that
     changes the gutter — the rail's width, the stage's padding — changes the
     budget, so re-measure G before touching this. */
  --pp-feather: 8px;
}

/* ══════════════════════════════════════════════════════════════════════════
   THE SHEET
   ══════════════════════════════════════════════════════════════════════════ */

#pp-sheet {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  /* strict containment: this element can never influence layout, and the
     browser is told so explicitly rather than being left to work it out. */
  contain: strict;
}

/* ── 1. THE STOCK ─────────────────────────────────────────────────────────
   What the sheet is made OF, as opposed to what its surface is like. Two
   layers, both low-frequency, both UNDER the ink — because these are
   properties of the material, and ink lies on top of the material.

     stock  900px tile, baseFrequency 0.006 — cloud. A sheet of paper is not
            one colour; it is slightly uneven, and the unevenness is much
            larger than the grain. Without it the tooth alone reads as
            television static rather than as a material.

     fibre  560px tile, baseFrequency 0.014 x 0.55 — the anisotropy is the
            entire point. Stretched 40:1, turbulence stops being static and
            becomes laid fibre lying in the direction the sheet was made.

   THE FIBRE LIVES HERE AND ONLY HERE, and that rule was bought expensively.
   It used to be declared on BOTH strata — this one and the tooth — same tile,
   same seed, same size, same origin. Two identical anisotropic patterns in
   perfect register do not average out; they reinforce, stroke for stroke, and
   the sheet stopped reading as paper and started reading as brushed metal:
   long bright scratches running the full width of the page.

   ── THE FIBRE WAS DEAD, AND THE NOTE THAT USED TO BE HERE SAID IT WAS LOUD ──

   The correction above was over-applied and then recorded as if it had landed.
   The threshold ended at `0.38*sum - 0.88`; fractalNoise sits around 0.5 per
   channel, so alpha only goes positive where the three channels average above
   0.772, which this turbulence essentially never reaches. Measured two ways
   before a single value was touched:

     · isolated on the live page with `background-size: 0 0` on the other
       layer, the fibre came back BYTE-IDENTICAL to bare ground on four
       separate clean patches;
     · rendered alone at FULL opacity on the ground colour, it measured mean
       10.28, std 0.00 — which is exactly #0c0a08 and therefore exactly
       nothing.

   So the sheet had no direction at all. What was reading as "fibre far too
   strong" was the STOCK cloud: on a wide clean patch it was lifting the ground
   +3.72 on its own, with a std across the width of 1.70 — a big soft gradient,
   which is the thing that was actually too loud, and which no amount of
   weakening the fibre could ever have fixed.

   ── WHAT IS HERE NOW, MEASURED THE SAME WAY ──────────────────────────────

   Threshold `0.60*sum - 0.78`, which does clear. Strength is carried per
   LAYER — the stock's rect has its own `opacity` inside the SVG — so the two
   can be balanced against each other without either one having to be a
   separate element, and the cloud is pulled down hard to make room for the
   grain that replaces it.

   On the wide clean patch (x 1055-1380, y 180-300 at 1440x900), against the
   untextured ground underneath:

                            ground    was (no fibre)    now
     mean luminance /255     10.28        15.22        13.42
     std (grain)              0.00         2.63         2.24
     std of ROW means         0.00         0.56         1.06
     std of COL means         0.00         1.70         0.55

   Both of the last two lines are the point. ROW-mean std is how much the sheet
   varies as you travel DOWN it — it is what the eye integrates into laid
   grain, and it went from nothing to 1.06. COL-mean std is the soft cloud
   across the width, and it went from 1.70 to 0.55. The sheet swapped a
   gradient for a direction, and got DARKER doing it: +3.14 over ground where
   it used to be +4.94.

   1.06 is deliberately under the 1.44 that the earlier note called "grain that
   happens to lie down", and a long way under the 3.12 where streaks became
   nameable. Rendered ladders at 0.20 and 0.26 fibre opacity were looked at and
   rejected: both were back to brushed metal.

   So: fibre is IN paper, tooth is ON it, and nothing may be in both places.
   If a texture ever needs to appear on two strata, it must be a different
   seed at minimum, and honestly it should be a different texture — the two
   strata are meant to be different materials, not one material at two
   opacities.

   The two tile sizes are deliberately not multiples of each other, so the
   repeats never coincide into a visible grid.

   IF YOU CHANGE A NUMBER HERE, RE-MEASURE ISOLATED. Both times this stratum
   went wrong, it went wrong because a value was reasoned about instead of
   rendered, and the resulting note read as confidently as this one does. */
#pp-sheet::before {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='560'%20height='560'%3E%3Cfilter%20id='n'%20x='0'%20y='0'%20width='100%25'%20height='100%25'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.014%200.55'%20numOctaves='3'%20seed='3'%20stitchTiles='stitch'/%3E%3CfeColorMatrix%20type='matrix'%20values='0%200%200%200%201%200%200%200%200%200.95%200%200%200%200%200.88%200.6%200.6%200.6%200%20-0.78'/%3E%3C/filter%3E%3Crect%20width='560'%20height='560'%20filter='url(%23n)'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='900'%20height='900'%3E%3Cfilter%20id='n'%20x='0'%20y='0'%20width='100%25'%20height='100%25'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.006'%20numOctaves='4'%20seed='11'%20stitchTiles='stitch'/%3E%3CfeColorMatrix%20type='matrix'%20values='0%200%200%200%201%200%200%200%200%200.93%200%200%200%200%200.83%200.34%200.34%200.34%200%20-0.3'/%3E%3C/filter%3E%3Crect%20width='900'%20height='900'%20filter='url(%23n)'%20opacity='0.42'/%3E%3C/svg%3E");
  background-size: 560px 560px, 900px 900px;
  /* One master, and each layer's own share carried inside its SVG (the stock's
     rect has opacity 0.42). Two layers on one element cannot otherwise be
     balanced against each other, and they have to be: the grain needs to come
     up while the cloud goes down. */
  opacity: .05;
}

/* ── 2. THE INK ───────────────────────────────────────────────────────────
   paper.js plots into this. Empty until it runs, and the sheet is a complete,
   correct material without it — the texture does not depend on JavaScript. */
#pp-plot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  overflow: visible;
}

/* ── 3. THE TOOTH ─────────────────────────────────────────────────────────
   ONE layer, painted OVER the ink: 240px tile, baseFrequency 0.9 — grains of
   stock, about a pixel across, isotropic. Isotropic is the point. Tooth has no
   direction; the moment it acquires one it is fibre, and fibre belongs in the
   stratum below (see THE STOCK above for what happened when it was in both).

   It is what makes the marks read as drawn rather than as CSS borders: the ink
   is under it, so the surface breaks the ink up instead of the ink floating on
   the surface.

   It is far below the level at which it would be described as an effect. If
   you can point at the texture, it is too strong — and it was, at .20 over
   two tiles. The number below is not restraint, it is the contrast arithmetic
   at the bottom of this file. */
#pp-sheet::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image:
    url("data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20width='240'%20height='240'%3E%3Cfilter%20id='n'%20x='0'%20y='0'%20width='100%25'%20height='100%25'%3E%3CfeTurbulence%20type='fractalNoise'%20baseFrequency='0.9'%20numOctaves='4'%20seed='7'%20stitchTiles='stitch'/%3E%3CfeColorMatrix%20type='matrix'%20values='0%200%200%200%201%200%200%200%200%200.94%200%200%200%200%200.85%200.5%200.5%200.5%200%20-0.72'/%3E%3C/filter%3E%3Crect%20width='240'%20height='240'%20filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 240px 240px;
  opacity: .11;
}

/* ══════════════════════════════════════════════════════════════════════════
   THE PLOT — line types

   Every stroke paper.js emits carries one of these classes. Keeping the
   weights here rather than in the plotter means the line-type system is
   readable in one place, which is the only way it stays a system.
   ══════════════════════════════════════════════════════════════════════════ */

#pp-plot .pp-l {
  fill: none;
  stroke-width: var(--pp-pen);
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}

/* The compass work. Six concentric arcs, one per section, swung from a single
   pivot below the rail — the construction that PUT the sections where they
   are. Left on the sheet the way a draughtsman leaves compass work: not
   erased, not emphasised. */
#pp-plot .pp-construction { stroke: var(--pp-ink-construction); }

/* Sheet furniture: registration crosses, foot ticks, the trim corners. */
#pp-plot .pp-margin { stroke: var(--pp-ink-margin); }
#pp-plot .pp-drawn  { stroke: var(--pp-ink-drawn); }

/* ── THE LEAD ─────────────────────────────────────────────────────────────
   The one inked line on the sheet: the active section's own construction
   arc, gone over in ink. This is the connection, and there is never more
   than one of it.

   IT IS A CHAIN LINE, AND THAT IS THE WHOLE ANSWER TO A BUG THAT KEPT COMING
   BACK. Twice now the lead has ended up wearing the same colour as a data
   line — first when both were the accent orange, then again when the chart's
   line was moved to warm --ink and met the lead there. Both times the fix was
   to repaint something, and both times it lasted until the next token change,
   because a colour is a coincidence and coincidences recur.

   ISO 128 already solved this, in 1982, for people drawing on actual paper.
   The line's MEANING is carried by its TYPE, not its colour:

     continuous thin ....... an edge, an outline, a thing that is there
     long-dash / short-dash  an AXIS. A centre line. Construction. Not an
                             object — the geometry an object was placed by.

   So the lead is dashed 13-3.5-2-3.5 and every data line in this app is
   continuous, and now they cannot be confused whatever the palette does next.
   A dashed line is also, usefully, a line that gives text back the gaps it
   crosses through.

   Value is the second separator and it comes from the paper's own scale, not
   from the app's: --pp-ink-lead is the top of a four-step ink ramp that starts
   at .075. It is the paper system's own loudest value and it is still nowhere
   near the app's quietest one.

   THE THIRD SEPARATOR IS THE ONE THAT WAS MISSING: the lead has to be visible.
   It sat at an effective .165 alpha, which is a line you can fail to notice,
   and a connection nobody sees is not a connection — the whole point of this
   arc is that the section does not get marked, it gets CONTINUED, and that
   argument is worth nothing if the continuation is below threshold. It is now
   .34 x .72 = .245 effective. Measured on rendered pixels that composites to
   about 66/255 against the chart's data line at 245: still 3.7x apart in
   value, still a different line type, and now actually legible as structure.

   Raising it was the right direction precisely BECAUSE the type separation is
   sound. A dashed construction line cannot be mistaken for a continuous
   recorded measurement however bright it gets, so the value was free to go up
   until the line read; whereas when the two shared a hue, no amount of value
   difference would have made them different KINDS of line. */
#pp-plot .pp-lead {
  stroke: var(--pp-ink-lead);
  stroke-width: var(--pp-pen);
  stroke-dasharray: 13 3.5 2 3.5;
  opacity: .72;
}

/* The reveal pen. It lives inside a <mask> and is never seen — it is a fat
   white stroke of the SAME path, and the chain line is only visible where it
   has passed. This is the one way to have both a dash PATTERN and a dash
   REVEAL on one line: stroke-dasharray can only mean one thing at a time, so
   the pattern goes on the ink and the reveal goes on the mask.

   Its width is the whole clearance of the line, not the line's own width,
   because a mask edge narrower than the stroke would shave the chain line's
   ends as it went. */
#pp-plot .pp-pen-reveal {
  fill: none;
  stroke: #fff;
  stroke-width: 6;
  stroke-linecap: round;
}

/* The balloon — a drafting item callout. The bracket digit from the rail,
   re-struck on clear ground where the arc runs under the content, so the
   number physically travels from the nav to the thing it opened.

   This is the one place the sheet is LOUDER than its construction, on
   purpose. Everything else here is geometry you are not meant to read; the
   balloon is a label, and a label you cannot read is not a quiet label, it is
   a dirty mark. Structure gets to be faint. Annotation does not. */
#pp-plot .pp-balloon-ring {
  fill: none;
  stroke: var(--pp-ink-figure);
  stroke-width: var(--pp-pen);
  opacity: .7;
}
#pp-plot .pp-balloon-fig {
  fill: var(--pp-ink-figure);
  font: 500 10px/1 ui-monospace, "SFMono-Regular", Menlo, monospace;
  letter-spacing: .02em;
  text-anchor: middle;
  stroke: none;
}

/* The dot terminator. ISO leaders end in a dot when they point at an area
   rather than at an edge — which is what a section is. */
#pp-plot .pp-dot {
  fill: var(--pp-ink-lead);
  stroke: none;
  opacity: .72;
}

/* ══════════════════════════════════════════════════════════════════════════
   MOTION — a mark is DRAWN, not faded in.

   The lead arc is stroked from the nav item outward across the sheet at a
   plotter's pace. `stroke-dashoffset` on a path whose dash length is its own
   total length is the only honest way to do this: the line genuinely does not
   exist ahead of the pen.

   ~820ms and a decelerating curve. A pen carriage has mass; it arrives, it
   does not snap. Faster than this and it reads as a wipe; slower and it is
   a loading spinner.

   The property is set inline by paper.js (--pp-len, the measured path length)
   because only the DOM knows it.
   ══════════════════════════════════════════════════════════════════════════ */

#pp-plot .pp-draw {
  stroke-dasharray: var(--pp-len);
  stroke-dashoffset: var(--pp-len);
  animation: pp-plot-in 820ms cubic-bezier(.22, .61, .36, 1) forwards;
}
@keyframes pp-plot-in { to { stroke-dashoffset: 0; } }

/* The balloon and its dot are struck WHEN THE PEN REACHES THEM — not on a
   fixed delay. Because the balloon now looks for clear ground it can land
   anywhere along the arc, so a hardcoded 700ms was right only by accident and
   wrong by up to the full duration of the stroke.

   paper.js sets --pp-delay per mark, from the fraction of the path at which
   that mark sits. For a circular arc that fraction is exactly the fraction of
   the swept ANGLE, so it is arithmetic rather than an approximation. The
   origin dot gets 0ms: the pen is already there.

   Scale, not fade: the pen presses. */
#pp-plot .pp-strike {
  transform-box: fill-box;
  transform-origin: center;
  animation: pp-strike-in 260ms cubic-bezier(.22, .61, .36, 1) var(--pp-delay, 700ms) both;
}
@keyframes pp-strike-in {
  from { opacity: 0; transform: scale(.55); }
  to   { opacity: 1; transform: scale(1); }
}

/* A REDRAW IS NOT A NEW DRAWING.

   The sheet is replotted whenever a surface it measured changes size, which
   is how it stops being stale when a panel finishes loading its data. That
   is a redraw of the same drawing, and it must land silently: re-striking
   every mark each time the tape appends a row would turn the reveal into a
   tic. paper.js sets this class when the navigation did not change, and the
   pen's own dash animation is simply not attached in that case. */
#pp-plot .pp-strike.pp-still {
  animation: none;
  opacity: 1;
  transform: none;
}

/* ── REDUCED MOTION ───────────────────────────────────────────────────────
   Land at the END STATE, which is the finished drawing — never at the empty
   sheet. Every animation here is a reveal, so cancelling one without pinning
   its final values would delete the connection outright for the people who
   most need it to be static. */
@media (prefers-reduced-motion: reduce) {
  #pp-plot .pp-draw {
    animation: none !important;
    stroke-dashoffset: 0 !important;
    stroke-dasharray: none !important;
  }
  #pp-plot .pp-strike {
    animation: none !important;
    opacity: 1 !important;
    transform: none !important;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   THE RAIL SIDE OF THE CONNECTION

   The arc starts at the rail's right edge, at the active item's own height.

   ── THE "GOOD ACCIDENT" WAS NOT ONE, AND IT IS GONE ──────────────────────

   This note used to say that the arc passing UNDER the rail arrived softened
   by its backdrop-filter, "like ink read through vellum", and that this was
   a good accident worth keeping. It was neither good nor an accident: a 1px
   hairline convolved with a 20px blur is not softened ink, it is a stain,
   and differencing the page against itself with the ink hidden showed
   exactly that — formless grey blooms under the rail and the header, which
   is a fair part of what "the quality looks cheap" was pointing at.

   The rail, the header and the footer are now measured as surfaces like any
   other and the field mask holds the ink off them entirely. Verified on
   rendered pixels: zero ink under any of them, on eight sections at two
   widths.

   ── WHICH MEANS THE STUB IS THE WHOLE JOIN ───────────────────────────────

   With nothing drawn under the rail, the last stretch of the line has to be
   drawn BY the rail: a take-off stub on the active row, inside the rail's
   own stacking context. It is the only part of the leader that lives on that
   side, so the two have to meet exactly, and getting that right took three
   separate measurements — the vertical drop of the arc, the weight the stub
   ends on, and where the dot sits. Each is recorded at its own declaration
   below rather than here, because each is a different number.

   `right: -14px` is kept, but not for the reason this note used to give: the
   rail carries overflow:auto, so the stub is CLIPPED at the rail's edge and
   cannot overhang it at all. The offset only buys the last few pixels up to
   that clip.
   ══════════════════════════════════════════════════════════════════════════ */

@media (min-width: 900px) {
  body nav.rail .rl-item {
    position: relative;
  }

  body nav.rail .rl-item.on::after {
    content: "";
    position: absolute;
    /* Not 50%. The arc is a curve and it has already fallen by the time it
       reaches the rail's edge; paper.js measures that drop against the item's
       own centre and publishes it here. Without it the stub and the arc miss
       each other by ~3px, which on a hairline is three line-widths. */
    top: calc(50% + var(--pp-takeoff, 0px));
    right: -14px;
    /* 40, up from 26. The stub is clipped at the rail's edge either way, so
       the extra width is not reach — it is RUN, the distance over which the
       gradient below can get from full ink down to its handover weight
       without the falloff itself reading as an event. */
    width: 40px;
    height: 1px;
    /* It does NOT fade to nothing, and that is the measured part. The rail
       carries overflow:auto, so this stub is clipped dead at x=208 whatever
       width it is given — it cannot reach across the mask's ramp, it can only
       hand over to it. With the gradient running to transparent the stub was
       back on bare ground by x=206 while the arc had not yet lifted off it:
       a dead patch in the middle of what is meant to be one stroke.

       So it is cut at the rail's edge while still carrying ink, at roughly
       the weight the arc's ramp reaches a few pixels later. The handover is
       a crossfade between two marks that are both present, rather than a gap
       between two marks that are both absent. */
    background: linear-gradient(90deg,
      var(--pp-ink-lead) 0 72%,
      color-mix(in srgb, var(--pp-ink-lead) 62%, transparent) 100%);
    opacity: .58;
    pointer-events: none;
    /* Scales from the label end outward — the take-off is drawn in the same
       direction the pen travels, so the stub and the arc are one gesture. */
    transform-origin: left center;
    animation: pp-stub-out 240ms cubic-bezier(.22, .61, .36, 1) both;
  }

  @keyframes pp-stub-out {
    from { transform: scaleX(0); }
    to   { transform: scaleX(1); }
  }
}

@media (prefers-reduced-motion: reduce) {
  body nav.rail .rl-item.on::after {
    animation: none !important;
    transform: none !important;
  }
}

/* ══════════════════════════════════════════════════════════════════════════
   CONTRAST — the note that outranks every aesthetic decision in this file.

   Texture and translucency are ALPHA COMPOSITES. Computing a ratio from
   declared CSS values is wrong here, and has been wrong by roughly 4x on this
   page before. Every pair below was measured by rendering the live page,
   screenshotting it, and reading the composited pixels.

   The tooth raises the ground's mean luminance, and the ground's luminance is
   the denominator of every contrast ratio on the page. That is the ONLY
   reason the opacities above are as low as they are — not restraint, arithmetic.
   The binding pair is --ink3 (#9b8e82), the dimmest type the app ships, over
   the textured ground. Measured on rendered pixels with the sheet live it
   clears 4.5:1; that measurement is what caps `#pp-sheet::after { opacity }`,
   and raising it without re-measuring will break the quietest text on the
   page first, which is also the text nobody will notice going.

   THE OTHER RULE THIS FILE MUST NOT BREAK: n/a is not 0. Nothing here paints
   into a cell, a row or an empty state — the sheet is strictly BELOW content
   (z-index:-1, pointer-events:none) and it draws no fills. A decorative
   system that can tint an empty cell can make an absence look like a value,
   so this one is built where it cannot reach them.
   ══════════════════════════════════════════════════════════════════════════ */
