/* ============================================================================
   Ridgecap — site styles. The home page cover as one continuous sequence,
   then the product, legal and 404 pages that share its chrome.
   ============================================================================
   THE ONLY STYLESHEET ON THE SITE. It began as the v4 design variant's own
   file, deliberately sealed off from the old site's /styles.css so the two
   could be compared side by side. v4 won on 2026-10-02, every page now loads
   this file, and /styles.css was deleted (it is in git history). The tokens
   below were copied by value from it.

   The other self-hosted assets are /vendor/gsap.min.js,
   /vendor/ScrollTrigger.min.js, /topo.js and /ember.js.

   ---------------------------------------------------------------------------
   WHAT THIS PAGE IS

   v2's cover was a composition you scrolled past. This one is a sequence you
   scroll THROUGH, and the difference is structural rather than decorative:

     at rest      absolute black. The mark builds. Two seconds later the match
                  line arrives and says the page continues. Nothing else exists
                  — no grid, no header.
     on scroll    the cover is PINNED and the sequence is scrubbed against the
                  scrollbar: grid, then header, then the sheet frame, then the
                  handover where the emblem dissolves and RIDGECAP resolves.
     released     the pin ends and the page scrolls normally into the horizon,
                  the products, the reviews.

   The runway is ~110vh of scrolling for the whole thing. That is deliberate:
   the beats are connected, not long. A sequence that takes four screens to
   finish is a chore, and the point here is that it reads as one intentional
   move.

   ---------------------------------------------------------------------------
   THE FAILURE MODE THIS FILE IS BUILT AROUND

   Two shipped bugs on this site had the same shape: something was parked at
   opacity 0 for a timeline to bring in, and a code path existed where the
   timeline never ran. Phones got a cover page with no words on it and nothing
   reported an error.

   So the rule here is inverted. THE DEFAULT STATE OF THIS STYLESHEET IS THE
   FINISHED PAGE. Everything is visible, at its final value, with no JS at all.
   The pre-animation state is applied only under `html.seq` — a class an inline
   script adds synchronously before first paint and that the motion module
   REMOVES on any failure, plus a hard timeout that removes it regardless. If
   GSAP 404s, fails to parse, or the visitor asked for reduced motion, what is
   left is a complete, legible, static page.

   Anything you park at opacity 0 goes in the `html.seq` block at the bottom,
   and nowhere else.
   ========================================================================== */

:root {
  /* ---- Ink & paper — copied by value from the old /styles.css ---- */
  --ink: #16181d;
  --ink-deep: #101114;
  --paper: #f6f6f8;
  --surface: #ffffff;
  --sunken: #efeff2;
  --hairline: #e2e3e8;
  --hairline-strong: #d4d5db;
  --body: #464956;
  --muted: #6f7280;

  /* ---- Brand copper. The three text steps exist for contrast reasons that
          the README's Accessibility floor sets out; they are reproduced here
          so nobody has to go and look them up.
          --copper      #a85c2e  FILLS ONLY. 4.32:1 on --sunken, fails AA.
          --copper-text #8c4a23  small copper text on paper/sunken. ~6.4:1
          --copper-deep #703a1b  copper text on --copper-wash
          --copper-dark #c96a3c  the AA-safe accent ON GRAPHITE — this page's
                                 working copper, since this page is all dark. */
  --copper: #a85c2e;
  --copper-hover: #8c4a23;
  --copper-text: #8c4a23;
  --copper-deep: #703a1b;
  --copper-dark: #c96a3c;
  --copper-dark-hover: #d67e4d;
  --copper-wash: #f9f0e9;

  /* ---- On-dark neutrals ---- */
  --d-surface: #16181d;
  --d-raised: #1d2026;
  --d-hairline: #292d36;
  --d-hairline-strong: #3d424e;
  --d-text: #f0f0f2;
  --d-body: #b9bcc4;
  --d-muted: #878b96;

  /* ---- v3 only ----
     THE COVER IS ABSOLUTE BLACK, not --ink-deep. That is Sam's call and it is
     load-bearing for the sequence: the mark has to build on nothing, and
     #101114 is a colour you can see, which makes it a surface. #000 is not a
     surface, so the first frame reads as a mark in space rather than a logo on
     a dark box. The products surface below IS --ink-deep, and the step between
     the two is exactly what the horizon divides. */
  --void: #000000;
  --panel: #191c22;
  --panel-hi: #20242c;

  --font-display: 'Montserrat', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-sans: 'Roboto', system-ui, -apple-system, 'Segoe UI', sans-serif;
  --font-label: 'Work Sans', system-ui, -apple-system, 'Segoe UI', sans-serif;

  --s1: 4px;  --s2: 8px;   --s3: 12px;  --s4: 16px;  --s5: 24px;
  --s6: 32px; --s7: 48px;  --s8: 64px;  --s9: 96px;  --s10: 128px;

  --r-sm: 4px;
  --r-md: 8px;
  --r-lg: 14px;

  --ease: cubic-bezier(0.2, 0, 0, 1);
  --dur: 180ms;

  --shell: 1160px;

  /* THE GRID CELL. One number, used by the background grid and by the sheet
     frame, so the frame lands on grid lines instead of near them. */
  --cell: 68px;
}

/* ---------- Reset ----------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
html {
  -webkit-text-size-adjust: 100%;
  /* NO `scroll-behavior: smooth`. ScrollTrigger reads scroll position every
     frame and pins against it; a smooth-scrolling root fights the pin and also
     makes every measurement taken during a programmatic scroll wrong, which is
     documented in the handoff as a thing that already cost a session. */
  background: var(--void);
  /* ITEM 10 — the sideways swipe on iPhone.
     There is no horizontal overflow to remove: measured at 375x812 the
     document's scrollWidth equals its clientWidth exactly. What Sam is
     dragging is iOS Safari's horizontal RUBBER BAND, which happily pulls the
     whole page sideways past its own edge and shows the black behind it even
     when nothing overflows. `overscroll-behavior-x: none` is the property that
     turns that off.
     `overflow-x: hidden` is the usual folk remedy and is the wrong tool twice
     over here: on the root it propagates to the viewport, and GSAP explicitly
     warns against it on html/body because it breaks pinning. */
  overscroll-behavior-x: none;
}
body {
  margin: 0;
  background: var(--void);
  color: var(--d-body);
  font-family: var(--font-sans);
  font-size: 1.0625rem;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overscroll-behavior-x: none;
  /* Belt and braces for the same thing, and it does a second job: it tells the
     browser this page only ever pans vertically, so the compositor can start
     scrolling without waiting to find out whether a touch handler will call
     preventDefault. `pinch-zoom` is kept deliberately — removing it would
     block zooming, which is an accessibility regression, not a fix. */
  touch-action: pan-y pinch-zoom;
}

/* THE CLIP GOES ON <main>, NOT ON <body>, and this cost a real bug.
   `overflow-x: clip` on <body> does nothing, because the used overflow of the
   root and body PROPAGATES TO THE VIEWPORT — the browser moves the value up to
   the viewport and computes body's own overflow back to `visible`. So the
   full-bleed elements were never clipped by it. Measured: scrollWidth 2586
   against clientWidth 2545, and the page genuinely scrolled sideways 41px.
   <main> is an ordinary element, so its clip is its own and actually applies.
   `clip` rather than `hidden` on purpose: `hidden` would make this a scroll
   container, which breaks position:sticky and ScrollTrigger's pin inside it. */
main { overflow-x: clip; }
img, svg { display: block; max-width: 100%; }
h1, h2, h3 {
  font-family: var(--font-display);
  color: var(--d-text);
  margin: 0;
  letter-spacing: -0.025em;
  line-height: 1.08;
  font-weight: 700;
}
p { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
a { color: inherit; }

.skip {
  position: absolute; left: -9999px; top: 0;
  background: var(--copper); color: #fff;
  padding: var(--s3) var(--s5); z-index: 100; border-radius: 0 0 var(--r-md) 0;
}
.skip:focus { left: 0; }

:where(a, button):focus-visible {
  outline: 2px solid var(--copper-dark);
  outline-offset: 3px;
  border-radius: var(--r-sm);
}

.shell {
  max-width: var(--shell);
  margin-inline: auto;
  padding-inline: var(--s5);
}

/* ============================================================================
   THE GRID — item 4 and item 12
   ============================================================================
   ONE grid, and it is FIXED. v2 had two: a fixed `.table` at 0.022 alpha and a
   second one painted onto `.hero__wash`, which lives inside the scrolling hero.
   Two grids at different alphas sliding against each other is what "the boxes
   move with the scroll" actually was — not the fixed one moving, but a second
   one travelling over it. There is only one here and it cannot move, because
   `position: fixed` has nothing to move relative to.

   CENTRING (item 12). `background-position: center` is not a rounding-friendly
   guess, it is exactly the requested result, and the arithmetic is worth
   writing down because the obvious alternative is wrong.

   For background-position, a percentage aligns the P-point of the tile with the
   P-point of the container, so the offset is (W - tile) x P. At `center`:

       offset = (W - 68) / 2  =  W/2 - 34

   A tile paints its line on its own left edge, so lines land at W/2 - 34, and
   then every 68px after: W/2 + 34, W/2 + 102 ... The viewport centre sits
   exactly half a cell from the lines either side of it. That is the ask: the
   grid is centred, the dividing lines are equidistant from centre, and there is
   NO line down the middle.

   (`calc(50% + 34px)` — the intuitive version — resolves to W/2 - 34 + 34 =
   W/2 and puts a line precisely down the centre, which is the opposite.)
   ========================================================================== */
.grid {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  /* 0.042, not the 0.022 v2 used and not the 0.030 this file started at. The
     grid ARRIVING is a beat in the sequence — it is the thing that turns the
     void into a drafting table — and a beat you have to hunt for is not a beat.
     Measured against #000 a 0.042 white line composites to rgb(11,11,11),
     which is visible on a decent panel and still nowhere near competing with
     the mark. */
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.042) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.042) 1px, transparent 1px);
  background-size: var(--cell) var(--cell);
  background-position: center center;
  /* A vignette, so the grid is a table under the work rather than wallpaper
     over it. It also stops the lines fighting the mark at the exact centre of
     the screen, which is where the mark is. */
  -webkit-mask-image: radial-gradient(ellipse 120% 95% at 50% 50%, #000 20%, rgba(0,0,0,0.40) 74%, transparent 100%);
          mask-image: radial-gradient(ellipse 120% 95% at 50% 50%, #000 20%, rgba(0,0,0,0.40) 74%, transparent 100%);
}

main, .foot { position: relative; z-index: 1; }

/* ============================================================================
   THE HEADER — items 2, 3, 5
   ============================================================================
   Item 2: the emblem is gone from the nav. Only the wordmark. On a page whose
   whole cover is the emblem building itself, a 26px copy of it in the corner is
   the same information twice, and the small one loses.

   Item 3: `.nav__inner` is NOT `.shell`. A 1160px centred rail on a 2545px
   monitor puts RIDGECAP and "Sign in" 1160px apart in the middle of the screen
   with 690px of nothing either side, which is what "everything is so close to
   the middle" is. It now spans the viewport with a real margin, so the two ends
   of the header sit at the two ends of the header.

   Item 5: it is not visible on arrival. See the `html.seq` block.
   ========================================================================== */
.nav {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 40;
  padding-block: var(--s5);
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease), padding var(--dur) var(--ease);
  border-bottom: 1px solid transparent;
}
.nav.is-stuck {
  background: rgba(0, 0, 0, 0.72);
  -webkit-backdrop-filter: saturate(140%) blur(10px);
          backdrop-filter: saturate(140%) blur(10px);
  border-bottom-color: var(--d-hairline);
  padding-block: var(--s4);
}
.nav__inner {
  max-width: none;
  margin-inline: 0;
  /* The whole point of item 3. 4vw keeps the ends off the bezel on a laptop and
     still reads as a margin on a 2545px monitor; the 72px ceiling stops it
     drifting into the middle again on an ultrawide. */
  padding-inline: clamp(var(--s5), 4vw, 72px);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s5);
}
/* THE HIT AREA IS PADDING, THE LAYOUT IS THE NEGATIVE MARGIN. A wordmark is a
   wide, very short shape — the header's is 12px tall on a phone and the
   footer's is 13 — so as a link it was a 102 x 12 target, which is a quarter of
   the 44px a thumb needs and thinner than the two rows of the grid behind it.
   The footer's text links already solve this with `padding-block: 8px`; these
   need more because they start shorter.
   The negative margin cancels the padding in layout only, so the header keeps
   its height and the footer keeps its spacing while the tappable box grows to
   44px. Verified: no element moved. */
.logo {
  display: inline-flex;
  align-items: center;
  color: var(--d-text);
  text-decoration: none;
  padding-block: 16px;
  margin-block: -16px;
}
.logo__word { height: 17px; width: auto; }
.logo--sm .logo__word { height: 13px; }
/* The header's emblem. Present in the markup at every width and shown only on a
   phone — see the note in the 640px block. Hidden here rather than absent from
   the DOM so the swap is a CSS decision with nothing to keep in step. */
.logo__mark { display: none; }

.nav__links {
  display: flex;
  align-items: center;
  /* Wider than v2's --s6. The right-hand cluster is the other end of a
     full-width header now, so it can afford to breathe. */
  gap: clamp(var(--s5), 2.4vw, var(--s7));
}

/* ---- Phone nav: one "Products" control instead of two links (item 12) ----
   Above 640px the two product links show and the menu is hidden; below, the
   reverse. Both are in the DOM at all times and CSS picks — so the correct nav
   is present on first paint with no JS, and JS is only needed to OPEN the menu.
   The reason this exists: at 375px the wordmark plus two links plus a Sign in
   button do not fit on one line, so Sign in wrapped underneath. */
.nav__menu { position: relative; display: none; }
.nav__toggle {
  display: inline-flex; align-items: center; gap: 6px;
  background: none; border: 0; padding: 10px 0;
  color: var(--d-body);
  font-family: var(--font-label); font-weight: 500; font-size: 0.95rem;
  cursor: pointer;
}
.nav__toggle svg { width: 11px; height: auto; transition: transform var(--dur) var(--ease); }
.nav__toggle[aria-expanded="true"] { color: var(--d-text); }
.nav__toggle[aria-expanded="true"] svg { transform: rotate(180deg); }
.nav__drop {
  position: absolute;
  top: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
  min-width: 168px;
  display: grid;
  background: var(--panel);
  border: 1px solid var(--d-hairline-strong);
  border-radius: var(--r-md);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.55);
  overflow: hidden;
  z-index: 50;
}
.nav__drop[hidden] { display: none; }
.nav__drop a {
  padding: 13px 18px;
  text-decoration: none;
  color: var(--d-body);
  font-family: var(--font-label); font-weight: 500; font-size: 0.95rem;
}
.nav__drop a + a { border-top: 1px solid var(--d-hairline); }
.nav__drop a:hover, .nav__drop a:focus-visible { background: var(--panel-hi); color: var(--d-text); }

@media (max-width: 640px) {
  .nav__menu { display: block; }
  .nav__wide { display: none; }
  /* The budget at 375px, measured: 375 - 32 of padding leaves 343.
     wordmark 124 + gap 16 + Products 78 + gap 16 + Sign in 84 = 318, with 25
     to spare. Every one of those numbers had to come down for the row to fit,
     which is why the wordmark shrinks here too. */
  .nav__links { gap: var(--s4); }
  .nav__inner { padding-inline: var(--s4); gap: var(--s4); }
  .logo__word { height: 12px; }
  .nav__toggle { white-space: nowrap; flex: none; }

  /* THE HEADER SHOWS THE MARK, NOT THE WORDMARK. The cover already ends on a
     288px wordmark — 77% of a 375px screen — and a 124px copy of the same
     lockup sitting above it read as the brand stuttering rather than as chrome.
     The emblem says the same thing in a shape the wordmark is not, and the two
     stop competing.

     It also buys the row real space, which is the constraint the note above is
     about: at 375px the wordmark took 124px of a 343px budget and the emblem
     takes 41. That is 83px back for the Products control and Sign in, on the
     one width where they were 25px from wrapping.

     24px tall rather than the wordmark's 12: an emblem is a compact shape where
     a wordmark is a long thin one, so matching their HEIGHTS would make the
     mark look like a smudge. Matching optical weight means going taller and
     much narrower. 24 is still inside the 45px header row.

     SCOPED TO `.nav`, AND THAT IS NOT TIDINESS. Written as a bare
     `.logo__word { display: none }` this also hid the FOOTER's wordmark — which
     has no emblem beside it to fall back to, so the footer logo became an empty
     link. Measured that before catching it. The footer is nowhere near the
     cover and has no duplication to solve, so it keeps its wordmark. */
  .nav .logo__word { display: none; }
  .nav .logo__mark { display: block; height: 24px; width: auto; }
}
.nav__links > a:not(.btn) {
  position: relative;
  color: var(--d-body);
  text-decoration: none;
  font-family: var(--font-label);
  font-weight: 500;
  font-size: 0.95rem;
  /* 10px, not --s1's 4px. Measured at 375px the nav links were 33px tall
     against a 44px minimum tap target; this takes them to ~45px without
     changing where a single line of text sits, because the padding is
     symmetric. */
  padding-block: 10px;
}
/* The underline sits 2px under the WORD, not under the tap target. It used to
   be `bottom: -2px`, which followed the padding above down to 12px below the
   text and left it floating near the header's bottom edge — a line under
   nothing that Jack read as part of the header looking off-centre. 8 = the
   10px of padding less the 2px it always meant to sit below the word. */
.nav__links > a:not(.btn)::after {
  content: '';
  position: absolute; left: 0; right: 0; bottom: 8px; height: 1px;
  background: var(--copper-dark);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--dur) var(--ease);
}
.nav__links > a:not(.btn):hover { color: var(--d-text); }
.nav__links > a:not(.btn):hover::after { transform: scaleX(1); }

