/* generated by scripts/build-board-thumbs.mjs from
 * public/lab/pressing-home.html — do not edit. Tune the lab. */

  /*
    The homepage, stratified. LAB FILE v4 — the design surface.

    v2 mixed everything into one feed and the mix read as a mess as a
    RESTING state. v3 fixed the state: idle, the page is a magazine of
    strata (the work, the practice, the board), each type in its own
    register; asked, matches from every stratum deal at the top.

    v4 fixes the chrome. v3 pinned the question in a second bar under
    the masthead — two stacked strips, and the lower one had to carry
    paper, which is the one surface the burn pill cannot act on. So the
    question moved INTO the masthead as its middle zone. One bar, still
    transparent, the feed still running under it. The three links it
    replaced did not die: they became queries, riding the same rotation
    as "marble" and "does this company do master bedrooms?".

    The bar itself is a verbatim lift of the shipped masthead — same
    16px Helvetica, same pill, same three-stage heat, same reversal —
    because it IS the shipped masthead with a question where the links
    were. The matcher is what went conversational: common words
    set aside, best tier wins unless the tier is thin, and the narrator
    reports all of it — where each word landed, what found nothing,
    what was skipped. Tune HERE first, port second.
  */
  :root {
    --paper: #FFFFFF;
    --ink: #000000;
    /* The small-type tokens scale with the viewport the way the
       statement does, rather than sitting at one fixed px. The captions
       under the thumbnails and the whole left rail read these, so the
       secondary type shrinks on a narrow screen instead of staying
       laptop-sized next to copy that has already come down. Max holds at
       the old fixed value, so desktop is unchanged; the floor is the
       mobile size. --lbl barely travels on purpose — a 10px label has
       nowhere to shrink to and stay legible. */
    --lbl: clamp(9px, 0.75vw, 10px);
    --lbl-track: 0.04em;
    --lbl-weight: 500;
    /* ONE SMALL SIZE, not a ramp. 12 was already this token's floor,
       so a 16px ceiling only ever showed up on wide screens; holding
       it at 12 makes the secondary type the same everywhere and lets
       weight and colour do the hierarchy on their own. */
    --note: 12px;
    --cap: clamp(10px, 0.82vw, 11px);
    /* --pp-r in globals.css, which is what PressingIndex reads through
       --ix-r. One radius for every framed asset on the site. */
    --r: 16px;
    --gap: 6px;
    --nav: 54px;
    --gut: 50px;
    /* The column gap, at the root so anything can reason about the
       grid — the answer's spoken line sits outside .ixbody and still
       has to land on the same column the statement does. The daybook
       overrides it on its own .ixbody for its wider alley. */
    --ixgap: clamp(24px, 3.4vw, 56px);
    /* THE RAIL PAYS FOR THE WIDER MARGINS. Going from a 40px gutter to
       50 costs the content 20px, so the note column gives up the same
       20 and the two image columns keep the width they had. It was
       never declared before, only ever the 200px fallback. */
    --ix-note-w: 180px;
  --stratum-air: clamp(56px, 9dvh, 120px);
    --sans: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
  }
  * { box-sizing: border-box; margin: 0; }
  html { background: var(--paper); scroll-behavior: smooth; }
  /* The app's shell paints a cream ground with graph paper and grain
     under every route (FilmOverlay). This page is white paper, and
     that stack was showing through the masthead's translucent burn —
     the one place on this page where anything behind the content can
     be seen at all. No such element exists in the lab; the rule is
     written here so the port carries it rather than being patched on
     the far side, where it would drift. */
  html.rh-home [data-film-ground] { display: none; }

  body { font-family: var(--sans); color: var(--ink);
    -webkit-font-smoothing: antialiased; }
  a { color: inherit; }
  button { font-family: inherit; }

  /* ── the masthead ─────────────────────────────────────────────────
     Lifted value-for-value from src/components/shell/Masthead.module.css
     so the lab bar and the shipped bar are the same object: 16px
     Helvetica at 0.04em on transparent ground, the burn pill inset
     18px past the gutter, resting at rgba(243,240,237,0.05). Nothing
     here may drift — if one of these numbers wants tuning it gets
     tuned in the module and copied back, the same contract every
     other pressing value lives under.

     v4's one structural change is what sits in the middle: the
     question, where the three links used to be. It wears the bar's
     type exactly, so at rest the centre of the bar still reads like
     the centre of the bar. */
  #nav { --nav-size: 16px; --nav-gutter: 40px; --nav-overhang: 18px;
    --masthead-ink: #000000; --masthead-paper: #FFFFFF;
    position: sticky; top: 0; z-index: 60; height: var(--nav);
    display: grid; align-items: center;
    grid-template-columns: 1fr auto 1fr;
    gap: 24px; padding: 0 var(--nav-gutter);
    font-family: var(--sans); font-size: var(--nav-size);
    letter-spacing: 0.04em; font-weight: 500;
    -webkit-font-smoothing: antialiased; color: var(--masthead-ink); }

  /* The burn sits BEHIND everything in the bar. Over flat cream a
     saturate/contrast pump has nothing to act on, so it is invisible;
     over photography it goes off. That asymmetry is the whole effect.
     A pill, not a band: backdrop-filter clips to the border-box,
     radius included, so it reads as a floating lens. */
  #navBurn { position: absolute; pointer-events: none; z-index: 0;
    left: calc(var(--nav-gutter) - var(--nav-overhang));
    right: calc(var(--nav-gutter) - var(--nav-overhang));
    top: 5px; bottom: 5px; border-radius: 999px;
    backdrop-filter: url(#mastheadMelt) blur(0px) saturate(1) contrast(1);
    -webkit-backdrop-filter: blur(0px) saturate(1) contrast(1);
    background: rgba(243, 240, 237, 0.05); }

  /* the field is NOT in this list — it is positioned, not merely
     raised, and #nav .ask would out-specify the .ask rule below */
  #nav .mark, #nav .meta { position: relative; z-index: 1; }
  /* the logotype closes up where the navigation stays tracked — the
     shipped pair is .mark in Masthead.module.css and .covermark below */
  #nav .mark { font-weight: 600; letter-spacing: -0.01em; }
  /* the asterisks sit a weight under the words, so the name reads
     as two words with a mark between them rather than one bold run */
  #nav .mark i { font-style: normal; font-weight: 400; margin: 0 -0.01em; }
  #nav a { color: inherit; text-decoration: none; }
  #nav a:hover { opacity: 0.45; }
  #nav .mark { grid-column: 2; justify-self: center; white-space: nowrap; }
  #nav a.meta { grid-column: 3; justify-self: end; white-space: nowrap;
    color: rgba(0, 0, 0, 0.42); }
  /* #nav a:hover fades every link in the bar to 0.45, which is the
     wordmark's cue and not this one's. The address answers with the
     rule instead — see ONE WAY THROUGH — so it holds its tone and only
     has to opt out of the fade. */
  #nav a.meta:hover { opacity: 1; }

  /* ── the question, which travels ─────────────────────────────────
     There is one field, not two. It begins at display size in the
     cover and ends in the bar, and the only thing that moves it is
     the page: drop is the distance from its reserved slot to the bar's
     centre, so while the slot is on screen the field rides with the
     document exactly like any other block, and when the slot reaches
     the bar it simply parks. Nothing crossfades, nothing hands over.

     It is absolutely positioned inside the bar rather than in the
     cover, because the destination is the thing that has to be exact —
     landing on the gutter to the pixel matters more than where it
     starts. And it stays LEFT the whole way, which is what makes the
     travel read as one move: the wordmark went to the middle to leave
     the slot open for it.

     Sizes come from the driver as custom properties. #query inherits
     the lot, so the field and its own chrome can never disagree. */
  #nav .ask { position: absolute; left: var(--ask-left, var(--nav-gutter)); z-index: 1;
    top: 50%; transform: translateY(-50%) translateY(var(--ask-drop, 0px));
    width: var(--ask-w, 420px); display: flex; align-items: center; gap: 8px;
    font-family: inherit; font-size: var(--ask-fs, 16px);
    font-weight: var(--ask-fw, 500); letter-spacing: var(--ask-ls, 0.04em);
    line-height: 1.04; }
  /* only while the answer opens or closes — never during a scroll,
     where a transition would drag behind the page */
  .ask.settling { transition: transform 0.45s cubic-bezier(0.2, 0.55, 0.2, 1),
    width 0.45s cubic-bezier(0.2, 0.55, 0.2, 1), font-size 0.45s ease; }
  #query { flex: 1 1 auto; min-width: 0; border: 0; outline: 0;
    background: transparent; color: inherit; font: inherit;
    letter-spacing: inherit; padding: 4px 0; border-radius: 0;
    border-bottom: 1px solid transparent;
    transition: border-color 0.2s ease, color 0.28s ease; }
  /* HOVER, WHERE THERE IS ONE. iOS holds :hover on whatever was
     tapped last, so on a phone this rule painted a rule under the
     field and left it there. */
  @media (hover: hover) { #nav:hover #query { border-bottom-color: rgba(0, 0, 0, 0.22); } }
  #query:focus { border-bottom-color: var(--masthead-ink); }
  #query::placeholder { color: rgba(0, 0, 0, 0.42); opacity: 1;
    transition: opacity 0.3s ease; }
  /* at display size it wears a real rule; in the bar it goes back to
     the module's bare centre slot */
  .ask.big #query { border-bottom-width: 2px;
    border-bottom-color: rgba(0, 0, 0, 0.14); padding-bottom: 10px; }
  .ask.big #query::placeholder { color: var(--masthead-ink);
    transition: color 0.25s ease; }
  /* Click in and the question goes grey. At display size the
     placeholder is full ink — it reads as a statement, which is right
     until someone means to type over it. Grey says the line is theirs
     now. */
  .ask.big #query:focus::placeholder { color: rgba(0, 0, 0, 0.3); }
  #nav.rev .ask.big #query:focus::placeholder { color: rgba(255, 255, 255, 0.42); }
  /* display, not visibility: hidden it still holds its width and the
     flex gap, and the question is fitted to the width it actually has */
  .clear { display: none; background: none; border: 0; padding: 0 2px;
    cursor: pointer; font-size: var(--nav-size); line-height: 1;
    color: rgba(0, 0, 0, 0.42); }
  body.asked .clear { display: block; }
  .clear:hover { color: var(--masthead-ink); }

  /* Reversed over dark ground, same as the module: mix-blend-mode
     cannot work because sticky makes the bar its own stacking
     context, so a driver toggles this against declared
     [data-nav-dark] zones. The transition is what keeps the handover
     from reading as a flicker at the boundary. */
  /* .meta is not in this list: it carries colour AND background-size on
     one transition in the shared rule below, which out-specifies this
     one, so naming it here would only look like it did something. */
  #nav .mark, #nav #query { transition: color 0.28s ease; }
  #nav.rev { color: var(--masthead-paper); }
  #nav.rev a.meta { color: rgba(255, 255, 255, 0.5); }
  /* THE RULE INVERTS WITH THE BAR. Black on a dark zone is no line at
     all, and the address is the one live word that crosses those
     boundaries — it sits in the bar the whole way down the page while
     the ground under it changes. Custom properties rather than a
     second copy of the rule: they inherit from #nav to the link, so
     the shared declaration below reads whichever pair is in force. */
  #nav.rev { --rule-ink: var(--masthead-paper);
    --rule-rest: rgba(255, 255, 255, 0.32); }
  #nav.rev #query::placeholder { color: rgba(255, 255, 255, 0.6); }
  @media (hover: hover) { #nav.rev:hover #query { border-bottom-color: rgba(255, 255, 255, 0.35); } }

  /* ── the cover: the question, and what the question is for ───────
     Term left, definition right. The left column is the way in — the
     field at display size, and under it what the house holds, which is
     the same job Pentagram's "showing the latest 27 projects" does. The
     right column says what asking does and then lists the vocabulary,
     as an overlay. That overlay is gone, and the room it had to
     reserve went with it. */
  /* Height gives the question the presence a display line needs, and
     drops the count to the foot of its column. It does NOT lengthen
     the travel, which was the guess: the question sits at the TOP of
     the cover, so its distance to the bar is unchanged by how tall the
     cover is (measured: 522px cover, 130px travel). The only lever on
     travel length is how far down the question starts, and it starts
     at the top on purpose. */
  /* One column now. The question, then who is asking — the two
     paragraphs explaining the field are gone, because a field that
     needs explaining in two paragraphs has not been designed yet. */
  /* THE COVER STARTS IN THE BAR, NOT UNDER IT. The wordmark and the
     address ARE the navigation now, so the bar cannot also be a 54px
     empty band above them — that read as a nav with nothing in it,
     then padding, then the page. Pulling the section up by the bar's
     own height puts its first line in that band, and the bar's own
     ends stay hidden underneath (body.attop) so nothing doubles.

     No horizontal padding: the .ixbody inside supplies the page
     gutter, and doubling it would push the cover's columns off the
     index's. */
  /* AIR AT THE TOP, and the rules take it too. The header row shares
     the bar's band, so the offset is measured from the viewport
     rather than from the bar: pull up by the bar's height, push down
     by the air wanted above everything. The rules hang off .covercols,
     whose top is this section's content top, so they start on the same
     line the type does instead of running up into the white. */
  .cover { --cover-air: 50px;
    margin-top: calc(var(--cover-air) - var(--nav));
    padding: 0 0 clamp(30px, 4dvh, 56px);
    position: relative; }
  /* One line, three columns, TOPS even. Baseline alignment put the
     three on a shared baseline, which meant their tops stepped: a 16px
     line box and a 36px one hang different amounts of air above the
     letters, so the wordmark and the address sat visibly low.

     Start alignment evens the BOXES, which is not the same as evening
     the letters — the big line box still carries more air above its
     caps. --cap-align is that difference, so the small type is nudged
     down by exactly the amount the statement's own leading puts above
     it and the three cap-heights land on one line.

     The two constants are the font's OWN, measured rather than taken
     from a metrics table: each is the distance from a line box's top
     to its cap top, over the font size — a per-font, per-line-height
     ratio that holds at any size. .2031 for the statement's 1.18
     leading, .0417 for the small type's 1.04. A first attempt used
     published ascent/descent/cap figures and was 1.5px out, then a
     single-point correction fixed 36/16 and broke 32/12; ratios
     measured off the rendered pixels hold at both. */
  .cover .ixbody { align-items: start; }
  /* --nav-size is declared on .nav, which the cover is not inside, so
     without this the wordmark and the address fell back to the 16px
     default while everything around them came down to 12. */
  .cover { --nav-size: 12px;
    --stmt-fs: clamp(21px, 2.4vw, 32px);
    --cap-align: calc(0.2031 * var(--stmt-fs)
      - 0.0417 * var(--nav-size, 12px)); }
  .covermark, .covermeta { padding-top: var(--cap-align); }

  /* ── the cover's three columns ───────────────────────────────────
     The wordmark takes the note rail's column and the address the last
     run of work, so the top of the page is the same grid as the body
     of it. Both are set in the masthead's own type — this IS the
     masthead, laid into the page instead of floating over it. */
  /* THE MARK IS SET, THE BAR IS TRACKED. 700 and -0.01em, against the
     bar's own 500 and +0.04em. Small navigation type needs the air to
     read as separate words; a logotype has the opposite job and has to
     close into one object.

     This number and the masthead's are ONE VALUE IN TWO FILES, because
     the cover holds the name until it hands over to the bar on scroll.
     They were unified at 500 to stop the name changing weight across
     that handover, and they are unified at 700 now for the same
     reason. If this moves, move .mark in Masthead.module.css with it. */
  .covermark { font-size: var(--nav-size, 12px); font-weight: 600;
    letter-spacing: -0.01em; line-height: 1.04; white-space: nowrap; }
  /* THE ASTERISKS ARE THE NAME, not punctuation between three words.
     Upright and the same weight as the letters, so nothing reads as a
     lighter joint. The tag is <i> only because it is the shortest
     element there is; every visual thing it would normally do is
     turned off here. */
  .covermark i { font-style: normal; font-weight: 400; margin: 0 -0.01em; }
  .covercols { display: grid; grid-template-columns: 1fr 1fr;
    column-gap: var(--ixgap); align-items: start;
    /* THE RULES REACH THE INDEX'S. Without this the three verticals
       stop at the statement and start again under it, and the columns
       read as two stacked headers rather than one structure. The
       bridge is the only two values between the last rule pixel here
       and the first one there: this section's bottom padding and the
       index stratum's top padding. Measured at 117px on a 900px
       window, which is exactly 36 + 81. */
    --cover-bridge: calc(clamp(30px, 4dvh, 56px) + clamp(56px, 9dvh, 120px)); }
  /* ALL THREE RULES ON ONE PSEUDO, because .ixrows draws its middle
     rule as a BACKGROUND and a background cannot paint outside its own
     box — sizing it past 100% just clips. So the two edge rules
     bridged the gap and the middle one could not, which is exactly
     what it looked like.
     This is a single overflowing box spanning covercols plus half a
     gutter each side, carrying three 1px gradients at 0, 50% and 100%.
     Its 50% is the gap's centre, the same point the background used.
     Doubled selectors on purpose: .covercols alone ties .ixrows on
     specificity and loses on source order, since .ixrows is defined
     further down this file. */
  /* The cover draws no rules of its own. The pinned set behind it
     covers the same band at the same x, and two identical lines where
     one of them scrolls is what read as the rules drifting. */
  .ixrows.covercols { background-image: none; }
  .ixrows.covercols::before, .ixrows.covercols::after { display: none; }
  .covermeta { justify-self: end; text-align: right;
    font-size: var(--nav-size, 16px); font-weight: 500;
    letter-spacing: 0.04em; line-height: 1.04;
    color: rgba(0, 0, 0, 0.42); }
  .covermeta a { text-decoration: none; transition: color 0.2s ease; }
  .covermeta a:hover { color: var(--ink); }

  /* THE BAR GIVES UP ITS ENDS WHILE THE COVER HOLDS THEM. attop is the
     driver's own flag for "the field is still at full size in the
     cover", which is exactly the moment the wordmark and the address
     are on the page below. Opacity rather than display, so nothing
     reflows when they come back on the first scroll.

     SCOPED TO rh-cover, NOT rh-home, and the difference is the whole
     rule. rh-home means "this page is white paper", which PaperGround
     puts on every white route so the bar's burn has something to
     filter. rh-cover means "this page's own cover is holding the bar's
     ends", which is true of the homepage alone. They were one class
     for a night, and the daybook inherited a bar with no name and no
     address in it: the hidden state applied, and headgone, which drops
     them back, is only ever set by the homepage driver, which does not
     run there. A flag that means two things is a flag that will be
     wrong on one of them. */
  /* THE BAR IS THE COLUMNS, COMPRESSED. The wordmark takes the left
     gutter, over the note rail; the field stays in the middle column
     and only ever travels vertically; the address keeps the right
     edge. So a scroll reads as the three column heads rising into the
     bar, not as three elements swapping places on the way up. The
     phone block below keeps its own arrangement, where there is one
     column and the wordmark takes the right end. */
/* ── the handover ────────────────────────────────────────────────
     The cover's wordmark and address scroll away with the page, like
     any other line on it. The bar's copies do not cross-fade in behind
     them — they DROP, from just above the bar into their seat, once
     the page's own pair has left the screen. So the name is never in
     two places at once, and arriving reads as a thing falling into
     position rather than a thing materialising.

     visibility rides along so the hidden copies cannot be tabbed to;
     it switches at the end of the way out and immediately on the way
     in. The address follows the name by a beat. */
  html.rh-cover #nav [data-mark],
  html.rh-cover #nav [data-meta] {
    opacity: 0; visibility: hidden;
    transform: translateY(-14px);
    transition: opacity 0.2s ease,
      transform 0.32s cubic-bezier(0.2, 0.9, 0.25, 1),
      visibility 0s linear 0.32s; }
  html.rh-cover body.headgone #nav [data-mark],
  html.rh-cover body.headgone #nav [data-meta] {
    opacity: 1; visibility: visible; transform: none;
    transition: opacity 0.22s ease,
      transform 0.34s cubic-bezier(0.2, 0.9, 0.25, 1),
      visibility 0s linear 0s; }
  html.rh-cover body.headgone #nav [data-meta] {
    transition-delay: 0.05s, 0.05s, 0s; }
  @media (prefers-reduced-motion: reduce) {
    html.rh-cover #nav [data-mark],
    html.rh-cover #nav [data-meta] { transition: none; transform: none; }
  }
  /* COLUMNS ONE AND THREE COME UP ON THEIR OWN. The cover is as tall
     as the statement, and the statement lives in the middle column —
     so without these the wordmark and the address each sit hundreds of
     pixels above their own column's next thing, holding a gap that
     belongs to a neighbour. The values are MEASURED by the driver
     (fitColumns), because the statement rewraps at every width and an
     authored number would be right at exactly one viewport. Zero until
     the driver writes them, so a failed script leaves the honest
     untightened page rather than an overlapped one. */
  @media (min-width: 861px) {
    #stIndex .ixnotes { margin-top: calc(-1 * var(--rail-lift, 0px)); }

  }
  html.rh-home #nav [data-burn] { transition: opacity 0.25s ease; }
  html.rh-home body.coverbar #nav [data-burn] { opacity: 0; }
  @media (prefers-reduced-motion: reduce) {
    html.rh-home #nav [data-burn] { transition: none; }
  }
  /* The cover's pair fades no more: it scrolls off the top with the
     rest of the page, and the bar's copies drop in only once it has
     gone. Vanishing at the first pixel of scroll was what made the
     handover read as a swap rather than a hand-off. */
  /* THE BAR'S GUTTER IS THE PAGE'S GUTTER. The masthead carries its
     own 40px, invisible while the wordmark only ever lived in the bar
     — but the cover holds a second copy on the page's 50px gutter now,
     and on scroll the name jumped 10px left as the two swapped.
     Scoped to this route: other pages have no second copy to agree
     with. */
  html.rh-home #nav { --nav-gutter: var(--gut); }
  html.rh-home #nav [data-mark] { transition: opacity 0.3s ease; }
  /* THE ADDRESS HAS TO CARRY THREE THINGS ON ONE LIST. It fades in
     with the wordmark here, it fades its tone across a dark zone's
     edge, and it draws the rule every live word on the site draws.
     transition is not additive: this selector is (1,2,1) and the
     masthead's own .nav a.meta is (0,2,1), so naming only opacity
     here left the fill with no transition and the rule snapped on —
     on the homepage alone, which is the sort of difference nobody
     finds by looking at one page. */
  html.rh-home #nav [data-meta] {
    transition: opacity 0.3s ease, color 0.28s ease,
      background-size 0.55s cubic-bezier(0.22, 1, 0.36, 1); }
  @media (prefers-reduced-motion: reduce) {
    html.rh-home #nav [data-mark], html.rh-home #nav [data-meta] {
      transition: none; }
  }
  body.asked .cover { display: none; }
  /* The statement leads now and the field follows as its continuation,
     so the top padding that used to clear the field is gone. */
  .coverR { padding-top: 0;
    width: min(52%, 700px); }
  /* The statement is set exactly as PressingCover sets it on a case
     study — clamp(24px, 3.2vw, 44px)/1.18/-0.02em at 500, over a 21em
     measure. Same voice in both places, so the homepage opens at the
     scale a study opens at.

     The measure is what makes 44px work here: 21em at 44px is 924px,
     wider than the field's 52% column, so the statement is allowed
     out past it rather than being crushed into eight short lines. */
  .coverR { width: auto; min-width: 0; }
  /* Sized to the COLUMN it now lives in. 3.2vw was set when the
     statement had a 700px block to itself; in a third of the grid the
     same clamp gave about twelve characters a line. 2.5vw holds the
     same proportion the cover always had between type and measure. */
  .coverR .statement { font-size: var(--stmt-fs, clamp(21px, 2.4vw, 32px));
    line-height: 1.18; letter-spacing: -0.05em; font-weight: 600;
    max-width: 21em; }
  .ledelbl { display: block; font-size: var(--lbl);
    font-weight: var(--lbl-weight); letter-spacing: var(--lbl-track);
    opacity: 0.5; padding-bottom: 14px; }
  /* The slot is the sentence's last word: inline-block at the
     statement's own type size, wide enough for the longest rotation
     line, wrapping to its own line when the paragraph's last line has
     no room — exactly what a long word would do. em units on purpose,
     so it scales with the statement rather than with the viewport. */
  #askSlot { display: inline-block; width: min(9.5em, 92%);
    height: 1.12em; vertical-align: middle; }
  /* The field wears the terms' own hairline while it rides the
     sentence — same 1px, same grey, hugging the words rather than
     running the slot's full width. --ask-uw is the driver's measure of
     the visible text, so the rule ends where the words do, exactly as
     an underline would. Focus fills it black left to right, the same
     move the terms make on hover. Parked in the bar it all reverts to
     the masthead's own border behaviour. */
  /* border-bottom 0, not transparent: the masthead's #nav:hover
     #query rule re-colours the border with two ids of specificity, so
     a transparent border comes back as a second, lower line on hover.
     A zero-width border has nothing to colour. */
  .ask.big #query { border-bottom: 0; padding-bottom: 0; }
  /* a field has no focus-visible distinction worth drawing — it either
     has the caret or it does not, and the rule should be full either
     way, so this is :focus and carries no outline */
  .ask.big #query:focus {
    text-decoration-color: var(--rule-ink, #000); }

  /* 32, with the rest of the ramp. The cover sets its own --stmt-fs
     to the same ceiling; this is what the daybook and staples read. */
  .statement { font-size: clamp(20px, 2.4vw, 32px); line-height: 1.2;
    letter-spacing: -0.05em; font-weight: 600; max-width: 30em; }
  .statement .dim { color: rgba(0, 0, 0, 0.42); }
  .stratum .statement { padding: 0 var(--gut) clamp(26px, 4dvh, 48px); }

  .explain { font-size: var(--note); line-height: 1.5;
    color: rgba(0, 0, 0, 0.8); }
  .explain + .explain { padding-top: 1.1em; }
  /* the room the travelling field needs. Fixed at the largest size the
     rotation can take, so a long question cycling in never reflows the
     page under it. */

  @media (max-width: 860px) {
    #askSlot, .coverR { width: 100%; }
  }

  /* ── section furniture ──
     Each stratum announces itself on one line: what it is at the left,
     the way further in at the right, both on the page gutter so the
     row reads as the same container the cards below it sit in. */
  /* The air above a stratum, as a token rather than a number, because
     the note rail pins on it: a rail whose sticky top is not exactly
     its resting top slides before it catches, and the two values have
     to be the same quantity, not two copies of one. */
  .stratum { padding-top: var(--stratum-air); }
  .stratum .shead { display: flex; justify-content: space-between;
    align-items: baseline; gap: 24px; flex-wrap: wrap;
    padding: 0 var(--gut) clamp(20px, 3dvh, 36px); }
  .stratum .slbl { font-size: var(--lbl); font-weight: var(--lbl-weight);
    letter-spacing: var(--lbl-track); opacity: 0.55; }
  .stratum .slink { font-size: var(--lbl); font-weight: var(--lbl-weight);
    letter-spacing: var(--lbl-track); text-decoration: none; }
  .stratum .slink:hover { text-decoration: underline; }

  /* ── the daybook strip ──
     A ledger, not a feed: date left, entry in the middle, project right,
     one hairline per row. The middle column carries the page's --note
     size, which is the only size on this block — the date and project
     are labels, so they take --lbl like every other marker on the page.

     The measure is capped at 62ch rather than filling the row. A dated
     line that runs the full width of a 1400px page reads as a headline
     and this is not one; held to a book measure it reads as an entry
     someone wrote. */
  .dbrows { padding: 0 var(--gut); }
  /* THE TAG FOLLOWS THE ENTRY, it does not sit at the far page edge.
     A 1fr middle column pushed the project 900px clear of the line it
     labels on a wide screen, with nothing crossing the gap: the two
     stopped reading as one row. Capping the middle at the measure and
     packing the tracks left keeps date, entry and project together as a
     single object, and the hairline still runs the full width because
     the row element does. The void moves to the right of the block,
     where it is air rather than a broken relationship. */
  .dbrow { position: relative; display: grid;
    grid-template-columns: 80px minmax(0, 62ch) max-content;
    justify-content: start;
    column-gap: clamp(20px, 2.6vw, 40px);
    align-items: start;
    padding: 15px 0; }
  /* The rule is a ::before rather than a border so it can be DRAWN:
     it scales from the left as the row arrives, the compose form's
     entrance. Base state is the finished row; .dbpre is applied by JS
     (ledger-arrival) below the fold and removed on the way in, so a
     failed script leaves a plain complete strip, never a hidden one. */
  .dbrow::before { content: ""; position: absolute; top: 0; left: 0; right: 0;
    height: 1px; background: rgba(0, 0, 0, 0.14);
    transform: scaleX(1); transform-origin: 0 50%;
    transition: transform 0.66s cubic-bezier(0.2, 0.7, 0.2, 1) var(--dbd, 0ms); }
  .dbd, .dbt, .dbp {
    transition: opacity 0.5s ease calc(var(--dbd, 0ms) + 90ms),
      transform 0.56s cubic-bezier(0.2, 0.7, 0.2, 1) calc(var(--dbd, 0ms) + 90ms); }
  .dbpre .dbd, .dbpre .dbt, .dbpre .dbp {
    opacity: 0; transform: translateY(10px); transition: none; }
  .dbpre::before { transform: scaleX(0); transition: none; }
  .dbd, .dbp { font-size: var(--lbl); font-weight: var(--lbl-weight);
    letter-spacing: var(--lbl-track); text-transform: uppercase;
    /* seats the caps on the note's first baseline */
    padding-top: 7px; }
  .dbp { opacity: 0.55; }
  .dbt { margin: 0; font-size: var(--note); line-height: 1.55; max-width: 62ch; }
  .dbt a { color: inherit; text-decoration: underline; text-underline-offset: 3px; }

  @media (max-width: 760px) {
    /* One track: the project tag drops under the entry rather than
       taking a third column out of a 375px screen. */
    .dbrow { grid-template-columns: 64px minmax(0, 1fr); column-gap: 14px; }
    .dbp { grid-column: 2; text-align: left; padding-top: 6px; }
  }

  /* ── feeds (shared card grammar) ── */
  /* The feed sits in the same container as the masthead, the question
     and the section labels: one gutter down the whole page, so an
     image's edge lines up with the type above it. The 6px stays
     BETWEEN the columns, which is what keeps it reading as a feed
     rather than a layout. */
  .fd-cols { display: flex; gap: var(--gap); padding: 0 var(--gut); }
  .fd-col { flex: 1 1 0; min-width: 0; display: flex;
    flex-direction: column; gap: var(--gap); }

  .fd-it { position: relative; display: block; width: var(--w, 100%);
    text-decoration: none; }
  .fd-it.insetR { align-self: flex-end; }
  /* The frame holds the ratio and the clip; the image is cut taller
     than the frame so it has somewhere to go. Nothing here moves on
     its own — the drift driver writes the transform, and it writes it
     to the IMAGE, never to the card, so the arrival animation on the
     card and the drift never fight over one transform. */
  /* No fill. The frame reserves its space through the declared ratio,
     not through a visible plate — and with the curtain a grey ground
     would sit there being uncovered, which draws attention to the
     mechanism instead of the picture. */
  .fd-it .shot { display: block; width: 100%; aspect-ratio: var(--ar, 1);
    overflow: hidden; border-radius: var(--r); background: none; }
  .fd-it .shot img { display: block; width: 100%;
    height: calc(100% + var(--drift, 16%)); object-fit: cover;
    /* the rest position is the MIDDLE of the slack, not the top: a
       frame the driver has not reached yet — or will never reach,
       under reduced motion — is still a centred crop rather than one
       that has quietly lost its bottom sixth. */
    transform: translate3d(0, calc(var(--drift, 16%) / -2), 0); }

  /* The drift is a CROP before it is a movement: the image is drawn
     taller than its frame and object-fit: cover pays for that out of
     the WIDTH, 13.8% of it at the default 16%. A photograph with air
     around it never misses the trimmed edge. A picture whose subject
     runs to its own edges does, and no amount of recropping the file
     fixes it, because the same percentage goes whatever ratio the tile
     is declared at.

     Those tiles ask for 4 instead, which costs 3.8%. AUTHORED PER
     TILE, in the WORK row beside the size — see the note on `drift` in
     pressingHomeDriver.js. A CSS rule used to key on "-laptop" in the
     filename, which held while every such tile happened to hold a
     laptop and stopped holding the moment a street display case needed
     the same thing. The travel is smaller and still there either way;
     the driver measures its slack off the element. */

  /* ── the page transition ─────────────────────────────────────────
     Three beats, each an edge moving in one direction:

       1  white falls from the top until it covers
       2  black rises from the bottom until it covers the white
       3  black's BOTTOM edge rises, uncovering the next page

     Beat 3 is why there are two panels. Uncovering from the bottom
     means the last thing you see leaving is the top of the screen —
     the opposite direction to beat 1 — and the reversal is what stops
     the whole thing reading as one long wipe. The white exists only so
     beat 2 has something to travel over; a black panel rising over the
     live page would just be beat 3 played backwards.

     clip-path, not transform: the panels never move, only what is
     drawn of them, so nothing composites at 100dvh scale and the
     edges stay exactly straight. Each beat eases hard at the end. */
  /* ── the transition owns its own type ────────────────────────────
     It must not inherit from the page it is covering, and it was.
     The panels are children of <body>, the homepage puts .rh-home on
     <html> while it is mounted, and that rule sets body's family,
     colour and smoothing. So the stacked words rendered in Satoshi
     over a case study and Helvetica over the homepage — and because
     the class is added and removed AS THE ROUTE SWAPS, the typeface
     changed halfway through the sequence.

     That is the weight shift. Going to a case study it ended in the
     narrower face and jumped lighter; going home it ended in the wider
     one and jumped heavier. Measured: the same string is 358.3px in
     one and 378.2px in the other.

     Stated here, so the sequence looks the same over every route and
     nothing a page does to <body> can reach it. */
  .pt { position: fixed; inset: 0; z-index: 200; pointer-events: none;
    visibility: hidden;
    font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
    -webkit-font-smoothing: antialiased; }
  .pt.pt-run { visibility: visible; pointer-events: auto; }
  .ptw, .ptb { position: absolute; inset: 0; }

  /* The destination's name, repeated down the whole page, drawn TWICE
     — once in each panel, at identical coordinates, in the colour that
     panel needs. Neither copy ever changes colour: the CLIP does it.
     As a curtain's edge crosses a line, the words above the edge
     belong to one panel and the words below to the other, so the type
     inverts ALONG the moving edge instead of snapping when a panel
     lands. Both stacks must register exactly or the inversion shows a
     seam, which is why the geometry is declared once here and neither
     panel is allowed its own.

     The lines also arrive and leave in sequence, and the two
     directions are deliberately opposite: IN from the top, chasing
     the white curtain down the page; OUT from the bottom, running
     just ahead of the black curtain as it lifts. Beat 3 was already
     removing them bottom-up — its clip does exactly that — so the
     stagger is not inventing a direction, it is making one that
     already existed legible. */
  .ptw, .ptb { overflow: hidden; }
  /* The same inset all round — the gutter the rest of the page keeps.
     The lines are then given an EXACT line-height so a whole number of
     them fills that box: otherwise the leftover fraction sits as extra
     air top and bottom, and the inset stops being uniform. */
  .ptstack { position: absolute; inset: 0; padding: var(--gut);
    display: flex; flex-direction: column; justify-content: center; }
  /* NEVER `transform: none`. A line resting at none has no transform,
     so Chrome discards its compositing layer and re-rasterises the
     glyphs at different subpixel positions — which reads, unmistakably,
     as the type changing weight the instant it lands. The measured
     values never move (same font, same 500, same width to the
     hundredth of a pixel); only the rasterisation does. An identity
     translate3d keeps the layer for the whole sequence, so the text is
     drawn one way from first frame to last. */
  /* THE SIZE IS MEASURED, like the line count above it. nowrap is what
     makes each repeat one line and the stack fill the screen, and at
     the clamp's 20px floor a long name plus its category runs about
     430px on a 375px phone: the line simply ran off the right edge and
     was cut. The component measures the longest line and writes --ptfs
     so it fits; the clamp is the fallback and the ceiling. */
  .ptl { display: block; white-space: nowrap;
    font-size: var(--ptfs, clamp(20px, 2.6vw, 38px)); font-weight: 600;
    line-height: var(--ptlh, 1.18); letter-spacing: -0.05em;
    -webkit-font-smoothing: antialiased;
    opacity: 0; transform: translate3d(0, 14px, 0);
    transition: opacity 0.3s ease,
      transform 0.34s cubic-bezier(0.16, 1, 0.3, 1);
    transition-delay: var(--d, 0s); }
  /* declared only while the sequence runs, so 38 layers are not held
     for the life of the page */
  .pt-run .ptl { will-change: transform, opacity; }

  /* ── the two copies have to WEIGH the same ──────────────────────
     They already are the same: identical family, identical 500,
     identical size, identical width to the hundredth of a pixel. The
     only difference is the ground, and that is enough. White on black
     with grayscale antialiasing renders visibly thinner than black on
     white, because light type on a dark ground loses the bleed that
     normally fattens it. As the black curtain crosses a line, the
     words change apparent weight along the moving edge.

     A smoothing property cannot fix it here: these lines are held on
     compositing layers for the whole sequence (see above), and a
     composited layer rasterises grayscale whatever the property says.
     So the compensation goes on the glyphs. A hairline stroke in the
     text's own colour puts back exactly what the inversion took, and
     it is the light copy that gets it, because medium is the weight
     both are supposed to read as. */
  .ptb .ptl { -webkit-text-stroke: 0.36px currentColor; }
  .pt-1 .ptl { opacity: 1; transform: translate3d(0, 0, 0); }

  /* ── the wait, and why it is CSS ─────────────────────────────────
     While a heavy route renders, the MAIN THREAD IS BLOCKED. A.R.C.
     commits several thousand SVG nodes in one go, and for the length
     of that commit no timer fires and no inline style gets written —
     which is exactly what a JavaScript-driven loader needs to do, so
     the first version of this froze precisely when it was supposed to
     be reassuring someone.

     A keyframe animation on transform and opacity runs on the
     compositor, off the main thread. It keeps going through the
     commit. The main thread only has to add one class before the work
     starts and take it off after.

     The rest band is long on purpose. Lines are staggered by --d, up
     to about 0.57s across the stack, and the animation holds its
     settled state from 45% of a 1.8s cycle onward — a 0.99s window.
     That is wider than the stagger, so at the moment the first line
     completes a lap, every other line is also sitting still and
     upright. Stopping there can never cut one mid-flight. */
  @keyframes ptwait {
    0%   { opacity: 1; transform: translate3d(0, 0, 0); }
    22%  { opacity: 0; transform: translate3d(0, -12px, 0); }
    23%  { opacity: 0; transform: translate3d(0, 14px, 0); }
    45%  { opacity: 1; transform: translate3d(0, 0, 0); }
    100% { opacity: 1; transform: translate3d(0, 0, 0); }
  }
  .pt-wait .ptl { animation: ptwait 1.8s var(--d, 0s) infinite both;
    /* the entry transition would fight the keyframes for the same two
       properties, and the keyframes are the ones in charge here */
    transition: none; }
  .pt-3 .ptl { opacity: 0; transform: translate3d(0, -10px, 0);
    transition-duration: 0.22s, 0.24s; }
  .ptw .ptl { color: var(--ink); }
  .ptw .ptl .sub { color: rgba(0, 0, 0, 0.42); }
  /* The last resort when even a scaled line will not fit: the name
     stays, the category goes. Set by the component, which measures. */
  .ptstack.pt-nosub .ptl .sub { display: none; }
  .ptb .ptl { color: var(--paper); }
  .ptb .ptl .sub { color: rgba(255, 255, 255, 0.48); }
  .ptw { background: var(--paper); clip-path: inset(0 0 100% 0); }
  .ptb { background: var(--ink); clip-path: inset(100% 0 0 0); }
  /* the beats. Durations live here so the driver only sequences. */
  .pt-run .ptw { transition: clip-path 0.52s cubic-bezier(0.33, 0.28, 0.12, 1); }
  .pt-run .ptb { transition: clip-path 0.52s cubic-bezier(0.33, 0.28, 0.12, 1); }
  .pt-1 .ptw { clip-path: inset(0 0 0 0); }
  .pt-2 .ptb { clip-path: inset(0 0 0 0); }
  /* The white is dropped instantly once the black is FULL — invisible
     under it, and it has to be gone before beat 3 or it is what gets
     revealed. Keyed to pt-3, not pt-2: pt-2 is added when the black
     STARTS rising, and clearing the white then uncovers the live page
     for the length of that beat. Measured at 95% exposed before this
     moved. */
  .pt-3 .ptw { transition: none; clip-path: inset(0 0 100% 0); }
  .pt-3 .ptb { transition: clip-path 0.68s cubic-bezier(0.33, 0.28, 0.12, 1);
    clip-path: inset(0 0 100% 0); }

  /* ── the ring: the index as the body ───────────────────────────
     Rows, not rails. Every row is top-aligned across the page, takes a
     generous interval below it, and is ruled off from the next — so
     the page has a horizontal spine instead of two columns flowing
     past each other. Both frames in a row hang from the same line;
     the difference in their depth falls as air, which is the same
     move the uneven pair made, now stated with a rule under it.

     The working notes are the exception, and deliberately so: they
     hold the left column and ignore the row rhythm completely,
     crossing the rules as they go. That is why they are laid over the
     rows rather than placed in them — a note is not a cell. */
  /* Two tracks: the notes column, then everything else. The notes used
     to be absolutely positioned and floated OVER an empty first column
     in every row, which is why they could not stick — an absolute box
     has nothing to stick inside of. Making the column real costs the
     rows their empty cell and buys the pin. */
  /* Air before the footer. The credits beat is full-bleed ink and it
     was starting the frame after the last caption, which cropped the
     name off the final project. 200px is the interval, not a margin:
     the page needs to finish before the next thing begins.

     The ANSWER takes the same interval from the same declaration, for
     the same reason and not a similar one: it is the ring re-dealt,
     and the same ink footer follows it. Written as one selector so the
     two cannot drift into two slightly different intervals. */
  #stIndex, .answer { padding-bottom: 200px; }

  /* --ixgap is THE gap token, declared once here so anything that
     needs to reason about the columns can read the real value. The
     daybook widens it for its own alley; the pinned rules inherit
     whatever it is rather than assuming the default, which is how
     they ended up 47px off that page. */
  .ixbody { position: relative;
    display: grid; grid-template-columns: var(--ix-note-w, 200px) minmax(0, 1fr);
    column-gap: var(--ixgap); align-items: start;
    padding: 0 var(--gut); }

  /* ── the standing rules, pinned ──────────────────────────────────
     They used to be drawn on .ixrows and scrolled away with the work,
     which made them read as decoration on the frames rather than as
     the page's own structure. The wrapper is absolute and spans the
     whole index, so the lines can never outlive it; the child inside
     is sticky and one viewport tall, so it holds its distance from the
     top the whole way down and stops when the index does.

     The wrapper spans rule ONE to rule THREE — from half a gutter past
     the note rail to half a gutter past the last column — so the three
     lines are simply 0, 50% and 100% of it. Offsets count from the
     padding box, which is why --gut appears: absolute positioning does
     not know about the gutter the grid is padded by. */
  /* THE WRAPPER STARTS ABOVE THE FOLD so the pin is already stuck at
     scroll zero. Anchored at the index's own top it had to travel
     ~380px before reaching the pin line, and for those first 380px the
     rules scrolled with the page — which is exactly the drift they
     were meant to stop doing. A viewport of headroom costs nothing:
     sticky clamps the child to `top` regardless, so nothing is ever
     drawn above the bar. */
  .ixrules { position: absolute; pointer-events: none;
    --cover-air: 50px;
    top: calc(-1 * 100dvh); bottom: 0;
    left: calc(var(--gut) + var(--ix-note-w, 200px) + var(--ixgap) / 2);
    right: calc(var(--gut) - var(--ixgap) / 2); }
  /* 50, the same as the margins, rather than the bar's height — the
     top of the frame should read as one value with the sides. */
  /* ONE RULE, for the pages that have one boundary rather than two:
     a note rail and a single column of content. Same pinned wrapper,
     same x, just the left line. */
  .ixrules.ixrule1 .ixrulesPin {
    background-image:
      linear-gradient(rgba(0, 0, 0, 0.13), rgba(0, 0, 0, 0.13));
    background-size: 1px 100%;
    background-position: 0 0; }
  .ixrulesPin { position: sticky; top: var(--cover-air, 50px);
    height: calc(100dvh - var(--cover-air, 50px));
    /* TWO, not three. The far-right rule drew a boundary the browser
       edge already draws. */
    background-image:
      linear-gradient(rgba(0, 0, 0, 0.13), rgba(0, 0, 0, 0.13)),
      linear-gradient(rgba(0, 0, 0, 0.13), rgba(0, 0, 0, 0.13));
    background-size: 1px 100%, 1px 100%;
    background-position: 0 0, 50% 0;
    background-repeat: no-repeat; }

  /* PINNED. The four statements hold at the top while the magazine
     runs past them, which is the point of a column that was never
     part of the row rhythm: it is a standing note, not a caption.
     align-self: start is load-bearing — a stretched grid item is as
     tall as the whole index, and a sticky box that tall never has
     anywhere to travel. */
  /* IT PINS WHERE IT RESTS, so it does not move at all. The top used
     to be a flat calc that had nothing to do with where the column
     actually started, so the rail slid up to meet it and then caught:
     34px on the homepage, 48 on the daybook, 129 in an answer, all
     read as the column drifting out from under its own heading before
     settling. The standing rule beside it never did this, because its
     pin and its start are the same number.

     The default is right for any page whose rail starts at the top of
     its stratum, which is the daybook and Staples. The homepage lifts
     its rail to meet the first frame instead, so the driver measures
     that line and writes --rail-pin per rail. */
  .ixnotes { position: sticky;
    top: var(--rail-pin, calc(var(--nav) + var(--stratum-air)));
    align-self: start;
    display: flex; flex-direction: column;
    /* Measured against the WINDOW, not the page width, because pinning
       changed what constrains this column. It used to float and could
       be any height; now it holds at the top, and a pinned column
       taller than the viewport cannot pin — the bottom simply never
       arrives. At 7.5vw the five blocks ran 915px against a 900px
       window and the filter was being cut off.
       vh is the honest unit here: 40px is the ceiling and it holds
       from roughly 910px of window upward, so this reads as a flat 40
       on any normal screen. Below that it shrinks rather than pushing
       the last block off the bottom, which is the one thing a pinned
       column must never do.

       40 rather than 60. At 12px the blocks are small and the air
       between them was reading as three blank lines, which separates
       them further than they are actually separate: they are five
       notes on one card, not five cards. Two lines of air still
       breaks them apart and stops the column looking like a list of
       unrelated fragments.

       It buys headroom as well, which is the constraint above. Five
       blocks came to 506px at the old ceiling and come to 426px at
       this one, so the height at which the rail stops being able to
       pin moves down by 80px. */
    --rail-gap: clamp(24px, 4.4vh, 40px);
    gap: var(--rail-gap); }
  /* ── the rail's fold ──────────────────────────────────────────────
     Above 860 there is nothing to disclose: the summary is gone and
     the details is a plain wrapper. The driver holds it open at these
     widths, so the blocks are simply in the column where they always
     were. display: contents keeps the gap between blocks on .ixnotes
     rather than making the wrapper a second flex item that swallows
     it. */
  /* A FLEX COLUMN, NOT display: contents. contents looked right — the
     wrapper disappears and the blocks become the rail's own flex items
     — and it is not what a <details> does: the UA renders its children
     through a slot, so the box came out of layout and the blocks
     stacked with no gap at all. row-gap: inherit takes .ixnotes's own
     number rather than restating it, so the two cannot drift. */
  /* The fold is a plain box; .railbody is the column. --rail-gap is
     declared on .ixnotes below and inherits down, so the rail and the
     fold's body are one number rather than two that agree today. */
  .railfold { display: block; }
  /* Above 860 the slot is a plain box: the fold is held open and there
     is nothing to travel. */
  .railslot { display: block; }
  .railbody { display: flex; flex-direction: column; gap: var(--rail-gap); }
  .railfold > summary { display: none; }
  .ixnotes .blk { font-size: var(--note); line-height: 1.45;
    color: rgba(0, 0, 0, 0.78); }
  .ixnotes .blk .tag { display: block; font-size: var(--lbl);
    font-weight: var(--lbl-weight); letter-spacing: var(--lbl-track);
    opacity: 0.5; padding-bottom: 7px; }
  /* the model receipt: key-value rows, grey key, plain value, one per
     line. Machine chrome states facts; the paragraph version narrated
     its own process and read as a robot easing you into it. */
  /* THE WAY THROUGH, on its own line, wearing the same rule every
     live word wears. See ONE WAY THROUGH below.
     block + fit-content, not inline-block: it has to START a line, and
     an inline-block simply carried on from the address. The width still
     hugs the words so the rule is the length of the link rather than
     the width of the column. */
  .ixnotes .blk .blkgo { display: block; width: fit-content;
    margin-top: 10px; color: inherit; }
  .ixnotes .blk .rl { display: block; padding-top: 4px; }
  .ixnotes .blk .rk { color: rgba(0, 0, 0, 0.45); }
  /* The right column of the ledger, in the same hand as the left. */
  .ansmeta .working { display: block; }
  .ansmeta .working .tag { display: block; color: rgba(0, 0, 0, 0.26);
    font-size: 10px; font-weight: 600; letter-spacing: 0.08em;
    text-transform: uppercase; line-height: 1.6; padding-bottom: 2px; }
  .ansmeta .working .rprose { display: block; padding-bottom: 4px; }
  .ansmeta .working .rl { display: block; padding-top: 14px; }
  .ansmeta .working .rk { display: block; color: rgba(0, 0, 0, 0.26);
    font-size: 10px; font-weight: 600; letter-spacing: 0.08em;
    text-transform: uppercase; line-height: 1.6; padding-bottom: 2px; }
  .ansmeta .working .rv > span { display: block; }
  /* the filter reads as the column's own list: same body size, one per
     line, ruled rather than boxed. A button that looked like a button
     would be the only chrome on the page.

     IT LOST ITS OWN UNDERLINE. There was a ::after here that scaled
     from zero on hover: a different curve, a different duration, and
     nothing at rest, so four live words sat in this column looking
     like four dead ones. It wears the shared rule now — see ONE WAY
     THROUGH below — which means the hairline is there before the
     pointer is, and the fill is the same move the sentence makes.

     PADDING BECAME MARGIN. The 7px that separates one filter from the
     next used to be padding, and a background paints to the padding
     box: the rule would have hung 7px under its own word. As margin it
     is air between the buttons and the line sits on the type.

     background-color rather than the background shorthand, which would
     reset the image the shared rule draws with. */
  .ixnotes .filt button, .ixnotes .filt a {
    display: block; width: fit-content; text-align: left;
    background-color: transparent; border: 0; padding: 0;
    margin: 0 0 7px; cursor: pointer;
    font: inherit; font-size: var(--note); line-height: 1.45;
    color: rgba(0, 0, 0, 0.78); }

  /* ── THE RAIL AS AN INDEX (lab: rail-variants.html, variant L) ────
     Rows, drawers and counts in place of the stacked blocks. The
     hairline, the curve and the 90ms text lag are the daybook
     ledger's own values, so the rail and the ledger move as one
     language. Rows bleed 12px past the column so the ink band reads
     as a band while the closed labels hold the column's left edge. */
  /* no rules: the owner asked to see it bare. The rows are labels in
     air until one floods; the hairline version is one commit back. */
  /* ── THE CHIPS ─────────────────────────────────────────────────────
     Each row is a rounded chip on grey that hugs its own words, and
     opens by un-hugging: it runs the column, floods to ink, squares
     its corner and reveals the drawer. Ported from lab/rail-variants
     variant M.

     THE HUG IS A CLIP, NOT A WIDTH. Width will not transition from
     fit-content, and the chip has to travel from its own words to the
     full column. So every row is laid out at full width and clipped
     back with an inset — which does animate — and the type never
     reflows, because it was always sitting where it ends up.

     ONE CLOCK. The inset, the corner and the ink all ride one
     clip-path transition on the drawer body's own 0.56s, so the width,
     the height and the radius land together. That is how every image
     on this site squares off: the index frame puts border-radius on
     exactly the transition its growth rides, and the rising plate
     scrubs r = radius * (1 - p). Nothing here is delayed.

     --hug is MEASURED per row by the driver, because every label is a
     different length and a guessed percentage clips a word in half. */
  .ixnotes .rdrawer .rrow {
    background: rgba(0, 0, 0, 0.045);
    /* 12px, not 16. The chip's own height came down with its padding
       and a radius held at 16 turned a 35px row into a lozenge — the
       corner has to fall with the box or the shape changes meaning. */
    clip-path: inset(0 var(--hug, 45%) 0 0 round 12px);
    transition: clip-path 0.56s cubic-bezier(0.2, 0.7, 0.2, 1),
      background-color 0.56s cubic-bezier(0.2, 0.7, 0.2, 1),
      color 0.56s cubic-bezier(0.2, 0.7, 0.2, 1); }
  .ixnotes .rdrawer .rrow.open {
    clip-path: inset(0 0 0 0 round 0px);
    background: var(--ink); color: #fff; }
  .ixnotes .rdrawer .rhead { display: block; width: 100%;
    text-align: left; background: none; border: 0;
    /* 14/16, not the lab card's 15/20. The variants page gave the rail
       236px to play with; the real note column is 180 (--ix-note-w),
       and at 15px with 20px of side padding the two longest labels —
       Digital Experiences and Campaign/Creative — needed exactly 180,
       so they filled the column edge to edge and stopped reading as
       chips at all. Measured, not guessed: their ink is 140px, and
       140 + 40 is the whole column.

       This is the one value that had to move on the way in, and it
       moved because the column is narrower, which is a real material
       difference rather than a preference.

       PADDING 8/12, DOWN FROM 12/15. At 12 the chip stood 43px around
       an 19px word and read as a bubble rather than a label — the grey
       was the loudest thing in the column. 35px is the same chip with
       the air taken out of it. The side value carries twice into the
       measured hug, so the resting width came in 6px with it. */
    padding: 8px 12px; margin: 0; cursor: pointer; font: inherit;
    font-size: 13px; font-weight: 500; letter-spacing: -0.01em;
    line-height: 1.45; color: inherit; }
  .ixnotes .rdrawer .rhead:focus-visible {
    outline: 1px solid currentColor; outline-offset: -3px; }
  /* the words themselves, so their width can be measured — the button
     is full-column and would only ever report that */
  .ixnotes .rdrawer .rink { display: inline-block; }
  /* NOTHING DRAWS HERE ANY MORE. .rglyph held a 13px box for a mark
     beside each category — a Swiss solid, then a Lucide icon — and no
     row emits one now. The rule stays as the box a mark would take if
     one ever comes back, and costs nothing while nothing uses it. */
  .ixnotes .rdrawer .rglyph { display: inline-block; width: 13px;
    height: 13px; margin-right: 5px; vertical-align: -2px;
    line-height: 1; }
  /* the slash is ink, like the word it introduces */
  .ixnotes .rdrawer .rslash { color: var(--ink);
    transition: color 0.56s cubic-bezier(0.2, 0.7, 0.2, 1); }
  .ixnotes .rdrawer .rrow.open .rslash { color: #fff; }
  .ixnotes .rdrawer .rbody { display: grid; grid-template-rows: 0fr;
    transition: grid-template-rows 0.56s cubic-bezier(0.2, 0.7, 0.2, 1); }
  .ixnotes .rdrawer .rrow.open .rbody { grid-template-rows: 1fr; }
  .ixnotes .rdrawer .rbody > div { overflow: hidden; min-height: 0; }
  /* the drawer's type is on its own clock: 90ms behind the structure
     on the way in, gone at once on the way out (the .dbpre pattern) */
  /* THE AIR ABOVE THE CONTENT IS ONE NUMBER NOW, and it is the stamp
     band's — see the note on .rcat below. The text drawers carried 4px
     against the band's 16 because a picture was thought to want more
     room than a paragraph does; opened side by side that read as two
     drawers built to different rules rather than as one rail. Same
     number, so Info and Connect open the way a category does. */
  .ixnotes .rdrawer .rpad { padding: 64px 16px 18px;
    opacity: 0; transform: translateY(10px); transition: none; }
  /* ── THE AIR ABOVE THE STAMP IS THE OWNER'S NUMBER ────────────────
     Not a derived value and not a guess. It has moved four times: 16
     at first, then 32 by Jeremy's explicit call (27d906b, "owner's
     call"), then 8 as collateral in the chips rework, which was a
     change about chip SHAPE and took the band's air with it without
     saying so, then 16 again, which put back half of what was lost.

     64 now, which is the gap he can actually see tripled: 8px of that
     is the chip's own bottom padding, so the word-to-picture distance
     goes 24 to 72. Measured, not counted from the declaration.

     If this looks wrong to a later reader, it is still not a value to
     re-derive. Ask. */
  /* Only the trailing air differs now: the one-liner under the reel
     wants a little more beneath it than a paragraph does. */
  .ixnotes .rdrawer .rpad.rcat { padding-bottom: 20px; }
  .ixnotes .rdrawer .rrow.open .rpad { opacity: 1; transform: none;
    transition: opacity 0.5s ease 90ms,
      transform 0.56s cubic-bezier(0.2, 0.7, 0.2, 1) 90ms; }
  .ixnotes .rdrawer .rsub { font-size: var(--lbl);
    font-weight: var(--lbl-weight); letter-spacing: var(--lbl-track);
    text-transform: uppercase; opacity: 0.6; margin: 12px 0 5px; }
  .ixnotes .rdrawer .rsub:first-child { margin-top: 0; }
  .ixnotes .rdrawer .rtxt { font-size: var(--note); line-height: 1.5; }
  .ixnotes .rdrawer .rgo { display: inline-block; margin-top: 8px;
    color: inherit; font-size: var(--note);
    text-decoration: underline; text-underline-offset: 3px; }
  /* Set like .rgo above it rather than like the page's other links:
     the drawer reverses to white on ink when it opens, and the shared
     skip-ink treatment draws its rule in --rule-rest, which is a black
     tone and disappears there. currentColor, so the line reverses with
     the words. */
  .rmail { color: inherit;
    text-decoration: underline; text-underline-offset: 3px; }
  .ixnotes .rdrawer { display: flex; flex-direction: column; gap: 10px; }
  .ixnotes .rdrawer .rgap { height: 8px; }

  /* ── PULL TO ASK (phones) ─────────────────────────────────────────
     The gesture every phone already knows, pointed at the house. It
     rides above the masthead and stays out of the way until pulled. */
  #pullask { position: fixed; left: 0; right: 0; top: 0; z-index: 60;
    display: flex; align-items: flex-end; justify-content: center;
    height: 0; overflow: hidden; pointer-events: none;
    font-size: var(--note); letter-spacing: -0.01em;
    color: rgba(0, 0, 0, 0.62); background: var(--paper, #F3F0ED); }
  #pullask b { font-weight: 500; color: var(--ink); }
  #pullask .pa-in { padding-bottom: 9px; text-align: center;
    line-height: 1.35; }
  #pullask.pa-snap { transition: height 0.42s cubic-bezier(0.2, 0.7, 0.2, 1); }
  @media (prefers-reduced-motion: reduce) {
    #pullask { display: none; }
  }
  /* one column, stacked: the reel, its caption, then the one-liner —
     the emptied right half is the band's negative space. THE REEL IS
     THE COUNT: a stamp-size faux reel cutting through the category's
     own tiles, the cover reel's signature at rail scale. */
  .ixnotes .rdrawer .rreel { width: 128px; height: 96px;
    border-radius: 10px; overflow: hidden;
    /* held and dragged sideways to scrub; vertical stays the page's,
       and pinch-zoom is named so two fingers still zoom */
    touch-action: pan-y pinch-zoom; user-select: none; -webkit-user-select: none;
    -webkit-touch-callout: none; }
  .ixnotes .rdrawer .rdesc { font-size: 11px; line-height: 1.5;
    opacity: 0.75; margin-top: 12px; max-width: 24ch; }

  /* ── THE STAMP RUNS THE REAL REEL ─────────────────────────────────
     The sz-* rules below are SizzleReel.tsx's own, copied verbatim —
     the photo beats only (shutter, fade, pinch, slat, burn, curtain,
     color curtain, cut) — so the rail stamp and the case-study covers
     are one mechanism, not a lookalike. At port time the real
     homepage imports the component's stylesheet instead of this. */
  .sz-stage{position:relative;overflow:hidden;background:#0E0E0E;display:block;container-type:inline-size}
  .sz-fill{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block}
  .sz-layer{position:absolute;inset:0;will-change:transform,opacity,clip-path}
  .sz-shutter{animation:szShutter var(--d) cubic-bezier(.7,0,.15,1) both}
  @keyframes szShutter{from{clip-path:inset(50% 0 50% 0)}to{clip-path:inset(0 0 0 0)}}
  .sz-fade{animation:szFade var(--d) ease both}
  @keyframes szFade{from{opacity:0}to{opacity:1}}
  .sz-cut{animation:szCut var(--d) steps(2) both}
  @keyframes szCut{from{opacity:0}to{opacity:1}}
  .sz-curtain{animation:szCurtain var(--d) cubic-bezier(.7,0,.15,1) both}
  @keyframes szCurtain{from{clip-path:inset(0 100% 0 0)}to{clip-path:inset(0 0 0 0)}}
  .sz-pinchP{position:absolute;left:0;right:0;height:50.5%}
  .sz-pinchT{top:0;transform:translateY(-102%);animation:szPinchT var(--d) cubic-bezier(.7,0,.15,1) both}
  .sz-pinchB{bottom:0;transform:translateY(102%);animation:szPinchB var(--d) cubic-bezier(.7,0,.15,1) both}
  @keyframes szPinchT{0%{transform:translateY(-102%)}45%{transform:translateY(0)}55%{transform:translateY(0)}100%{transform:translateY(-102%)}}
  @keyframes szPinchB{0%{transform:translateY(102%)}45%{transform:translateY(0)}55%{transform:translateY(0)}100%{transform:translateY(102%)}}
  .sz-pinchswap{animation:szPinchSwap var(--d) linear both}
  @keyframes szPinchSwap{0%{opacity:0}49%{opacity:0}50%{opacity:1}100%{opacity:1}}
  .sz-strip{position:absolute;top:0;height:100%;overflow:hidden;clip-path:inset(0 0 100% 0);animation:szSlat var(--sd) cubic-bezier(.6,0,.15,1) both}
  .sz-strip img{position:absolute;top:0;height:100%;max-width:none;object-fit:cover;display:block}
  @keyframes szSlat{from{clip-path:inset(0 0 100% 0)}to{clip-path:inset(0 0 0 0)}}
  .sz-burnimg{animation:szBurnImg var(--d) cubic-bezier(.72,0,.18,1) both}
  @keyframes szBurnImg{0%{opacity:0;transform:scale(1.02);filter:brightness(1.5) saturate(2.6) contrast(1.5)}22%{opacity:1;transform:scale(1.015);filter:brightness(1.35) saturate(2.2) contrast(1.4)}100%{opacity:1;transform:scale(1);filter:brightness(1) saturate(1) contrast(1)}}
  .sz-burnpop{position:absolute;inset:0;pointer-events:none;background:rgba(243,240,237,.35);opacity:0;-webkit-backdrop-filter:blur(0px) saturate(1) contrast(1);backdrop-filter:blur(0px) saturate(1) contrast(1);animation:szBurnPop var(--d) cubic-bezier(.72,0,.18,1) both}
  @keyframes szBurnPop{0%{opacity:0;-webkit-backdrop-filter:blur(0px) saturate(1) contrast(1);backdrop-filter:blur(0px) saturate(1) contrast(1)}18%{opacity:1;-webkit-backdrop-filter:blur(4px) saturate(4.5) contrast(2.2);backdrop-filter:blur(4px) saturate(4.5) contrast(2.2)}100%{opacity:0;-webkit-backdrop-filter:blur(0px) saturate(1) contrast(1);backdrop-filter:blur(0px) saturate(1) contrast(1)}}
  .sz-burnwash{position:absolute;inset:0;pointer-events:none;background:#F3F0ED;opacity:0;animation:szBurnWash var(--d) cubic-bezier(.72,0,.18,1) both}
  @keyframes szBurnWash{0%{opacity:0}16%{opacity:.95}40%{opacity:.3}100%{opacity:0}}
  @media (prefers-reduced-motion:reduce){
    .sz-layer,.sz-strip{animation:none!important}
    .sz-shutter,.sz-curtain,.sz-strip{clip-path:inset(0 0 0 0)!important}
    .sz-fade,.sz-cut{opacity:1!important}
    .sz-pinchswap{animation:none!important;opacity:1!important}
    .sz-pinchT{animation:none!important;transform:translateY(-102%)!important}
    .sz-pinchB{animation:none!important;transform:translateY(102%)!important}
    .sz-burnimg{opacity:1!important;filter:none!important;transform:none!important}
    .sz-burnpop,.sz-burnwash{opacity:0!important;animation:none!important;-webkit-backdrop-filter:none!important;backdrop-filter:none!important}
  }
  @media (prefers-reduced-motion: reduce) {
    .ixnotes .rdrawer .rrow, .ixnotes .rdrawer .rbody,
    .ixnotes .rdrawer .rglyph, .ixnotes .rdrawer .rpad {
      transition: none; }
  }

  /* the row: notes column left empty (the notes float over it), then
     two frames, each hung from the top and pushed to the right of its
     half — the two right edges are the only vertical lines the eye
     needs to hold the grid together */
  /* The band is top-anchored, not centred. row-gap is the air BELOW a
     row's pictures (before its own rule); margin-bottom is the air
     below the rule (before the next row's pictures). They used to be
     equal, which floated each row in the middle of its band. Weighting
     the gap under the pictures and tightening the gap under the rule
     pulls every row up to the line above it, with the white space
     falling below — which is the ask. */
  /* THE LEAD FRAME IS OUT OF FLOW. It is the third column's first
     picture and it has no partner: it sits up beside the statement,
     and the pairs below start where they would have anyway. Given a
     row of its own it would hold that row's height and push them
     down, which is the opposite of the point. Width is the right
     column exactly: half the track less half the gutter. */
  /* One track: .ixlead is also .ixrow, whose two columns put its lone
     cell in the LEFT half of its own box — a column short of where it
     belongs. */
  .ixrow.ixlead { grid-template-columns: 1fr; margin-bottom: 0; }
  .ixlead { position: absolute; right: 0;
    top: calc(-1 * var(--col3-lift, 0px));
    width: calc((100% - var(--ixgap)) / 2); }
  /* ── THE ROOM UNDER THE LEAD FRAME ────────────────────────────────
     Every other row opens the page under itself with margin-bottom.
     This one cannot: it is absolutely positioned, so its own margin
     moves nothing, and the zero above says so rather than choosing it.

     What sat under it was RESIDUE. The lead's top is pinned to the
     cover's address by --col3-lift and its bottom falls wherever the
     picture's height puts it, while the first pair starts where it
     would have anyway. Measured on production: 156px, against the
     165px every other row was keeping at that width. Close enough to
     look deliberate, and it was not — it had been 156 while the rows
     were at 82, and it stayed 156 when they doubled to 165. It could
     never track the interval, because nothing connected the two.

     --lead-room is the difference, measured in the driver's fit() pass
     and written on #ixRows, so the first pair clears the lead frame by
     exactly the interval the rest of the index keeps. Zero until it is
     measured, which is what the page did before.

     PADDING, NOT MARGIN, and that is the whole trick. The first pair
     is the first in-flow child of .ixrows — the lead above it is
     absolute and takes no part — and .ixrows is position:relative,
     which is not a block formatting context. So a margin-top on this
     row COLLAPSES THROUGH its parent: it moves .ixrows instead of
     opening a gap inside it, and the frame below stays exactly where
     it was. Measured: margin-top 40px moved the first frame 0px,
     padding-top 40px moved it 40. Padding does not collapse. */
  .ixlead + .ixrow { padding-top: var(--lead-room, 0px); }
  /* THE LEFT COLUMN HANGS OFF ITS OWN RULE. Frames are a fraction of
     their column, so which edge they sit on is a real choice: the
     right column reads against the page edge, the left against the
     rule beside the note rail. Growth follows the anchor, or a hover
     would walk the frame off its line. */
  /* WHICH COLUMN A FRAME IS IN IS A CLASS, not its position among its
     siblings. Position was fragile in both directions: a row missing
     its right-hand partner put the survivor in the wrong column, and
     the lead frame's only cell is a first child while belonging to
     the right. The name sits on the edge its frame is hung from and
     swings to the other on hover — see --slide in the driver.

     The explicit track is desktop-only: on a phone the rows dissolve
     to display:contents and a grid-column: 2 would invent a second
     track in a one-track grid. */
  @media (min-width: 861px) {
    .ixrow .cell.colL { grid-column: 1; }
    .ixrow .cell.colR { grid-column: 2; }
    .ixrow.ixlead .cell { grid-column: 1; }
  }
  .ixrow .cell.colL { justify-content: flex-start; }
  .ixrow .cell.colL .fd-it { transform-origin: left center; }
  .ixrow .cell.colL .fd-it .shot { transform-origin: left top; }
  .ixrow .cell.colR { justify-content: flex-end; }
  .ixrow .cell.colR .fd-it { transform-origin: right center; }
  .ixrow .cell.colR .fd-it .shot { transform-origin: right top; }
  .ixrow .cell.colR .fd-it .lbl { text-align: right; }
  .ixrow { --ixgap: clamp(24px, 3.4vw, 56px);
    display: grid; align-items: start;
    grid-template-columns: 1fr 1fr;
    column-gap: var(--ixgap);
    /* THE ANSWER'S GAP, not the index's. The answer still deals an
       .ixrule into every row, so this is the space above it there. The
       index stopped emitting one, which leaves each index row a single
       grid track with nothing for a row-gap to fall between — measured
       on production: rows are 801px of cell and 801px tall. So this
       value is live on the answer and inert on the index, and the note
       below is the whole interval on the page you actually scroll. */
    row-gap: clamp(56px, 6.6vw, 108px);
    /* THE INTERVAL BETWEEN PAIRS, and on the index it is the ENTIRE
       interval. Measured from the taller frame in a row to the top of
       the next pair, so a row that leaves negative space beside a short
       frame still hands over on the same beat.

       DOUBLED, Aug 2026: 100px to 200px at the cap, the floor and the
       vw term with it, so the interval opens at every width rather
       than only on a wide screen. The index reads as a gallery hang
       rather than a feed, and the frames needed the wall.

       Only ever desktop: below 860 the rows dissolve to
       display:contents and the container's own rhythm takes over, so
       a phone keeps its tighter beat without an override. */
    margin-bottom: clamp(128px, 14vw, 200px); }
  .ixrow:last-child { margin-bottom: 0; }
  .ixrow .cell { display: flex; justify-content: flex-end; }

  /* One rule, one length: the full width of the image area, every row.
     grid-column 1/-1 spans both cells and the gap between them; nothing
     indents it any more, so a small left frame and a large one sit over
     rules of exactly the same length. It still lives inside .ixrow, in
     the content column, so it never crosses the notes column. */
  /* Kept in the grid, no longer drawn. The band rhythm above is built
     out of the gap ABOVE this element and the margin BELOW it, so
     removing it outright would retune every row; zeroing the border
     leaves the spacing exactly where it was tuned and takes the line
     away. It draws again on a phone, where there are no columns for a
     vertical to stand between. */
  .ixrule { grid-column: 1 / -1; height: 0; border-top: 0; }
  .ixrow:last-child .ixrule { display: none; }

  /* STANDING RULES, instead of one per row.
     The comment above already says it: the frames hang from the right
     edge of each half, so "the two right edges are the only vertical
     lines the eye needs". These draw the gutters those edges are
     measured from, once down the whole index, rather than every row
     restating the same interval horizontally.

     Three of them, and the rule that places all three is one sentence:
     every column's frames hang from a right edge, and the line stands
     half a gutter after that edge. The rail's edge, then the first
     column's, then the last. Both gutters are the same clamp, so one
     offset serves all three, and the middle lands on 50% exactly
     because the tracks are 1fr 1fr around that gap.

     ALL THREE COME OFF .ixrows' OWN BOX, and that is the point rather
     than a convenience. Built instead out of .ixbody's variables — page
     gutter plus rail width plus half a gap — the first line landed 2.5px
     wide of the gutter it was meant to halve, because .ixbody's contents
     sit 3px left of where its own padding says they do. Its box knows
     the answer; its variables only nearly know it.

     Two are pseudo-elements, an element having only two. The middle one
     is a 1px background instead, which is the same hairline by another
     route and takes a percentage position without needing a third. */
  .ixrows { position: relative; --ixgap: clamp(24px, 3.4vw, 56px); }
  /* .ixrules carries these now. The cover keeps its own copy, which is
     correct: those scroll away with the cover, and the pinned set
     stands behind them at the same three x positions. */
  .ixrows::before, .ixrows::after {
    content: ""; position: absolute; top: 0; bottom: 0; width: 1px;
    background: none; pointer-events: none; }
  /* half a gutter out from each edge: the middle of the gap to the note
     rail on the left, and the matching interval past the last column */
  .ixrows::before { left: calc(var(--ixgap) / -2); }
  .ixrows::after { left: calc(100% + var(--ixgap) / 2); }

  /* ── THE BOARD'S OWN PACKING ─────────────────────────────────────
     Staples deals the whole board and it packs rather than pairs — see
     buildAnswer. Three columns at the tiles' own ratios, which is
     staples.module.css's layout brought onto the answer so the board
     reads the same wherever it is met: dense, unsorted, no two tiles
     the same height.

     CSS columns, not a grid. A masonry has to know each tile's height
     before it can place the next one, and only the browser knows that;
     a grid would want a row span per tile computed from a measurement
     that is not final until the picture lands. `columns` does it with
     one declaration and break-inside on the child.

     Gap and count are the board page's values, not new ones. */
  .ixrows.ansboard { columns: 3; column-gap: clamp(16px, 2vw, 28px); }
  .ansboard .btile { break-inside: avoid;
    margin-bottom: clamp(16px, 2vw, 28px); }
  /* Full column width, and no hover growth: a board tile is already as
     wide as its column, so there is nowhere for it to open to. */
  .ansboard .fd-it { width: 100%; }
  /* The standing name a work frame carries under it, and the caption a
     pull shows on hover, both stay as they are — a pull has only the
     caption, and it is the one thing naming a picture nobody made
     here. */

  .ixrow .fd-it { width: calc(var(--share, 0.7) * 100%);
    transform-origin: right center;
    transition: opacity 0.8s ease,
      transform 0.62s cubic-bezier(0.16, 1, 0.3, 1); }
  /* arrival is opacity-ONLY: a translateY here would be overwritten by
     the hover transform, and they are the same property */
  .ixrow .fd-it, .ixrow .fd-it.fd-on { transform: none; }
  .ixrow .fd-it .shot { aspect-ratio: var(--ar, 1); }
  .ixrow .fd-it .cap { display: none; }
  .ixrow .fd-it .lbl { display: block; padding-top: 10px;
    font-size: var(--note); line-height: 1.35; font-weight: 600;
    letter-spacing: -0.004em; text-align: left; }

  /* ── hover: the frame opens down and left ────────────────────────
     transform-origin is the top right corner, so the frame keeps its
     TOP LINE — the one thing a row of top-aligned frames cannot afford
     to lose — and opens downward and leftward from there.

     The scale is on the FRAME, not the card, so the name underneath is
     not scaled with it. It is displaced instead: the driver measures
     how far the frame grew and slides the label down by exactly that,
     so the type stays its own size and simply keeps its place under
     the picture.

     And the frame beside it is pushed rather than covered. The driver
     works out whether the opening frame would actually reach its
     neighbour and moves it only as far as it has to — a frame that
     was never in the way does not move at all. */
  .ixrow .fd-it, .ixrow .fd-it .shot, .ixrow .fd-it .lbl {
    transition: transform 0.62s cubic-bezier(0.16, 1, 0.3, 1); }
  .ixrow .fd-it .shot { transform-origin: right top; }

  @media (hover: hover) and (min-width: 761px) {
    /* the ceiling; each frame takes as much of it as it has room for */
    .ixbody { --ix-grow-max: 1.32; }
    .ixrow .fd-it .shot {
      transition: transform 0.62s cubic-bezier(0.16, 1, 0.3, 1),
        border-radius 0.62s cubic-bezier(0.16, 1, 0.3, 1); }
    /* THE PAGE MAKES ROOM. The frame opens with a transform, and a
       transform costs no layout, so the index used to sit still and get
       covered. --drop is exactly what the transform added in pixels —
       the same number the label rides down — so giving the card that
       much real bottom margin lets normal flow carry every row below it
       out of the way. The frame can then reach the column from any row
       instead of stopping wherever its own band ran out.

       On the same clock as the scale, so the picture opening and the
       page opening under it are one movement. */
    .ixrow .fd-it {
      transition: opacity 0.8s ease,
        transform 0.62s cubic-bezier(0.16, 1, 0.3, 1),
        margin-bottom 0.62s cubic-bezier(0.16, 1, 0.3, 1); }
    .ixrow .fd-it:hover { z-index: 2; margin-bottom: var(--drop, 0px); }
    .ixrow .fd-it:hover .shot { transform: scale(var(--ix-grow, 1.32));
      /* squares off as it opens: a rounded frame reads as an object on
         the page, losing the corner turns it into the picture itself */
      border-radius: 0; }
    /* The name swings to the right edge as the frame opens — the same
       edge every frame in the index is hung from.

       It SLIDES rather than re-aligns. text-align cannot be
       transitioned, so switching it on hover snapped the type across
       in a single frame. The driver measures how far the text
       actually is from the right edge and translates it there, on the
       same transform and the same clock as the drop — so the label
       makes one diagonal move rather than two unrelated ones. */
    .ixrow .fd-it:hover .lbl {
      transform: translate(var(--slide, 0px), var(--drop, 0px)); }
    /* The lead frame's room, since it cannot make its own: the first
       pair takes the drop as real margin and the whole index follows
       it down, on the same clock as the picture opening. */
    /* ONE PROPERTY, NO CALC, and that is not tidiness. This was
       `calc(var(--lead-room, 0px) + var(--lead-drop, 0px))` and it
       computed to ZERO on production, with --lead-room inheriting a
       perfectly good 120.4px. A custom property that has been declared
       and then emptied is VALID AND EMPTY, not invalid — so var() does
       not reach for its fallback, it substitutes nothing, the calc
       becomes `calc(120.4px + )`, and an invalid calc takes the whole
       declaration down to the property's initial value. The hover
       push wrote --lead-drop and the standing interval disappeared
       with it.

       So the driver adds the two itself and writes one number. There
       is nothing here for a second property to poison.

       The transition is all this rule does now. Padding, not margin,
       for the reason the standing rule gives — which also means the
       hover push had never worked, and the lead frame has been growing
       over the pair below it since it was written. */
    /* NO STANDING TRANSITION HERE, and that is the fix rather than a
       simplification. Declared on the rule, it applied to the room the
       fit() pass writes on first paint — so the interval under the lead
       frame ANIMATED IN from zero over 0.62s on every load, which is a
       layout constant behaving like a gesture. It also hid itself:
       getComputedStyle reports the animating value, so every
       measurement of this row read 0px while the transition was still
       running, and in a throttled tab it never finished at all.

       The hover push arms the transition inline for the length of the
       push and the driver takes it off again. See the lead branch in
       the pointer handler. */
  }

  @media (max-width: 860px) {
    /* no room for a floating column: the notes run first, then the
       rows go one frame wide */
    .ixbody { display: block; }
    .ixnotes { position: static; width: auto; left: auto;
      padding-bottom: clamp(40px, 9vw, 64px); }
    /* Tighter down here: the rail's 60px is the air a pinned column
       beside the work can afford, and inside a fold on a phone it just
       makes the open state long. One override, and the body reads it
       through the same property. */
    .ixnotes { --rail-gap: clamp(22px, 5.5vw, 34px); }
    /* The air under the rule is the FIRST BLOCK'S, not the body's.
       min-height: 0 lets a grid item shrink past its content and not
       past its own padding, so 22px of padding on the body was a floor
       the closed row could never get under: the fold shut to 22 rather
       than to nothing. As the block's padding it is content, and the
       slot's clip reaches it. */
    .railbody > .blk:first-child { padding-top: var(--rail-gap); }
    /* 0fr to 1fr, which is the one height animation that does not need
       a measured pixel value: the track resolves to the content's own
       height at 1fr and collapses at 0. min-height 0 on the child is
       load bearing, since a grid item refuses to shrink below its
       content without it and the row would never close. */
    .railslot { display: grid; grid-template-rows: 0fr;
      /* The clip belongs to the SLOT. On the child it hid the child's
         CONTENT while the child's own padding still took height, so a
         closed fold left 22px of dead space under the rule. */
      overflow: hidden;
      transition: grid-template-rows 0.42s cubic-bezier(0.2, 0.7, 0.2, 1); }
    .railslot.on { grid-template-rows: 1fr; }
    .railslot > .railbody { min-height: 0; }
    @media (prefers-reduced-motion: reduce) {
      .railslot { transition: none; }
    }
    .railfold > summary {
      display: flex; align-items: baseline; justify-content: space-between;
      gap: 12px; cursor: pointer; list-style: none;
      font-size: var(--note); line-height: 1.45;
      padding-bottom: clamp(14px, 3.5vw, 20px);
      border-bottom: 1px solid rgba(0, 0, 0, 0.13); }
    .railfold > summary::-webkit-details-marker { display: none; }
    /* The marker is authored so it can rotate: the UA triangle cannot
       be transitioned and sits on the wrong side of the line. */
    .railfold > summary::after {
      content: "";
      width: 8px; height: 8px; flex: none;
      border-right: 1px solid currentColor;
      border-bottom: 1px solid currentColor;
      transform: translateY(-2px) rotate(45deg);
      transition: transform 0.28s cubic-bezier(0.2, 0.7, 0.2, 1);
      opacity: 0.55; }
    .railfold[open] > summary::after {
      transform: translateY(2px) rotate(-135deg); }
    .railfold > summary:focus-visible { outline: 1px solid var(--ink);
      outline-offset: 3px; }
    @media (prefers-reduced-motion: reduce) {
      .railfold > summary::after { transition: none; }
    }
    .ixrow { grid-template-columns: 1fr; row-gap: clamp(30px, 7vw, 48px); }
    /* The columns dissolve and their frames become the container's own
       children, ordered by the deal index each carries — so one track
       reads 0,1,2,3 rather than the whole left column then the whole
       right. */
    /* ── THE PHONE'S OWN RHYTHM ──────────────────────────────────
       Thirty frames at one width is a stack, and a stack has no
       shape: every project arrives at the same size, so nothing is
       emphasised and scrolling it is a chore rather than a read.

       Six tracks, and a cell takes a span. The cycle is seven cells:

         hero        (6)     one full-width frame
         pair        (3 + 3) an even split
         hero        (6)
         hero        (6)
         pair        (2 + 4) a third against two thirds

       The two halves of a pair are NOT the same height and are not
       meant to be: every card carries its own --ar off the index, and
       the ratios in here run from a 3:2 landscape to a 3:4 portrait.
       That difference is the whole reason a pair is worth having,
       which is what the Big Bend spread already does on desktop.

       The span classes come from the dealer, not from nth-child. The
       phone dissolves .ixrow with display:contents, which takes the
       box out of layout and leaves it in the DOM, so a positional
       selector counts inside a two-cell row and never sees a seventh
       child. Only the deal knows a frame's flat place. */
    .ixrows.ixcols { display: grid;
      grid-template-columns: repeat(6, minmax(0, 1fr));
      align-items: start;
      /* ONE VALUE, TWO READERS. The grid sets the gutter and the pair's
         standing rule centres itself in it, so a variable rather than
         the number twice: change it here and the line follows. Widened
         from clamp(10px, 3vw, 18px) now that a rule stands in the gap —
         the old note said a wider gutter would break a pair apart, and
         it was right while nothing divided them. A divider wants air on
         both sides or it reads as touching the pictures. */
      --ixcolgap: clamp(18px, 5vw, 30px);
      column-gap: var(--ixcolgap);
      /* DOUBLED. The rule between two frames had about 50px of air
         around it, which on a phone reads as a list with dividers
         rather than as work with room. The column gap is untouched:
         that one holds a pair together and widening it would break
         the pair apart. */
      /* AND HALF AGAIN, the owner's call. The doubling above was
         measured against a layout where every row carried a rule under
         it; with the pairs' rules now standing between them instead,
         the horizontal air is the only thing separating one row of
         work from the next and it was doing two jobs at the old
         value. */
      /* AND DOWN AGAIN, because the layout under it changed twice. The
         90-to-144 was measured against a deal with full-width frames
         in it, where the air had to separate one big picture from the
         next. Every tile is half now and most are shorter than they
         were, so the same interval left a phone scrolling through more
         gap than work. Owner's call if this wants to go back up. */
      row-gap: clamp(52px, 12vw, 88px); }
    .ixrows.ixcols .cell { grid-column: span 6; }
    .ixrows.ixcols .cell.mHalf { grid-column: span 3; }
    /* mThird and mTwoThirds are not dealt — see rhythmClasses, which
       deals halves and nothing else. The spans stay declared: they cost
       nothing, and a varied pair is the kind of thing that comes back. */
    .ixrows.ixcols .cell.mThird { grid-column: span 2; }
    .ixrows.ixcols .cell.mTwoThirds { grid-column: span 4; }
    /* ── THE DEALT SHAPES ─────────────────────────────────────────────
       Two overrides of the frame's declared ratio, on the phone only
       and on a cycle — see M_HEIGHTS. They crop, which is the trade:
       at 155px these are thumbnails, and a thumbnail grid where every
       tile is the same landscape reads as a table of contents. The
       majority of tiles keep their own ratio, so this is the accent.

       On .shot, which is the box that holds the row height. The image
       inside is already object-fit: cover at any shape, and the plate
       that bleeds on scroll takes its height from this box, so both
       follow without knowing. */
    .ixrows.ixcols .cell.mTall .shot { aspect-ratio: 3 / 4; }
    .ixrows.ixcols .cell.mSquare .shot { aspect-ratio: 1 / 1; }
    .ixrow, .ixlead { display: contents; }
    /* ── THE COPY IS A COLUMN, NOT A HEADER ───────────────────────────
       The statement and the filters used to run full width above the
       work, so a phone opened on a screen of words with the first
       picture below the fold. They are grid items now — the driver
       moves both into this grid on a phone, see placeCopy — which puts
       them in the left half with the work climbing the right from the
       very top.

       dense is what makes that work. The statement is placed first and
       spans two rows, so the packer has a two-row hole beside it;
       without dense the tiles would queue after the filters and leave
       that hole empty, which is the layout this replaces. Every tile
       is the same span, so dense reorders nothing: it fills the holes
       in dealing order. */
    .ixrows.ixcols { grid-auto-flow: row dense; }
    .ixrows.ixcols > .ixcopy {
      grid-column: 1 / span 3; grid-row: span 2;
      align-self: start; min-width: 0; }
    .ixrows.ixcols .ixcopy > .coverR {
      padding-top: 0; width: auto; }
    /* The filters follow the sentence directly. The interval is the
       air between two things that belong together, not the grid's own
       row gap, which separates unrelated work. */
    .ixrows.ixcols .ixcopy > .ixnotes { padding-top: 30px; }
    /* THE MEASURE HALVED, SO THE TYPE COMES DOWN. This is set at the
       cover's own size, which was right when the statement ran the
       full width of a phone; in a 155px column it gave three words a
       line and pushed the question off the right edge. 17px is about
       twenty characters here, which is a column rather than a stack of
       fragments. */
    .ixrows.ixcols .ixcopy .statement { font-size: clamp(15px, 4.5vw, 18px); }
    /* And the question fills the column rather than 9.5 of its own
       ems, which at this size overflowed it. */
    .ixrows.ixcols .ixcopy #askSlot { width: 100%; }

    .ixlead { position: static; }
    .ixrow .cell { justify-content: stretch; }
    .ixrow .fd-it { width: 100%; }
    /* ── NO HORIZONTAL RULES ─────────────────────────────────────────
       Every cell drew one above itself, which turned the index into a
       ruled list: a line, a picture, a line, a picture. With the row
       interval half again as wide and the pairs divided by a standing
       rule of their own, the air is doing the separating and the lines
       were only repeating it. Owner's call, and the same instinct as
       the four category marks that came out of the rail.

       The PADDING stays. It was never the rule's — it is the air above
       each frame, and it is what the interval is measured from. */
    .ixrows.ixcols .cell { padding-top: clamp(40px, 10vw, 68px); }
    .ixrows.ixcols .cell.first { padding-top: 0; }
    /* THE ANSWER'S ROW RULE IS NOT A GRID ITEM. buildAnswer appends an
       .ixrule to every row for the wide layout's divider, and the row
       is display:contents here — so that div became a cell in the six
       column grid, 329px wide, sitting between a pair and shoving its
       two halves onto separate rows. The halves were correctly span 3
       and correctly marked pairL/pairR; something invisible was simply
       standing between them.

       display:none rather than a zero border, because the box is the
       problem and not the line. The line itself went earlier. */
    .ixrows.ixcols .ixrule { display: none; }

    /* ── THE FRAME OPENS AS YOU COME DOWN THE PAGE ────────────────────
       Each picture arrives inset by the page gutter and rounded, and
       widens to the full glass with its corners squaring off as it
       climbs. A pair opens the same way and meets in the middle: the
       left frame takes the left gutter and half the column gap, the
       right one mirrors it, so the two edges land against each other
       with nothing between them.

       THE LAYOUT NEVER MOVES. .shot keeps its rest width and its
       declared ratio, so it holds the row height and the caption under
       it stays exactly where it was — which is the point, because a
       caption that slid while its picture opened would read as the page
       reflowing rather than as a frame opening. What actually grows is
       the plate INSIDE the frame, taken out of flow and pushed past
       both edges. Nothing in the grid is re-measured; the driver writes
       one custom property per tile.

       The plate was already there, holding the arrival curtain and the
       settle from 1.25. It gains the clip and the radius that .shot
       used to own — same box, same result — and .shot gives up its
       overflow so the plate has somewhere to go. clip-path stays the
       curtain's; the corners are border-radius, so the two never
       collide over one property. */
    .ixrows.ixcols .fd-it .shot {
      position: relative;
      overflow: visible;
      border-radius: 0;
    }
    .ixrows.ixcols .fd-it .shot > * {
      position: absolute;
      top: 0;
      height: 100%;
      /* !important, and it is armReels that makes it necessary: the
         reel writes width:100%;height:100% as an INLINE style, which
         beats any stylesheet, so a reel tile took left:-20px and then
         sized itself to the frame anyway. It bled off the left edge and
         stayed 40px short of the right one — the one tile in forty that
         opened crooked. The inline pair has to stay for the wide
         layout, where nothing sets the box but the reel itself. */
      width: auto !important;
      /* AND THE RATIO, for the same reason and the same offender. The
         reel element carries aspect-ratio: 16/9, and a box with a ratio
         and a fixed height DERIVES its width from them — so left and
         right were computed, ignored, and the two reel tiles in ten
         opened 61 and 121px wide instead of 40 and 30. Measured with
         offsetWidth: the rects had said the plain tiles were wrong too,
         because a rect carries every ancestor transform and the
         arrival's scale was still running. */
      aspect-ratio: auto !important;
      left: calc(-1 * var(--bl, 0px) * var(--gp, 0));
      right: calc(-1 * var(--br, 0px) * var(--gp, 0));
      overflow: hidden;
      border-radius: calc(var(--r) * (1 - var(--gp, 0)));
    }
    /* HOW FAR EACH SIDE HAS TO TRAVEL, declared here because only the
       stylesheet knows the gutter and the gap. A full-width frame owes
       the page gutter on both sides. A half owes the gutter outward and
       half the column gap inward, so the pair closes on the centre line
       rather than one of them crossing it. */
    .ixrows.ixcols .cell .fd-it { --bl: var(--gut); --br: var(--gut); }
    .ixrows.ixcols .cell.mHalf.pairL .fd-it {
      --bl: var(--gut);
      --br: calc(var(--ixcolgap) / 2);
    }
    .ixrows.ixcols .cell.mHalf.pairR .fd-it {
      --bl: calc(var(--ixcolgap) / 2);
      --br: var(--gut);
    }
    /* An unpaired half sits alone in its row and has no middle to meet,
       so it opens outward only. */
    .ixrows.ixcols .cell.mHalf:not(.pairL):not(.pairR) .fd-it {
      --bl: var(--gut);
      --br: var(--gut);
    }
    /* Reduced motion keeps every frame at its rest inset, rounded. The
       driver never writes --gp, and the fallback of 0 is the whole
       stand-down. */
    /* NO RULE HERE AT ALL. This drew a spine down the index at 50%,
       broken around the full-width frames by paper bands. It replaced
       eight per-pair segments, which replaced a horizontal rule under
       every cell. Three treatments of the same line, and the owner's
       call on the third was the same as on the first two: the air is
       doing the separating and the line was repeating it.

       The pairL / pairR classes stay on the cells. They cost nothing,
       they are the only place that knows which frames share a row, and
       that knowledge is what any future treatment of the gutter would
       need. Nothing reads them today. */

    /* The board keeps packing on a phone, at two columns rather than
       one: three tiles across a 329px column is a contact sheet nobody
       can read, and one is a hundred and seven full-width pictures to
       scroll past. Both numbers are staples.module.css's own phone
       values, so the board reads the same on a phone as it does on the
       page it came from. */
    .ixrows.ansboard { columns: 2; column-gap: 14px; }
    .ansboard .btile { margin-bottom: 14px; }
    /* One column and no note rail, so there are no gutters left for a
       vertical to stand in. The row rule comes back to do the work. */
    .ixrows::before, .ixrows::after { display: none; }
    .ixrules { display: none; }
    .ixrows { background-image: none; }
    /* The row rule goes with them. It is the ANSWER's divider rather
       than the index's, and on a phone the two share this block. */
    .ixrule { border-top: 0; }
    /* THE COVER'S FIRST LINE IS NAME AND ADDRESS, and it used to be
       three stacked things: the wordmark, the statement, then the
       address at the very bottom, 284px down and 260 below the name it
       belongs beside. On a wide screen those are two ends of one row
       and it read that way; stacked, the address just became the last
       thing before the work.

       They fit. The name measures 120 and the address 102 in a 329
       column, so putting them on one line costs nothing and leaves 107
       between them.

       display: contents on .covercols so the statement and the address
       become children of this grid and can be placed independently:
       the address belongs on row one with the name, the statement on
       row two under both. */
    .cover .ixbody { display: grid;
      grid-template-columns: auto auto;
      justify-content: space-between;
      align-items: baseline;
      column-gap: 12px; }
    .cover .covercols { display: contents; }
    .cover .covermark { grid-column: 1; grid-row: 1; }
    .cover .covermeta { grid-column: 2; grid-row: 1;
      justify-self: end; text-align: right; }
    .cover .coverR { grid-column: 1 / -1; grid-row: 2; }
    /* Less air on a phone, where the fold is the scarce thing. */
    .cover { --cover-air: 24px; }
    /* .ixbody carries the gutter down here, so the head must not add
       a second one. It used to sit outside and needed its own. */
    /* ONE COLUMN ON A PHONE, and the lead falls back into the flow
       where its DOM position puts it: under the receipts and above the
       rows, which is the reading order it already had. */
    .anshead { padding-left: 0; }
    /* .answer prefixed, and the note below says why: a media query
       adds no specificity and the head's two-column rule is written
       further down this stylesheet, so a bare .anshead here loses on
       source order. It lost exactly that way first time out — the
       prose, the receipts and the back link were squeezed into 152px
       with the lead frame beside them on a 375 screen. */
    .answer .anshead {
      grid-template-columns: minmax(0, 1fr); }
    .answer .anshead > .anslead { grid-column: 1; grid-row: auto;
      padding-top: 26px; }
    /* TWO CLASSES, because one loses. A media query adds no
       specificity, and the desktop .ansprose cap is written further
       down this stylesheet, so a bare .ansprose here has never won:
       the answer's line has been capped at half a phone's width since
       the cap was added, which on a 375 screen is 152px of measure.
       The .anshead rule above had the same fault and the same effect,
       an answer indented by a rail that is not on screen; it agrees by
       accident now that the head sits in the grid and its desktop
       padding is 0. This one is made to win outright. */
    .answer .ansprose { max-width: none; }
    /* The air moves from under the NAME to above the STATEMENT: the
       name now has the address beside it, and padding under one half
       of a row would push only that half down. */
    .covermark { display: block; padding-bottom: 0; }
    .cover .coverR { padding-top: clamp(22px, 5vw, 40px); }
  }

  /* ── the work: blocks, and the air between them ──────────────────
     Twelve tracks, the house grid. This is not a feed any more: each
     block owns its own rows, so blocks never interlock, and the space
     between them is a real interval rather than a gutter.

     Three shapes, and only three:

       1        one image the full width
       5|7      the uneven pair, 41/58, tops aligned
       7|5      the pair reversed

     The pair's asymmetry is not authored twice. Both frames carry a
     landscape ratio — 4:3 for the wide one, 5:4 for the narrow — so
     the narrow frame comes out about three quarters the depth of the
     wide one purely BECAUSE it is narrower. That is what stops it
     reading as a system: nobody chose "half". The ratios were chosen
     and the proportion fell out of them.

     The air under the shallow frame is simply the rest of its row, and
     the row is as tall as the deep frame. No spacer, nothing placed —
     which only became possible once blocks stopped sharing rows. */
  .workrows { display: grid; align-items: start;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: clamp(12px, 1.6vw, 22px);
    row-gap: clamp(56px, 7vw, 130px);
    padding: 0 var(--gut); }

  .workrows .fd-it.hero     { grid-column: 1 / -1; }
  .workrows .fd-it.pNarrow  { grid-column: 1 / span 5; }
  .workrows .fd-it.pWide    { grid-column: 6 / span 7; }
  .workrows .fd-it.pWideL   { grid-column: 1 / span 7; }
  .workrows .fd-it.pNarrowR { grid-column: 8 / span 5; }
  .workrows > .cell         { grid-column: 1 / span 5; }

  .workrows .fd-it.hero .shot { aspect-ratio: 2 / 1; }
  .workrows .fd-it.pWide .shot,
  .workrows .fd-it.pWideL .shot { aspect-ratio: 4 / 3; }
  .workrows .fd-it.pNarrow .shot,
  .workrows .fd-it.pNarrowR .shot,
  .workrows > .cell .shot { aspect-ratio: 5 / 4; }
  .workrows .fd-it.hero .shot img { --drift: 20%; }

  /* the standing words, set off to one side with a row to themselves —
     the beat between two pairs, and the only place on the page where
     the house mentions itself in passing */
  /* Not a card here. On the work's white ground a filled box reads as
     one more tile in a run of tiles; the same words set plainly read
     as an aside, which is what they are. Nothing to open either — the
     text is simply there, at the size the rest of the page speaks in. */
  .workrows .fd-note { aspect-ratio: auto; background: none; border: 0;
    border-radius: 0; padding: 0; color: var(--ink);
    display: block; cursor: default; }
  .workrows .fd-note .word { display: block; font-size: var(--lbl);
    font-weight: var(--lbl-weight); letter-spacing: var(--lbl-track);
    opacity: 0.5; padding-bottom: 12px; }
  .workrows .fd-note .full { display: block; font-size: var(--note);
    font-weight: 400; line-height: 1.55; letter-spacing: 0;
    color: rgba(0, 0, 0, 0.78); padding-top: 0; white-space: pre-line; }
  .workrows .fd-note .full .att { display: block; margin-top: 12px;
    font-size: var(--cap); opacity: 0.5; }
  .workrows .fd-note.noteL { grid-column: 1 / span 4; }
  .workrows .fd-note.noteR { grid-column: 9 / span 4; }

  /* the hover caption is the board's device; the work has a real one */
  .workrows .fd-it .cap { display: none; }

  @media (max-width: 860px) {
    .workrows { row-gap: clamp(38px, 9vw, 72px); }
    /* one column: every block is simply the width of the page */
    .workrows .fd-it.hero, .workrows .fd-it.pNarrow, .workrows .fd-it.pWide,
    .workrows .fd-it.pWideL, .workrows .fd-it.pNarrowR, .workrows > .cell,
    .workrows .fd-note.noteL, .workrows .fd-note.noteR { grid-column: 1 / span 8; }
    .workrows .fd-it.hero .shot { aspect-ratio: 3 / 2; }
  }

  /* the standing label: the case study's body size (--pp-note is 16px
     there), carrying more weight so it reads as a name and not as
     running copy */
  .fd-it .lbl { display: none; }
  .workrows .fd-it .lbl { display: block;
    box-sizing: border-box; padding-top: 12px;
    font-size: var(--lbl-size, var(--note)); font-weight: 600;
    line-height: 1.35; letter-spacing: -0.004em; }
  .fd-it .lbl .sub { opacity: 0.42; }
  .fd-it .cap { position: absolute; left: 10px; bottom: 8px; right: 10px;
    font-size: var(--cap); font-weight: 400; line-height: 1.5;
    color: #fff; text-shadow: 0 1px 8px rgba(0,0,0,0.55);
    opacity: 0; transition: opacity 0.25s ease; pointer-events: none; }
  @media (hover: hover) { .fd-it:hover .cap { opacity: 1; } }

  /* ── solid cards — the house's own words ──
     Words, not pictures: solid ink, no outline, and for the info cards
     no eyebrow either. An outlined white card on white ground reads as
     an empty box; a solid one reads as a deliberate beat in a grid of
     photographs. They also declare themselves dark, so the masthead's
     reversal driver flips the wordmark as they pass under it. */
  .fd-note { position: relative; width: var(--w, 100%);
    border: 0; border-radius: var(--r);
    background: var(--ink); color: var(--paper);
    cursor: pointer; text-align: left;
    display: flex; align-items: flex-end; padding: 22px; }
  .fd-note { aspect-ratio: var(--nar, 4 / 3); }
  .fd-note .word { font-size: clamp(19px, 1.7vw, 30px); font-weight: 700;
    letter-spacing: -0.05em; line-height: 1.05; }
  .fd-note .full { display: none; }
  .fd-note .idx { position: absolute; top: 14px; left: 22px;
    font-size: var(--lbl); font-weight: var(--lbl-weight);
    letter-spacing: var(--lbl-track); opacity: 0.5; color: inherit; }
  /* the info cards carry no label at all — the word is the whole card */
  .fd-note.plain .idx { display: none; }
  .fd-note.open { aspect-ratio: auto; align-items: flex-start; }
  .fd-note.open .word { display: none; }
  .fd-note.open .full { display: block; font-size: var(--note);
    font-weight: 400; line-height: 1.6; letter-spacing: 0;
    color: rgba(255,255,255,0.82); padding-top: 26px; white-space: pre-line; }
  .fd-note.open .full .att { display: block; margin-top: 12px;
    font-size: var(--cap); opacity: 0.55; }
  .fd-note:focus-visible { outline: 1px solid var(--ink); outline-offset: 4px; }
  .fd-note.plain.open .word { display: block; font-size: var(--note);
    font-weight: 700; letter-spacing: 0; padding-bottom: 4px; }

  /* the practice: a band of white cards, not a masonry */
  /* ── arrival ─────────────────────────────────────────────────────
     A curtain, not a fade. The frame is already there — its ratio is
     declared, so it holds its space from first layout — and the
     picture is UNCOVERED downward rather than faded up.

     clip-path is the mechanism, and it goes on the frame's CONTENT —
     which is an <img> on every card but one, and the reel's own stage
     on that one, so the selector takes whatever is in there rather
     than naming a tag.

     Content, not the frame: the frame carries the radius, and clipping
     it would square its corners for the length of the reveal and fight
     the hover's own squaring-off. Clip rather than transform: the
     drift driver already writes transform to the image and the hover
     grow writes it to the card, and a third party on either would be
     the two-drivers-one-property bug this kit keeps paying for. A clip
     belongs to nobody.

     The ease is the point, and the first attempt got it wrong in an
     instructive way: cubic-bezier(.16,1,.3,1) puts its first control
     point at y=1, which throws the output almost straight to the top
     — 47% of the picture was uncovered in the first 120ms, so the
     curtain appeared to BLINK to halfway and then crawl. You never
     saw it fall.

     This curve keeps a fast, readable descent through the first half
     and saves the deceleration for the end, over nearly twice the
     duration. The falling edge is the thing being animated; it has to
     be legible as an edge that moves. */
  .fd-it .plate { display: block; width: 100%; height: 100%; }
  /* Curtain and settle together, on the same clock and the same ease:
     the frame is uncovered downward while what is inside it comes to
     rest from 125%. Two properties, one element, one timing — so they
     land on the same beat rather than merely near each other. */
  .fd-it .shot > * {
    clip-path: inset(0 0 100% 0);
    transform: scale(1.25); transform-origin: center;
    transition: clip-path 1.4s cubic-bezier(0.33, 0.28, 0.12, 1),
      transform 1.4s cubic-bezier(0.33, 0.28, 0.12, 1);
    /* A row's two frames do not arrive together. The second waits, so
       the row reads left to right as one gesture with two beats rather
       than as a pair of shutters dropping at once. The wait is set on
       the card by the dealer, not here, because only the dealer knows
       which of the two a frame is. */
    transition-delay: var(--lag, 0s); }
  .fd-on .shot > * { clip-path: inset(0 0 0 0); transform: none; }
  /* AND CLOSING, IN THE SAME DIRECTION IT OPENED. The frame's curtain
     draws back from the top on the way in; on the way out the top edge
     keeps travelling down until nothing is left, so a query reads as
     one continuous sweep rather than a rewind. Quicker than the way in
     and on a sharper ease, the same rule the page curtain follows:
     arriving is the flourish, leaving should not hold anyone up.

     After .fd-on and therefore winning it: a frame being swept out
     carries both classes. */
  .fd-out .shot > * {
    clip-path: inset(100% 0 0 0);
    transform: scale(1.06);
    transition: clip-path 0.4s cubic-bezier(0.5, 0, 0.75, 0),
      transform 0.4s cubic-bezier(0.5, 0, 0.75, 0);
    transition-delay: var(--lag, 0s); }
  /* A note card IS the .fd-note element and carries the sweep class
     itself, so this is a compound and not a descendant. Written the
     other way first, where it matched nothing and the text cards
     simply blinked out under the swap. */
  .fd-note.fd-out { opacity: 0;
    transition: opacity 0.28s ease var(--lag, 0s); }
  @media (prefers-reduced-motion: reduce) {
    .fd-out .shot > *, .fd-note.fd-out { transition: none; }
  }
  /* the text cards keep the quiet fade — there is no picture to draw
     back a curtain from */
  .fd-note { opacity: 0; transform: translateY(14px);
    transition: opacity 0.7s ease, transform 0.7s cubic-bezier(0.2, 0.55, 0.2, 1); }
  .fd-note.fd-on { opacity: 1; transform: none; }

  /* ── A KEPT LINE, AMONG THE PICTURES ──────────────────────────────
     The board page's quote tile (InspirationQuoteTile), in the
     answer's own grid. Sizes are that component's, written as a clamp
     across the two breakpoints it steps between, so a line reads the
     same wherever it is met.

     It takes the TEXT arrival above rather than the curtain: there is
     no plate to uncover, and a quote sliding out from behind a falling
     edge would be a picture pretending to be words. --lag still
     applies, so a line is the second beat of its row when it is dealt
     on the right. */
  .fd-quote { display: flex; flex-direction: column; justify-content: center;
    padding: 6px 0 20px;
    opacity: 0; transform: translateY(14px);
    transition: opacity 0.7s ease var(--lag, 0s),
      transform 0.7s cubic-bezier(0.2, 0.55, 0.2, 1) var(--lag, 0s); }
  .fd-quote.fd-on { opacity: 1; transform: none; }
  .fd-quote.fd-out { opacity: 0;
    transition: opacity 0.28s ease var(--lag, 0s); }
  /* UPRIGHT, AND A WEIGHT UP. It was italic at 400, which is the
     convention for a quotation and was the wrong call here: everything
     around it is upright Avenir at 600, so the one italic block read as
     a caption from another document rather than as a line held up
     beside the pictures. 500 rather than the display 600 — these are
     somebody else's words, and matching the site's own weight exactly
     would put them in its voice. The quote marks do the quoting. */
  .fd-quote .qtext { margin: 0; font-weight: 500;
    font-size: clamp(20px, 1.7vw, 26px); line-height: 1.35;
    letter-spacing: -0.02em; }
  /* One block per written line, so text-indent can reapply: it only
     ever indents the FIRST line of a block, which is why pre-line and
     a hanging indent cannot be combined — every line of the lyric is
     one line box inside a single block, and only the top one moves.
     Negative indent plus matching padding leaves the written line
     flush and hangs its runover under it. */
  .fd-quote .verse .qline { display: block;
    text-indent: -1.1em; padding-left: 1.1em; }
  /* A blank line between stanzas. None of the three kept lines has one
     and one may arrive; an empty block is zero pixels tall without
     this, so the stanza break would silently vanish. */
  .fd-quote .verse .qline:empty { height: 0.6em; }
  .fd-quote .qatt { margin: 24px 0 0; font-size: clamp(10px, 0.85vw, 11px);
    letter-spacing: 0.04em; text-transform: uppercase; font-weight: 500;
    opacity: 0.45; }
  @media (prefers-reduced-motion: reduce) {
    .fd-quote, .fd-quote.fd-out { transition: none; }
  }
  /* the card itself no longer fades: the curtain IS the arrival, and a
     fade under it would grey the picture while it was being drawn */
  .fd-it { opacity: 1; }

  /* ── the answer ──────────────────────────────────────────────────
     Ask, and the page becomes the answer: the magazine steps back and
     every kind assembles at the top, under the bar you asked in.
     Heterogeneity only ever shows up as an answer, which is the one
     place it is right. The head carries the receipts. */
  .answer { display: none; }
  body.asked .answer { display: block; }
  body.asked .stratum { display: none; }

  /* THE ANSWER SPEAKS FROM THE SAME COLUMN THE STATEMENT DOES, and now
     from INSIDE the grid rather than from beside it. It used to sit
     above .ixbody with a left padding that restated the rail width and
     the gutter to line the two up by arithmetic. That put the voice in
     the right column and the note rail in the wrong ROW: the rail
     could only begin under a paragraph whose height is the answer's
     own, so "How it works" hung 80px below every other first-column
     block on the site.

     Row 1 is the answer; the rail and the frames share row 2. Same
     shape the daybook and Staples use, and alignment is by
     construction now rather than by two numbers agreeing.

     100% still means what it meant: column two's width is exactly what
     the old left padding left over, so the one-column cap below is
     unchanged. */
  /* THE AIR IS THE GRID'S, not the head's. Held on .anshead it began
     column two 36px below column one, so the rail sat level with the
     top of the box rather than with the sentence in it. On the body it
     is the air above BOTH, which is what the stratum's own token does
     for the other three pages. */
  .answer .ixbody { padding-top: clamp(26px, 4dvh, 52px); }
  /* ── THE HEAD IS TWO COLUMNS ──────────────────────────────────────
     The sentence and its receipts on the left, the deal's first frame
     on the right. It was one box with the prose capped at half its
     width, which left a column of nothing beside every answer and put
     the work a screen below the fold.

     Every child takes column one by default, so the compose form and
     the back link need no rule of their own; the lead is the single
     exception and spans whatever rows the left side runs to. */
  .anshead { grid-column: 2; grid-row: 1;
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    column-gap: var(--ixgap);
    align-items: start;
    padding: 0 0 clamp(18px, 3dvh, 32px); }
  .anshead > * { grid-column: 1; min-width: 0; }
  .anshead > .anslead { grid-column: 2; grid-row: 1 / -1; }
  .answer .ixnotes { grid-column: 1; grid-row: 1 / -1; }
  #ansRows { grid-column: 2; grid-row: 2; }
  /* ── the answer, spoken ──────────────────────────────────────────
     One line at the statement's own scale, typed out. The receipts
     did not go away, they went into the writing: every clause is
     derived from the matcher's output, and the mechanical trace keeps
     one quiet line underneath for anyone who wants the working. */
  /* The quiet tone. Grey enough to fall back a full step, dark enough
     to still be read — this is subordinate copy, not a disclaimer. One
     value for both the statement and the answers, because they are the
     same voice at the same size. */
  .statement .q, .ansprose .q { color: rgba(0, 0, 0, 0.42); }
  /* ── THE STAT BAND ────────────────────────────────────────────────
     The instrument panel's Read/Caught/Time rows, set at the answer's
     own scale instead of buried in the 12px rail — Jeremy's call: the
     numbers are the good stuff, and a millisecond lookup over 358
     photographs deserves the same register as the sentence it feeds.
     The recessive tone keeps the hierarchy: the model's line is ink,
     the machine's rows are the site's grey, both at reading size.
     Lines land one at a time on the rail's own landing move. */
  /* ── THE ANSWER'S LEDGER ──────────────────────────────────────────
     What was searched on the left, how it was searched on the right.
     Two real columns rather than two blocks a screen apart, which is
     what they were: the readings sat under the say line and the
     mechanics down in the note rail past "Back to the house".

     Both columns at one small size. These went 23px, then 18, and both
     were the same mistake in different amounts — display type for a
     receipt. The say line above is the voice; this is the paperwork,
     and paperwork is small. */
  .ansmeta { display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    align-items: start;
    column-gap: var(--ixgap); margin-top: 22px;
    /* Full width of the head's first column, which IS the sentence's
       measure now that the head is a real two-column grid. The two
       receipts are sub-columns within it, not a second layout. */
    max-width: none;
    font-size: 12px; font-weight: 500;
    line-height: 1.45; letter-spacing: 0;
    color: rgba(0, 0, 0, 0.42); }
  .ansmeta[hidden] { display: none; }
  .ansstats { font: inherit; color: inherit; }
  /* ── LABEL ABOVE, FACTS UNDER ─────────────────────────────────────
     The label had its own left-hand track, which works at full measure
     and does not survive being put in a half-width column: 78px of
     label inside a 160px column leaves the facts a ribbon. Above it,
     the way the reference sets SOLUTIONS over its list, every value
     gets the whole column.

     The label is a field name, so it is small, lettered out and
     recessive. It is not the first word of a sentence any more. */
  .ansstats .asl { display: block; padding-top: 14px;
    opacity: 0; transform: translateY(6px);
    transition: opacity 0.5s ease, transform 0.55s cubic-bezier(0.2, 0.7, 0.2, 1); }
  .ansstats .asl:first-child { padding-top: 0; }
  .ansstats .asl.on { opacity: 1; transform: none; }
  .ansstats .ask2 { display: block; color: rgba(0, 0, 0, 0.26);
    font-size: 10px; font-weight: 600; letter-spacing: 0.08em;
    text-transform: uppercase; line-height: 1.6; padding-bottom: 2px; }
  /* ONE FACT PER LINE, which is the whole point: three countable things
     stack and can be read down instead of parsed along a wrapped
     sentence. */
  .ansstats .asv > span { display: block; }
  @media (prefers-reduced-motion: reduce) {
    .ansstats .asl { transition: none; transform: none; opacity: 1; }
  }
  @media (max-width: 860px) {
    /* Two columns still, on the full width. A phone's content column is
       329px, so each of these is about 150 — narrow, which is why the
       labels sit above rather than beside. */
    .ansmeta { max-width: none; column-gap: 18px; }
  }

  /* The statement's nouns, clickable. They have to read as the sentence
     first and as controls second, so they take the paragraph's type
     whole and carry a hairline underneath — more than a hover cue, less
     than a link. */
  .statement .term { display: inline; color: inherit; cursor: pointer; }

  /* ── ONE WAY THROUGH ──────────────────────────────────────────────
     Every live word on the site wears this and nothing wears anything
     else. There were three languages: the statement's terms drew a
     rule that filled from the left, the rail's filters grew a rule
     that was not there at rest, and the masthead's address only
     changed colour. Three ways of saying "this is a door" is two more
     than a reader should have to learn.

     SKIP-INK WON, and the directional grow is what it cost.

     These were two stacked background gradients, because
     text-decoration cannot animate its width: the grey hairline spanned
     the word and an ink one grew over it from the left on hover. The
     note here used to argue that cutting straight through the p and y
     tails WAS the look — a drawn rule over the words rather than type
     furniture stepping politely around them.

     Set in Avenir at display size that stopped reading as a decision
     and started reading as a rule someone forgot to finish: the tails
     of photography and campaign are struck through rather than
     underlined. A real underline parts around them, which is the
     finished thing, and it cannot be had from a background image at
     any offset.

     So: text-decoration, skip-ink on, and the hover moves the COLOUR
     from the rest tone to full ink over the same beat. One language
     still, one property animating instead of another. What is gone is
     the left-to-right wipe, which no single element can do while a
     line also breaks around a descender — the growing layer would have
     to be a second copy of the text, absolutely placed, and the
     statement's terms wrap.

     THICKNESS IS PROPORTIONAL NOW, which is the other half of what a
     finished underline means: max(1px, 0.05em) holds a hairline at
     12px and gives the 32px display line the ~1.6px that matches its
     stroke. 0.15em of offset is the clearance the descenders need to
     part cleanly.

     THE HAIRLINE AT REST IS THE POINT, and it is what the filters
     gained: a word that only admits it is live once the pointer is
     already on it has told the reader nothing.

     COLOUR IS INHERITED, never set here. Most of the statement's terms
     sit inside its recessive grey run and have to grey with it, and
     the masthead's address is quiet on purpose. The RULE goes to full
     ink; the type stays whatever tone it was. That is why the address
     could give up its colour change and lose nothing — the fill says
     the same thing, in the language everything else speaks.

     Three properties tune it. --rule-w is the line's length, defaulting
     to the element's own width; the ask field overrides it, because
     there the line belongs to the typed text and not to the box the
     sentence reserves for it. --rule-ink and --rule-rest are the two
     tones, and they invert under the masthead over a dark zone, where
     black on black is no line at all. */
  .statement .term,
  .ixnotes .blk .blkgo,
  .ixnotes .filt button,
  .ixnotes .filt a,
  #nav a.meta {
    text-decoration: underline;
    text-decoration-color: var(--rule-rest, rgba(0, 0, 0, 0.22));
    text-decoration-thickness: max(1px, 0.05em);
    text-underline-offset: 0.15em;
    text-decoration-skip-ink: auto;
    background-image: none;
    /* colour rides along for the masthead, which fades its own tone
       across a dark zone's edge. */
    transition: text-decoration-color 0.3s ease, color 0.28s ease; }
  /* THE FIELD TAKES IT TOO, and the objection to that turned out to be
     wrong. The worry was that an input with no value has no text for a
     text-decoration to underline, so the line would vanish in the one
     state it most needs to be in. It does not: a placeholder takes the
     decoration the same as a value, tested in the browser with the
     tour's own question in the box, descenders parting and all.

     What it retires is --ask-uw. The driver measured the visible text
     on a ghost span so the drawn rule could be sized to the words
     rather than the box — "the line ends where the words do" — which
     is what a text-decoration does natively, and exactly. The measure
     is still written, because the same ghost fits the field's size,
     but nothing reads it for the rule any more. */
  .ask.big #query {
    text-decoration: underline;
    text-decoration-color: var(--rule-rest, rgba(0, 0, 0, 0.22));
    text-decoration-thickness: max(1px, 0.05em);
    text-underline-offset: 0.15em;
    text-decoration-skip-ink: auto;
    background-image: none;
    transition: text-decoration-color 0.3s ease, color 0.28s ease; }
  @media (hover: hover) {
    .statement .term:hover,
    .ixnotes .blk .blkgo:hover,
    .ixnotes .filt button:hover,
    .ixnotes .filt a:hover,
    #nav a.meta:hover {
      text-decoration-color: var(--rule-ink, #000); }
    .ask.big #query:hover {
      text-decoration-color: var(--rule-ink, #000); }
  }
  .statement .term:focus-visible,
  .ixnotes .blk .blkgo:focus-visible,
  .ixnotes .filt button:focus-visible,
  .ixnotes .filt a:focus-visible,
  #nav a.meta:focus-visible {
    outline: 1px solid var(--ink); outline-offset: 3px;
    background-size: var(--rule-w, 100%) 1px, var(--rule-w, 100%) 1px; }
  @media (prefers-reduced-motion: reduce) {
    .statement .term,
    .ask.big #query,
    .ixnotes .blk .blkgo,
    .ixnotes .filt button,
    .ixnotes .filt a,
    #nav a.meta { transition: none; }
  }

  /* THE SAME SIZE AS THE STATEMENT IT REPLACES. It was 44 against the
     statement's 36 — same voice, same position on the page, arriving a
     fifth larger. */
  /* One column wide, like the statement. The box it sits in spans two
     columns and the gutter between them, so half of that less half a
     gutter is exactly one. */
  /* THE COLUMN IS THE MEASURE NOW. This carried its own cap — half the
     frames columns less half a gutter — which is exactly what the
     head's first grid column is, so the cap would only ever halve it
     again. */
  .ansprose { max-width: none;
    font-size: clamp(21px, 2.4vw, 32px); font-weight: 600;
    line-height: 1.18; letter-spacing: -0.05em;
    min-height: 2.4em; }
  #ansSay.typing::after { content: ""; display: inline-block;
    width: 0.45ch; height: 0.92em; margin-left: 4px;
    background: currentColor; vertical-align: -0.1em;
    animation: sayblink 0.9s steps(2, start) infinite; }
  @keyframes sayblink { 50% { opacity: 0; } }

  /* while the brain works, the field's rule runs a scan: grows from
     the left, collapses to the right. Contained, so nothing juts. */
  .ask::after { content: ""; position: absolute; bottom: 0; left: 0;
    width: 0; height: 1px; background: currentColor; opacity: 0; }
  body.working .ask::after { opacity: 0.7;
    animation: askscan 0.8s cubic-bezier(0.45, 0.05, 0.35, 0.95) infinite; }
  @keyframes askscan {
    0% { left: 0; width: 0; }
    52% { left: 0; width: 100%; }
    100% { left: 100%; width: 0; }
  }
  /* the spoken line can carry one link: the email address, live */
  .ansprose a { color: inherit; text-decoration: underline;
    text-decoration-thickness: 2px; text-underline-offset: 5px; }

  /* ── the answer in the ring's columns ────────────────────────────
     The registers still never mix: work takes the left frames, the
     board the right, and the working notes column explains itself.
     What changed is the container: the answer uses the SAME rows the
     resting page deals, so asking re-deals the magazine rather than
     replacing it with a different-looking page. */
  .answer .ixnotes { opacity: 0; transition: opacity 0.4s ease; }
  .answer .ixnotes.on { opacity: 1; }
  /* THE WORKING BLOCK LEFT THE RAIL. It is the right column of
     .ansmeta now, beside the readings it belongs with, so these
     selectors follow it there — written .ixnotes .blk.working they
     matched nothing and the rows would have appeared with no landing
     at all. The stagger is per-row transition-delay written by the
     renderer, so newcomers land and settled rows hold still. */
  .ansmeta .working .rprose { display: block; margin-bottom: 10px; }
  .ansmeta .working .rl { opacity: 0; transform: translateY(5px);
    transition: opacity 0.45s ease, transform 0.5s cubic-bezier(0.2, 0.7, 0.2, 1); }
  .ansmeta .working .rl.on { opacity: 1; transform: none; }
  @media (prefers-reduced-motion: reduce) {
    .ansmeta .working .rl { transition: none; transform: none; opacity: 1; }
  }
  /* a pull's name is its description: it reads as a caption, not a
     title, or the board would look like it was claiming credit */
  .ixrow .fd-it.k-pull .lbl { font-weight: 400; font-size: var(--cap);
    opacity: 0.55; }

  /* ── the compose box ──
     A LEDGER, not a form. Label left, field right, one hairline per
     row: the same object as a daybook entry, a thread message and the
     spec rows in the studies, so a contact form stops being the one
     place on the site that imported somebody else's design system.

     The label column is var(--ix-note-w), which is what the index below
     uses, so the form lines up with the page rather than floating in
     the prose measure with two thirds of the width empty beside it.

     REAL LABELS, not placeholders. A placeholder is not a label: it
     disappears the moment there is a value, so anyone who tabs back to
     check a field finds an unnamed box, and a screen reader gets
     nothing dependable. The placeholder now carries an example; the
     label carries the name. */
  .cmp { margin-top: clamp(30px, 5dvh, 56px); }

  /* Weighted to the top: the label rides just under the rule and the
     air falls BELOW the field rather than being split either side of
     it. A row padded evenly reads as a box; padded like this it reads
     as an entry hanging off its own line. */
  .cmprow { position: relative; display: grid;
    grid-template-columns: var(--ix-note-w, 200px) minmax(0, 1fr);
    column-gap: clamp(24px, 3.4vw, 56px);
    align-items: start;
    padding: 11px 0 30px; }

  /* ── the arrival ──
     The rule is a ::before rather than a border because a border cannot
     be drawn: this one scales from the left the way the brief's section
     rules do, and the label and field rise into it a beat later.

     THE BASE STATE IS THE FINISHED ONE. A form that starts at opacity 0
     in CSS is an invisible, still-focusable form the moment a script
     fails, which is the trap this kit already paid for on the crossing
     headline. The driver ARMS the start state for a single frame and
     lets go; if it never runs, the form is simply there. */
  .cmprow::before { content: ""; position: absolute; top: 0; left: 0; right: 0;
    height: 1px; background: rgba(0, 0, 0, 0.14);
    transform: scaleX(1); transform-origin: 0 50%;
    transition: transform 0.66s cubic-bezier(0.2, 0.7, 0.2, 1) var(--cd, 0ms),
                background 0.2s ease; }
  .cmplbl, .cmpf {
    transition: opacity 0.5s ease calc(var(--cd, 0ms) + 90ms),
                transform 0.56s cubic-bezier(0.2, 0.7, 0.2, 1) calc(var(--cd, 0ms) + 90ms); }

  .cmparm .cmprow::before { transform: scaleX(0); transition: none; }
  .cmparm .cmplbl, .cmparm .cmpf,
  .cmparm .cmpfoot { opacity: 0; transform: translateY(9px); transition: none; }
  .cmpfoot { transition: opacity 0.5s ease calc(var(--cd, 0ms) + 90ms),
                transform 0.56s cubic-bezier(0.2, 0.7, 0.2, 1) calc(var(--cd, 0ms) + 90ms); }

  /* One ladder down the form: line, then its words, then the next line. */
  .cmprow:nth-of-type(1) { --cd: 0ms; }
  .cmprow:nth-of-type(2) { --cd: 95ms; }
  .cmprow:nth-of-type(3) { --cd: 190ms; }
  .cmpfoot { --cd: 285ms; }

  /* THE LEFT RAIL'S LABEL, exactly: same size, weight, tracking and
     opacity as .ixnotes .blk .tag, and sentence case like it. The caps
     were mine and they were the odd one out — nothing else in this
     column shouts. Sits near the rule rather than on the field's
     baseline, which is what opens the space underneath. */
  .cmplbl { font-size: var(--lbl); font-weight: var(--lbl-weight);
    letter-spacing: var(--lbl-track); opacity: 0.5; padding-top: 1px; }

  .cmpf { width: 100%; max-width: 620px; display: block;
    background: none; border: 0; border-radius: 0;
    padding: 2px 0; font: inherit; font-size: var(--note);
    color: var(--ink); }
  .cmpf::placeholder { color: rgba(0, 0, 0, 0.3); }
  /* 16px on touch, or iOS zooms the page the moment this is focused and
     never zooms back out. The compose box is where a visitor writes to
     the house, so the zoom lands mid-sentence. pointer: coarse rather
     than a width: the trigger is the input device, and a 744px iPad
     zooms exactly the same way a phone does.
     16 is the threshold, not a size choice — --note stays 12 for
     everything on this page that is not a field. */
  @media (pointer: coarse) {
    .cmpf { font-size: 16px; }
  }

  /* The row is the box, so focus marks the ROW rather than drawing a
     second outline inside one that is already there. */
  .cmpf:focus { outline: none; }
  .cmprow:focus-within::before { background: var(--ink); }
  .cmpbody { resize: vertical; min-height: 96px; line-height: 1.55;
    font-family: inherit; }

  /* Off-screen rather than display:none. A bot reads the DOM and fills
     what it can reach; a hidden field is the oldest tell there is. */
  .cmphp { position: absolute; left: -9999px; width: 1px; height: 1px;
    opacity: 0; pointer-events: none; }

  .cmpfoot { position: relative; display: grid;
    grid-template-columns: var(--ix-note-w, 200px) minmax(0, 1fr);
    column-gap: clamp(24px, 3.4vw, 56px);
    padding-top: 22px; align-items: center; }
  .cmpfoot::before { content: ""; position: absolute; top: 0; left: 0; right: 0;
    height: 1px; background: rgba(0, 0, 0, 0.14);
    transform: scaleX(1); transform-origin: 0 50%;
    transition: transform 0.66s cubic-bezier(0.2, 0.7, 0.2, 1) var(--cd, 0ms); }
  .cmparm .cmpfoot::before { transform: scaleX(0); transition: none; }
  .cmpact { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
  .cmpsend { background: var(--ink); color: var(--paper); border: 0;
    border-radius: 999px; padding: 11px 26px; cursor: pointer;
    font: inherit; font-size: var(--lbl); font-weight: var(--lbl-weight);
    letter-spacing: var(--lbl-track); text-transform: uppercase; }
  .cmpsend[disabled] { opacity: 0.45; cursor: default; }
  .cmpnote { font-size: var(--note); color: rgba(0, 0, 0, 0.55); }
  .cmpnote a { color: inherit; text-underline-offset: 3px; }

  @media (max-width: 760px) {
    /* One track: the label sits above its own field rather than taking
       200px out of a 375px screen. */
    .cmprow, .cmpfoot { grid-template-columns: 1fr; }
    .cmplbl { padding-top: 0; padding-bottom: 6px; }
    .cmpfoot { padding-top: 16px; }
  }

  .ansback { padding-top: 16px; font-size: var(--lbl);
    font-weight: var(--lbl-weight); letter-spacing: var(--lbl-track); }
  .ansback button { background: none; border: 0; padding: 0; cursor: pointer;
    font: inherit; letter-spacing: inherit; color: var(--ink);
    text-decoration: underline; }

  .foot { display: flex; justify-content: space-between; gap: 24px;
    flex-wrap: wrap; align-items: baseline;
    margin: clamp(70px, 10vw, 150px) 40px 0; padding: 18px 0 40px;
    border-top: 1px solid rgba(0,0,0,0.14);
    font-size: var(--lbl); font-weight: var(--lbl-weight);
    letter-spacing: var(--lbl-track); }
  .foot .dim { opacity: 0.45; }

  /* The module's mobile block: the nav keeps its own tighter gutter
     (16px) while the page keeps 20px — that difference is in the real
     stylesheet, not a slip. No overflow-x here, unlike the module: a
     field flexes where four links could not, and the vocabulary row
     hangs off the bar, which an overflow context would clip. */
  @media (max-width: 760px) {
    :root { --nav: 48px; --gut: 20px; }
    #nav { --nav-size: 11px; --nav-gutter: 16px; --nav-overhang: 10px;
      grid-template-columns: 1fr auto; gap: 10px; letter-spacing: 0.02em;
      /* RESET, not merely "not set". The comment above says this bar
         wants no overflow context, and the homepage duly never added
         one — but Masthead.module.css sets overflow-x: auto on phones
         as its own safety net, and a sheet that stays silent inherits
         it. overflow-x on one axis computes overflow-y to auto too, so
         the bar became a clip, and the field it lends to the cover is
         translated ~318px below the bar: outside the clip, invisible
         on every phone. The id outranks the module's class. */
      overflow: visible; }
    /* 390pt cannot centre a wordmark AND seat a field beside it, so
       the wordmark takes the right end (the mailto's slot, which is
       hidden here anyway) and the field keeps the left gutter — the
       travel stays a straight vertical drop on a phone too. */
    #nav .mark { grid-column: 2; justify-self: end; }
    #nav .meta { display: none; }
    /* the shead and the feeds read --gut, which halves above, so the
       only thing left to restate here is the footer's margin */
    .foot { margin-left: var(--gut); margin-right: var(--gut); }
  }
  @media (prefers-reduced-motion: reduce) {
    html { scroll-behavior: auto; }
    .fd-it .shot > * { clip-path: none !important;
      transform: none !important; transition: none !important; }
    .fd-it, .fd-note { opacity: 1 !important; transform: none !important;
      transition: none !important; }
    #query { transition: none; }
    /* the travel itself is scroll position, not animation, so it stays;
       the glide when an answer opens is animation, so it goes */
    .ask.settling { transition: none; }
  }
