  /* The cursor glow, on paper only. It is one fixed sheet behind the whole
     page rather than a layer per band, and the bands that are not paper --
     .band.alt on tint, .end on ink -- lift themselves over it with a z-index
     rather than each masking it out. So the glow lights the white grounds and
     nothing else. */
  /* Absolute and body-sized, NOT fixed to the viewport. Fixed, the grid had
     to be scrolled by hand -- and a background-position written from a scroll
     listener lands a frame or more behind a page the compositor has already
     moved, so the dots visibly juddered against the text they sit under.
     Absolute, the sheet is part of the document and the browser scrolls it
     with everything else, exactly in step and with no JS in the loop at all.
     What moves by hand instead is the lit patch, whose edge is a 100px-wide
     gradient -- a frame of lag there is not something an eye can find. */
  .dots{
    position:absolute;inset:0;z-index:0;pointer-events:none;opacity:0;
    background-image:radial-gradient(circle at center, var(--dot-lit) .8px, transparent 1.1px);
    background-size:12px 12px;
    /* --mx/--my are DOCUMENT coordinates, since that is now the sheet's own
       frame of reference: the JS feeds them pageX/pageY and keeps y in step
       with the scroll. */
    /* The gradient is a fixed 300px tile that gets MOVED, rather than a
       document-sized gradient whose centre is rewritten every frame. .dots
       is inset:0 on a relative body, so its box is the whole document --
       about 4.5 million pixels of mask being re-solved per pointer frame at
       1280 wide. The area outside a non-repeating mask is alpha 0, which is
       what this gradient already resolved to past its 72% stop, so the
       painted result is the same picture. */
    -webkit-mask-image:radial-gradient(circle 150px at 150px 150px, #000 0%, rgba(0,0,0,.45) 45%, transparent 72%);
    mask-image:radial-gradient(circle 150px at 150px 150px, #000 0%, rgba(0,0,0,.45) 45%, transparent 72%);
    -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
    -webkit-mask-size:300px 300px; mask-size:300px 300px;
    -webkit-mask-position:calc(var(--mx,-500px) - 150px) calc(var(--my,-500px) - 150px);
    mask-position:calc(var(--mx,-500px) - 150px) calc(var(--my,-500px) - 150px);
    transition:opacity .5s ease;
  }
  /* Text for the accessibility tree only. The dashed stops say "not written
     up yet" in a border style, which is not a thing a screen reader can
     read, and the explanatory .aside exists on the homepage but not on
     /phases. */
  .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;}
  /* Skip link. Off screen until focused, and the first thing the keyboard
     reaches on both pages, because /phases otherwise starts with four tab
     stops of pinned furniture and no way past them. */
  .skip{position:absolute;left:-9999px;top:0;z-index:10;}
  .skip:focus{
    left:0;background:var(--paper);color:var(--ink);
    padding:10px 16px;border:1px solid var(--ink);border-radius:2px;
    outline:2px solid var(--red);outline-offset:2px;
  }
  main:focus{outline:none;}
  .dots.on{opacity:1;}
  @media (prefers-reduced-motion:reduce){.dots{display:none;}}

  /* The rail's fitted width is 6 x 112 + 5 x 12 = 732px. On the homepage it
     sits in the second track of .sec, which is min(vw,1120) - 60 - 200 - 40:
     at 1024 that is 724px, eight short, so the sixth stop was clipped with
     no edge and no scroll cue. Below 901 the bleed rule above already puts
     the cut on the screen edge; this is the same idiom for the range in
     between, at that range's 30px page padding. */
  /* Scoped to the phase page. On the homepage the rail is an element inside
     the measure, not a bar, so bleeding it 30px past the column put its
     hairline proud of every other element on the page and ran it off the
     right edge -- the "there is more, scroll" cue -- at a width where all six
     stops already fit and nothing scrolls. The homepage was only ever 8px
     short, so it gets the 8px instead: two pixels off each of the five gaps. */
  /* The phase page's own bleed is gone from here. What it was solving --
     the sixth stop clipped with no cue -- was a two-line stop needing
     112px; a one-line stop needs 104, and six of those plus the gaps fit
     inside the measure at every width in this range with room to spare, so
     nothing is cut and there is nothing to bleed. The homepage's 8px stays:
     its rail is an element inside the measure, not a bar. */
  @media (min-width:901px) and (max-width:1040px){
    .rail ol{column-gap:10px;}
  }


  @media (prefers-reduced-motion:no-preference){
    /* .creed sits directly under the CTA in the same hero and used to carry
       no animation at all, so it was at full ink from the first paint while
       the two lines above it were still invisible: the curtain read 1, 2,
       already there, 3. It goes last so the hero reveals top to bottom. */
    header,.head,.sub,.sub-lead,.cta,.hero .creed{opacity:0;animation:fade .6s cubic-bezier(.2,.7,.3,1) forwards;}
    .head{animation-delay:.08s;}
    .sub{animation-delay:.16s;}
    .sub-lead{animation-delay:.22s;}
    .cta{animation-delay:.30s;}
    .hero .creed{animation-delay:.36s;}
  }
  @keyframes fade{from{opacity:0;transform:translateY(9px);}to{opacity:1;transform:none;}}
  /* The header is the one box in that list that pins. Riding the 9px lift
     means the sticky bar is 9px down the screen for the first 600ms, and
     rail.js reads that rect to decide whether the bar is holding its place:
     arrive at /phases#develop and the test ran, saw 9, 5, 4, and settled on
     "not stuck" before the animation finished, so the bar sat pinned with no
     stuck edge under it until the reader's first scroll. It fades, it does
     not move. */
  @keyframes fadein{from{opacity:0;}to{opacity:1;}}
  @media (prefers-reduced-motion:no-preference){
    header{animation-name:fadein;}
  }