/* ---------- Buttons --------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--s2);
  font-family: var(--font-label);
  font-weight: 600;
  text-decoration: none;
  border-radius: var(--r-md);
  border: 1px solid transparent;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease), color var(--dur) var(--ease);
  cursor: pointer;
  /* A BUTTON LABEL NEVER WRAPS. This was the actual cause of "Sign in on two
     lines" — not the nav layout. As a flex child the button was allowed to
     shrink below its content width: measured 78px wide against a label needing
     ~84px, so "Sign in" broke across two lines INSIDE the button and made the
     whole header 67px tall. `flex: none` stops it being squeezed and `nowrap`
     stops it breaking if something else ever squeezes it anyway. */
  white-space: nowrap;
  flex: none;
}
.btn--sm { padding: 9px 16px; font-size: 0.9rem; }
.btn--lg { padding: 15px 30px; font-size: 1.05rem; }
.btn--ghost { border-color: var(--d-hairline-strong); color: var(--d-text); }
.btn--ghost:hover { border-color: var(--copper-dark); color: var(--copper-dark); }
.btn--accent { background: var(--copper); color: #fff; }
.btn--accent:hover { background: var(--copper-dark-hover); }

/* ============================================================================
   THE COVER — items 1, 6, 10, 11
   ============================================================================
   ITEM 11, WHICH IS THE ONE THAT MATTERED MOST.

   The complaint was that the mark sits higher or lower depending on the
   machine. It did, and the cause was not a missing aspect-ratio check — it was
   that v2 never centred the mark in the VIEWPORT at all. It centred it inside a
   170vh hero whose inner stage was sticky, so where the mark landed was a
   function of how far through a 170vh box you happened to be and how tall your
   window was. Two laptops at two heights genuinely resolved it to two places.

   There is nothing to detect. `.cover__inner` is exactly one viewport tall and
   `place-items: center` puts the mark's box centre on the viewport's centre, on
   every screen, at every aspect ratio, with no measurement. The mark is then
   made to agree with its own box by giving the build SVG a viewBox that is
   symmetric about the mark's true axis — see the viewBox note in index.html.

   ── svh, NOT dvh, AND THIS WAS THE iPHONE BUG ────────────────────────────
   This used to be `100dvh`, with a note explaining that dvh tracks the URL bar
   so the mark stays centred in what is actually on screen, and that
   ScrollTrigger is configured with `ignoreMobileResize` so the bar collapsing
   does not tear the pin down mid-sequence.

   Those two sentences are in direct conflict and the note did not notice.
   dvh means THIS ELEMENT'S HEIGHT CHANGES when the URL bar collapses.
   ignoreMobileResize means SCROLLTRIGGER DOES NOT RE-MEASURE when it does. So
   the cover grew by the height of the bar — 60-90px on an iPhone — under
   measurements that were deliberately frozen, and the pin's end plus every
   position after the pin spacer was wrong by that much. Everything below the
   cover then sat waiting on triggers that could not be reached, which is what
   Jack saw: "all of the other sections are totally empty", and then, once the
   safety net caught it, "it took almost 5 seconds".

   svh is the SMALL viewport — the height with the browser chrome showing — and
   it is defined not to change as you scroll. The layout is stable, so frozen
   measurements stay correct and the triggers below the cover land where they
   were computed. `ignoreMobileResize` stays: it is still right that a pin
   should not rebuild mid-scroll, and now nothing depends on it being wrong.

   The cost is that with the bar hidden the cover is ~80px shorter than the
   glass, so a strip of the page below shows under it. That strip is the pin
   spacer over `body`, which is #000, under a cover that is also #000 — the
   seam is invisible, and it is a straight trade for the sections below
   actually appearing. vh stays as the fallback for anything predating svh; on
   desktop all three units are identical.

   ITEM 11, SECOND HALF: the match line is absolutely positioned against the
   bottom of that same one-viewport box, so it is at the bottom of the screen
   with almost nothing under it — independent of where the mark is, because they
   are no longer in the same flow.
   ========================================================================== */
.cover {
  position: relative;
  height: 100vh;
  height: 100svh;
}
.cover__inner {
  position: relative;
  height: 100vh;
  height: 100svh;
  display: grid;
  place-items: center;
}

/* The drawn sheet frame that used to sit here is gone as of v3.2 (item 4).
   It was a grey rule boxing in the cover. The reasoning for it was sound —
   a drawing has a border — but the cover's job is to hold one mark, and a
   rectangle around the mark is a second thing to look at. */

/* ---------- The mark -------------------------------------------------------- */
.plate {
  position: relative;
  display: grid;
  place-items: center;
  /* Sized against BOTH axes, which is the part that is easy to get wrong. The
     width term gives the mark real presence on a wide monitor — on a 2545px
     screen v2's fixed 660px cap rendered the emblem at 549px, which is a small
     logo floating in a lot of nothing on the one screen that holds nothing
     else. The 80vh term is what stops that from becoming a collision on a
     short laptop: at a 735/407 aspect the plate is 1.806x as wide as it is
     tall, so an unbounded width would push the dimension string down into the
     cover copy the moment the window got short. Whichever limit binds first
     wins, and the mark stays centred either way because the centring is done
     by the viewBox, not by the size.
     THIS IS THE KNOB. One number, 740px, sets how big the mark reads. */
  width: min(clamp(280px, 42vw, 740px), 80vh);
  aspect-ratio: 735 / 407;
}
/* Both layers occupy the same grid cell, so the wordmark sits exactly behind
   the emblem and the handover is a cross-fade in place rather than a move. */
.plate > * { grid-area: 1 / 1; }

.cover__word {
  width: 100%;
  height: auto;
  color: var(--d-text);
  /* THE FINISHED VALUE, the one the sequence ends on (2026-10-16). This used
     to be the 0.07 ghost — the wordmark as it sits behind the emblem BEFORE
     the handover — and since this is what a page without the motion layer
     shows, every fallback landed on a cover that never happens: the emblem at
     full strength in front of a ghost, with every line of copy under it. Jack
     called that "glitched" on an iPhone and "all ugly" on a desktop. The
     ghost is still there during the sequence, which tweens to it from the
     `html.seq` park; the copper diamond inside the A is invisible at 0.07 and
     resolves in the handover, so the reward for scrolling is still the brand
     assembling itself. */
  opacity: 0.96;
}

/* ---------- The pass: the wordmark's continuous animation -------------------
   A narrow band of light travels across the letterforms, pauses, and comes
   round again. A plotter head going over a finished sheet.

   WHY A BAND AND NOT A BREATH. A pulse on a static screen is noticeable for
   about three cycles and irritating for every one after, and there is no way
   to make one slow enough to be calm without making it invisible. A pass has a
   REST — the band is off the sheet for most of the cycle — so the animation is
   an event that recurs rather than a state you are stuck inside. Roughly three
   seconds crossing, four and a half of nothing; the exact figures are in the
   timing note below.

   WHY NOT A TRANSFORM. The obvious version scales or slides the mark itself.
   It cannot be done here: GSAP writes an inline `transform` on .cover__word
   during the scrubbed handover, and a CSS animation on the same property wins
   over inline styles — so the loop would fight the scrub for the same matrix
   and the handover would land somewhere different depending on when the loop
   happened to be. Animating the MASK leaves transform untouched and out of it.

   The mask-position arithmetic, because percentages here are not intuitive:
   the offset is (area - image) x P, so with a band 46% wide the travel is
   0.54W per 100%. -140% parks it fully off the left, 240% fully off the right.
   Larger P moves it RIGHT only because the band is narrower than the box. */
.cover__word--pass {
  color: #fff;
  -webkit-mask-image: linear-gradient(101deg,
      rgba(0,0,0,0) 0%, rgba(0,0,0,0.08) 32%, rgba(0,0,0,0.85) 50%,
      rgba(0,0,0,0.08) 68%, rgba(0,0,0,0) 100%);
          mask-image: linear-gradient(101deg,
      rgba(0,0,0,0) 0%, rgba(0,0,0,0.08) 32%, rgba(0,0,0,0.85) 50%,
      rgba(0,0,0,0.08) 68%, rgba(0,0,0,0) 100%);
  -webkit-mask-size: 46% 100%;
          mask-size: 46% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  /* Parked off the sheet. This is also the no-animation resting state, so a
     browser that ignores the keyframes shows nothing rather than a stripe. */
  -webkit-mask-position: -140% 0;
          mask-position: -140% 0;
}
/* Opt IN to the motion, so prefers-reduced-motion never has to remember to
   switch it off — same posture as `html.seq`.

   LINEAR, AND THAT IS NOT LAZINESS. An ease-in-out spends its speed in the
   middle of the travel, which is exactly where the band is on the mark: the
   crossing measured 1.7s of an 8s cycle and read as a flash rather than a head
   moving over a sheet. Linear is also free of the usual cost — you never see
   the start or the stop, because the band is fully off the sheet at both ends
   of the travel. So the only part anyone watches is the constant-speed middle,
   which is what a plotter actually does.

   The timing, measured rather than guessed: the band is on the mark for 71% of
   the travel, so a 4.5s travel is a 3.2s crossing followed by 4.5s of nothing. */
@media (prefers-reduced-motion: no-preference) {
  .cover__word--pass { animation: rc-pass 9s linear infinite; }
}
@keyframes rc-pass {
  0%        { -webkit-mask-position: -140% 0;  mask-position: -140% 0; }
  50%, 100% { -webkit-mask-position: 240% 0;   mask-position: 240% 0; }
}

.rcbuild { width: 100%; height: auto; overflow: visible; }

/* THE FINISHED COVER HAS NO EMBLEM. The sequence dissolves it into the
   wordmark behind it, so the page the motion layer falls back to — reduced
   motion, no GSAP, a recovery — does not show it either. Until 2026-10-16 it
   did, at full strength over the wordmark's ghost, which is a state the
   sequence never produces; see the note on `.cover__word` above.
   Under `html.seq` it is up, because the build draws it there. That is the
   one place this file un-hides something under `seq` rather than parking
   it, and it is written that way so the CSS default stays the finished
   page, which is the rule everything in the motion layer leans on. */
.rcbuild { opacity: 0; }
html.seq .rcbuild { opacity: 1; }

/* The dashed layout guides, their stroke and their phone weights are gone with
   the guides themselves (2026-10-08) — see the note on the markup. So is
   `.rcbuild__draw`, in v4: a centred stroke leaves half its width outside the
   fill it traces, and those strokes stayed up after the build, so the finished
   mark wore a permanent outline. */
.rcbuild__fill .fl--plane { fill: var(--d-text); }
.rcbuild__fill .fl--cap { fill: var(--copper-dark); }

/* The glow. A copper bloom behind the cap, and in v4 it fires on the DOWNBEAT —
   once when the cap seats at the end of the build, and once per idle cycle as
   the tip settles back down, rather than swelling while the tip is lifted. A
   light that blooms on the way UP reads as the mark breathing; on the way DOWN
   it reads as the cap landing, which is the thing that is actually happening.
   It is a separate element rather than a filter on the cap because filters on an
   SVG that is also being scaled and faded are the expensive combination, and
   this one only has to be soft. */
/* SLOW AND WARM (Jack, 2026-08-10). Three separate things were making this read
   as a hard flash rather than as heat, and the timing was only one of them:

   1  IT WAS SMALL AND TIGHT. 34% wide with a 6px blur is a hot spot — a bright
      disc with an edge you can find. Brightness and size are not independent
      here: a small source has to be bright to register at all, so spreading it
      is what lets the peak opacity come down without the light vanishing. It is
      now 44% wide with a 14px blur, and the peak drops from 0.55 to 0.34 in the
      build and to 0.24 at rest.

   2  IT WAS ONE STOP OF #c96a3c, which is the accent copper — a fairly RED
      copper, and red at speed reads as an alarm rather than as warmth. It is
      now an amber core over a copper falloff, the same two-part construction
      the dome's sunrise uses: the yellow is what the eye reads as warm, the
      copper is what keeps it this brand's light rather than a generic glow.

   3  THE FALLOFF WAS FASTER THAN THE RISE — 0.26s up against 0.90s down. That
      is backwards for anything warm: heat arrives quickly and leaves slowly.
      The ratio is now about 1:2.6 the other way. See site.js.

   TRANSFORM RECALCULATED, and it had to be. This offset centres the light on
   the cap and it is a percentage of the ELEMENT's own height — so widening 34%
   -> 44% would have lifted it by 29% of the extra size and floated the glow off
   the top of the mark. Holding the absolute offset constant at 0.0918 x the
   plate's width: 0.27 x 0.34 = 0.0918, and 0.0918 / 0.44 = 0.2086. Hence -21%. */
.plate__glow {
  width: 44%;
  aspect-ratio: 1;
  border-radius: 50%;
  background:
    radial-gradient(circle, rgba(255, 201, 148, 0.30), rgba(255, 188, 133, 0) 54%),
    radial-gradient(circle, rgba(201, 106, 60, 0.34), rgba(201, 106, 60, 0) 76%);
  /* Sits on the cap, which is in the upper third of the plate, not its middle. */
  transform: translateY(-21%);
  opacity: 0;
  pointer-events: none;
  filter: blur(14px);
}

/* The dimension string ("740 PX" under the wordmark) is gone as of v3.2
   (item 3). It measured the plate's real rendered width, which is why it was
   defensible — but it also meant the number changed per monitor, and a figure
   that reads differently on every machine is noise on a cover, not a spec. */

/* ---------- The match line (items 1b, 11) ----------------------------------- */
.matchline {
  position: absolute;
  left: 0; right: 0;
  /* "almost no spacing". This is as close to the edge as it can sit without
     colliding with a phone's home indicator. */
  bottom: clamp(10px, 1.6vh, 22px);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  color: var(--copper-dark);
  padding-inline: var(--s5);
}
.matchline__row {
  display: flex;
  align-items: center;
  gap: var(--s4);
  width: 100%;
  max-width: 1400px;
}
/* `flex: 1 1 0` with `min-width: 0` — the rules give up their space first and
   can collapse to nothing. Paired with `flex: 0 0 auto` on the label below,
   this makes it structurally impossible for the two dashed rules to squeeze the
   words out, which is the shape of the bug Sam is seeing on iOS (the arrow
   renders, the text does not). It did NOT reproduce at 375px in Chromium — the
   label measured 238px inside a 327px row — so this is hardening rather than a
   confirmed fix, and it is called out as such. */
.matchline__rule {
  flex: 1 1 0;
  min-width: 0;
  height: 0;
  border-top: 1px dashed currentColor;
  opacity: 0.75;
}
.matchline__label {
  flex: 0 0 auto;
  font-family: var(--font-label);
  font-weight: 600;
  font-size: 11px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  white-space: nowrap;
}
.matchline__tick { width: 18px; height: 11px; color: currentColor; }
@media (max-width: 560px) {
  .matchline__label { font-size: 9px; letter-spacing: 0.16em; }
  .matchline__row { gap: var(--s3); }
}

/* ---------- The cover copy --------------------------------------------------
   Arrives at the end of the sequence, with the wordmark. Sam's item 1 makes the
   first screen the mark alone, so the copy can no longer be legible at rest —
   the match line is what carries "there is more here" until you scroll.      */
/* THE HEADLINE IS TWO LINES, AND THE WIDTH IS WHAT MAKES IT TWO.
   At 46rem the second sentence did not fit on one line at any desktop size, so
   the h1 was always three lines — and a third line is ~47px that has to come
   from somewhere on a short window. Measured at 1280 x 700, which is a 1920 x
   1080 laptop at 150% display scaling and therefore a very ordinary machine:

     wordmark          y 324.8 .. 375.2
     headline          y 369.0 .. 510.0   three lines, 141px
     gap               -6.2px             they OVERLAP

   The second sentence needs ~800px at that size and had 736. Widening to 58rem
   gives it 928 and the headline becomes two lines everywhere above the 860px
   breakpoint — verified at the two places it could still fail: 1365px, where
   the font stops growing at 46.4px and the line needs 853 of 928, and 1009px,
   where the width stops being 92vw. Below that the font is 3.4vw against a
   92vw box, a constant 27em of room for an 18.4em line, so it cannot bind.

   Then `bottom` drops from 12vh to 8vh. Two lines alone would have left the
   headline about 20px clear of the wordmark, which is a near-miss rather than
   a composition. Measured after both changes, at the same 1280 x 700:

     wordmark          y 324.8 .. 375.2
     headline          y 444.0 .. 538.0   two lines, 94px
     gap               +68.8px

   It survives a deliberately extreme window too — at 1280 x 620 the gap is
   still +37.2px, and the copy clears the match line by 8.8px. */
.cover__copy {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  width: min(58rem, 92vw);
  text-align: center;
  bottom: clamp(48px, 8vh, 112px);
}
.cover__copy h1 {
  font-size: clamp(1.6rem, 3.4vw, 2.9rem);
  color: var(--d-text);
}
.cover__lede {
  margin-top: var(--s4);
  color: var(--d-body);
  font-size: clamp(0.98rem, 1.2vw, 1.08rem);
  max-width: 62ch;
  margin-inline: auto;
}
.cover__meta {
  margin-top: var(--s4);
  font-family: var(--font-label);
  font-size: 11px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--d-muted);
}
@media (max-width: 860px) {
  /* Measured at 375x812: with the copy at 13vh its top landed at y=429 while
     the wordmark's baseline sat at y=419 — ten pixels, on the one screen a QR
     visitor is guaranteed to see. Dropping it to 9vh puts 43px above the copy
     and 29px below it before the match line, which is a composition rather
     than a near-miss. The emblem's own box reaches y=484 at this size, but it
     is at opacity 0 by the time the copy arrives, so the clearance that
     matters is against the wordmark, not against the plate. */
  /* The width stays where it was on a phone. 58rem/92vw is a desktop fix for a
     desktop problem — the headline wraps to four lines here whatever the box
     is, and 92vw would only push the copy into the screen edges. */
  .cover__copy { width: min(46rem, 86vw); bottom: clamp(56px, 9vh, 104px); }
  .cover__copy h1 { font-size: clamp(1.35rem, 5.8vw, 1.9rem); }
  .cover__lede { font-size: 0.94rem; }
}

/* ITEM 8 — the wordmark reaching the edges on a phone.
   The desktop rule's `clamp(280px, 42vw, 740px)` hits its FLOOR on every phone
   (42vw is only 157px at 375), so all phones got a flat 280px plate — 272px of
   wordmark inside a 375px screen, which is 73% of the width with 51px of air
   either side. Not literally clipped, but "R at one edge, P at the other" is a
   fair description of how it reads, and it is far tighter than the desktop
   proportion. 62vw restores roughly the same ratio of mark to screen that a
   desktop gets, and the vh term keeps it sane in landscape. */
@media (max-width: 560px) {
  .plate { width: min(62vw, 42vh); }

  /* THE WORDMARK IS DECOUPLED FROM THE PLATE ON A PHONE — Jack: "the ridgecap
     word mark should be bigger on mobile."

     `.cover__word` is `width: 100%` of `.plate`, so until now the wordmark and
     the emblem were one size decision. They should not be: they are never on
     screen together at full strength — the whole cover is a handover from one
     to the other — and they have completely different proportions. The emblem
     is 735 x 407 and fills its box; the wordmark is 1417 x 137, so at the same
     WIDTH it is a tenth of the height. Sizing it to the mark's box means the
     thing the visitor is left looking at is set by the thing that has just
     faded out.

     62vw stays on the plate. That number was solved for the EMBLEM — the note
     above is about the mark reaching the edges — and it is still right for it.
     The wordmark goes to 78vw: 292px on a 375px screen against 233px, a quarter
     larger, with 41px of air each side.

     THE OLD NOTE ABOVE SAYS 272px OF WORDMARK ON A 375px SCREEN READ AS "R at
     one edge, P at the other", which is more than this. That judgement was made
     when the wordmark could only be widened by widening the plate, so it was
     really a judgement about the whole lockup growing. Sizing the two
     separately is what makes 78vw available without the emblem coming with it,
     and it is Jack's call on his own brand either way.

     It overflows `.plate`, which is intended and safe: the plate is a grid with
     `place-items: center` so a wider child stays centred, nothing on the way up
     to `main` clips vertically, and `main`'s `overflow-x: clip` means even a
     mistake here can never add horizontal scroll.

     `max-width: none` IS LOAD-BEARING and this file has been caught by it once
     before — see the same line on `.dome__sky/.dome__glow/.dome__arc`. The reset
     at the top sets `svg { max-width: 100% }`, so an <svg> can never be drawn
     wider than its containing block no matter what `width` says. Without this,
     `min(78vw, 60vh)` computes to 292px, gets silently capped at the plate's
     233px, and the rule looks like it simply did not apply. Measured exactly
     that before adding it. */
  /* AND THE TRACK HAS TO BE PINNED TO THE PLATE'S WIDTH, or the centring is a
     lie. `.plate` is a one-cell grid whose column is implicitly `auto`, and an
     `auto` track sizes to its largest item — so a wordmark wider than the plate
     grew the TRACK to 288px while the plate's own box stayed 233px, and the
     overflow all went to the right. Measured: 30px off centre, sitting 2px from
     the plate's left edge instead of hanging equally either side.
     `minmax(0, 1fr)` holds the track at exactly the plate's width, so
     `justify-items: center` has a fixed box to centre against and the overhang
     is symmetric. */
  .plate { grid-template-columns: minmax(0, 1fr); }
  .cover__word { width: min(78vw, 60vh); max-width: none; }
}

/* ── THE FINISHED COVER ON A TALL PHONE (2026-10-03) ───────────────────────
   Feedback from a phone, relayed by Jack: "There's a lot of space at the top.
   I would make the titles bigger and take up more space."

   Measured at 375 x 812 with the build finished: the emblem has faded, the
   wordmark sits dead centre (y 392–420), and the copy hangs off the bottom
   edge (y 475–751) — so the 314px between the header and the wordmark are
   empty black. site.js names the cure in its note on the header logo:
   re-compose the finished cover so the copy is not bottom-anchored.

   So, on a tall portrait phone only:
     · the plate lifts 18svh. Nothing in the motion layer touches `.plate`
       itself, which is why the short-viewport block below may lift it too;
     · the copy hangs off the WORDMARK instead of the bottom edge — its top is
       the lifted centre, plus half the wordmark (78vw × 137/1417 ÷ 2 =
       3.77vw), plus 40px. However many lines it wraps to, it cannot climb
       into the wordmark, and the slack is shared above and below the group
       instead of all sitting on top of it;
     · the headline grows from 5.8vw to 7.4vw (21.75px → 27.75px at 375).

   Keyed to min-height 701px so the short-viewport block keeps sole charge of
   anything shorter. Measured after: see the commit that added this. */
@media (max-width: 560px) and (min-height: 701px) {
  .plate { transform: translateY(-18svh); }
  .cover__copy {
    top: calc(50% - 18svh + 3.77vw + 40px);
    bottom: auto;
  }
  .cover__copy h1 { font-size: clamp(1.6rem, 7.4vw, 2.15rem); }
}


/* ── SHORT VIEWPORTS ───────────────────────────────────────────────────────
   EVERY RULE ABOVE IS KEYED TO WIDTH, AND THE COVER'S PROBLEM IS HEIGHT. The
   copy is positioned from the BOTTOM of the cover while the plate is centred in
   it, so the shorter the viewport, the further the copy climbs into the mark.
   No width query can see that coming, and two very different devices hit it:

       829 x 390  landscape phone   copy top 64px ABOVE the wordmark's bottom
       320 x 568  small portrait    copy top 64px ABOVE the wordmark's bottom

   Identical failure, opposite proportions — which is exactly why the first
   attempt at this block was wrong. It carried `min-width: 561px` to keep it
   away from portrait phones, on the assumption that the width queries already
   had them covered. They do not: at 320 x 568 the copy is 283px tall on a
   568px screen, half the viewport, and the lede alone is 122px of it (eight
   lines in a 275px column). Small portrait phones are SHORT phones.

   So this is keyed to height alone. 700px catches a 568 and a 667 device as
   well as landscape; a 390 x 844 phone is above it and keeps the portrait
   composition unchanged.

   Four moves, and they share the work rather than leaning on one:
     · the plate shrinks, so the mark takes less of a screen with none to spare
     · it LIFTS by 6vh — `place-items: center` parks it across the middle,
       which is right everywhere else and is most of the problem here. This is
       the move that buys clearance type alone could not, and it keeps the
       centred composition rather than abandoning it. Safe: nothing in the
       motion layer touches `.plate` itself, only its children.
     · the copy drops a step and sits closer to the bottom edge
     · the match line goes

   THE MATCH LINE GOES because there is no room — with the copy ending ~24px off
   the bottom, a scroll cue beneath it would overlap — and because it is the one
   element in the cover that is purely an affordance. On a short screen the copy
   runs to the bottom edge, which says "there is more" by itself. Nothing
   carrying information is hidden. */
