/* ==========================================================================
   house.css — the house: one ground, one travelling light, no hard cuts.

   THE LAYER ORDER. This file is the last stylesheet the page loads:

     css/tokens.css    the palette, the type scale, the one accent
     css/base.css      the page's furniture
     css/motion.css    the one-shot reveal
     css/stage.css     the acts, the doors, the figures
     css/sequence.css  the pinned workbench — an override over stage.css
     css/house.css     THIS FILE — an override over that one

   So it is an override layer over an override layer, and it is written as
   one: every rule below restates something the four files above have
   already said. Nothing here is load-bearing on its own — take the <link>
   out and the page underneath is the page as it was, with the alternating
   bands, the folio and the one-shot reveal all back. Its companion is
   js/house.js, which writes the numbers these rules spend.

   It adds six things and takes two away:

     + the house light, which now travels down the whole document
     + a pointer light on the three doors
     + a 1px progress hairline under the header bar
     + the redrawn socket figure's palette
     + soft edges: the hero's print and both panel bands now arrive and
       leave instead of starting and stopping on a line -- see "the seams"
     + a viewport cap on the one screenshot, so all of it is on screen
     - the folio
     - the knife edge at every act boundary

   Nothing here invents a colour. --house is still the only accent and every
   value below is a token, a token mixed with transparent, or --deep and
   --ground spelled out where a gradient stop needed an alpha.
   ========================================================================== */

/* ====================================================== the folio is off ==
   The number at the head of every act is gone. sequence.js still writes
   `data-act` -- it is what keeps the doors' count honest -- but nothing
   draws it any more. Position is carried by the hairline under the bar
   instead: continuous rather than counted, which suits a page whose whole
   claim is that the frame does not step. */

.act[data-act]::before { content: none; }

/* ================================================ the light, page-length ==
   The hero's lamp stops being a hero effect. #houselight is one fixed,
   screen-blended pool; js/house.js writes --lamp-y (where the pool sits in
   the viewport, aimed at whichever act is nearest the middle of the screen)
   and --lamp-a (how far up it has faded, so it does not double the hero's
   own lamp). Everything else is painted here.

   THE WARMTH IS DELIBERATELY LOW AND IT WAS MEASURED, NOT CHOSEN. --house at
   8% under .46 opacity is an effective ~3.7% screen over the ground. That
   lifts --ground from L .0100 to .0144, which takes the page's smallest text
   -- --faint at 11-13.5px -- from 4.94:1 to 4.61:1 at the very centre of the
   pool. Still over 4.5, and it was measured off a render rather than
   reasoned about. Any warmer and the figure captions stop passing, which is
   the one thing this effect is not allowed to cost. */

#houselight {
  position: fixed;
  inset: 0;
  /* Above the sections (z-index:auto) so the light falls ON the page rather
     than behind its opaque panels; below the header (50), which is chrome and
     is not in the room. (It used to name the choice chip at 40 as well. The
     chip is gone -- see "the questions, answered" below.) */
  z-index: 3;
  pointer-events: none;
  mix-blend-mode: screen;
  opacity: calc(.46 * var(--lamp-a, 0));
  background:
    radial-gradient(62vw 50vh at 50% var(--lamp-y, 46%),
      color-mix(in srgb, var(--house) 9.5%, transparent) 0%,
      color-mix(in srgb, var(--house) 4.5%, transparent) 44%,
      transparent 76%);
}

/* An engine with no color-mix gets a flat, even softer wash rather than no
   light at all -- the same shape, one stop fewer. */
@supports not (color: color-mix(in srgb, #000 50%, transparent)) {
  #houselight {
    background: radial-gradient(62vw 50vh at 50% var(--lamp-y, 46%),
      rgba(224, 175, 104, .08) 0%, transparent 76%);
  }
}

/* The still version, and it is the same light -- parked where a house lamp
   would be left, not switched off. js/house.js reads the same query and never
   attaches the scroll listener, so nothing is being computed behind it. */
@media (prefers-reduced-motion: reduce) {
  #houselight { opacity: .46; --lamp-y: 46%; }
}
.no-js #houselight { opacity: .46; }

/* ------------------------------------------------- position, as a hairline
   1px of --house under the bar, scaled to how far down the document the
   reader is. It is the folio's job done without a number: you can see how
   much is left, and it never claims a total that a door has just changed.
   scaleX rather than width, so the scroll path writes one property and the
   compositor does the rest. */

.bar { position: sticky; }
.bar::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: -1px;
  height: 1px;
  background: var(--house);
  opacity: .5;
  transform: scaleX(var(--lamp-p, 0));
  transform-origin: 0 50%;
  pointer-events: none;
}
/* Without JS it would sit at zero width for ever, which is a line that lies
   about where you are. Take it out. */
.no-js .bar::after { display: none; }