@media (max-height: 700px) {
  .plate { width: min(clamp(180px, 44vw, 520px), 38vh); transform: translateY(-6vh); }
  .cover__copy { width: min(46rem, 86vw); bottom: clamp(20px, 4vh, 44px); }
  .cover__copy h1 { font-size: clamp(1.15rem, 4.4vw, 1.5rem); }
  .cover__lede { font-size: 0.84rem; }
  .cover__meta { font-size: 0.74rem; }
  .matchline { display: none; }
}

/* A SHORT PORTRAIT PHONE gets the tall phone's composition, scaled down
   (2026-10-03). The block above lifts the plate 6vh and keeps the copy on the
   bottom edge, which on a 375 x 667 phone still left ~200px of black over the
   wordmark and an 18px headline. So the copy hangs off the wordmark here too,
   the lift is 16svh, and the headline is 6.4vw. Measured at 375 x 667 and
   320 x 568 — the two sizes the note above names. After the short-viewport
   block on purpose: same specificity, so source order decides. */
@media (max-width: 560px) and (max-height: 700px) {
  .plate { transform: translateY(-16svh); }
  .cover__copy {
    top: calc(50% - 16svh + 3.77vw + 32px);
    bottom: auto;
  }
  .cover__copy h1 { font-size: clamp(1.2rem, 6.4vw, 1.6rem); }
}

/* "Nothing to install · Nothing to learn" wraps on a phone; balanced, it
   breaks after the dot instead of leaving one word on a line of its own. */
.cover__meta { text-wrap: balance; }

/* ============================================================================
   THE DOME
   ============================================================================
   The boundary between the cover and the live half of the page — and in v3.2
   it became an actual boundary rather than a picture of one.

   v3.1 had this as a separate block sitting ABOVE the surface, which is exactly
   why Sam read it as "a divider just added using an image": the dot field began
   underneath it, so there was a dead band between the curve and the first dot,
   and the arc was decorating a join instead of being one. It now lives INSIDE
   the surface, and `.dome__sky` paints black over everything above the curve,
   so the surface runs right up to the underside of the arc and stops there.
   (Until 2026-10-08 it carried a live dot field; it now carries the cover's
   own drafting grid on its grey — see the note on .surface.)

   Four layers, because no single element can do all of it:
     .dome__sky     black fill above the curve. This is the split.
     .dome__glow    the light, masked by the same curve so it shines UP only.
     .dome__arc     SVG, preserveAspectRatio: none. A stretched arc is still a
                    smooth arc, so it spans any width. A stretched BADGE would
                    be an ellipse, which is why the badge is not inside it.
     .dome__badge   absolutely positioned at the apex, uniformly scaled.
   ========================================================================== */
/* THE ONE SHAPE, DEFINED ONCE.
   Three different layers need to know where the curve is: the black sky above
   it, the mask that stops the glow shining downward, and the arc itself. They
   are the same quadratic — ends at y=300, control at y=40, apex at y=170 of a
   0 0 1440 320 box.

   v4 FLATTENED IT. The control was y=-60, which put the apex at y=120: a rise
   of 180 units off the ends, and too much. It bulged rather than sat, and
   because `.surface` starts its content below the whole dome box while the
   curve peaks in the middle, the height of that bulge was also the height of
   the dead black band under the apex. Control y=40 gives an apex at y=170, a
   rise of 130, and takes 40 units of the box — 12.5% of the dome's height —
   straight out of that gap. Two complaints, one number.
   The two data URIs below fill the region ABOVE that curve;
   `preserveAspectRatio='none'` plus `background-size: 100% 100%` stretches them
   to any width, so the curve stays glued to the drawn arc at every viewport.
   If you edit the path, edit it in all three places or they will drift. */
.dome {
  position: absolute;
  top: 0; left: 0; right: 0;
  height: var(--dome-h);
  z-index: 1;
  pointer-events: none;

  /* ── THE CROP — how much wider than the screen the curve is DRAWN ────────
     1 = end to end, the whole dome in frame. Above 1 the curve is drawn wider
     than the viewport and centred, so its steep shoulders run off both edges
     and only the gentle middle is in frame. Nothing about the curve changes —
     it is the same quadratic at every size — you are just standing closer to
     it.

     WHY IT IS NEEDED, measured at 375 x 812:
     the layers are stretched with preserveAspectRatio: none, so the horizontal
     scale is viewport/1440 while the vertical scale is dome-h/320. On a phone
     those disagree by 2.5x, and the arc that leaves the edge of a desktop at
     20.1 degrees leaves the edge of a phone at 51.7 — rising 118.8px across
     the screen instead of sweeping. That is a bubble, not a horizon.

     The value is not a taste call, it is solved for: with a crop of m the
     visible edge slope is 1.266 / m squared, so m = 1.85 puts a 375px phone
     back on the desktop's 20 degrees, and m = 1.45 does the same for a tablet.

     ONE KNOB. The three layers below take their width from it, and the arc's
     edge fade divides by it, so they cannot disagree about where the frame is. */
  --dome-gx: 1;
}

/* All three curve layers are the SAME box, always — that is what stops the
   black sky, the masked glow and the drawn arc from drifting apart when the
   crop changes. At --dome-gx: 1 this is identical to `inset: 0`.
   They overflow .dome horizontally on purpose; .surface clips them. */
.dome__sky,
.dome__glow,
.dome__arc {
  position: absolute;
  top: 0;
  left: 50%;
  width: calc(var(--dome-gx) * 100%);
  height: 100%;
  /* CENTRED BY MARGIN, NOT BY TRANSFORM, and the reason is one property on the
     sky. These carried `transform: translateX(-50%)` until the grid had to be
     continued down to the horizon; a transformed element makes
     `background-attachment: fixed` resolve against ITSELF rather than the
     viewport, so the sky's grid would have scrolled with the page while the
     real `.grid` — which is `position: fixed` — stayed put, and the two would
     have sheared against each other at the seam.
     The arithmetic is identical. Width is gx x CB, so half of it is 0.5·gx·CB;
     `left: 50%` puts the left edge at 0.5·CB and this pulls it back by exactly
     that half, leaving the centre on 0.5·CB at every value of gx. All three
     layers keep the same box, which is the invariant this block exists for. */
  margin-left: calc(var(--dome-gx) * -50%);
  /* THE RESET AT THE TOP OF THIS FILE SETS `svg { max-width: 100% }`, and it
     silently caps the arc at the viewport while the two divs either side of it
     widen — so the drawn line stays steep over a cropped sky and a cropped
     glow, which is worse than not cropping at all. Measured: the arc came back
     331.5px narrower than the layers it has to agree with. */
  max-width: none;
}

/* Black above the curve, AND THE DRAFTING GRID CARRIED DOWN ONTO IT.
   This is what makes the dome a boundary rather than an ornament: the surface
   underneath spans the full width, and this stops it existing above the line.
   Below the curve — the same grid on the surface's grey. Above — the grid on
   black, continuous with the page above.

   Jack: "make it so the squares in the background go all the way down to the
   line so it dosnt feel like an empty black space." They could not, because
   `.surface` is opaque — deliberately, it is what stops the fixed grid showing
   through the dot field that was here then — and this layer then painted flat
   black on top of it.
   Between the last of the grid and the arch there were 190-300px of nothing.

   THE SHAPE MOVED FROM background-image TO mask-image, which is the whole
   trick. As a background it WAS the black; as a mask it clips whatever this
   element paints, so the element can now paint black plus grid lines and both
   get cut to the same curve. It is also byte-identical to the mask on
   `.dome__glow` below — the sky and the light are now provably the same shape
   rather than two shapes that agree.

   `background-attachment: fixed` IS LOAD-BEARING. `.grid` is `position: fixed`
   with `background-position: center center`, so its lines are anchored to the
   viewport centre. Anchoring these to the viewport too is the only way the two
   stay in phase while the page scrolls — otherwise the grid slides against
   itself across the seam at the top of `.surface`, which is far worse than the
   empty band it replaces. This is also why the layers above are centred with a
   margin instead of a transform; see the note there.

   0.036 RATHER THAN `.grid`'s 0.042, and it is a deliberate approximation.
   `.grid` carries a viewport-centred vignette, so its lines are already dimmed
   to somewhere around 0.76-0.95 of full where the dome sits. A mask cannot be
   viewport-anchored the way a background can, so that vignette is not
   reproducible here — 0.036 is the middle of that range, which leaves the two
   within about one RGB level of each other anywhere along the seam instead of
   three at the extremes. */
.dome__sky {
  background-color: #000;
  background-image:
    linear-gradient(rgba(255, 255, 255, 0.036) 1px, transparent 1px),
    linear-gradient(90deg, rgba(255, 255, 255, 0.036) 1px, transparent 1px);
  background-size: var(--cell) var(--cell);
  background-position: center center;
  background-attachment: fixed;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 320' preserveAspectRatio='none'%3E%3Cpath d='M0 0H1440V300Q720 40 0 300Z' fill='%23ffffff'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 320' preserveAspectRatio='none'%3E%3Cpath d='M0 0H1440V300Q720 40 0 300Z' fill='%23ffffff'/%3E%3C/svg%3E");
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

/* THE GLOW SHINES UP ONLY — Sam's item 1, and the mask is the whole mechanism.
   The gradients themselves are radial and would spill in every direction; the
   mask is the same above-the-curve shape, so light can exist above the line and
   nowhere below it. That is what makes it read as a sun behind a planet's
   circumference rather than a lamp buried in the page: on a real horizon the
   body is opaque and the light is only ever above it.
   Three gradients because one ellipse cannot be a source and a wash at once —
   a hot core that falls off fast, a bloom around it, and a wide low spread
   along the line. All three sit on the apex so they cannot drift apart. */
.dome__glow {
  /* EVERY HORIZONTAL RADIUS IS DIVIDED BY THE CROP, and it has to be. These
     are percentages of this element, and this element is --dome-gx times wider
     than the screen — so without the division a cropped dome would spread the
     sun sideways by the same factor and turn a source into a wash. Dividing
     holds the glow at exactly the same width in real pixels no matter how much
     of the curve is in frame. The vertical radii are untouched: the element's
     height never changes. */
  /* CUT HARD FOR THE UPDRAFT — Jack: "the area near the icon is too bright and
     static." These two complaints were the same object. This wash peaks at the
     apex and never moves, and it was drowning the field that does: measured at
     the icon, the static wash contributed 90 of 106 luminance — 85% of what you
     saw there — against 25-30% out on the shoulders, which is exactly where the
     light read as alive and where it did not.
     Dimming the moving layer near the centre was tried and is the wrong fix; it
     makes the ratio worse, and the arithmetic is on `dip` in ember.js. Cutting
     the STATIC layer fixes both complaints with one change, because at the apex
     brightness and staticness were the same gradient. Measured at u = 0.5:
         wash 0.56   mean luminance 106,  modulation 18-40%
         wash 0.22   mean  76,            modulation 79%
         wash 0.16   mean  68,            modulation 94%   <- here
         wash 0.10   mean  61,            modulation 110%
     0.16 also stops the apex being a hotspot at all: 68 there against 66 at
     u=0.38. Below about 0.10 the dome starts losing its base warmth on the
     shoulders, where the field alone cannot carry it. */
  background:
    radial-gradient(ellipse calc(13% / var(--dome-gx)) 30% at 50% 53.125%, rgba(255, 218, 190, 0.16), rgba(255, 180, 132, 0) 70%),
    radial-gradient(ellipse calc(30% / var(--dome-gx)) 46% at 50% 53.125%, rgba(201, 106, 60, 0.26), rgba(201, 106, 60, 0) 72%),
    radial-gradient(ellipse calc(88% / var(--dome-gx)) 34% at 50% 53.6%, rgba(168, 92, 46, 0.18), rgba(168, 92, 46, 0) 76%);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 320' preserveAspectRatio='none'%3E%3Cpath d='M0 0H1440V300Q720 40 0 300Z' fill='%23ffffff'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1440 320' preserveAspectRatio='none'%3E%3Cpath d='M0 0H1440V300Q720 40 0 300Z' fill='%23ffffff'/%3E%3C/svg%3E");
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  /* The lit state. The scrubbed tween drives this up from 0 as the dome comes
     into view; with no JS it is simply already lit, which is a finished page. */
  opacity: 1;
}

/* THE UPDRAFT. The wash above is the hearth; this is what comes off it.
   Deliberately NOT given its own mask, opacity or crop — it is inside
   `.dome__glow` precisely so it inherits all three. See the note on the markup.

   The wash came down twice as this landed — 0.70/0.58/0.30 originally, then
   0.56/0.46/0.24 when the updraft first arrived and was only meant to add to
   it, and now 0.16/0.26/0.18 once it turned out the wash was the thing making
   the apex read as a bright static blob. The two layers are a pair: the wash is
   the base warmth, this is the part that moves, and if you raise one you have
   to look at the other. The full arithmetic is on the background above.

   `screen` RATHER THAN NORMAL, and it matters more than it looks. Painted
   normally, a 0.2-alpha warm pixel over the bright core of the wash AVERAGES
   with it — so the updraft would darken the hottest part of the dome and
   brighten only the dim edges, which is the exact inverse of how light behaves
   and reads as a grey film over the middle. Screen can only ever lighten. On a
   near-black backdrop it is indistinguishable from adding, which is what the
   field's own internal maths already assumes. */
.dome__ember {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  mix-blend-mode: screen;
}

/* THE ARC'S EDGE FADE, and it only exists once the dome is cropped.
   The fade built into #arcfade is a gradient across the PATH, so it dies out
   at the path's own ends — which is correct while the whole dome is in frame
   and wrong the moment it is not: cropped, the line would meet the screen edge
   at roughly 44% opacity and simply stop, which reads as a broken line rather
   than one continuing past the frame.
   This mask is sized to the VISIBLE window instead — 100% of the element
   divided by the crop is exactly the viewport — so the arc always fades out at
   the edge of the screen, whatever fraction of the curve is on it. At
   --dome-gx: 1 the mask is the full element and changes nothing, so desktop
   renders exactly as before. */
.dome__arc {
  -webkit-mask-image: linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16%, rgba(0,0,0,1) 84%, rgba(0,0,0,0) 100%);
          mask-image: linear-gradient(90deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16%, rgba(0,0,0,1) 84%, rgba(0,0,0,0) 100%);
  -webkit-mask-size: calc(100% / var(--dome-gx)) 100%;
          mask-size: calc(100% / var(--dome-gx)) 100%;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

/* The crop, per device. See the derivation on .dome — these are the values that
   put a phone and a tablet back on the desktop's ~20 degree edge slope.

   BOTH CAME DOWN IN v4, because the curve they were correcting got gentler.
   The edge slope of a quadratic is 2(P1 - P0) — at the old control y=-60 that
   was -720/1440 = 0.500 in viewBox units, and at y=40 it is -260/1440 = 0.361.
   The crop needed goes as the square root of the slope it has to cancel, so
   each value scales by sqrt(0.361/0.500) = 0.850: 1.45 -> 1.23 and 1.85 ->
   1.57, rounded to the values below. Leaving them alone would have over-cropped
   an already-flatter curve and pushed the shoulders off screen for nothing. */
@media (max-width: 860px) { .dome { --dome-gx: 1.25; } }
@media (max-width: 560px) { .dome { --dome-gx: 1.55; } }

.dome__badge {
  position: absolute;
  left: 50%;
  /* The apex is at 170/320 = 53.125% of the box. Same number as the path. */
  top: 53.125%;
  transform: translate(-50%, -50%);
  width: clamp(54px, 6vw, 76px);
  aspect-ratio: 1;
  border-radius: 50%;
  background: #0a0b0d;
  border: 1px solid rgba(201, 106, 60, 0.55);
  box-shadow: 0 0 0 7px rgba(0, 0, 0, 0.9), 0 0 38px rgba(201, 106, 60, 0.34);
  display: grid;
  place-items: center;
  color: var(--d-text);
}
.dome__badge svg { width: 52%; height: auto; }

/* ============================================================================
   THE SURFACE — the drafting table runs on under the products
   ============================================================================
   THE DOT FIELD IS GONE (2026-10-08). This used to carry a live canvas of dots
   across the products. Jack: "get rid of the dotted background its pretty
   ugly. Just make it grey." Plain grey then read as empty — "the blank
   background looks bland" — the same complaint he made about the black under
   the dome before the squares were carried down to the arc ("so it dosnt feel
   like an empty black space"). The answer is the same one: the cover's grid
   continues under the products, so the home page is one drafting table from
   the mark down to the copper band, with the arc as its horizon.

   IT IS THE SAME GRID, NOT A COPY OF IT. The surface used to be opaque, which
   is what stopped the fixed `.grid` showing through the dots ("get rid of the
   lines behind the live dots"). With the dots gone the grid has nothing left
   to crowd, so the surface is now a TINT over it rather than a wall in front:
   rgba(128,136,160,0.125) over the black page composites to exactly
   --ink-deep, rgb(16,17,20), so the grey is unchanged; a grid line shows
   through at 7/8 of its strength on the cover. Being the real fixed grid,
   its lines stay in phase with the ones `.dome__sky` paints above the arc,
   and it costs nothing to scroll — painting a second grid here would have
   needed `background-attachment: fixed` on the tallest block on the page,
   which repaints on every scroll frame.

   The demo band below the copper keeps the opaque grey on purpose: a form is
   the one place on the page where every line should belong to a control. */
.surface {
  position: relative;
  background: rgba(128, 136, 160, 0.125);
  overflow-x: clip;
  /* The dome is absolutely positioned over the top of this element, so the
     first section needs to start below it. One variable drives the dome's
     height, this padding and the badge's position — they cannot disagree. */
  --dome-h: clamp(190px, 26vh, 300px);
  padding-top: var(--dome-h);
  /* THE CLOSING GAP LIVES HERE, NOT ON THE LAST CHILD'S MARGIN, and it stays
     here now that the last child has changed.

     A bottom margin on whatever ends this element COLLAPSES THROUGH it: there
     is no bottom padding, no bottom border and no block formatting context —
     `overflow-x: clip` with `overflow-y: visible` does not make one. It escapes
     and becomes a gap between the surface and the copper band, showing `body`
     rgb(0,0,0), which is DARKER than this element's own rgb(16,17,20) and so
     reads as a black stripe rather than as absence, with the fixed `.grid`
     visible through it. Measured when it happened: surface bottom 2912.5,
     reviews top 2934.9, 22.4px.
     Padding cannot collapse. Keep the closing gap on this line. */
  padding-bottom: clamp(var(--s4), 1.6vw, 28px);
}
.surface > .band { position: relative; z-index: 1; }

.band { padding-block: clamp(var(--s8), 7vw, 116px); }
/* TIGHTENED IN v4. Both of Jack's spacing notes were the same measurement seen
   from two ends: 116px of band padding on top of the dome's own dead height
   under the apex, and a hundred more between the last product and what followed
   it. The dome half is fixed by flattening the arch — see .dome — so this only
   has to carry the rest.
   The bottom value now closes the whole surface, since the products are the
   last thing in it. It is deliberately smaller than the top: the copper band
   below is a hard colour change and does not need a run-up, where the dome
   above is soft and does. */
.band--index {
  padding-top: clamp(var(--s6), 3.4vw, 60px);
  padding-bottom: clamp(var(--s5), 2.2vw, 40px);
}

.eyebrow {
  font-family: var(--font-label);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--copper-dark);
}

/* ---------- Products -------------------------------------------------------- */
.index__head {
  display: grid;
  gap: var(--s5);
  align-items: end;
  margin-bottom: var(--s7);
}
@media (min-width: 861px) {
  .index__head { grid-template-columns: minmax(0, 1fr) minmax(0, 26rem); gap: var(--s8); }
}
.index__head h2 { font-size: clamp(1.9rem, 3.4vw, 2.9rem); }
.sindex__fit { color: var(--d-muted); min-width: 0; }

/* ONE COLUMN, NOT A GRID OF CARDS.
   Two products side by side in 340px columns is two slabs in a lot of nothing,
   and it forces the product name down to 1.9rem to fit the column — which is
   how the name ended up the same size as its own status pill. Full-width rows
   let the name run at display scale, which is the actual point of the section:
   these are two finished tools, not a catalogue.
   A hairline between entries, and one above the first, so the list reads as
   ruled rather than boxed. */
.sindex {
  display: grid;
  border-top: 1px solid var(--d-hairline);
}
.srow { min-width: 0; border-bottom: 1px solid var(--d-hairline); }
.srow__link {
  display: grid;
  /* Sheet number in its own margin column on desktop, so the names all start on
     one line and the numbers read as a column of references rather than as
     labels stuck to the front of each name. Collapses to one column below. */
  grid-template-columns: 6.5rem minmax(0, 1fr);
  align-items: baseline;
  column-gap: var(--s5);
  row-gap: var(--s3);
  padding: clamp(var(--s5), 2.6vw, var(--s7)) 0;
  text-decoration: none;
  /* NO background, no border, no radius. The products sit on the page — the
     drafting grid on the surface's grey, now that the dot field is gone. */
  transition: padding-left var(--dur) var(--ease);
}
/* The whole row shifts a few pixels on hover — the only hover affordance here,
   and it is the row moving rather than a colour changing, because there is no
   panel left to tint. */