/* ========================================================= the sequence ==
   The beats lost their paragraphs; what is left is a heading and, only where
   the picture cannot define a word, one short line. The stacked fallback
   needs a little less air between them now that each is two lines.

   Three of those lines then got the job of an act this prototype deleted.
   The "three words" glossary defined WAIT, the diff and `ok` on a screen of
   its own, three acts after the capture that prints all three of them; the
   claims now ride the beats that ring those exact rails, so the word is on
   screen while the sentence about it is being read. That is the whole
   difference between showing and asserting, and it cost the beats one line
   each -- still inside the 8.4em floor sequence.css gives the pinned
   stack, because the release beat was already two lines plus a framenote. */

/* Scoped to `.seq-live` on purpose. sequence.js writes that class only when
   the pinned layout is actually on, and only there is a beat centred
   (`.seq-live .beat { text-align: center }`) -- so auto margins put the line
   under the middle of its own heading. In the stacked fallback a beat is
   flush left inside a 66ch column, and centring a 46ch paragraph inside it
   indents the copy ~90px away from the heading it belongs to. That fallback
   is what a phone, a short window, a reader with no JavaScript and a reader
   who asked for less motion all get, which is to say most of the ways this
   page is read on something other than a laptop. Measured at 1400x900 under
   prefers-reduced-motion: heading at x=389, paragraph at x=477. */
.seq-live .beat p { max-width: 46ch; margin-left: auto; margin-right: auto; }

/* FAIL(2) arrives in the processes beat as the other half of `ok`. It is a
   failure token and the daemon draws it in --danger; css/acts.css has the
   rule but this page has never loaded that file, so the code element would
   otherwise inherit --ink and read as just another machine word. 6.6:1 on
   --ground, and the house light only ever lifts that. */
.beat code.k-fail { color: var(--danger); }

/* The head lost its "keep scrolling" sentence. The instruction is now the
   same mark the hero uses to hand over, which costs no words at all. */
.cue-seq { text-align: center; margin: 30px 0 0; }
.cue-seq span {
  display: inline-block; width: 11px; height: 11px;
  border-right: 1.5px solid var(--house);
  border-bottom: 1.5px solid var(--house);
  transform: rotate(45deg);
  animation: cue-fall 2.9s var(--ease) 2;
}
@media (prefers-reduced-motion: reduce) { .cue-seq { display: none; } }

/* Two more captions that describe what is directly above them. The matrix
   draws its own dated note inside the SVG and the figcaption under it says
   the same date; one of them goes, and the one that stays is the one that
   also asks to be corrected. */
.mx-note { display: none; }

/* The rail figure captions itself in js/figures.js -- a whole sentence
   describing the row that just turned red, directly under the row that just
   turned red. That is the exact thing this prototype is cutting, and the
   heading above it already says it. */
.rail-cap { display: none; }

/* ============================================================ the doors ==
   The three rows lost their explanatory paragraphs: the heading is the
   problem, stated, and a second sentence only softened it.

   In their place, the light -- the same lamp as the hero and the same lamp as
   the page, now on the one control on the page.

   IT IS A BAND, NOT A POOL, AND IT BELONGS TO THE BLOCK. Both of those were
   the other way round and both were wrong:

     · A pool has a centre, and a centre is a place, so the reader's eye was
       pulled to the point under their own cursor. What the control needs to
       say is only THIS ROW. The band has no centre: it is at full strength
       across the column and fades out above, below, and off both sides.

     · The pool was drawn inside the row, in a layer the exact size of the
       row, under `overflow: hidden`. A 190px circle in a 90px box is sliced
       flat top and bottom -- the gradient never finished, and what the eye
       read was a rectangle that had got slightly warmer. (There was a real
       rectangle underneath it too, `background: #171e29` in stage.css. That
       is gone.) The layer is on the container now and overhangs the row on
       every side, so nothing clips the light. It also has to be there: the
       collapse below needs `overflow: hidden` on the rows, and a row that
       clips its contents clips its own light.

   js/house.js moves the band to whichever row is live. Nothing follows the
   pointer inside a row, which is why this is the one effect on the page that
   is identical under prefers-reduced-motion. */

.doors {
  position: relative;
  /* The band is a child at z-index -1, so `isolation` is what keeps it from
     falling behind the section's own ground and out of sight. */
  isolation: isolate;
}

/* The spill has to stop at the viewport, not past it. The band overhangs the
   block horizontally on purpose, but on a 390px screen the block is nearly
   the width of the window, so the overhang is 90px of real horizontal scroll.
   `body { overflow-x: hidden }` in base.css does not catch it -- the document
   element is what scrolls. Clipped at the section, which is full-bleed, so
   the light still falls past the column at every width and its ends are cut
   where the screen ends, which is not an edge anyone can see. `clip` rather
   than `hidden`, so the section does not quietly become a scroll container. */
#doors { overflow: clip; }
@supports not (overflow: clip) { #doors { overflow: hidden; } }

.door {
  position: relative;
  /* stage.css used to slide the row 8px right on hover. On a page whose
     argument is that the frame does not move, the one control should not jump
     either -- and the light is a better signal than the shove was. */
  padding-left: 4px;
}
.door:hover,
.door[aria-pressed="true"] { background: none; }

.doors::before {
  content: "";
  position: absolute;
  /* 18vw past the column on each side, and 46px past the row at each end, so
     the fade finishes on the ground rather than at an edge. */
  left: -18vw;
  right: -18vw;
  top: calc(var(--band-top, 0px) - 46px);
  height: calc(var(--band-h, 0px) + 92px);
  z-index: -1;
  pointer-events: none;
  background:
    linear-gradient(to bottom,
      transparent 0%,
      color-mix(in srgb, var(--house) 8%, transparent) 34%,
      color-mix(in srgb, var(--house) 8%, transparent) 66%,
      transparent 100%);
  /* Horizontally the band is at full strength across the column and gone
     before the reader can find either end of it. A mask rather than a second
     gradient, so the vertical fade above is not restated in two directions
     and left for someone to keep in sync. */
  -webkit-mask-image: linear-gradient(to right, transparent 0%, #000 26%, #000 74%, transparent 100%);
  mask-image: linear-gradient(to right, transparent 0%, #000 26%, #000 74%, transparent 100%);
  opacity: 0;
  transition: opacity .28s var(--ease), top .3s var(--ease), height .3s var(--ease);
}
/* Live under the pointer or the focus ring; resting on the row already
   answered, so the block still reads as answered once the pointer has gone. */
.doors.lit::before  { opacity: 1; }
.doors.rest::before { opacity: .8; }

/* #houselight and the socket figure both get a flat rgba fallback where
   color-mix is missing; the band needs one too, or the whole `background`
   declaration is dropped and the one control on the page has no light at all
   on that engine. */
@supports not (color: color-mix(in srgb, #000 50%, transparent)) {
  .doors::before {
    background:
      linear-gradient(to bottom,
        transparent 0%, rgba(224, 175, 104, .08) 34%,
        rgba(224, 175, 104, .08) 66%, transparent 100%);
  }
}

/* No chase to switch off. The band does not follow a pointer within a row --
   it is the row, lit -- so this is the same effect at the same strength,
   arriving at once instead of sliding. */
@media (prefers-reduced-motion: reduce) {
  .doors::before { transition: none; }
}

/* The cost label is written by js/house.js from the sections themselves. It
   is the one number left on the page, so it gets to look like a number.

   It is also the only --faint text inside a door, and --faint over a lit row
   measures 3.7:1 -- so the row lighting up takes it to --muted, which is
   4.6:1 on the same ground. The light is not allowed to cost a reader the
   one label that says what the button does. */
.door .cost { font-variant-numeric: tabular-nums; }
.door:hover .cost,
.door:focus-visible .cost,
.door:active .cost { color: var(--muted); }
.door[aria-pressed="true"] .cost { color: var(--house); }

/* --------------------------------------------- the questions, answered ---
   There is no "reading as" bar any more. It was a sticky strip under the
   header carrying a label, a value and two buttons, and it stayed in front of
   the reader for the entire rest of the document -- a caption for a decision
   three inches below it that the doors were already showing, in --house, with
   `aria-pressed`.

   The control is its own receipt instead. The two questions you did not
   answer fold away, the one you did stays exactly where it was, and one line
   appears under it with what the choice cost and the two ways back. Scroll
   once and all of it is behind you, which is what a decision already made
   should do. js/stage.js drives it. */

.doors .door {
  /* A folding row has to clip its own contents. This is also the reason the
     band above is on the container. */
  overflow: hidden;
  transition: max-height .42s var(--ease), opacity .26s var(--ease),
              padding .42s var(--ease), border-color .42s var(--ease);
}
/* The open height is measured and written by js/stage.js: max-height cannot
   transition from a number to `auto`, and a guessed ceiling makes the fold
   finish early on the way back open. */
.doors .door.fold {
  max-height: 0 !important;
  padding-top: 0;
  padding-bottom: 0;
  border-top-color: transparent;
  opacity: 0;
  pointer-events: none;
}
/* The row that is left keeps its rule to the line under it, and the line
   brings its own -- so the bottom of the block is drawn once. */
.doors.picked .door { border-bottom: 0; }

.pick-line {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px 18px;
  max-width: 900px;
  margin: 0 auto;
  padding: 16px 4px 0;
  border-top: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.7;
  letter-spacing: .04em;
  color: var(--faint);
}
.pick-line[hidden] { display: none; }
.pick-line .cost { font-variant-numeric: tabular-nums; }
.pick-line .acts { margin-left: auto; display: flex; gap: 16px; }
/* Text buttons, not pills: a pill here would be a second call to action on a
   page that has one. */
.pick-line button {
  appearance: none;
  background: none;
  border: 0;
  border-bottom: 1px solid var(--rule);
  padding: 0 0 2px;
  font: inherit;
  color: var(--muted);
  cursor: pointer;
  transition: color .2s, border-color .2s;
}
.pick-line button:hover { color: var(--house); border-bottom-color: var(--house); }

@media (max-width: 700px) {
  .pick-line .acts { margin-left: 0; }
}

/* ====================================================== the socket figure ==
   Drawn by js/house.js into #fig-socket. The old drawing was a two-storey
   comparison with a legend and two tallies -- a slide from a different deck.
   This one has the page's own vocabulary and nothing else: a dark ground,
   one warm light falling on one slot, and the workbench's own four zones as
   the thing on the other side of it.

   Every mark is load-bearing. Three names, three lines, one slot, one beam,
   one frame, and three words with a line through them. */


/* The three routes in. One colour, because they are one kind of thing --
   the old drawing gave each machine a hue and made the picture about the
   machines instead of about the socket. */

/* The picture is the argument, and above 700px it makes it: three names,
   one slot, one screen, three words struck through. Below that the SVG
   renders at 0.35 and its type lands under 4px -- unreadable, so the "no
   port, no relay, no token" claim would simply leave the page for anyone on
   a phone. The live page said it in a figcaption; this says it there too,
   and only there. Nine words is what the cut can afford to spend to keep a
   security claim on the page. */

/* ---------------------------------------------- the socket exhibit ------
   Static styling for the connection diagram. Packet motion is controlled
   by visibility, reduced-motion preferences and its Pause button in house.js. */
.ex-plate  { fill: var(--sunken); stroke: var(--rule); stroke-width: 1.2; }
.ex-lit    { fill: color-mix(in srgb, var(--house) 10%, var(--sunken)); stroke: var(--house); stroke-width: 1.3; }
.ex-slot   { fill: var(--house); }
.ex-line   { fill: none; stroke: var(--house); stroke-width: 1.1; opacity: .5; }
.ex-hair   { fill: none; stroke: var(--rule); stroke-width: 1; }
.ex-zone   { fill: none; stroke: var(--rule); stroke-width: 1; opacity: .8; }
.ex-zone.on { stroke: var(--house); opacity: .85; }
.ex-name   { fill: var(--muted); font-size: 13px; font-family: var(--mono); }
.ex-key    { fill: var(--house); font-size: 12.5px; font-family: var(--mono); letter-spacing: .08em; }
.ex-small  { fill: var(--faint); font-size: 11px; font-family: var(--mono); letter-spacing: .04em; }
.ex-tag    { fill: var(--faint); font-size: 10px; font-family: var(--mono); letter-spacing: .24em; }
.ex-tab    { fill: var(--faint); font-size: 10.5px; font-family: var(--mono); }
.ex-tab.on { fill: var(--house); }
.ex-absent { fill: none; stroke: var(--rule); stroke-width: 1; stroke-dasharray: 3 4; }
.ex-absent-t { fill: var(--faint); font-size: 11.5px; font-family: var(--mono); opacity: .8; }
.ex-glow-a { stop-color: var(--house); stop-opacity: .20; }
.ex-glow-b { stop-color: var(--house); stop-opacity: 0; }
.ex-glow { opacity: .85; }

.sk-cap { display: none; }
@media (max-width: 700px) { .sk-cap { display: block; } }

@supports not (color: color-mix(in srgb, #000 50%, transparent)) {
}

/* ============================================================== phones === */

@media (max-width: 700px) {
  /* sequence.css moves .pick into column 2 as a flex row on a phone; the
     cost keeps its own baseline there. Nothing else about the doors changes:
     the band is the width of the row and the row is the width of the screen,
     which is the same effect at any width. (There was a `--door-r: 220px`
     here, widening the pool to suit a narrower row. There is no pool and no
     radius any more.) */
  /* A pool sized in vw is a smear on a portrait screen; hold it to something
     that still reads as a lamp aimed at one act. */
  #houselight { background:
    radial-gradient(120vw 38vh at 50% var(--lamp-y, 46%),
      color-mix(in srgb, var(--house) 9.5%, transparent) 0%,
      color-mix(in srgb, var(--house) 4.5%, transparent) 44%,
      transparent 76%); }
}

/* ============================================================= the seams ==
   THE CLIENT: "the transition between the pages should not be hard cuts
   actually 4 does that partly really well take some from 4."

   What was actually wrong. Every act on this page is about a viewport tall
   and the page separates them by alternating two grounds -- body is --deep
   (#0c1017) and `.panel` is --ground (#151a23). That alternation was drawn
   with a knife: sampled off a render, the pixel row at an act boundary went
   (12,16,23) -> (21,26,35) in ONE ROW, five times down the document. The
   hero was the worst of the five, because css/sequence.css lands the print's
   scrim on --ground while the act underneath it is on --deep -- so the
   woodblock did not dissolve into the page at all, it stopped on a line
   across the full width of the screen.

   WHAT WAS TAKEN FROM PROTOTYPE 4, and it is three things:

     1. Its diagnosis of the hero. p4's own comment: "the last stop of the
        hero scrim was --ground, which over a --deep page draws a visible
        horizontal seam right under the hero." Same bug here, same fix --
        restate the scrim's tail in the ground that is actually below it.

     2. Black between the acts as MATERIAL rather than absence. In p4 that
        went all the way: `.panel { background: transparent }`, one ground,
        the light as the only separator. Here it goes halfway on purpose --
        see "what was left" below.

     3. The follow-spot's LAG (in js/house.js). p4 eases the light toward its
        aim instead of writing it, so the light arrives after the reader
        does. That is the thing that actually bridges a boundary.

   WHAT WAS DELIBERATELY LEFT IN PROTOTYPE 4:

     · Its ground. p4 prints the whole page on --deep and deletes the panel
       bands entirely. That works there because it also has a house rail, a
       follow-spot at .36 and 100vh acts to carry the structure. Here the
       folio is already gone and the panels are the only rhythm left; erasing
       them would leave one undifferentiated dark scroll. So the bands stay
       and only their EDGES go.
     · The operator's swing (--spot-x), which leans the pool sideways while
       it travels. This page's pool is centred and stays centred.
     · The grain dither, the bigger type, the 100vh acts, the house rail, the
       whole-screen concession. All of that is p4's mood, not its mechanism.

   Nothing below is darker, slower or warmer than what it replaces. Every
   value is --deep or --ground, which the page was already made of. */

/* ------------------------------------------------- the hero hands over ---
   css/sequence.css restates .backdrop::after and ends its linear layer on
   `var(--ground)`. The section below the hero is #workbench, which is not a
   .panel, so it is on body's --deep -- and the print therefore ended on a
   9-level step across 1400px. Only the tail of the linear layer moves: the
   radial above it is what keeps 62px of headline legible over a woodblock
   and is not touched, and the linear's top is what holds the header bar off
   the lanterns and is not touched either.

   The two literals are --deep and --ground spelled out, because a gradient
   stop needs a colour with an alpha and there is no token for "--deep at
   86%". They are the same rgb() values tokens.css defines. */
.backdrop::after {
  background:
    radial-gradient(88% 60% at 50% 38%,
      rgba(12, 15, 22, .62) 0%, rgba(12, 15, 22, .38) 52%, rgba(12, 15, 22, .06) 100%),
    linear-gradient(180deg,
      rgba(21, 26, 35, .72) 0%,
      rgba(21, 26, 35, .28) 26%,
      rgba(21, 26, 35, .52) 60%,
      rgba(12, 16, 23, .72) 82%,
      rgba(12, 16, 23, .94) 93%,
      var(--deep) 100%);
}

/* ------------------------------------------------ one ground, no bands --
   THE CLIENT: "make the background like a smooth gradient instead of a hard
   line."

   The band system is the thing being removed here, not tuned again. The page
   used to separate acts by alternating two grounds -- body --deep, `.panel`
   --ground -- and every fix since has been an attempt to hide the edge that
   alternation necessarily has. A 240px ramp at each end of each band is still
   a band: it has a beginning, and a beginning is what the eye finds.

   So there are no bands. ONE gradient down the whole document, at a much
   lower frequency than the acts, so the ground is always drifting and never
   arrives anywhere. Nothing about it lines up with an act boundary, which is
   the point: a tone change that coincides with a structural edge reads as a
   line even when it is a ramp, and one that does not coincide with anything
   reads as depth.

   Sized to the body box rather than the viewport, so it spans the document
   once instead of repeating per screen -- a viewport-sized version would put
   an identical tonal cycle in every screenful, which is a pattern, and a
   pattern is a rhythm the reader can feel arriving. */
body {
  background: linear-gradient(180deg,
    var(--deep)   0%,
    var(--ground) 13%,
    var(--deep)   27%,
    var(--ground) 44%,
    var(--deep)   59%,
    var(--ground) 76%,
    var(--deep)   90%,
    var(--ground) 100%) no-repeat;
  background-attachment: scroll;
}
/* The panels stop painting a ground of their own. They keep every other job
   they had -- they are still the acts, they still carry --near, the doors
   still live in one. */
.panel { background: transparent; }

/* ------------------------------------------------- the acts hand over ----
   --near is written per act by js/house.js: 1 while the act owns the screen,
   easing down to .34 as it leaves. Two acts are therefore legible at once
   for the length of a crossing, which is the difference between a page that
   cuts and a page that dissolves.

   Its ramp was re-measured after --rv landed and re-cut, because it had
   become the roughest thing left: it was a straight line between two hard
   clamps, so on a 40px ladder it moved by 0.000 for 99% of samples and then
   by 0.102, and it reached the .34 floor while a sliver of the act was still
   on screen. It is now the same smoothstep --rv uses, over a wider window
   -- peak rate 0.0996 per 40px, just under what it replaced, no kink at
   either end, full strength held exactly where it was, and the floor moved
   out past the moment the act's last row leaves. The arithmetic and the
   numbers behind each of those claims are in js/house.js.

   It is set on the act rather than on its inner block so that the act's own
   background ramp and its contents fade as one object; opacity on a section
   makes a stacking context, which is exactly what is wanted here -- the
   fixed house light sits above it and is not dimmed with it.

   No transition: the value is already rewritten every frame by an eased
   loop, and a transition on top of that would fight it. Unset -- no JS, or
   reduced motion -- resolves to 1, which is the page as it was. */
.act { opacity: var(--near, 1); }
@media (prefers-reduced-motion: reduce) { .act { opacity: 1; } }

/* --------------------------------------------- and so does every block ---
   MEASURED FIRST, on a 40px ladder at 1400x900, down the page and back up
   it, reading computed opacity rather than looking at stills:

     · every block of copy in every act stepped 0.000 -> 1.000 in ONE 40px
       sample -- eight blocks, eight switches, up to 11.4 luminance levels in
       that single step;
     · --near, the rule directly above, was 1.000 for BOTH acts through the
       whole visible part of every crossing. It only moves once an act is
       more than half off screen, which is once there is nothing left of it
       to see. Biggest step it ever made: 0.102 per 40px;
     · and the reveal never reversed: 116 (position, block) pairs read
       differently going up than going down.

   So the crossing was not "a soft ground plus a fade", it was a soft ground
   plus a SWITCH, and a switch is what the eye reports as a cut. --near was
   not fighting it; it was asleep.

   `flow` on <html> is written by js/house.js one paint after --rv has been
   given to every block, so the switch-over is invisible, and it is never
   written at all if the module fails, if scripting is off, or if the reader
   asked for less motion. In all three of those cases the rules in
   css/motion.css below are untouched and the page reveals exactly as it does
   today. That is why this is an override and not an edit to motion.css.

   opacity AND a few px of drift, because a thing that only dims reads as a
   light going out and a thing that also moves reads as a thing leaving.
   .rvf keeps css/motion.css's own distinction and only fades: large media
   looks wrong sliding. */

html.flow .rv,
html.flow .rvf {
  /* No transition. The value already changes once per scroll frame, and a
     transition on top of a per-frame value is a second system arriving late
     -- which is the bug this block exists to remove, not to re-import. */
  opacity: var(--rv, 1);
  transition: none;
}
html.flow .rv  { transform: translateY(var(--rvt, 0px)); }
html.flow .rvf { transform: none; }

/* The pinned sequence is driven as one object -- see js/house.js -- and it
   reads --pin-rv rather than --rv. Custom properties inherit: the capture
   inside it is a .rvf whose own rule is `opacity: var(--rv, 1)`, so a --rv
   set here would cascade into it and be applied a second time. Measured
   before it was caught: .72 on the sticky and .72 again on the capture,
   .518 in all, the one picture on the page fading twice as fast as the words
   around it.

   --pin-dy is the difference between where `position: sticky` puts the block
   and where a rounded version of the same corner would: a few px, only within
   about 80px of scroll either side of the pin's two ends, zero everywhere in
   between. It is a transform rather than a `top` so it stays off the layout
   path, and it does not disturb sticky's own arithmetic -- sticky is resolved
   in layout, transforms are applied after it. Unset (no JS, reduced motion,
   or the stacked fallback where nothing is sticky at all) it is 0px, which is
   the page exactly as it was. */
html.flow .seq-sticky {
  opacity: var(--pin-rv, 1);
  transform: translateY(var(--pin-dy, 0px));
}

/* A door can leave two panels visually adjacent: path=embed hides the three
   acts between the doors and Caliper. Two soft edges meeting make a shallow
   dark trough, which reads as "two acts" -- which is true, and it is what
   renders.

   The rule below is insurance rather than a repair, and it matters more than
   it did: stage.css's `.panel + .panel { border-top: 1px }` used to be unable
   to match on this page in any door state, because the choice chip was a
   sibling between the two panels and a hidden section is still a sibling. The
   chip is gone now, so the two panels ARE adjacent siblings and that rule
   would draw a hard 1px line at the one boundary this whole block exists to
   soften. This is the line that stops it. */
.panel + .panel { border-top: 0; }

/* An engine with no calc() in a gradient stop gets the band it always had
   rather than a broken background. */
@supports not (width: calc(100% - 150px)) {
  .panel { background: var(--ground); }
}

/* ------------------------------------------------------------ the fold ---
   The footer's 1px rule is the last edge on the page and it stays: a footer
   is not an act, and the page ending on a line is the one place a line is
   the honest mark. */

/* =================================================== the whole capture ====
   THE CLIENT: "in the screen shot show all things on the screen."

   Measured, at three viewports, before anything was changed:

     the Caliper screenshot   1400x900: 1360 x 852, in the 852px left under
                              the header bar. Exactly the fold, with nothing
                              in hand -- and at 1400x700 it was 852 tall in a
                              652px window, so a third of the picture was
                              always off-screen and the reader never saw the
                              thing the act is about in one look.
     the pinned capture       1400x900: 934 x 533 inside a 1360 x 579 row.
                              Whole, with air. 1400x700: 584 x 333, whole but
                              at scale 0.44. See the note at the bottom.
     #fig-socket              364 tall at 1400. Whole.
     #fig-matrix              479 tall at 1400. Whole.
     the tmux rail            182 tall. Whole.

   So there was one real offender and it is fixed here. The others are
   reported rather than adjusted, because they already fit.

   The cap is against the VIEWPORT, not the column, and the number in it is
   the header bar plus SCROLL LATITUDE. "Whole on screen" is not a state the
   page can hold the reader in -- this act is 1.5 screens tall and nothing
   snaps -- so what matters is how many pixels of scroll exist where the
   picture is entirely visible. At a cap of bar + 96px the picture came out
   756 tall in the 852 left under the bar, which is 96px of latitude: less
   than one notch of most scroll wheels, so a reader could step straight over
   the only position where the whole thing was on screen. bar + 152 gives 700
   tall at a 900px window (1117 x 700 -- width stops being the binding
   constraint) and 500 at a 700px one, and 152px of latitude at BOTH, because
   the cap and the room are the same subtraction. Above 900px of natural
   height the rule does nothing at all.

   `svh` second, on its own line, because a phone's URL bar makes `vh` the
   TALL viewport -- the one measurement guaranteed not to be on screen. An
   engine that has not heard of svh drops that line at parse time and keeps
   the vh one.

   AND THAT IS WHY 144px IS SPELLED OUT RATHER THAN `var(--bar) + 96px`.
   Written with the token it read `calc(100svh - var(--bar) - 152px)`, and a
   var() inside a calc() defers the whole declaration's validity to computed-
   value time: it PARSES, so it wins the cascade over the vh line above it,
   and only then does the unknown unit make it invalid -- at which point
   max-height falls to its initial value, `none`, not to the declaration it
   overrode. Measured in Chrome 73: computed max-height `none` and the
   picture back at its full 852. 200 is var(--bar) at its declared 48px plus
   the 152 of latitude; if --bar ever moves, this moves with it by hand. */

.media.whole { width: fit-content; max-width: 100%; margin-left: auto; margin-right: auto; }
.media.whole img {
  display: block;
  width: auto;
  max-width: 100%;
  height: auto;
  max-height: calc(100vh - 200px);
  max-height: calc(100svh - 200px);
}
/* No fit-content: the figure falls back to full width and the image to the
   column, which is what it did before this block existed. */
@supports not (width: fit-content) {
  .media.whole { width: auto; }
}

/* ------------------------------------------------------ the short window --
   At 1400x700 the pinned panel is 652px tall and the capture's natural size
   is 1337 x 763, so the scale is bound by height and nothing else: the row
   had 379px of the 652 and the capture landed at 0.44, which is a 3.5px cell
   -- structure still, text no longer. That is inherent to standing a
   1337-wide terminal frame inside a 652-tall panel and it cannot be fixed by
   scaling; what CAN be given back is the furniture around it.

   Below 820px of window the pinned panel's own padding comes in: the
   sequence's inner box, the foot's lead, and the gap under the ticks. That
   is 34px handed straight to the picture.

   The tick row was the other 40px on the table and it is NOT taken. It is
   five buttons that name the four zones and jump to them -- a control and a
   map, not furniture -- and trading a control away for a 12% bigger capture
   is not a trade this page should make at one window size and not another.
   The residual is stated in the handoff instead of hidden.

   Nothing here runs at 1400x900, which is where the page is read. */
@media (max-height: 820px) and (min-width: 901px) {
  .seq-live .seq-in   { padding-top: 6px; padding-bottom: 10px; }
  .seq-live .seq-foot { padding-top: 8px; }
  .seq-live .ticks    { margin-bottom: 10px; }
}


/* ------------------------------------------- the print fills the frame --
   stage.css: `@media (min-height: 1000px) { .hero { min-height: 880px } }`.
   Measured at 1400x1400 before this override, the woodblock ended at y=885
   -- 63% of the screen -- and the first screenful finished on bare ground.
   svh rather than vh because a phone's toolbars make vh the tall value and
   the print would be cropped by the chrome that is covering it; the plain
   vh line above it is the fallback for engines without svh. The down cue
   keeps the "there is more" hint that the 88vh gap used to give. */
.hero { min-height: 100vh; min-height: 100svh; }
@media (min-height: 1000px) { .hero { min-height: 100vh; min-height: 100svh; } }

/* ------------------------------------------ scrims end on nothing ------
   Every stop that used to name --deep or --ground now ends on transparent.
   With one gradient down the document there is no single colour that is
   "the ground" any more, so a scrim that ends on one is right at exactly
   one scroll position and wrong everywhere else. */
.backdrop::after {
  background:
    radial-gradient(88% 60% at 50% 38%,
      rgba(12, 15, 22, .62) 0%, rgba(12, 15, 22, .38) 52%, rgba(12, 15, 22, .06) 100%),
    linear-gradient(180deg,
      rgba(12, 15, 22, .74) 0%, rgba(12, 15, 22, .28) 26%,
      rgba(12, 15, 22, .54) 66%, rgba(12, 15, 22, 0) 100%);
}
.closing .plate::after {
  background: linear-gradient(180deg,
    rgba(12, 15, 22, .96) 0%,
    rgba(12, 15, 22, .48) 38%,
    rgba(12, 15, 22, .62) 78%,
    rgba(12, 15, 22, 0) 100%);
}


/* --------------------------------------- the images fade, not the ground --
   Ending the scrims on `transparent` was half the fix and it exposed the
   other half. A scrim is ground painted OVER a picture: ending it on a
   colour hides the picture's edge by covering it, which is why it had to
   name --deep, and why it broke the moment the ground stopped being one
   colour. Ending it on transparent stops it lying about the ground -- and
   leaves the picture itself at full strength exactly where the hero ends,
   so the print gets cut off by a boundary instead of dissolving through it.
   Measured after the first fix: a 34-level one-row step at the hero's foot.

   So the picture's OWN alpha does the fading. A mask on the whole backdrop
   takes the image, the lit layer and the lamp down together, and whatever
   the page's gradient happens to be at that scroll position shows through
   underneath -- correct at every position and every viewport, because it
   never has to know what colour it is dissolving into. The scrim keeps only
   the job it is actually for: holding contrast under 62px type. */
.backdrop {
  -webkit-mask-image: linear-gradient(180deg, #000 0%, #000 58%, rgba(0,0,0,.55) 82%, transparent 100%);
  mask-image: linear-gradient(180deg, #000 0%, #000 58%, rgba(0,0,0,.55) 82%, transparent 100%);
}
/* Same argument at the other end of the page: the closing print arrives out
   of the ground rather than starting on an edge. */
.closing .plate {
  -webkit-mask-image: linear-gradient(180deg, transparent 0%, rgba(0,0,0,.55) 16%, #000 42%, #000 100%);
  mask-image: linear-gradient(180deg, transparent 0%, rgba(0,0,0,.55) 16%, #000 42%, #000 100%);
}

/* SSH keeps readable labels at phone widths and only animates while in view. */
#fig-socket { text-align: center; }
.ssh-packet { fill: var(--house); }
.ssh-label-bg { fill: var(--ground); stroke: var(--rule); }
.ssh-toggle {
  margin: 14px auto 0; padding: 8px 12px; border: 1px solid var(--rule);
  border-radius: var(--pill); background: transparent; color: var(--faint);
  min-height: 44px; font: 14px var(--mono); cursor: pointer;
}
.ssh-toggle:hover { color: var(--house); border-color: var(--house); }
@media (max-width: 700px) {
  #fig-socket .ssh-diagram { max-width: 320px; margin: 0 auto; }
  .ssh-diagram .ex-name { font-size: 16px; }
  .ssh-diagram .ex-key, .ssh-diagram .ex-small { font-size: 14px; letter-spacing: 0; }
  .ssh-diagram .ex-tab { font-size: 13px; }
}

.mobile-comparison { display: none; }
.mobile-comparison-tip { position: fixed; z-index: 100; width: max-content; max-width: min(280px, calc(100vw - 24px)); padding: 12px 16px; border: 1px solid var(--rule); border-radius: 8px; background: var(--surface); color: var(--ink); font: 16px/1.5 var(--sans); box-shadow: 0 8px 28px #0006; pointer-events: none; }
@media (max-width: 900px) {
  body { font-size: 18px; }
  .hero .sub, .head .sub, .keyed p { font-size: 19px; line-height: 1.6; }
  .hero .gloss, .eyebrow, .mobile-app-view .where { font-size: 14px; }
  .capture-note, .release-note, .mx-hint, figure figcaption, .hero .meta, .hero .fine, footer p, footer .links a { font-size: 16px; line-height: 1.6; }
  #install .fine { font-size: 16px !important; }
  .install code { font-size: 16px; }
  .install button, .bar nav a { font-size: 14px; }
  #fig-matrix svg.mx { display: none; }
  .mobile-comparison { display: table; border-collapse: separate; border-spacing: 0; table-layout: fixed; width: 600px; font: 16px/1.45 var(--sans); color: var(--muted); }
  .mobile-comparison th, .mobile-comparison td { padding: 12px 8px; border-bottom: 1px solid var(--rule); text-align: center; }
  .mobile-comparison th { font-weight: 500; }
  .mobile-comparison th:first-child { position: sticky; left: 0; z-index: 1; width: 180px; background: var(--ground); text-align: left; padding-left: 0; }
  .mobile-comparison thead th { font-size: 17px; color: var(--ink); }
  .mobile-comparison thead th:nth-child(2) { color: var(--house); }
  .mobile-comparison td:nth-child(2) { border-right: 1px solid var(--rule); }
  .mobile-comparison button { display: inline-grid; place-items: center; width: 44px; height: 44px; padding: 0; border: 0; background: none; color: inherit; cursor: pointer; }
  .comparison-dot { display: block; width: 16px; height: 16px; border-radius: 50%; border: 1.5px solid currentColor; }
  .comparison-dot.self { color: var(--house); }
  .comparison-dot.level-1 { background: linear-gradient(90deg, currentColor 50%, transparent 50%); }
  .comparison-dot.level-2 { background: currentColor; }
  .mx-hint { display: block; }
}