.srow__link:hover { padding-left: var(--s3); }
.srow__no {
  grid-column: 1;
  font-family: var(--font-label);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.18em;
  color: var(--copper-dark);
  white-space: nowrap;
}
.srow__name {
  grid-column: 2;
  font-family: var(--font-display);
  font-weight: 700;
  /* Display scale. This is the size the section heading runs at, deliberately:
     the products are the content, "Available products." is only the label on
     them. */
  font-size: clamp(2rem, 4.2vw, 3.1rem);
  line-height: 1.06;
  color: var(--d-text);
  letter-spacing: -0.03em;
  transition: color var(--dur) var(--ease);
}
.srow__link:hover .srow__name { color: var(--copper-dark); }
.srow__what {
  grid-column: 2;
  color: var(--d-body);
  font-size: 1.0625rem;
  max-width: 62ch;
  min-width: 0;
}
/* Audience and status on one quiet line under the description. ONE uppercase
   treatment in the whole row, and it is this pair — the sheet number is small
   caps copper but not shouting, and everything else is sentence case. */
.srow__meta {
  grid-column: 2;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--s3) var(--s5);
  margin-top: var(--s2);
  font-family: var(--font-label);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}
.srow__for { color: var(--d-muted); }

/* RC-003 — THE HELD SHEET. It has to read as a number being kept open, not as a
   third product, so everything that gives the two real rows their presence is
   turned down and nothing is added: no border, no dashes, no "coming soon"
   badge. Just the same row, quieter.

   THE SHEET NUMBER STAYS COPPER. That is the whole idea — the number is real
   and reserved, it is the CONTENT that is not there yet. Muting it too would
   make the row look like a mistake rather than a decision.

   There is no hover because there is nothing to click: `.srow__link` is an <a>
   on the two products and a <span> here, so the hover rules below simply never
   match. These two declarations exist for the day someone makes it a link
   without reading the markup note. */
.srow--held .srow__name { color: var(--d-muted); }
.srow--held .srow__what { color: var(--d-muted); }
.srow--held .srow__link { cursor: default; padding-left: 0; }
/* The status, as text. It was a bordered capsule, which is a badge for its own
   sake — the word is the information and the box around it was decoration.

   NAMED --near AND --far, NOT --live AND --soon. The old names encoded a claim:
   `--live` said a thing was available, and it was on the product that is
   furthest from being so. Neither product can be used by another company yet,
   so the only honest distinction left is which one is closer, and the names now
   say exactly that and nothing more. The accent goes to the nearer one — it is
   the loudest state on the page and it was pointing at the wrong product. */
.srow__state--near { color: var(--copper-dark); }
.srow__state--far  { color: var(--d-muted); }
.srow__state--far::before,
.srow__state--near::before {
  content: '';
  display: inline-block;
  width: 5px; height: 5px;
  margin-right: 0.7em;
  vertical-align: 0.15em;
  border-radius: 50%;
  background: currentColor;
}
@media (max-width: 720px) {
  .srow__link { grid-template-columns: minmax(0, 1fr); row-gap: var(--s2); }
  .srow__no, .srow__name, .srow__what, .srow__meta { grid-column: 1; }
  .srow__no { margin-bottom: var(--s1); }
}

/* The trial block's rules are gone with the block — `.cta`, `.cta__mail` and
   the `band--cta` padding. See the note in index.html for why the section went.
   `.btn--accent` and `.btn--lg` are KEPT: the header's Sign in still uses
   `.btn--ghost .btn--sm`, and the accent pair is the one styled call to action
   this design system has, so deleting it would only mean rewriting it the day a
   product is genuinely open. */

/* ============================================================================
   REVIEWS — item 9
   ============================================================================
   Still reserved, still blank on purpose: Ridgecap has no customers yet and
   inventing one is the fastest way to lose a contractor who checks with peers.

   What it gains is the live topographic surface, in BLACK, per Sam. Contours
   are the survey drawing a project actually starts from, and on flat copper
   they read as a printed drawing rather than as an effect — which is why black
   works here and would not on graphite. The band is the only place on the page
   with a light-enough field to carry black line work.

   The band clips its own canvas and /topo.js is mounted in host-measured mode,
   because this is a normal in-flow section, not a fixed full-viewport layer.
   ========================================================================== */
.band--reviews {
  position: relative;
  background: var(--copper);
  color: #fff;
  text-align: center;
  overflow: hidden;
  isolation: isolate;
}
.band--reviews > .topo {
  position: absolute;
  inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  z-index: 0;
  /* TWO MASKS, INTERSECTED — and they are doing two unrelated jobs.

     1  The radial hole keeps the contours clear behind the type. The first pass
        held them off until 78% of the way out, which on a 391px band meant the
        only visible line work was in the far left and right thirds — the band
        read as flat copper with something happening off in the corners. The
        hole is now just big enough for the words.

     2  The linear ramp is the second half of the topo fix (v4). The contours
        used to start at full strength on the band's very first row, one pixel
        below a dot field that also ran at full strength to its last row — the
        hard splice that made this read as pasted in. They come up over the
        band's first stretch instead, which is the copper arriving before the
        drawing on it does.

        SHORTENED 18% -> 9%, at the same time the dot field's falloff was
        recurved, and for the same complaint: "it looks like there is too much
        blank space now." The two fades are one join and they were BOTH long.
        18% of a ~335px band is 60px of near-empty copper sitting directly under
        60px of near-empty surface, so the transition was ~120px of nothing with
        texture at each end. Halving this closes the copper half; the curve on
        the dot field closed the dark half. The dot field is gone now
        (2026-10-08), so the gridded grey simply meets the copper. */

  /* ⛔ THE COMMENT OPENER ON THIS BLOCK WAS MISSING, AND IT HAD CONSEQUENCES.
     The block above closes on the "18% to 9%" line, so everything from here
     down to the closer below was not a comment at all — the CSS parser read
     four lines of English prose as declarations inside the .topo rule. Error
     recovery skips to the next semicolon, which silently ate the
     -webkit-mask-image declaration on the line below it.

     Present since before the cover-sequence work; the opener and closer counts
     are unequal by one at 6ef683a too. Nothing reported it, because an
     unbalanced comment is not an error in any tool being run here — it shows up
     only as a declaration that mysteriously fails to apply.

     NOTE FOR ANYONE EDITING THIS FILE: do not write a comment closer inside
     comment prose. Comments do not nest, so the first one ends the block early
     and the rest of the sentence becomes CSS. Fixing this the first time
     reintroduced the same bug that way, in this very comment.

     mask-composite: intersect is what makes a pixel need BOTH masks to show,
     rather than the later one simply replacing the earlier. The -webkit-
     property is a different syntax with different keywords, so it gets its own
     declaration — `source-in` is that prefix's spelling of the same operation. */
  -webkit-mask-image:
    radial-gradient(ellipse 46% 62% at 50% 50%, transparent 4%, #000 56%),
    linear-gradient(to bottom, transparent 0%, #000 9%);
          mask-image:
    radial-gradient(ellipse 46% 62% at 50% 50%, transparent 4%, #000 56%),
    linear-gradient(to bottom, transparent 0%, #000 9%);
  -webkit-mask-composite: source-in;
          mask-composite: intersect;
}
.band--reviews > .topo canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
.band--reviews .shell { position: relative; z-index: 1; }
.band--reviews h2 { color: #fff; font-size: clamp(1.9rem, 3.4vw, 2.9rem); margin-block: var(--s3) var(--s5); }
.band--reviews .eyebrow { color: #ffe0cc; }
.reviews__note { color: #fbe7db; max-width: 54ch; margin-inline: auto; }

/* ============================================================================
   REQUEST A DEMO
   ============================================================================
   BACK TO GRAPHITE, and the reason is contrast rather than taste. The copper
   band above is the only light field on the page; a form on it would have to
   restate every input colour to stay legible on #a85c2e, and the footer under
   it is dark anyway — so a light band between two dark ones would leave the
   copper reading as a divider rather than as a section. `--ink-deep` matches
   the products surface, so this reads as a return to the sheet.

   ⛔ DARK-MODE CONTRAST, THE RULE THIS SITE HAS BROKEN TWICE — AND ONCE MORE
   HERE, IN DRAFT. The fill cannot define the field: `--panel` (#191c22) on an
   `--ink-deep` (#101114) band measures 1.11:1, which is invisible. So the
   BORDER is what makes a field a field, and a border is a UI component under
   WCAG 1.4.11, which wants 3:1 against what is behind it.

   The first version of this section used `--d-hairline-strong` (#3d424e) for
   that border because it is the file's existing "strong" hairline. MEASURED IN
   THE BROWSER IT IS 1.88:1 — under the bar, and the comment here originally
   claimed 3.0:1 from arithmetic I had not checked. That is precisely the
   failure mode this site keeps repeating: a contrast value asserted rather
   than measured.

   `--field-line` exists because of that. It is not decoration and it is not
   interchangeable with the hairline tokens: those are for RULES between blocks,
   which are decoration and may be faint, while this is the edge of a control
   somebody has to find and click. Measured, not calculated — see the figures on
   the token itself.
   ========================================================================== */
.band--demo {
  background: var(--ink-deep);
  border-top: 1px solid var(--d-hairline);
  /* The border of an interactive control on `--ink-deep`. 3.38:1 measured in
     the browser against the band, so it clears WCAG 1.4.11's 3:1 for non-text
     UI with a little room. Do not swap this for `--d-hairline-strong`, which
     looks similar and measures 1.88:1.

     ⚠️ THE APP USES #3d424e FOR THIS AND WE DELIBERATELY DO NOT MATCH IT. That
     is the app's `--border-strong`, and it is right THERE because it sits on
     the app's `--surface`, a lighter dark than this band. Copying the hex would
     import the look and lose the contrast — the same value measures 1.88:1
     against `--ink-deep`. Match the app's SHAPE, not its colours; the surfaces
     underneath are not the same. */
  --field-line: #616878;

  /* ── SHARPER, TO MATCH THE CLOSEOUT APP ──────────────────────────────────
     Jack: "Can you make these a little bit sharper, like how the boxes are in
     the closeout app."

     Read out of the app's live `src/styles/` rather than guessed, because the
     design-system folder is stale. The app's `.input` and `.btn` both use
     `--radius-md`, which its scale sets to 5px. This band was on v4's `--r-md`,
     which is 8px. Those 3px are the whole of "sharper": a marketing radius on
     what is really a piece of application UI.

     SCOPED TO THIS BAND ON PURPOSE. `--r-md` is v4's global and also shapes the
     header's Sign in button and every other control on the page. Jack pointed
     at the form, so only the form moves. Making the whole page match the app is
     one token, but it is a different decision and it changes chrome he has
     already signed off on. */
  --field-radius: 5px;
}
/* Two columns on a wide screen — the argument on the left, the form on the
   right — collapsing to one below 900px. `start` rather than `stretch` so the
   intro does not grow to the form's height and float its own text. */
.demo__inner {
  display: grid;
  gap: clamp(var(--s6), 5vw, 88px);
  align-items: start;
}
@media (min-width: 900px) {
  .demo__inner { grid-template-columns: minmax(0, 0.85fr) minmax(0, 1fr); }
}
.demo__intro h2 {
  color: var(--d-text);
  font-size: clamp(1.7rem, 3vw, 2.5rem);
  margin-block: var(--s3) var(--s5);
}

/* The home page's section headings on a phone (2026-10-03, the same feedback
   as the cover): larger, so each one spans the screen like a title rather
   than a label. After the three rules above on purpose — same specificity,
   so source order decides. */
@media (max-width: 560px) {
  .index__head h2,
  .band--reviews h2 { font-size: 2.2rem; }
  .demo__intro h2 { font-size: 2.05rem; }
}
.demo__note { color: var(--d-body); max-width: 46ch; }
.demo__meta {
  margin-top: var(--s5);
  font-family: var(--font-label);
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--d-muted);
}

/* ---------- The product choice ---------------------------------------------
   A fieldset with the browser's own border and padding removed. `min-width: 0`
   is not decoration: a fieldset has an intrinsic minimum width that ignores
   grid, and without it a long legend can push the whole column wider than its
   track and produce a horizontal scrollbar on a phone. */
.demo__products { border: 0; padding: 0; margin: 0 0 var(--s5); min-width: 0; }
.demo__legend {
  padding: 0;
  font-family: var(--font-label);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--d-muted);
  margin-bottom: var(--s3);
}
.demo__hint {
  margin: 0 0 var(--s3);
  font-size: 0.86rem;
  color: var(--d-muted);
}
/* Two, not three — "Both" is gone and is expressed by ticking both. */
.demo__choices { display: grid; grid-template-columns: repeat(2, 1fr); gap: var(--s3); }
.demo__choice {
  position: relative;
  display: grid;
  gap: var(--s1);
  padding: var(--s4) var(--s3);
  background: var(--panel);
  border: 1px solid var(--field-line);
  border-radius: var(--field-radius);
  cursor: pointer;
  text-align: center;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
}
.demo__choice:hover { border-color: var(--copper-dark); }
/* THE RADIO IS NOT `display: none`. A hidden-that-way input cannot be focused,
   so the whole group would drop out of the keyboard tab order and the arrow
   keys that move between radios would do nothing. It is made invisible but
   still focusable, and the visible state is drawn by the label around it. */
.demo__choice input {
  position: absolute;
  opacity: 0;
  width: 1px; height: 1px;
  margin: 0;
  pointer-events: none;
}
/* ── THE TICK ────────────────────────────────────────────────────────────────
   The one thing on the card that says "you may choose more than one". Without
   it these are two identical panels that look exactly like the radios they used
   to be, and somebody who assumes single-select never tries the second.

   It is `aria-hidden` and purely decorative: the real checkbox is still in the
   accessibility tree, and a screen reader already announces "checkbox, not
   checked". This square exists for the people who cannot hear that. */
.demo__tick {
  position: absolute;
  top: 10px; right: 10px;
  width: 16px; height: 16px;
  border: 1px solid var(--field-line);
  /* --radius-sm in the app's scale. A tick box wants to be squarer than the
     control it sits on, or it reads as a dot. */
  border-radius: 3px;
  transition: background var(--dur) var(--ease), border-color var(--dur) var(--ease);
}
/* The check itself: two borders of a rotated box. Drawn rather than a glyph so
   it needs no font and cannot arrive late. */
.demo__tick::after {
  content: '';
  position: absolute;
  left: 4px; top: 0px;
  width: 5px; height: 9px;
  border: solid #fff;
  border-width: 0 2px 2px 0;
  transform: rotate(45deg) scale(0.6);
  opacity: 0;
  transition: opacity var(--dur) var(--ease), transform var(--dur) var(--ease);
}
/* THE SELECTED STATE AVOIDS `:has()` — FOR BROWSER SUPPORT, NOT BECAUSE IT IS
   BROKEN. The first version of this note claimed `:has()` "did not apply here",
   after the tick, the checkmark and the card border all measured unchanged with
   the box genuinely checked. That diagnosis was WRONG and it is recorded here
   because it was expensive.

   Every property that appeared dead is a TRANSITIONED one, and the only one
   that worked — `color` on the sheet number — is the only one with no
   transition on it. The verification browser was not compositing, so its
   animation clock never advanced and no transition ever left its start value.
   `:has()` was working the whole time. Re-measured with
   `transition: none !important` forced, every one of them changes correctly.

   THE RULE THAT WOULD HAVE CAUGHT IT IMMEDIATELY: when a state change looks
   like it did not apply, kill transitions before believing it.

   The class stays anyway, on its own merits: `:has()` is absent in Safari
   before 15.4 and in older Firefox, and on a public form "which one did I pick"
   cannot be a progressive enhancement — a visitor who cannot see their own
   selection cannot use the control. The tick uses the SIBLING combinator, which
   needs no ancestor query at all; the card, which genuinely does need its
   ancestor styled, is driven by an `is-selected` class that site.js toggles. The
   form already requires JS to submit, so that adds no new dependency. */
.demo__choice input:checked ~ .demo__tick {
  background: var(--copper);
  border-color: var(--copper);
}
.demo__choice input:checked ~ .demo__tick::after {
  opacity: 1;
  transform: rotate(45deg) scale(1);
}

.demo__choice-no {
  font-family: var(--font-label);
  font-size: 10px;
  letter-spacing: 0.18em;
  color: var(--d-muted);
}
.demo__choice-name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 1.02rem;
  color: var(--d-text);
}
/* Class, not `:has()` — see the note on the tick above. site.js keeps it in step
   with the checkbox on every change. */
.demo__choice.is-selected {
  border-color: var(--copper-dark);
  background: var(--panel-hi);
}
.demo__choice.is-selected .demo__choice-no { color: var(--copper-dark); }
/* The focus ring goes on the LABEL, because the input it belongs to is the
   invisible one above. Without this the group is keyboard-navigable but gives
   no sign of where you are.

   `:focus-within` rather than `:has(input:focus-visible)`: it is the purpose-
   built selector for exactly this and it predates `:has()` by years in every
   engine, so the same support argument as the selected state applies. The cost
   is that it also fires on a mouse click, where `:focus-visible` would not — on
   a card the size of a tap target that reads as feedback rather than noise. */
.demo__choice:focus-within {
  outline: 2px solid var(--copper-dark);
  outline-offset: 2px;
}

/* ---------- Fields ---------------------------------------------------------- */
.demo__grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s4); }
.demo__field { display: grid; gap: var(--s2); margin: 0; }
.demo__field--wide { grid-column: 1 / -1; }
@media (max-width: 560px) {
  .demo__grid { grid-template-columns: 1fr; }
  .demo__choices { grid-template-columns: 1fr; }
  .demo__choice { display: flex; align-items: baseline; gap: var(--s3); text-align: left; }
}
.demo__field label {
  font-family: var(--font-label);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--d-body);
}
.demo__opt {
  font-weight: 400;
  letter-spacing: 0.06em;
  text-transform: none;
  color: var(--d-muted);
}
.demo__field input,
.demo__field textarea {
  width: 100%;
  font-family: var(--font-sans);
  font-size: 1rem;
  color: var(--d-text);
  background: var(--panel);
  border: 1px solid var(--field-line);
  border-radius: var(--field-radius);
  padding: 12px 14px;
  transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease);
  /* A textarea is inline-block by default, which leaves a descender gap under
     it inside a grid cell, and it is resizable on both axes — dragging it wider
     breaks the column. Vertical only. */
  resize: vertical;
}
.demo__field textarea { min-height: 96px; }

/* ── THE FOCUS TREATMENT IS THE APP'S TOO ───────────────────────────────────
   The other half of why the app's boxes read differently, and it is not the
   radius. The app's `.input:focus` drops the outline entirely and uses the
   accent as the BORDER plus a soft 3px ring at 40% accent — so the field
   deepens and glows rather than gaining a second hard rectangle sitting a pixel
   outside the first. Copied here, 40% included.

   `outline: none` IS ONLY DEFENSIBLE BECAUSE SOMETHING REPLACES IT. Removing an
   outline and putting nothing back is the standard way to make a form unusable
   by keyboard. The forced-colors block is the case a box-shadow cannot cover:
   Windows High Contrast discards shadows entirely, so the outline returns there
   and that is what makes dropping it safe rather than merely pretty. */
.demo__field input:focus,
.demo__field textarea:focus {
  outline: none;
  border-color: var(--copper-dark);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--copper-dark) 40%, transparent);
}
@media (forced-colors: active) {
  .demo__field input:focus,
  .demo__field textarea:focus,
  .demo__choice:focus-within {
    outline: 2px solid Highlight;
    outline-offset: 1px;
  }
}
/* `:user-invalid`, not `:invalid`. `:invalid` matches an empty required field
   from the moment the page loads, so the form would greet everyone with four
   red boxes before they had typed anything. `:user-invalid` waits until the
   field has actually been interacted with and left wrong. */
.demo__field input:user-invalid,
.demo__field textarea:user-invalid { border-color: #e2795a; }

/* Off-screen, NOT hidden — see the note on the markup. `display: none` would
   defeat the purpose, because the point is that a bot fills it in and submits
   it. Height and overflow keep it from adding scroll length. */
.demo__hp {
  position: absolute;
  left: -9999px;
  width: 1px; height: 1px;
  overflow: hidden;
}

/* The button follows the fields. Leaving it on v4's 8px next to 5px inputs
   would read as a mistake rather than as a choice — and the app puts `.btn` on
   the same `--radius-md` as `.input`, so matching them here is what the app
   does, not just what looks tidy. */
.demo__actions .btn { border-radius: var(--field-radius); }

.demo__actions {
  margin-top: var(--s5);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s4);
}
.demo__status { font-size: 0.95rem; color: var(--d-body); margin: 0; }
.demo__status--ok { color: var(--copper-dark); }
.demo__status--bad { color: #e2795a; }
.demo__fine {
  margin-top: var(--s5);
  font-size: 0.86rem;
  color: var(--d-muted);
  max-width: 52ch;
}
.demo__fine a { color: var(--d-body); text-underline-offset: 3px; }
.demo__fine a:hover { color: var(--copper-dark); }

/* A submitted form is disabled while it is in flight, so the state has to read
   as "working", not as "broken". */
.demo__form.is-sending .btn { opacity: 0.6; pointer-events: none; }

/* The confirmation that replaces the form. It has to hold roughly the form's
   visual weight or the footer jumps up the screen the moment it is submitted. */
.demo__done {
  padding: var(--s6) var(--s5);
  background: var(--panel);
  border: 1px solid var(--copper-dark);
  border-radius: var(--field-radius);
  color: var(--d-body);
  line-height: 1.7;
}
.demo__done strong { color: var(--d-text); font-family: var(--font-display); }
.demo__status a { color: var(--copper-dark); text-underline-offset: 3px; }

/* ============================================================================
   THE OTHER PAGES — Closeout, Roster, Terms, Privacy and the 404
   ============================================================================
   Only the home page is dark. Jack, 2026-10-02: the product pages should not
   look like the home page, everything else should be much simpler, and
   nothing on these pages may look generated. So every other page is one white
   sheet between the same dark header and footer, built under the three rules
   the home page's sheet index was rebuilt under when Jack said it still looked
   AI (the long comment above .sindex in index.html says why):

     · NO BOX. Nothing is a rounded, bordered panel on a tinted fill. Content
       sits on the page and rules separate it, the way a drawing sheet does.
     · NO PILL. A status is a word of text — copper for the near one, grey for
       the far one — never a bordered capsule.
     · ONE UPPERCASE. Sheet numbers (RC-001, RC-002) are the only letter-spaced
       capitals. Every other label is set in sentence case.

   The structure is the decoration: a heavy ink rule opens a schedule and a
   hairline separates its rows, the same two weights a printed sheet uses.
   Copper is spent on the primary button, a near status, links, and the one
   row on the Roster screen that needs somebody to act. Nowhere else.

   IT WORKS BY REMAPPING THE DARK TOKENS INSIDE <main>, so anything written
   against --d-text, --d-body or --copper-dark reads correctly here without a
   second rule. The header and footer sit outside <main> and keep the dark
   values. The generator puts `is-light` on <body> of every page it writes.

   Measured on white / --paper: --ink 17.8 / 16.5, --body 8.9 / 8.3, muted
   #61646f 5.9 / 5.5, --copper-text 6.7 / 6.2, white on the --copper button
   4.96. ⛔ --copper-dark (#c96a3c) is 3.7:1 on white and must never be text on
   these pages, which is why it is remapped away rather than reused.
   ========================================================================== */

/* The current page's own nav link. Not a colour change alone — a rule under it,
   so it still reads as "you are here" for anyone who cannot separate the two
   greys. `aria-current="page"` carries it for everyone else. In the header
   row it is the hover underline held on, so the two are the same line in the
   same place; a border here would sit at the foot of the tap target instead,
   12px below the word. */
.nav__links > .nav__wide.is-current { color: var(--d-text); }
.nav__links > .nav__wide.is-current::after { transform: scaleX(1); }
.nav__drop a.is-current {
  color: var(--d-text);
  border-bottom: 1px solid var(--copper-dark);
}

body.is-light { background: var(--surface); }

.is-light main {
  --d-text: var(--ink);
  --d-body: var(--body);
  --d-muted: #61646f;
  --d-hairline: var(--hairline);
  --d-hairline-strong: var(--hairline-strong);
  --copper-dark: var(--copper-text);
  --copper-dark-hover: var(--copper-deep);
  /* The Closeout app's control radius, so a button here is the same shape as
     the one on the demo form and the one in the product. */
  --r-ctl: 5px;
  background: var(--surface);
  color: var(--d-body);
}

/* The header is fixed and transparent over the home page's black cover. Over a
   white page that would be white type on white, so here it is a solid band
   that takes up its own room (sticky, not fixed) and keeps one height —
   site.js still toggles is-stuck, and there is deliberately nothing for it to
   change. */
.is-light .nav,
.is-light .nav.is-stuck {
  position: sticky;
  background: var(--ink-deep);
  -webkit-backdrop-filter: none;
          backdrop-filter: none;
  border-bottom-color: var(--d-hairline);
  padding-block: var(--s4);
}

/* ---------- The head of a sheet --------------------------------------------- */
.sheethead { padding-block: clamp(56px, 8vw, 120px) clamp(64px, 8vw, 112px); }
.sheethead__grid {
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
  column-gap: clamp(40px, 6vw, 112px);
  align-items: start;
}

/* What a title block would say, in one line: sheet, name, state. Cells are
   divided by a hairline, as they are in a title block, rather than by dots. */
.sheetref {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  row-gap: 8px;
  margin-bottom: clamp(20px, 2.6vw, 32px);
  font-family: var(--font-label);
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1.2;
  color: var(--d-muted);
}
.sheetref > * + * {
  margin-left: 14px;
  padding-left: 14px;
  border-left: 1px solid var(--hairline-strong);
}
.sheetref__no {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink);
}
.sheetref a { color: inherit; text-decoration: none; }
.sheetref a:hover { color: var(--ink); }
.state--near { color: var(--copper-text); }
.state--far { color: var(--d-muted); }

.sheethead h1 {
  max-width: 15ch;
  font-size: clamp(2.6rem, 6vw, 4.6rem);
  line-height: 1.02;
  letter-spacing: -0.035em;
  text-wrap: balance;
}
.lede {
  max-width: 52ch;
  margin-top: clamp(20px, 2.4vw, 28px);
  font-size: clamp(1.06rem, 1.3vw, 1.2rem);
  line-height: 1.6;
  color: var(--body);
}
.lede + .lede { margin-top: 16px; }
/* The longhand on purpose: the `text-wrap` shorthand also resets the wrap MODE,
   which silently overrode white-space: nowrap on anything it matched (the
   "31 of 39 on site" count broke in two on a phone). */
.is-light main p,
.is-light main li,
.is-light main td,
.is-light main figcaption { text-wrap-style: pretty; }

.sheetactions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 18px 32px;
  margin-top: clamp(28px, 3.2vw, 40px);
}
.is-light main .btn { border-radius: var(--r-ctl); }
.is-light main .btn--lg { padding: 14px 26px; font-size: 1rem; }

/* The second way out is a sentence with an arrow, not a second button: two
   buttons side by side is the template, and only one of them is the point. */
.textlink {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  font-family: var(--font-label);
  font-size: 1rem;
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
  transition: color var(--dur) var(--ease);
}
.textlink::after {
  content: '';
  width: 15px;
  height: 10px;
  flex: none;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 15 10'%3E%3Cpath d='M0 5h13.5M9.5 1l4 4-4 4' fill='none' stroke='%23000' stroke-width='1.6'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 15 10'%3E%3Cpath d='M0 5h13.5M9.5 1l4 4-4 4' fill='none' stroke='%23000' stroke-width='1.6'/%3E%3C/svg%3E") center / contain no-repeat;
  transition: transform var(--dur) var(--ease);
}
.textlink:hover { color: var(--copper-text); }
.textlink:hover::after { transform: translateX(3px); }

/* The figure starts level with the headline, not with the reference line —
   the two columns share a top edge where the eye actually lands. */
.sheetfig { margin: 0; padding-top: clamp(0px, 3.6vw, 54px); }
.sheetfig figcaption {
  max-width: 48ch;
  margin-top: 14px;
  font-size: 0.875rem;
  line-height: 1.5;
  color: var(--d-muted);
}
@media (max-width: 960px) {
  .sheethead__grid { grid-template-columns: minmax(0, 1fr); row-gap: 64px; }
  .sheetfig { padding-top: 0; }
}

/* ---------- Schedules ---------------------------------------------------------
   The product, shown the way a drawing shows a schedule: a heavy rule to open
   it, a header row, hairlines between rows, an ink rule to close it. It is a
   real <table>, so a screen reader gets rows and columns, not a picture. */
.sched__top {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  padding-bottom: 14px;
  border-bottom: 2px solid var(--ink);
}
.sched__title {
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  color: var(--ink);
}
.sched__meta { margin-top: 3px; font-size: 0.875rem; color: var(--d-muted); }
.sched__count {
  font-size: 0.875rem;
  color: var(--d-muted);
  text-align: right;
  white-space: nowrap;
}
.sched__count b {
  display: block;
  font-family: var(--font-display);
  font-size: 2.1rem;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.03em;
  color: var(--ink);
}
.sched table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}
.sched th {
  padding: 10px 0;
  border-bottom: 1px solid var(--hairline-strong);
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--d-muted);
  text-align: left;
  white-space: nowrap;
}
.sched td {
  padding: 13px 0;
  border-bottom: 1px solid var(--hairline);
  color: var(--ink);
  vertical-align: baseline;
}
.sched tbody tr:last-child td { border-bottom: 1px solid var(--ink); }
.sched .sched__n {
  width: 3.4rem;
  padding-right: 18px;
  font-family: var(--font-label);
  font-weight: 600;
  text-align: right;
}
.sched__sub {
  display: block;
  margin-top: 2px;
  font-size: 0.8125rem;
  color: var(--d-muted);
}
.sched .sched__st {
  padding-left: 16px;
  font-family: var(--font-label);
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--d-muted);
  text-align: right;
  white-space: nowrap;
}
.sched .sched__st--short { color: var(--ink); font-weight: 600; }
.sched .sched__st--late { color: var(--copper-text); font-weight: 600; }

/* ---------- Sections: the label in the margin, the argument beside it -------
   An editorial split instead of a stack of centred bands. The margin label
   says what the section is; the headline beside it says what it claims. */
.sheetsec {
  border-top: 1px solid var(--hairline);
  padding-block: clamp(64px, 8vw, 112px);
}
.sheetsec__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2.4fr);
  column-gap: clamp(32px, 6vw, 112px);
}
.sheetsec__label {
  padding-top: 0.5em;
  font-family: var(--font-label);
  font-size: 0.9375rem;
  font-weight: 500;
  color: var(--d-muted);
}
.sheetsec__body { max-width: 62ch; }
@media (min-width: 961px) {
  .sheetsec__label { position: sticky; top: 104px; align-self: start; }
}
.sheetsec h2 {
  font-size: clamp(1.75rem, 3vw, 2.5rem);
  line-height: 1.1;
  letter-spacing: -0.03em;
  text-wrap: balance;
}
.sheetsec h2 + p { margin-top: 20px; }
.sheetsec p + p { margin-top: 16px; }
@media (max-width: 960px) {
  .sheetsec__grid { grid-template-columns: minmax(0, 1fr); row-gap: 14px; }
  .sheetsec__label { padding-top: 0; }
}

/* Numbered points, set as drawing notes: an ink rule to open the list, the
   mark in its own column, hairlines between. On Roster's "how it works" the
   marks are times of day rather than numbers, because that is the sequence. */
.sheetnotes {
  margin-top: clamp(32px, 4vw, 48px);
  border-top: 1px solid var(--ink);
}
.sheetnotes li {
  display: grid;
  grid-template-columns: 5rem minmax(0, 1fr);
  column-gap: 20px;
  padding-block: 24px;
  border-bottom: 1px solid var(--hairline);
}
.sheetnotes__mark {
  padding-top: 3px;
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--d-muted);
  font-variant-numeric: tabular-nums;
}
.sheetnotes h3 {
  font-size: 1.15rem;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.015em;
}
.sheetnotes h3 + p { margin-top: 8px; }
@media (max-width: 560px) {
  .sheetnotes li { grid-template-columns: minmax(0, 1fr); row-gap: 6px; }
}

/* ---------- Legal pages: /terms/ and /privacy/ --------------------------------
   A document to be read, not a page to be scrolled past: one column at a
   reading measure, the section numbers hanging in the margin so the text
   keeps one left edge, and a contents list that stays beside it on a wide
   screen. */
.legaldoc {
  border-top: 1px solid var(--hairline);
  padding-block: clamp(48px, 6vw, 88px) clamp(80px, 9vw, 136px);
}
.legaldoc__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2.4fr);
  column-gap: clamp(32px, 6vw, 112px);
  align-items: start;
}
/* On a phone it is simply not there: fourteen links above the first word
   would push the terms themselves off the screen, and a phone reader scrolls. */
.toc { position: sticky; top: 104px; }
.toc__head {
  padding-bottom: 12px;
  border-bottom: 1px solid var(--ink);
  font-family: var(--font-label);
  font-size: 0.9375rem;
  font-weight: 500;
  color: var(--ink);
}
.toc ol { margin-top: 8px; }
.toc a {
  display: grid;
  grid-template-columns: 1.9rem minmax(0, 1fr);
  padding-block: 5px;
  font-size: 0.875rem;
  line-height: 1.4;
  color: var(--d-muted);
  text-decoration: none;
  transition: color var(--dur) var(--ease);
}
.toc a span { font-family: var(--font-label); font-weight: 600; font-variant-numeric: tabular-nums; }
.toc a:hover,
.toc a[aria-current="true"] { color: var(--ink); }
.toc a[aria-current="true"] span { color: var(--copper-text); }
@media (max-width: 960px) {
  .legaldoc__grid { grid-template-columns: minmax(0, 1fr); }
  .toc { display: none; }
}

.legal__body { max-width: 66ch; }
.legal__body > * + * { margin-top: 16px; }
.legal__body h2 {
  margin-top: clamp(48px, 5vw, 64px);
  font-size: clamp(1.25rem, 1.8vw, 1.45rem);
  line-height: 1.25;
  letter-spacing: -0.02em;
  scroll-margin-top: 104px;
}
.legal__body h2:first-child { margin-top: 0; }
.legal__body h2 .no {
  display: inline-block;
  min-width: 2rem;
  font-family: var(--font-label);
  font-size: 0.9em;
  font-weight: 600;
  letter-spacing: 0;
  color: var(--d-muted);
}
@media (min-width: 961px) {
  .legal__body h2 { position: relative; }
  .legal__body h2 .no {
    position: absolute;
    right: calc(100% + 18px);
    top: 0.12em;
    min-width: 0;
    text-align: right;
  }
}
.legal__body h3 {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 12px;
  margin-top: 32px;
  font-size: 1.08rem;
  font-weight: 600;
  letter-spacing: -0.01em;
}
.legal__body h3 .state {
  font-family: var(--font-label);
  font-size: 0.875rem;
  font-weight: 500;
  letter-spacing: 0;
}
.legal__body strong { color: var(--ink); font-weight: 500; }
.legal__body a {
  color: var(--copper-text);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
.legal__body a:hover { color: var(--copper-deep); }
/* Hostnames and the ?c= tag. The brand retired monospace, so these are the
   label face — enough to read as a literal without a chip around it. */
.legal__body code {
  font-family: var(--font-label);
  font-size: 0.95em;
  font-weight: 500;
  color: var(--ink);
  overflow-wrap: anywhere;
}
.legal__list { display: grid; gap: 10px; }
.legal__list li { position: relative; padding-left: 26px; }
.legal__list li::before {
  content: '';
  position: absolute;
  left: 2px;
  top: 13px;
  width: 12px;
  height: 1px;
  background: var(--ink);
}
.legal__addr { color: var(--ink); line-height: 1.7; }

/* ---------- 404: the set, not three buttons -----------------------------------
   Somebody who reaches a page that does not exist is shown the sheet index,
   in the same vocabulary as the home page's: the cover and the two products. */
.setlist {
  max-width: 760px;
  margin-top: clamp(40px, 5vw, 64px);
  border-top: 2px solid var(--ink);
}
.setlist a {
  display: grid;
  grid-template-columns: 6.5rem minmax(0, 1fr) auto;
  align-items: baseline;
  column-gap: 20px;
  padding-block: 18px;
  border-bottom: 1px solid var(--hairline);
  color: var(--ink);
  text-decoration: none;
}
.setlist__no {
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--d-muted);
}
.setlist__name {
  font-family: var(--font-display);
  font-size: clamp(1.35rem, 2.4vw, 1.75rem);
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: -0.02em;
  transition: color var(--dur) var(--ease);
}
.setlist__what {
  font-family: var(--font-label);
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--d-muted);
  white-space: nowrap;
}
.setlist a:hover .setlist__name { color: var(--copper-text); }
@media (max-width: 560px) {
  .setlist a { grid-template-columns: 4.75rem minmax(0, 1fr); row-gap: 2px; }
  .setlist__what { grid-column: 2; white-space: normal; }
}
.sheetnote {
  max-width: 56ch;
  margin-top: 28px;
  font-size: 0.9375rem;
  color: var(--d-muted);
}
.nowrap { white-space: nowrap; }

/* ---------- Closeout: the work, shown (2026-10-04) ------------------------------
   Jack, four rounds in one day:
     1. Explain what the product actually does, never try to make it sound
        interesting, and "if something can't be done yet, say it's planned".
     2. "The text is only using a 1/3 of the page and makes scrolling feel
        infinite."
     3. Still "way too long for the average person to scroll through". Put the
        story in the first section; put everything else in a table of contents
        people click through "sort of like a slideshow", or jump straight to;
        animate it, make it pretty, make people want to click; and give the
        tables and lists more personality than "the black and white newspaper
        feel", with colour.
     4. Keep the slideshow and the animations, but: the squared graph-paper
        ground looked generated ("maybe just a white page works"); the story
        in its box read like "a review tab with a lot of junk in it"; the
        filled Next button was ugly ("just let it have that arrow [and] what
        it's going to"); and leaving for the home page's dark request band was
        "very harsh", so the request is a pop-up on this page.
     5. (2026-10-05, live) The story and the tour's introduction "kind of flow
        into each other": the same layout, on the same white, one after the
        other. So the tour sits on its own band, a shade off white, and the
        page reads white, band, white: the head and the story, the tour, the
        status.

   So: a white page. The head is the headline beside the seven windows Jack
   used to have open, with his story under them as plain text. The tour is
   one frame on a quiet band: the words on the left, and on the right a plate
   in the chapter's colour with the picture on it, so every chapter has the
   same shape and a short one never leaves a hole. Under the frame, a rail of
   the eight chapters that fills in as you go. Then the status, and the
   request form in a dialog.

   EVERYTHING IS IN THE HTML. Without closeout.js the chapters follow one
   another down the page and the rail is links to them; without ask.js,
   "Ask to see it" goes to the home page's form.

   The colour is never decoration: each program in the old way has its own,
   so the switching shows; each status has its own; each kind of document in
   the split has its own; each chapter has one, for its number, its plate and
   its place on the rail. Copper is still Ridgecap. A dashed outline means
   Planned, the way a drawing dashes in work that is not built yet.

   Still never: gradients, glass, emoji, rounded cards in rows of three,
   autoplay. Every animation answers a click or the tour arriving on screen,
   plays once, and is off under prefers-reduced-motion.

   Measured (palette.cjs and plates.cjs in the session notes): every text
   colour here is at least 4.5:1 on white, on its own wash, on every
   chapter's plate and on the tour's band, and white on every fill is at
   least 5. Copper itself is a fill and a rule only; copper TEXT is
   --c-copper-ink. */

.is-closeout {
  --frame: #e3dfd7;
  --frame-strong: #cdc8bf;
  --track: #e3ded5;
  --sheet: #ffffff;
  /* The tour's band: warm, like the frame's edge, and only just off white. */
  --stage: #f5f3ef;
  --card-edge: rgba(22, 24, 29, 0.12);
  --card-shadow: 0 1px 2px rgba(22, 24, 29, 0.05), 0 16px 30px -22px rgba(22, 24, 29, 0.5);

  /* Each colour: the colour, its wash (behind a row or a highlight) and its
     plate (behind a chapter's picture, half as strong as the wash). */
  --c-copper: #a85c2e;  --c-copper-ink: #8c4a23;  --c-copper-deep: #703a1b;
  --c-copper-wash: #f6e8dc;  --c-copper-plate: #fbf4ee;
  --c-green: #2a6c46;   --c-green-wash: #e1efe6;   --c-green-plate: #f0f7f2;
  --c-blue: #2d5f8f;    --c-blue-wash: #e2ebf4;    --c-blue-plate: #f0f5fa;
  --c-amber: #875705;   --c-amber-wash: #f7ebcf;   --c-amber-plate: #fbf5e7;
  --c-red: #b23a2f;     --c-red-wash: #f7e0dc;
  --c-plum: #7b4a8f;    --c-plum-wash: #eee3f2;    --c-plum-plate: #f7f1f9;
  --c-teal: #1e6b70;    --c-teal-wash: #daeeef;    --c-teal-plate: #edf7f7;
  --c-indigo: #4b56a5;  --c-indigo-wash: #e5e7f5;  --c-indigo-plate: #f2f3fa;
  --c-slate: #5f6672;   --c-slate-wash: #eaecef;   --c-slate-plate: #f5f5f4;
}

/* Who gets which colour. --tone fills and rules; --tone-ink is the same
   colour as text, which only copper needs to be different. */
.is-closeout [data-tone="slate"]  { --tone: var(--c-slate);  --tone-ink: var(--c-slate);  --tone-plate: var(--c-slate-plate); }
.is-closeout [data-tone="amber"]  { --tone: var(--c-amber);  --tone-ink: var(--c-amber);  --tone-plate: var(--c-amber-plate); }
.is-closeout [data-tone="green"]  { --tone: var(--c-green);  --tone-ink: var(--c-green);  --tone-plate: var(--c-green-plate); }
.is-closeout [data-tone="blue"]   { --tone: var(--c-blue);   --tone-ink: var(--c-blue);   --tone-plate: var(--c-blue-plate); }
.is-closeout [data-tone="teal"]   { --tone: var(--c-teal);   --tone-ink: var(--c-teal);   --tone-plate: var(--c-teal-plate); }
.is-closeout [data-tone="plum"]   { --tone: var(--c-plum);   --tone-ink: var(--c-plum);   --tone-plate: var(--c-plum-plate); }
.is-closeout [data-tone="indigo"] { --tone: var(--c-indigo); --tone-ink: var(--c-indigo); --tone-plate: var(--c-indigo-plate); }
.is-closeout [data-tone="copper"] { --tone: var(--c-copper); --tone-ink: var(--c-copper-ink); --tone-plate: var(--c-copper-plate); }

.is-closeout [data-app="excel"]    { --app: var(--c-green);  --app-wash: var(--c-green-wash); }
.is-closeout [data-app="procore"]  { --app: var(--c-amber);  --app-wash: var(--c-amber-wash); }
.is-closeout [data-app="outlook"]  { --app: var(--c-blue);   --app-wash: var(--c-blue-wash); }
.is-closeout [data-app="onedrive"] { --app: var(--c-teal);   --app-wash: var(--c-teal-wash); }
.is-closeout [data-app="bluebeam"] { --app: var(--c-plum);   --app-wash: var(--c-plum-wash); }
.is-closeout [data-app="other"]    { --app: var(--c-slate);  --app-wash: var(--c-slate-wash); }
.is-closeout [data-app="closeout"] { --app: var(--c-copper); --app-ink: var(--c-copper-ink); --app-wash: var(--c-copper-wash); }

.is-closeout [data-s="approved"]  { --s: var(--c-green); --s-wash: var(--c-green-wash); }
.is-closeout [data-s="received"]  { --s: var(--c-amber); --s-wash: var(--c-amber-wash); }
.is-closeout [data-s="requested"] { --s: var(--c-blue);  --s-wash: var(--c-blue-wash); }
.is-closeout [data-s="rejected"]  { --s: var(--c-red);   --s-wash: var(--c-red-wash); }
.is-closeout [data-s="missing"]   { --s: var(--c-slate); --s-wash: var(--c-slate-wash); }

.is-closeout [data-k="warranty"] { --k: var(--c-green); }
.is-closeout [data-k="lien"]     { --k: var(--c-plum); }
.is-closeout [data-k="om"]       { --k: var(--c-blue); }
.is-closeout [data-k="pd"]       { --k: var(--c-teal); }
.is-closeout [data-k="tab"]      { --k: var(--c-amber); }

/* Text for screen readers only. */
.vh {
  position: absolute !important;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
  border: 0;
}

/* Planned, the way a drawing marks work that is not built yet: a dashed
   line. Square, sentence case, never filled. */
.planned {
  display: inline-block;
  padding: 0 7px;
  border: 1px dashed var(--c-indigo);
  color: var(--c-indigo);
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.55;
  letter-spacing: 0;
  vertical-align: 0.08em;
  white-space: nowrap;
}

/* A screen from the product, on a chapter's plate: white, with the
   chapter's colour along its top edge. */
.card {
  background: var(--sheet);
  border: 1px solid var(--card-edge);
  border-top: 3px solid var(--tone);
  box-shadow: var(--card-shadow);
}

/* ---------- The head: the headline, and what it is talking about --------------- */
/* The head and the story are one section, and it ends with room of its own
   before the tour's band begins. The head alone fills the first screen
   (Jack: "that would be the entire screen when you first get on it"); the
   story follows it, or, as a scene, takes its place (see "The scroll scenes"
   below). */
.cohead { padding-bottom: clamp(56px, 6vw, 88px); }
.cohead__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: clamp(40px, 5.5vw, 88px);
  align-items: center;
  align-content: center;
  min-height: calc(100vh - 79px);
  min-height: calc(100svh - var(--nav-h, 79px));
  padding-block: clamp(28px, 4vw, 64px);
}
/* Sized so "Software for the" holds one line in a half-width column: four
   even lines, not five ragged ones. */
.cohead h1 {
  max-width: 14ch;
  font-size: clamp(2.6rem, 4.85vw, 3.875rem);
  line-height: 1.02;
  letter-spacing: -0.035em;
  text-wrap: balance;
}
@media (max-width: 960px) {
  .cohead__grid { grid-template-columns: minmax(0, 1fr); row-gap: 44px; }
  .cohead h1 { font-size: clamp(2.6rem, 7vw, 3.875rem); }
  .windows { max-width: 580px; }
}

/* "All of that open at once": the seven windows, cascaded the way Windows
   cascades them, each title bar the colour that program has in the old way
   below. Every name stays readable in the pile. */
.windows { margin: 0; }
.windows ol {
  --dx: 6.8%;
  --dy: 34px;
  --ww: 59%;
  --wh: 190px;
  position: relative;
  height: calc(6 * var(--dy) + var(--wh));
}
.windows li {
  position: absolute;
  left: calc(var(--i) * var(--dx));
  top: calc(var(--i) * var(--dy));
  width: var(--ww);
  height: var(--wh);
  overflow: hidden;
  background: var(--sheet);
  border: 1px solid rgba(22, 24, 29, 0.16);
  border-radius: 6px;
  box-shadow: 0 1px 2px rgba(22, 24, 29, 0.06), 0 18px 32px -24px rgba(22, 24, 29, 0.6);
  /* backwards, not both: once a window has landed, its place is the scroll's
     to move (scenes.js drives translate and rotate, which compose with this
     transform rather than fight it). */
  animation: win-in 480ms cubic-bezier(0.2, 0.7, 0.2, 1) backwards;
  animation-delay: calc(250ms + var(--i) * 140ms);
}
/* What is in a window does not matter, only that it is full. */
.windows li::after {
  content: '';
  position: absolute;
  inset: 42px 16px 14px;
  background: repeating-linear-gradient(transparent 0 13px, rgba(22, 24, 29, 0.075) 13px 14px);
}
.windows span {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  height: 28px;
  padding: 0 10px 0 12px;
  background: var(--app);
  color: #fff;
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 600;
  white-space: nowrap;
}
/* Minimise, maximise, close. */
.windows span::after {
  content: '';
  flex: none;
  width: 36px;
  height: 8px;
  background: rgba(255, 255, 255, 0.8);
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 36 8'%3E%3Cg fill='none' stroke='%23000' stroke-width='1.2'%3E%3Cpath d='M1 4.5h7M28.5.5l7 7M35.5.5l-7 7'/%3E%3Crect x='14.5' y='.5' width='7' height='7'/%3E%3C/g%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 36 8'%3E%3Cg fill='none' stroke='%23000' stroke-width='1.2'%3E%3Cpath d='M1 4.5h7M28.5.5l7 7M35.5.5l-7 7'/%3E%3Crect x='14.5' y='.5' width='7' height='7'/%3E%3C/g%3E%3C/svg%3E") center / contain no-repeat;
}
.windows figcaption {
  margin-top: 14px;
  font-size: 0.875rem;
  color: var(--d-muted);
}
@keyframes win-in {
  from { opacity: 0; transform: translate(-16px, -14px) scale(0.97); }
}
/* The words arrive with the windows: the sheet line, the headline, the lede. */
.cohead__copy > * { animation: co-in 760ms cubic-bezier(0.16, 1, 0.3, 1) backwards; }
.cohead__copy > :nth-child(1) { animation-delay: 60ms; }
.cohead__copy > :nth-child(2) { animation-delay: 150ms; }
.cohead__copy > :nth-child(3) { animation-delay: 260ms; }
@keyframes co-in { from { opacity: 0; transform: translateY(16px); } }

/* ---------- The story: plain text under the head, not a card -------------------
   Its two columns line up with the two above, so the words start where the
   windows do. */
.story {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: clamp(40px, 5.5vw, 88px);
  padding-top: clamp(28px, 3.2vw, 44px);
  border-top: 1px solid var(--hairline);
}
.story__kicker,
.tour__kicker {
  font-family: var(--font-label);
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--c-copper-ink);
}
.story h2 {
  margin-top: 10px;
  font-size: clamp(1.9rem, 3.4vw, 2.75rem);
  line-height: 1.06;
  letter-spacing: -0.03em;
  text-wrap: balance;
}
/* One line per sentence part, so each can rise on its own. */
.story h2 .line,
.story h2 .line > span { display: block; }
.story__by {
  display: flex;
  align-items: center;
  gap: 12px;
  margin-top: 20px;
  font-family: var(--font-label);
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--ink);
}
.story__by::before { content: ''; width: 28px; height: 2px; background: var(--c-copper); }
/* A shade bigger than body text: it is one person talking. */
.story__text { max-width: 56ch; padding-top: 34px; font-size: 1.125rem; line-height: 1.6; }
.story__text p + p { margin-top: 14px; }
@media (max-width: 960px) {
  .story { grid-template-columns: minmax(0, 1fr); row-gap: 18px; }
  .story__text { padding-top: 0; }
}

/* ---------- The scroll scenes (scenes.js) ---------------------------------------
   Jack, 2026-10-05: the head should be the whole first screen, loading in the
   windows, with a cue to scroll; scrolling should send the windows up and
   away in order and fade the headline, then fade "I Did Closeouts Eight
   Hours a Day." in, then bring you into the tour, which takes up the page.

   NOTHING HERE APPLIES WITHOUT THE SCRIPT. scenes.js adds one class to the
   head for each behaviour, and only when the visitor has not asked for
   reduced motion:
     .is-moving  the windows and the headline answer the scroll, through --p
                 (0 at the top, 1 when the scene is over). Every phone gets
                 at least this.
     .is-scene   the head pins and the story arrives over the same screen.
                 Only where both fit in one screen; scenes.js measures.
   Reveals (html.co-reveal) fade the story, the tour's introduction and the
   status in as each arrives, where the scene does not do it. The tour
   filling the screen is .is-filling on the tour, on a wide enough screen
   that the slide fits.

   The animated values are the individual transform properties, translate
   and rotate, so they compose with the windows' arrival animation (a
   transform) instead of fighting it. */

/* The cue at the foot of the first screen: the tour's own arrow in a ring,
   turned to point down, with the arrow dropping through it on a loop. It
   arrives once the windows have landed, and is gone at the first scroll.
   (Jack, 2026-10-05, on a hairline with a copper dash running down it:
   "polish the scroll animation".) */
.cue {
  position: absolute;
  left: 50%;
  bottom: clamp(16px, 3vh, 30px);
  translate: -50% 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  padding: 4px 12px 6px;
  border-radius: 12px;
  color: var(--d-muted);
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  line-height: 1;
  text-decoration: none;
  opacity: calc(1 - min(1, var(--p, 0) * 14));
  animation: cue-in 800ms cubic-bezier(0.16, 1, 0.3, 1) 1.5s backwards;
  transition: color 200ms;
}
.cue[hidden] { display: none; }
.cue__ring {
  position: relative;
  width: 40px;
  height: 40px;
  overflow: hidden;
  background: var(--sheet);
  border: 1px solid var(--frame-strong);
  border-radius: 50%;
  color: var(--c-copper);
  transition: border-color 200ms, background-color 200ms;
}
.cue__ring::after {
  content: '';
  position: absolute;
  inset: 0;
  width: 10px;
  height: 15px;
  margin: auto;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 15'%3E%3Cpath d='M5 0v13.5M1 9.5l4 4 4-4' fill='none' stroke='%23000' stroke-width='1.6'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 15'%3E%3Cpath d='M5 0v13.5M1 9.5l4 4 4-4' fill='none' stroke='%23000' stroke-width='1.6'/%3E%3C/svg%3E") center / contain no-repeat;
  animation: cue-flow 2.1s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
.cue:hover { color: var(--ink); }
.cue:hover .cue__ring { border-color: var(--c-copper); background: var(--c-copper-plate); }
@keyframes cue-in { from { opacity: 0; transform: translateY(10px); } }
/* In from above, a rest in the middle, out through the bottom. */
@keyframes cue-flow {
  0%   { transform: translateY(-165%); opacity: 0; }
  32%  { transform: translateY(0); opacity: 1; }
  64%  { transform: translateY(0); opacity: 1; }
  100% { transform: translateY(165%); opacity: 0; }
}
/* A short laptop screen has less room under the windows' caption. */
@media (max-height: 760px) {
  .cue { bottom: 10px; gap: 7px; }
  .cue__ring { width: 34px; height: 34px; }
}

/* The windows leave in the order they arrived, each lifting off a beat after
   the last, speeding up and shrinking a touch as it goes; the words fade up
   after them. */
.cohead.is-moving .windows li {
  --wp: clamp(0, (var(--p, 0) - (0.03 + var(--i) * 0.045)) / 0.24, 1);
  translate: 0 calc(var(--wp) * var(--wp) * var(--fly, 900px) * -1);
  rotate: calc(var(--wp) * -4deg);
  scale: calc(1 - var(--wp) * 0.05);
}
.cohead.is-moving .windows figcaption { opacity: calc(1 - min(1, var(--p, 0) * 9)); }
.cohead.is-moving .cohead__copy > * {
  --tp: clamp(0, (var(--p, 0) - var(--ts, 0.12)) / 0.2, 1);
  opacity: calc(1 - var(--tp));
  translate: 0 calc(var(--tp) * -48px);
}
.cohead.is-moving .cohead__copy > :nth-child(2) { --ts: 0.15; }
.cohead.is-moving .cohead__copy > :nth-child(3) { --ts: 0.18; }

/* The scene: the head pins under the header for the length of a runway, and
   the story arrives in the same place as the head leaves it. */
.cohead.is-scene { height: calc(var(--stage-h) + var(--runway)); padding: 0; }
.cohead.is-scene .cohead__stage {
  position: sticky;
  top: var(--nav-h, 79px);
  height: var(--stage-h);
  overflow: hidden;
}
.cohead.is-scene .cohead__inner { display: grid; height: 100%; }
.cohead.is-scene .cohead__grid,
.cohead.is-scene .story { grid-area: 1 / 1; align-self: center; }
/* Centred in the room above the cue, so the cue never sits on the caption. */
.cohead.is-scene .cohead__grid { min-height: 0; padding-block: 16px 76px; }
.cohead.is-scene .story { padding: 0; border: 0; }
/* Whichever layer is not showing gets out of the pointer's way. */
.cohead.is-scene:not(.is-story) .story,
.cohead.is-scene.is-story .cohead__grid { pointer-events: none; }
/* Each part of the story fades in, sharpening and settling as it comes, a
   beat after the one before; the headline's lines rise out of their own
   masks, and the rule before the name draws itself. --se is the eased
   progress. */
html .cohead.is-scene .story [data-in] {
  --sp: clamp(0, (var(--p, 0) - (0.4 + var(--d) * 0.045)) / 0.14, 1);
  --se: calc(1 - (1 - var(--sp)) * (1 - var(--sp)));
  /* And out again, once the scene has let go and the tour is coming up, so
     the story never sits half under the header (Jack: "the why it exists
     doesn't go away"). */
  --so: clamp(0, (var(--p, 0) - 1.02) / 0.32, 1);
  opacity: calc(var(--sp) * (1 - var(--so)));
  translate: 0 calc((1 - var(--se)) * 26px - var(--so) * 32px);
  filter: blur(calc((1 - var(--se)) * 5px + var(--so) * 2px));
  transition: none;
}
html .cohead.is-scene .story .line,
.co-reveal [data-reveal] .line {
  overflow: hidden;
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
  translate: none;
  filter: none;
}
html .cohead.is-scene .story .line > span { translate: 0 calc((1 - var(--se)) * 105%); transition: none; }
html .cohead.is-scene .story__by::before { transform-origin: left center; scale: var(--se) 1; }

/* Reveals: a part fades in, sharpening and settling, when its group comes
   into view, each a beat after the one before. Hidden only once scenes.js is
   running (html.co-reveal), and only until its group is seen. */
.co-reveal [data-reveal] [data-in] {
  opacity: 0;
  translate: 0 22px;
  filter: blur(4px);
  transition: opacity 700ms ease, translate 900ms cubic-bezier(0.16, 1, 0.3, 1), filter 700ms ease;
  transition-delay: calc(var(--d, 0) * 110ms);
}
.co-reveal [data-reveal].is-in [data-in] { opacity: 1; translate: none; filter: none; }
/* A headline line is its own mask: it stays put and its words rise in it. */
.co-reveal [data-reveal] .line[data-in] { translate: none; filter: none; }
.co-reveal [data-reveal] .line > span {
  translate: 0 105%;
  transition: translate 1000ms cubic-bezier(0.16, 1, 0.3, 1) calc(var(--d, 0) * 110ms);
}
.co-reveal [data-reveal].is-in .line > span { translate: none; }

/* ---------- The tour ----------------------------------------------------------- */
/* Its own band, so the story above and the tour's introduction, which share
   a layout, do not run into each other. */
.tour { padding-block: clamp(56px, 6vw, 92px); background: var(--stage); }
.tour__intro {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: clamp(40px, 5.5vw, 88px);
  align-items: end;
}
.tour__kicker { margin-bottom: 10px; }
.tour__intro h2 {
  font-size: clamp(1.9rem, 3.4vw, 2.75rem);
  line-height: 1.06;
  letter-spacing: -0.03em;
  text-wrap: balance;
}
.tour__intro > p { max-width: 50ch; }
@media (max-width: 960px) {
  .tour__intro { grid-template-columns: minmax(0, 1fr); row-gap: 14px; }
}

.tour__book { margin-top: clamp(28px, 3.6vw, 44px); scroll-margin-top: 88px; }
.tour__sheet {
  position: relative;
  overflow: hidden;
  background: var(--sheet);
  border: 1px solid var(--frame);
}
.tour__panels { touch-action: pan-y; }

/* One chapter: the words, and the plate with the picture on it. */
.tour__panel {
  display: grid;
  grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
  scroll-margin-top: 96px;
}
/* A chapter takes focus only when the address names it, and a ring around
   the whole frame then reads as a frame, not as focus. The rail carries it. */
.tour__panel:focus,
.tour__panel:focus-visible { outline: none; }
.tour__text { padding: clamp(26px, 3vw, 40px) clamp(22px, 3vw, 42px); }
.tour__fig {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: center;
  margin: 0;
  padding: clamp(22px, 2.8vw, 40px);
  background: var(--tone-plate);
}
/* A small laptop: the picture keeps the width its rows need, so a step in
   the old way stays on one line. */
@media (max-width: 1100px) and (min-width: 901px) {
  .tour__panel { grid-template-columns: minmax(0, 0.72fr) minmax(0, 1.28fr); }
}
@media (max-width: 900px) {
  .tour__panel { grid-template-columns: minmax(0, 1fr); }
  .tour__text { padding: 24px 20px 26px; }
  .tour__fig { padding: 22px 16px 24px; }
}
/* Without the script: one chapter after another, ruled apart. */
.tour__panel + .tour__panel { border-top: 1px solid var(--frame); }

.tour__ch {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 4px 12px;
  font-family: var(--font-label);
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--d-muted);
}
.tour__chno {
  font-family: var(--font-display);
  font-size: clamp(2.3rem, 3.4vw, 3rem);
  font-weight: 700;
  line-height: 0.9;
  letter-spacing: -0.04em;
  color: var(--tone);
}
.tour__panel h3 {
  margin-top: 14px;
  font-size: clamp(1.5rem, 2.3vw, 2rem);
  line-height: 1.1;
  letter-spacing: -0.028em;
  text-wrap: balance;
}
.tour__panel h3 + p { margin-top: 14px; }
/* A two-sentence headline balanced a sentence at a time: a forced break
   left "Go." alone on a third line. */
.tour__panel h3 .line { display: block; }
.tour__text > p + p { margin-top: 10px; }
.tour__later {
  margin-top: 16px !important;
  font-size: 0.9375rem;
  color: var(--d-muted);
}
.tour__later .planned { margin-left: 4px; }

.tour__fig > figcaption {
  margin-top: 12px;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--d-muted);
}
/* A picture of something not built yet is dashed in, with its label on the
   line, like a drawing. */
.tour__planned {
  position: relative;
  padding: 24px 18px 18px;
  border: 1.5px dashed rgba(75, 86, 165, 0.6);
}
.fig-tag {
  position: absolute;
  top: -12px;
  left: 14px;
  padding: 0 8px;
  background: var(--tone-plate);
  color: var(--c-indigo);
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 600;
  line-height: 1.6;
}

/* ---------- The slideshow, once closeout.js has set it up ------------------------
   On a wide screen every chapter shares one grid cell, so the frame keeps the
   height of the tallest and nothing under the pointer moves. On a phone a
   chapter that is not showing takes no room at all. */
.tour.is-live .tour__panels { display: grid; }
.tour.is-live .tour__panel {
  grid-area: 1 / 1;
  border: 0;
  visibility: hidden;
}
.tour.is-live .tour__panel.is-active,
.tour.is-live .tour__panel.is-leaving { visibility: visible; }
/* The old chapter lies on top and fades, so its plate melts into the new
   one's colour underneath; its words slide away. The new words slide in and
   the new picture fades up only once the old one has gone, so the two never
   read over each other. */
.tour.is-live .tour__panel.is-leaving { z-index: 1; animation: tour-gone 190ms ease-in both; pointer-events: none; }
.tour.is-live .tour__panel.is-leaving .tour__text { animation: tour-out 190ms ease-in both; }
.tour.is-live .tour__panel.is-active .tour__text { animation: tour-in 500ms var(--ease) 110ms both; }
.tour.is-live .tour__panel.is-active .tour__fig > * { animation: fig-in 360ms ease 150ms both; }
@keyframes tour-in { from { opacity: 0; transform: translateX(var(--from, 28px)); } }
@keyframes tour-out { to { opacity: 0; transform: translateX(var(--to, -28px)); } }
@keyframes tour-gone { to { opacity: 0; } }
@keyframes fig-in { from { opacity: 0; } }
/* Up to 900px a chapter is one column, words over picture, and the chapters
   differ in height by hundreds of pixels; sharing a cell there would leave a
   short chapter in a long empty frame. */
@media (max-width: 900px) {
  .tour.is-live .tour__panel:not(.is-active) { display: none; }
}

/* The slide takes up the screen (scenes.js adds .is-filling on a wide screen
   where it fits): the frame grows to the height the header leaves, less the
   rail, and the chapter stretches to fill it. As it arrives, scenes.js grows
   the whole book from a little under its size to its size. */
.tour.is-filling .tour__sheet {
  display: flex;
  flex-direction: column;
  min-height: var(--sheet-min);
}
.tour.is-filling .tour__panels { flex: 1 1 auto; }
/* Once it reaches its place under the header the book holds there for a
   stretch of scrolling, so the slide fills the screen wherever you stop in
   that stretch. The stretch is a spacer after it in the same block (--hold
   tall): a sticky box travels only inside its parent's content, so neither a
   margin on the book (it collapses away, and counts against the travel
   anyway) nor padding on the parent would do. This replaced a script that
   scrolled the page into place for you, which flicked. */
.tour.is-filling .tour__book {
  position: sticky;
  top: calc(var(--nav-h, 79px) + 8px);
  transform-origin: 50% 0;
}
.tour.is-filling > .shell::after {
  content: '';
  display: block;
  height: var(--hold, 0px);
}

/* The parts of a picture that animate, before it has been seen. */
.tour.is-armed .tour__panel.is-active [data-a] { opacity: 0; }

/* Each picture plays when its chapter arrives. --i orders the parts, --step
   spaces them, --d0 holds the first one back while the chapter slides in. */
.tour.is-live .is-playing [data-a] {
  animation-duration: 520ms;
  animation-timing-function: cubic-bezier(0.2, 0.7, 0.2, 1);
  animation-fill-mode: both;
  animation-delay: calc(var(--d0, 180ms) + var(--i, 0) * var(--step, 40ms));
}
.tour.is-live .is-playing [data-a="rise"]  { animation-name: a-rise; }
.tour.is-live .is-playing [data-a="slide"] { animation-name: a-slide; animation-duration: 420ms; }
.tour.is-live .is-playing [data-a="drop"]  { animation-name: a-drop; animation-duration: 640ms; }
.tour.is-live .is-playing [data-a="pop"]   { animation-name: a-pop; animation-duration: 420ms; }
.tour.is-live .is-playing [data-a="fade"]  { animation-name: a-fade; }
.tour.is-live .is-playing [data-a="grow"]  { animation-name: a-grow; animation-duration: 640ms; }
@keyframes a-rise  { from { opacity: 0; transform: translateY(12px); } }
@keyframes a-slide { from { opacity: 0; transform: translateX(-12px); } }
@keyframes a-drop  {
  0%   { opacity: 0; transform: translateY(-26px) rotate(-2.5deg); }
  65%  { opacity: 1; transform: translateY(2px) rotate(0.4deg); }
  100% { opacity: 1; transform: none; }
}
@keyframes a-pop {
  0%   { opacity: 0; transform: scale(0.8); }
  70%  { opacity: 1; transform: scale(1.06); }
  100% { opacity: 1; transform: none; }
}
@keyframes a-fade { from { opacity: 0; } }
@keyframes a-grow { from { transform: scaleX(0); } }

/* ---------- Turning the page: an arrow, and the chapter it goes to -------------- */
.tour__block {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  align-items: center;
  min-height: 66px;
  padding: 10px clamp(12px, 1.8vw, 20px);
  border-top: 1px solid var(--frame);
}
.tour__block[hidden] { display: none; }
.tour__prev,
.tour__next,
.tour__finish {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  min-height: 44px;
  padding: 2px 4px;
  background: none;
  border: 0;
  border-radius: 999px;
  color: var(--ink);
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  text-decoration: none;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.tour__prev { justify-self: start; color: var(--d-muted); }
.tour__prev[disabled] { visibility: hidden; }
.tour__next,
.tour__finish { justify-self: end; }
.tour__next[hidden],
.tour__finish[hidden] { display: none; }
.tour__arrow {
  position: relative;
  flex: none;
  width: 40px;
  height: 40px;
  border: 1px solid var(--frame-strong);
  border-radius: 50%;
  color: var(--ink);
  transition: transform 220ms var(--ease), border-color 220ms var(--ease);
}
.tour__arrow::after {
  content: '';
  position: absolute;
  inset: 0;
  width: 15px;
  height: 10px;
  margin: auto;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 15 10'%3E%3Cpath d='M0 5h13.5M9.5 1l4 4-4 4' fill='none' stroke='%23000' stroke-width='1.6'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 15 10'%3E%3Cpath d='M0 5h13.5M9.5 1l4 4-4 4' fill='none' stroke='%23000' stroke-width='1.6'/%3E%3C/svg%3E") center / contain no-repeat;
}
.tour__prev .tour__arrow::after { transform: scaleX(-1); }
/* Forward is the way most people are going: the circle is filled, in the
   colour of the chapter it leads to. */
.tour__next .tour__arrow,
.tour__finish .tour__arrow {
  background: var(--tone);
  border-color: var(--tone);
  color: #fff;
  transition: transform 220ms var(--ease), background-color 300ms, border-color 300ms;
}
.tour__prev:hover { color: var(--ink); }
.tour__prev:hover .tour__arrow { border-color: var(--ink); transform: translateX(-3px); }
.tour__next:hover .tour__arrow,
.tour__finish:hover .tour__arrow { transform: translateX(3px); }
.tour__count {
  padding-inline: 16px;
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--d-muted);
  text-align: center;
  white-space: nowrap;
}
.tour__count b { color: var(--tone-ink); font-weight: 600; }

/* ---------- The rail: every chapter, filling in as you go ------------------------
   A bar over each chapter's name: grey until you have seen it, its colour
   while you are on it, and a lighter shade of it afterwards. */
.tour__tabs { position: relative; margin-top: 12px; }
.tour__tabs ol { display: grid; grid-template-columns: repeat(8, minmax(0, 1fr)); gap: 6px; }
.tour__tabs li { min-width: 0; }
.tour__tabs a {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: 6px;
  padding: 13px 2px 8px;
  color: var(--d-muted);
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 500;
  line-height: 1.3;
  text-decoration: none;
  white-space: nowrap;
  transition: color 160ms;
}
.tour__tabs a::before,
.tour__tabs a::after {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 4px;
  border-radius: 2px;
}
.tour__tabs a::before { background: var(--track); transition: background-color 160ms; }
.tour__tabs a::after {
  background: var(--tone);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform 460ms var(--ease), opacity 260ms;
}
.tour__tabs a.is-seen::after { transform: none; opacity: 0.3; }
.tour__tabs a[aria-selected="true"]::after { transform: none; opacity: 1; }
.tour__tabs a[aria-selected="true"] { color: var(--ink); font-weight: 600; }
.tour__tabs a:hover { color: var(--ink); }
.tour__tabs a:hover::before { background: var(--frame-strong); }
.tour__no { flex: none; font-weight: 600; color: var(--tone-ink); font-variant-numeric: tabular-nums; }
.tour__name { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

/* On a phone or a tablet, where a chapter is taller than the screen, the rail
   moves above the frame, scrolls sideways, and stays under the header while
   you read. */
@media (max-width: 900px) {
  .tour__book { display: flex; flex-direction: column; }
  .tour__tabs {
    order: -1;
    position: sticky;
    top: var(--nav-h, 66px);
    z-index: 5;
    margin: 0 calc(-1 * var(--gutter, 24px)) 8px;
    padding: 10px var(--gutter, 24px) 2px;
    background: var(--stage);
    overflow-x: auto;
    scrollbar-width: none;
    transition: box-shadow 200ms;
  }
  .tour__tabs::-webkit-scrollbar { display: none; }
  .tour__tabs.is-stuck { box-shadow: 0 1px 0 var(--frame), 0 10px 18px -16px rgba(22, 24, 29, 0.4); }
  /* All eight across when they fit (a tablet), scrolling when they do not. */
  .tour__tabs ol { display: flex; width: max-content; min-width: 100%; gap: 6px; }
  .tour__tabs li { flex: 1 0 auto; }
  .tour__block { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); padding-inline: 12px; }
  .tour__count { display: none; }
}

/* ---------- 01 The old way ----------------------------------------------------- */
/* The count sits under the words, in the room the long picture leaves. Three
   columns at any width; the last label wraps inside its own. */
.oldway__stats {
  display: grid;
  grid-template-columns: auto auto minmax(0, 1fr);
  column-gap: clamp(20px, 2.4vw, 30px);
  align-items: start;
  margin-top: 24px !important;
  padding-top: 16px;
  border-top: 1px solid var(--frame);
}
.oldway__stats p {
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--d-muted);
}
.oldway__stats b {
  display: block;
  margin-bottom: 2px;
  font-family: var(--font-display);
  font-size: clamp(1.9rem, 2.8vw, 2.4rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--ink);
}
.oldway__cols {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px 16px;
}
.steps__head {
  margin-bottom: 8px;
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--ink);
}
.steps { display: grid; gap: 3px; }
.steps li {
  display: grid;
  grid-template-columns: 1.45rem minmax(0, 1fr) auto;
  align-items: center;
  column-gap: 6px;
  min-height: 27px;
  padding: 3px 8px;
  border-left: 4px solid var(--app);
  background: var(--app-wash);
  font-size: 0.875rem;
  line-height: 1.3;
  color: var(--ink);
}
.steps__n {
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--app-ink, var(--app));
  font-variant-numeric: tabular-nums;
}
/* The program is named only where it changes: every tag is a switch. */
.steps__app {
  font-family: var(--font-label);
  font-size: 0.6875rem;
  font-weight: 600;
  color: var(--app);
  white-space: nowrap;
}
.newway {
  margin-top: 18px;
  padding-top: 14px;
  border-top: 1px solid rgba(168, 92, 46, 0.4);
}
.newway__head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-bottom: 9px;
  font-family: var(--font-label);
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--c-copper-ink);
}
.newway__head b {
  font-family: var(--font-display);
  font-size: clamp(1.9rem, 2.8vw, 2.4rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--c-copper);
}
.steps--new { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
.steps--new li {
  grid-template-columns: 1.45rem minmax(0, 1fr);
  align-items: start;
  padding: 9px 10px 11px 8px;
}
.steps--new small {
  display: block;
  margin-top: 3px;
  font-size: 0.75rem;
  line-height: 1.35;
  color: var(--body);
}
@media (max-width: 560px) {
  .oldway__cols,
  .steps--new { grid-template-columns: minmax(0, 1fr); }
}

/* ---------- 02 Setup: three exports become a job ------------------------------ */
.import {
  display: grid;
  grid-template-columns: minmax(0, 0.72fr) 48px minmax(0, 1.5fr);
  align-items: center;
  column-gap: 12px;
}
.import__files { display: grid; gap: 10px; }
.file {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 12px;
  background: var(--sheet);
  border: 1px solid var(--card-edge);
  box-shadow: var(--card-shadow);
  font-family: var(--font-label);
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--ink);
}
.ext {
  flex: none;
  padding: 2px 5px;
  background: var(--c-green);
  color: #fff;
  font-family: var(--font-label);
  font-size: 0.625rem;
  font-weight: 700;
  letter-spacing: 0.04em;
  line-height: 1.4;
}
.ext--pdf { background: var(--c-red); }
.ext--jpg { background: var(--c-blue); }
.import__flow {
  position: relative;
  height: 2px;
  background: repeating-linear-gradient(90deg, var(--c-amber) 0 7px, transparent 7px 12px);
}
.import__flow::after {
  content: '';
  position: absolute;
  right: -2px;
  top: -4px;
  border: 5px solid transparent;
  border-left: 7px solid var(--c-amber);
  border-right: 0;
}
.tour.is-live .is-playing .import__flow { animation: flow 700ms linear 4; animation-delay: 700ms; }
@keyframes flow { from { background-position: -24px 0; } to { background-position: 0 0; } }
.import__job { padding: 14px 16px 6px; }
.import__title { font-family: var(--font-display); font-size: 1.02rem; font-weight: 600; color: var(--ink); }
.import__meta { margin-top: 2px; font-size: 0.8125rem; color: var(--d-muted); }
.import__meta b { color: var(--ink); font-weight: 600; }
.import__subs { margin-top: 10px; border-top: 1px solid var(--hairline); }
.import__subs li {
  display: grid;
  grid-template-columns: 2.1rem minmax(0, 1fr) auto;
  align-items: baseline;
  gap: 8px;
  padding: 8px 0;
  border-bottom: 1px solid var(--hairline);
  font-size: 0.875rem;
  color: var(--ink);
}
.import__subs li:last-child { border-bottom: 0; }
.import__div { font-family: var(--font-label); font-size: 0.75rem; font-weight: 700; color: var(--c-amber); }
.import__who { font-size: 0.75rem; color: var(--d-muted); white-space: nowrap; }
@media (max-width: 560px) {
  .import { grid-template-columns: minmax(0, 1fr); row-gap: 12px; }
  .import__files { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 6px; }
  .file { flex-direction: column; align-items: flex-start; gap: 6px; padding: 9px 8px; font-size: 0.75rem; }
  .import__flow {
    justify-self: center;
    width: 2px;
    height: 30px;
    background: repeating-linear-gradient(180deg, var(--c-amber) 0 7px, transparent 7px 12px);
  }
  .import__flow::after { right: auto; left: -4px; top: auto; bottom: -2px; border: 5px solid transparent; border-top: 7px solid var(--c-amber); border-bottom: 0; }
  .tour.is-live .is-playing .import__flow { animation-name: flow-down; }
  @keyframes flow-down { from { background-position: 0 -24px; } to { background-position: 0 0; } }
}

/* ---------- 03 The tracker ------------------------------------------------------ */
.tracker { padding: 16px 18px 4px; }
.tracker__top {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 16px;
  padding-bottom: 12px;
}
.tracker__name { font-family: var(--font-display); font-size: 1.12rem; font-weight: 600; color: var(--ink); }
.tracker__who { margin-top: 2px; font-size: 0.8125rem; color: var(--d-muted); }
.tracker__owed { font-size: 0.8125rem; color: var(--d-muted); text-align: right; white-space: nowrap; }
.tracker__owed b {
  display: block;
  font-family: var(--font-display);
  font-size: 2.2rem;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--ink);
}
/* The whole sub in one line: a segment per item, in its status colour. */
.tracker__bar { display: flex; gap: 3px; height: 8px; }
.tracker__bar span { flex: var(--n) 1 0; background: var(--s); border-radius: 1px; transform-origin: left center; }
.tracker__table {
  width: 100%;
  margin-top: 10px;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}
.tracker__table th {
  padding: 8px 8px;
  border-bottom: 1px solid var(--hairline-strong);
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--d-muted);
  text-align: left;
}
.tracker__table td {
  padding: 9px 8px;
  border-bottom: 1px solid var(--hairline);
  font-size: 0.9375rem;
  line-height: 1.3;
  color: var(--ink);
  vertical-align: middle;
}
.tracker__table tbody tr:last-child td { border-bottom: 0; }
.tracker__table td:first-child {
  width: 2.4rem;
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--d-muted);
}
.tracker__table td:last-child,
.tracker__table th:last-child { text-align: right; white-space: nowrap; }
.tracker__table small { display: block; margin-top: 2px; font-size: 0.75rem; color: var(--d-muted); }
/* The one row that is waiting on you. */
.tracker__table tr[data-s="received"] td { background: var(--c-amber-wash); }
.st {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--s);
}
.st::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--s); }

/* ---------- 04 The request ------------------------------------------------------ */
.request__tpl { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.request__tpl span {
  padding: 5px 10px;
  background: var(--sheet);
  border: 1px solid var(--card-edge);
  border-radius: 4px;
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--d-muted);
}
.request__tpl .is-on {
  background: var(--c-blue);
  border-color: var(--c-blue);
  color: #fff;
  font-weight: 600;
}
.letter__head { margin: 0; padding: 4px 16px 0; }
.letter__head > div {
  display: grid;
  grid-template-columns: 4.2rem minmax(0, 1fr);
  column-gap: 12px;
  padding-block: 6px;
  border-bottom: 1px solid var(--hairline);
  font-size: 0.875rem;
}
.letter__head dt {
  padding-top: 2px;
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 500;
  color: var(--d-muted);
}
.letter__head dd { margin: 0; color: var(--ink); }
.letter__body { padding: 12px 16px 14px; font-size: 0.9375rem; line-height: 1.45; color: var(--ink); }
.letter__body > * + * { margin-top: 9px; }
.letter__body ol { padding-left: 1.3em; list-style: decimal; }
.letter__link {
  color: var(--c-copper-ink);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}
/* What Closeout filled in, and what Sarah wrote, marked like a reviewer
   would: a highlighter stroke and a note on it. */
.hl {
  position: relative;
  margin-inline: -8px;
  padding: 8px 8px 8px 10px;
  border-left: 3px solid var(--hl);
  background: var(--hl-wash);
}
.hl--auto { --hl: var(--c-copper); --hl-wash: var(--c-copper-wash); }
.hl--you  { --hl: var(--c-blue);   --hl-wash: var(--c-blue-wash); }
.hl__tag {
  position: absolute;
  top: -9px;
  right: 8px;
  padding: 0 7px;
  background: var(--hl);
  color: #fff;
  font-family: var(--font-label);
  font-size: 0.6875rem;
  font-weight: 600;
  line-height: 1.6;
}
.tour.is-live .is-playing .hl { animation: hl-in 700ms var(--ease) both; animation-delay: calc(var(--d0, 180ms) + var(--i, 0) * var(--step, 40ms)); }
@keyframes hl-in { from { background-color: transparent; border-left-color: transparent; } }

/* ---------- 05 The sub's side: what the link opens ------------------------------ */
.portal__top { padding: 14px 16px 10px; }
.portal__job { font-size: 0.8125rem; color: var(--d-muted); }
.portal__sub { font-family: var(--font-display); font-size: 1.1rem; font-weight: 600; color: var(--ink); }
.portal__tabs {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 18px;
  padding: 0 16px;
  border-bottom: 1px solid var(--hairline);
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 500;
  color: var(--d-muted);
}
.portal__tabs span { padding: 8px 0 9px; }
.portal__tabs .is-on { color: var(--c-teal); font-weight: 600; box-shadow: inset 0 -2px 0 var(--c-teal); }
.portal__tabs b {
  display: inline-block;
  min-width: 1.5em;
  margin-left: 4px;
  padding: 0 4px;
  background: var(--c-teal);
  border-radius: 2px;
  color: #fff;
  font-size: 0.6875rem;
  line-height: 1.5;
  text-align: center;
}
.portal__list li {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--hairline);
  font-size: 0.9375rem;
  color: var(--ink);
}
.portal__list li:last-child { border-bottom: 0; }
.portal__list small { display: block; margin-top: 2px; font-size: 0.8125rem; color: var(--c-red); }
.portal__btn,
.portal__done {
  grid-column: 2;
  grid-row: 1;
  justify-self: end;
  padding: 6px 12px;
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 600;
  white-space: nowrap;
}
.portal__btn { background: var(--c-teal); color: #fff; border-radius: 4px; }
.portal__done { display: inline-flex; align-items: center; gap: 6px; color: var(--c-green); opacity: 0; }
.portal__done::before { content: ''; width: 8px; height: 8px; border-radius: 50%; background: var(--c-green); }
.portal__prog {
  position: absolute;
  left: 0;
  bottom: -1px;
  width: 100%;
  height: 3px;
  background: var(--c-teal);
  transform: scaleX(0);
  transform-origin: left center;
}
/* The first row uploads: the bar runs, the button goes, "Uploaded" stays.
   Without the script, or with motion reduced, the row simply waits to be
   uploaded, which is also true. */
.tour.is-live .is-playing .portal__prog {
  animation: up-run 900ms 1000ms ease-in-out forwards, gone 260ms 1950ms forwards;
}
.tour.is-live .is-playing .portal__first .portal__btn { animation: gone 240ms 1880ms forwards; }
.tour.is-live .is-playing .portal__done { animation: done-in 380ms 2080ms forwards; }
@keyframes up-run { to { transform: scaleX(1); } }
@keyframes gone { to { opacity: 0; visibility: hidden; } }
@keyframes done-in { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }

/* ---------- 06 The Vault: drop, suggest, approve, filed ----------------------- */
.vault__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  padding: 11px 16px;
  border-bottom: 1px solid var(--hairline);
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--c-plum);
}
.vault__head span { font-weight: 500; color: var(--d-muted); }
.vault__files li {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: 5px 12px;
  padding: 12px 16px;
  border-bottom: 1px solid var(--hairline);
}
.vault__files li:last-child { border-bottom: 0; }
.vault__file {
  display: flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--ink);
}
.vault__guess { grid-column: 1; font-size: 0.8125rem; color: var(--d-muted); }
.vault__guess b { font-weight: 600; color: var(--ink); }
.vault__ok {
  grid-column: 2;
  grid-row: 1 / span 2;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 11px;
  background: var(--c-green);
  border-radius: 4px;
  color: #fff;
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 600;
}
.vault__path {
  grid-column: 1 / -1;
  justify-self: start;
  margin-top: 3px;
  padding: 3px 8px;
  background: var(--c-plum-wash);
  font-family: var(--font-label);
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--c-plum);
}

/* ---------- 07 One combined PDF, split ------------------------------------------ */
.split__file {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 4px 10px;
  font-family: var(--font-label);
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--ink);
}
.split__pages { margin-left: auto; font-size: 0.8125rem; font-weight: 500; color: var(--d-muted); }
/* The file to scale: a segment per document, its width its page count. */
.split__bar { display: flex; gap: 3px; height: 40px; margin-top: 12px; }
.split__bar span { flex: var(--p) 1 0; min-width: 4px; background: var(--k); transform-origin: bottom center; }
.tour.is-live .is-playing .split__bar { animation: bar-split 760ms 380ms var(--ease) both; }
@keyframes bar-split { from { gap: 0; } }
.tour.is-live .is-playing .split__bar span { animation: bar-up 460ms var(--ease) both; animation-delay: calc(var(--i) * 45ms); }
@keyframes bar-up { from { transform: scaleY(0.15); opacity: 0.4; } }
.split__list {
  display: grid;
  gap: 6px;
  margin-top: 16px;
}
.split__list li {
  display: grid;
  grid-template-columns: 10px 4.6rem minmax(0, 1fr);
  align-items: center;
  gap: 8px;
  font-size: 0.8125rem;
  line-height: 1.3;
  color: var(--ink);
}
.split__list li::before { content: ''; width: 10px; height: 10px; background: var(--k); }
.split__pp {
  font-family: var(--font-label);
  font-weight: 600;
  color: var(--d-muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.split__count {
  display: flex;
  align-items: baseline;
  gap: 10px;
  margin-top: 14px;
  padding-top: 10px;
  border-top: 1px solid rgba(75, 86, 165, 0.25);
  font-family: var(--font-label);
  font-size: 0.875rem;
  font-weight: 500;
  color: var(--d-muted);
}
.split__count b {
  font-family: var(--font-display);
  font-size: 2rem;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--c-indigo);
}

/* ---------- 08 Handover: the manual's contents page ------------------------------ */
.manual { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: start; }
.manual__tab {
  margin-top: 26px;
  padding: 14px 7px;
  background: var(--c-copper);
  color: #fff;
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 700;
  line-height: 1;
  writing-mode: vertical-rl;
  border-radius: 6px 0 0 6px;
}
.manual__page { padding: 16px 18px 10px; }
.manual__title { font-family: var(--font-display); font-size: 1.02rem; font-weight: 600; color: var(--ink); }
.manual__meta { margin-top: 2px; font-size: 0.8125rem; color: var(--d-muted); }
.manual__toc { margin-top: 10px; }
/* A line that wraps keeps its dots and page number on its last line, as a
   printed contents page does. */
.manual__toc li {
  display: grid;
  grid-template-columns: auto minmax(16px, 1fr) auto;
  align-items: baseline;
  align-items: last baseline;
  gap: 8px;
  padding: 6px 0;
  font-size: 0.9rem;
  color: var(--ink);
}
.manual__toc i { border-bottom: 2px dotted rgba(22, 24, 29, 0.25); transform: translateY(-3px); }
.manual__toc b {
  font-family: var(--font-label);
  font-size: 0.875rem;
  font-weight: 600;
  color: var(--c-copper-ink);
  text-decoration: underline;
  text-underline-offset: 3px;
  font-variant-numeric: tabular-nums;
}
/* The owner's bookmark. */
.manual__toc .is-marked span::before {
  content: '';
  display: inline-block;
  width: 9px;
  height: 13px;
  margin-right: 8px;
  background: var(--c-copper);
  clip-path: polygon(0 0, 100% 0, 100% 100%, 50% 72%, 0 100%);
  vertical-align: -1px;
}
.tour.is-live .is-playing .manual__toc .is-marked span::before { animation: a-drop 600ms 1200ms both; }

/* ---------- The status band --------------------------------------------------
   White again after the tour's band. It was a copper tint, and two tints side
   by side only muddied the edge between them. A whole screen of its own, like
   the head and the tour (Jack, 2026-10-05: "the in development area should
   also take up some space"), with its words in the middle of it and set as
   large as the headline's. */
.coend {
  display: flex;
  align-items: center;
  min-height: calc(100vh - 79px);
  min-height: calc(100svh - var(--nav-h, 79px));
  padding-block: clamp(56px, 6.5vw, 96px);
  background: var(--sheet);
}
.coend > .shell { width: 100%; }
.coend__grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  column-gap: clamp(40px, 5.5vw, 88px);
  align-items: start;
}
.coend__label {
  margin-bottom: 10px;
  font-family: var(--font-label);
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--c-copper-deep);
}
.coend h2 {
  font-size: clamp(2.4rem, 4.85vw, 3.875rem);
  line-height: 1.02;
  letter-spacing: -0.035em;
}
.coend__text { max-width: 56ch; padding-top: 34px; font-size: 1.125rem; line-height: 1.6; }
.coend__text p + p { margin-top: 14px; }
@media (max-width: 960px) {
  .coend__grid { grid-template-columns: minmax(0, 1fr); }
  .coend__text { padding-top: 16px; }
}

/* ---------- The request form, on this page ---------------------------------------
   A dialog over the page rather than a trip to the home page's dark band. The
   fields, the honeypot and the endpoint are the home page's; ask.js has the
   behaviour. The page behind is dimmed, never blurred.

   ON A PHONE IT IS THE WHOLE SCREEN, slid up from the bottom the way a
   phone's own sheets are, not a card floating over a dimmed page (Jack,
   2026-10-05: "change it specifically for the mobile version"). The bar with
   the way out stays at the top, Send stays at the bottom, and the fields
   scroll between them. On a small phone that is the only way Send is ever on
   screen together with the fields: at 375x667 it used to sit below the fold,
   and the close button scrolled away the moment you went looking for it. */
.ask {
  width: min(600px, calc(100% - 32px));
  max-width: none;
  max-height: calc(100% - 32px);
  margin: auto;
  padding: 0;
  overflow: auto;
  overscroll-behavior: contain;
  background: var(--sheet);
  color: var(--body);
  border: 0;
  border-radius: 10px;
  box-shadow: 0 2px 6px rgba(16, 17, 20, 0.08), 0 40px 90px -36px rgba(16, 17, 20, 0.7);
}
.ask::backdrop { background: rgba(22, 24, 29, 0.46); }
html:has(.ask[open]) { overflow: hidden; scrollbar-gutter: stable; }
/* The sheet number and the way out, pinned while the form scrolls. */
.ask__bar {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 18px 16px 0 clamp(24px, 4vw, 40px);
  background: var(--sheet);
  border-top: 4px solid var(--c-copper);
}
/* Doubled up to outrank the phones' .sheetref margin further down the file. */
.ask .ask__ref { margin: 0; }
.ask__body { padding: 10px clamp(24px, 4vw, 40px) clamp(24px, 4vw, 36px); }
.ask h2 {
  max-width: 16ch;
  font-size: clamp(1.6rem, 3.2vw, 2.1rem);
  line-height: 1.08;
  letter-spacing: -0.03em;
}
.ask__lede {
  max-width: 50ch;
  margin-top: 10px;
  font-size: 0.9375rem;
  line-height: 1.55;
  color: var(--body);
}
.ask__close {
  position: relative;
  flex: none;
  width: 40px;
  height: 40px;
  background: none;
  border: 0;
  border-radius: 50%;
  color: var(--d-muted);
  cursor: pointer;
  transition: background-color 160ms, color 160ms;
}
.ask__close::before {
  content: '';
  position: absolute;
  inset: 0;
  width: 14px;
  height: 14px;
  margin: auto;
  background: currentColor;
  -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M1 1l12 12M13 1 1 13' fill='none' stroke='%23000' stroke-width='1.8'/%3E%3C/svg%3E") center / contain no-repeat;
          mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 14 14'%3E%3Cpath d='M1 1l12 12M13 1 1 13' fill='none' stroke='%23000' stroke-width='1.8'/%3E%3C/svg%3E") center / contain no-repeat;
}
.ask__close:hover { background: #f2f0ec; color: var(--ink); }
.ask__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 14px 16px;
  margin-top: 24px;
}
.ask__field--wide { grid-column: 1 / -1; }
.ask__field label {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 8px;
  margin-bottom: 6px;
  font-family: var(--font-label);
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--ink);
}
.ask__opt { font-weight: 500; color: var(--d-muted); }
.ask__field input,
.ask__field textarea {
  display: block;
  width: 100%;
  padding: 11px 12px;
  background: var(--sheet);
  border: 1px solid var(--hairline-strong);
  border-radius: 5px;
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: 1rem;
  line-height: 1.4;
  transition: border-color 160ms, box-shadow 160ms;
}
.ask__field textarea { min-height: 92px; resize: vertical; }
.ask__field input:hover,
.ask__field textarea:hover { border-color: #b8b9c1; }
.ask__field input:focus,
.ask__field textarea:focus {
  outline: none;
  border-color: var(--c-copper);
  box-shadow: 0 0 0 3px rgba(168, 92, 46, 0.2);
}
/* Marked only after a send that failed on them, never while still typing. */
.ask__form.was-checked :is(input, textarea):invalid { border-color: var(--c-red); }
/* The honeypot: posted, but out of sight and out of reach. */
.ask__hp { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; }
.ask__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 18px;
  margin-top: 24px;
}
.ask__form.is-sending [type="submit"] { opacity: 0.7; cursor: progress; }
.ask__status { flex: 1 1 200px; font-size: 0.875rem; line-height: 1.45; color: var(--d-muted); }
.ask__status--bad { color: var(--c-red); }
.ask__status a { color: var(--c-copper-ink); font-weight: 600; }
.ask__fine {
  margin-top: 20px;
  padding-top: 14px;
  border-top: 1px solid var(--hairline);
  font-size: 0.8125rem;
  color: var(--d-muted);
}
.ask__fine a { color: inherit; }
.ask__done:focus { outline: none; }
.ask__done .ask__lede b { color: var(--ink); font-weight: 600; overflow-wrap: anywhere; }
@keyframes ask-in { from { opacity: 0; transform: translateY(16px) scale(0.985); } }
@keyframes ask-out { to { opacity: 0; transform: translateY(8px) scale(0.99); } }
@keyframes ask-up { from { opacity: 0; transform: translateY(56px); } }
@keyframes ask-down { to { opacity: 0; transform: translateY(56px); } }
@keyframes ask-dim { from { opacity: 0; } }
@keyframes ask-undim { to { opacity: 0; } }
.ask[open] { animation: ask-in 300ms cubic-bezier(0.2, 0.7, 0.2, 1) both; }
.ask[open]::backdrop { animation: ask-dim 300ms ease both; }
.ask.is-closing { animation: ask-out 170ms ease-in both; }
.ask.is-closing::backdrop { animation: ask-undim 170ms ease-in both; }

@media (max-width: 560px) {
  .ask {
    inset: 0;
    width: 100%;
    height: 100%;
    height: 100dvh;
    max-height: none;
    margin: 0;
    border-radius: 0;
    box-shadow: none;
  }
  .ask[open] { animation-name: ask-up; }
  .ask.is-closing { animation-name: ask-down; }
  .ask__bar {
    padding: 10px 8px 10px 20px;
    border-bottom: 1px solid var(--hairline);
  }
  .ask__body { padding: 22px 20px 0; }
  .ask h2 { font-size: 1.7rem; }
  .ask__grid { grid-template-columns: minmax(0, 1fr); gap: 14px; margin-top: 22px; }
  .ask__field textarea { min-height: 84px; }
  /* Send, pinned to the bottom of the screen, with its message over it. */
  .ask__actions {
    position: sticky;
    bottom: 0;
    z-index: 1;
    flex-direction: column-reverse;
    align-items: stretch;
    gap: 8px;
    margin: 22px -20px 0;
    padding: 12px 20px calc(12px + env(safe-area-inset-bottom, 0px));
    background: var(--sheet);
    border-top: 1px solid var(--hairline);
  }
  .ask__actions .btn { width: 100%; }
  .ask__status { flex: none; }
  .ask__status:empty { display: none; }
  .ask__fine { margin-top: 0; padding: 14px 0 calc(22px + env(safe-area-inset-bottom, 0px)); border-top: 0; }
  .ask__done .ask__actions { position: static; margin-inline: 0; padding-inline: 0; border-top: 0; }
}

/* ---------- Phones -------------------------------------------------------------
   The same bigger titles as the other pages (2026-10-03), and a frame that
   goes nearly to the edge, so the pictures get the width. */
@media (max-width: 560px) {
  .cohead h1 { font-size: clamp(2.6rem, 11.6vw, 3rem); }
  .windows ol { --dx: 5.6%; --dy: 25px; --ww: 66%; --wh: 128px; }
  .windows span { height: 24px; padding: 0 8px 0 9px; font-size: 0.6875rem; }
  .windows span::after { width: 28px; }
  .windows li::after { inset: 34px 12px 10px; }
  .story h2 { font-size: clamp(2rem, 8.6vw, 2.4rem); }
  .tour > .shell { --gutter: 12px; padding-inline: 12px; }
  .tour__intro { padding-inline: 12px; }
  .tour__tabs li { flex: none; width: 112px; }
  .tour__intro h2 { font-size: clamp(2rem, 8.6vw, 2.4rem); }
  .tour__panel h3 { font-size: clamp(1.6rem, 7vw, 1.9rem); }
  .tour__text { padding: 22px 18px 24px; }
  .tour__fig { padding: 18px 12px 20px; }
  .tour__planned { padding: 22px 12px 14px; }
  .split__pages { flex-basis: 100%; margin-left: 0; }
  /* The Vault's rows stack: name, guess, Approve, folder. */
  .vault__head { flex-direction: column; gap: 2px; }
  .vault__files li { grid-template-columns: minmax(0, 1fr); }
  .vault__ok { grid-column: 1; grid-row: auto; justify-self: start; margin-top: 4px; }
  /* The manual's divider tab is decoration; on a phone the contents page
     needs its width, so every line fits on one. */
  .manual { grid-template-columns: minmax(0, 1fr); }
  .manual__tab { display: none; }
  .manual__page { padding: 14px 14px 8px; }
  .manual__toc li { font-size: 0.875rem; }
  .coend h2 { font-size: clamp(2.2rem, 10vw, 2.7rem); }
}

/* Printed, the tour is every chapter, as it is without the script. */
@media print {
  .tour.is-live .tour__panel { visibility: visible; display: grid; grid-area: auto; animation: none; }
  .tour.is-live .tour__panels { display: block; }
  .tour__tabs, .tour__block, .ask, .cue { display: none !important; }
  /* And the scenes are a page again: everything where it lands, nothing
     pinned, nothing half-faded. */
  .cohead.is-scene { height: auto; padding-bottom: 56px; }
  .cohead.is-scene .cohead__stage { position: static; height: auto; overflow: visible; }
  .cohead.is-scene .cohead__inner { display: block; }
  .cohead.is-scene .story { padding-top: 32px; border-top: 1px solid var(--hairline); }
  .cohead__grid { min-height: 0; }
  .cohead .windows li,
  .cohead__copy > *,
  [data-in],
  .line > span {
    opacity: 1 !important;
    translate: none !important;
    rotate: none !important;
    filter: none !important;
  }
  .tour__book { position: static !important; margin-bottom: 0 !important; scale: none !important; translate: none !important; }
  .coend { min-height: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .is-closeout main *,
  .is-closeout main *::before,
  .is-closeout main *::after,
  .ask::backdrop {
    animation: none !important;
    transition: none !important;
  }
  .tour.is-armed .tour__panel.is-active [data-a] { opacity: 1; }
  /* With no exit to play, a leaving chapter would only sit over the new one. */
  .tour.is-live .tour__panel.is-leaving { visibility: hidden; }
}

/* ---------- On a phone (2026-10-03) ------------------------------------------
   "There's a lot of space at the top. I would make the titles bigger and take
   up more space." Measured at 375: the head band opened with 56px of white
   under the header before the first word, and section titles were 28px. The
   top padding halves, the sections tighten, and every title steps up. */
@media (max-width: 560px) {
  .sheethead { padding-block: 28px 56px; }
  .sheetref { margin-bottom: 16px; }
  .sheethead h1 { font-size: clamp(2.6rem, 11.6vw, 3rem); }
  .sheetsec { padding-block: 52px; }
  .sheetsec h2 { font-size: clamp(2rem, 8.6vw, 2.4rem); }
  .sheetnotes h3 { font-size: 1.3rem; }
  .legal__body h2 { font-size: 1.55rem; }
  .legaldoc { padding-top: 40px; }
}

/* Terms and Privacy get printed, and signed against. Print the document, not
   the website around it. */
@media print {
  .is-light .nav,
  .is-light .foot,
  .is-light .skip,
  .toc { display: none !important; }
  .legaldoc__grid { display: block; }
  .is-light main { color: #000; }
}

/* ---------- Footer ---------------------------------------------------------- */
.foot {
  background: var(--ink-deep);
  border-top: 1px solid var(--d-hairline);
  padding-block: var(--s8);
}
.foot__inner { display: grid; gap: var(--s5); justify-items: center; text-align: center; }
.foot__links {
  display: flex;
  /* The old site's footer had no flex-wrap and overflowed a 375px viewport by
     84px. This one wraps. */
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--s3) var(--s6);
}
/* 8px of block padding takes these from 25px to ~41px tall. Not the full 44px
   — that would force the six links to wrap to three rows on a phone — but it
   closes most of the gap on the links a superintendent actually taps. */
.foot__links a { color: var(--d-body); text-decoration: none; font-size: 0.95rem; padding-block: 8px; }
.foot__links a:hover { color: var(--copper-dark); }
.foot__legal { color: var(--d-muted); font-size: 0.85rem; }

/* ============================================================================
   THE PRE-ANIMATION STATE
   ============================================================================
   EVERYTHING PARKED AT OPACITY 0 IS IN THIS ONE BLOCK, and nowhere else in the
   file. That is the whole safety property: there is exactly one class to remove
   to get a complete page back, the motion module removes it on any thrown
   error, and a 3.2s timeout removes it even if nothing throws.

   `html.seq` is set by an inline script in <head>, synchronously, before first
   paint — so the finished mark never flashes for a frame before it builds.
   It is not set at all under prefers-reduced-motion, which means a
   reduced-motion visitor gets the finished page immediately and no branch of
   the motion code has to remember to reveal anything.
   ========================================================================== */
/* `.sindex > *` USED TO BE IN THIS LIST AND IS DELIBERATELY NOT ANY MORE.
   Everything else here belongs to the cover, which the 3.2s build fail-safe
   covers. The product rows do not: they were raised by their own ScrollTrigger,
   and that fail-safe watches the BUILD — once the build completes it stands
   down and leaves `seq` in place, so a build that finished correctly and a
   trigger that then measured wrong left the page's only below-the-fold content
   invisible, with nothing logged. That is what Jack saw on an iPhone.
   They are parked in JS now, immediately before the thing that reveals them, so
   the CSS default is the finished page — which is this file's whole rule. */
html.seq .rcbuild__fill path,
html.seq .matchline,
html.seq .grid,
html.seq .nav,
html.seq .cover__copy > * { opacity: 0; }

/* THE HEADER'S LOGO IS NOT PARKED SEPARATELY, and that is a decision rather
   than an omission. It sits inside `.nav`, which IS parked above, so it arrives
   with the header at progress 0.22 and inherits its opacity — no second rule to
   keep in step with the sequence.

   Two revisions did park it separately, to stop a second smaller RIDGECAP being
   on screen while the big one was still in the middle. Jack has seen both and
   chosen the duplication over either fix; the reasoning is written out in full
   in site.js, at the point where the code used to be. */

/* The wordmark's ghost value is its pre-animation value too — it is BEHIND the
   emblem the whole time, and starting it at 0 would make the handover a fade-in
   rather than a resolve. */
html.seq .cover__word { opacity: 0; }

/* The dome starts unlit and is raised by the scrubbed tween. */
html.seq .dome__glow { opacity: 0; }

/* The stroke layer that used to need its own entry here is gone in v4. It was
   drawn by dash-offset rather than opacity and had to be set up in JS, where
   the path lengths are known — one more thing parked invisible waiting on a
   timeline, which is the exact shape of the two bugs this file is built around.
   The build drops the fills instead, and the fills are already covered by
   `html.seq .rcbuild__fill path` above. */

/* ---------- Reduced motion --------------------------------------------------
   Belt and braces. `html.seq` is never added under this preference, so these
   rules should be unreachable — but if anything ever adds it programmatically,
   this is the assertion that a reduced-motion visitor still sees a page.     */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 1ms !important;
    transition-duration: 1ms !important;
  }
  html.seq .rcbuild__fill path,
  html.seq .matchline,
  html.seq .grid,
  html.seq .nav,
  html.seq .cover__copy > *,
  html.seq .dome__glow { opacity: 1; }
  html.seq .cover__word { opacity: 0.96; }
  html.seq .rcbuild { opacity: 0; }
  /* `.sindex > *` is not listed because it is no longer parked by CSS at all —
     see the note on the `html.seq` block above. Leaving it here would imply a
     parked state that does not exist, and this block's whole job is to be an
     accurate mirror of that one. Under reduced motion the motion module bails
     before it reaches the code that parks the rows, so they are never hidden in
     the first place. */
}
