/* ============================================================
   ASKDOCS — LAYOUT & SECTIONS
   The 12-column grid, the asymmetric split, section rhythm,
   and the sets of three / four / five.
   ============================================================ */


/* ---- Masthead ---------------------------------------------
   Chrome frames, it never competes. */

/* No background, no border: the chrome is the mark and the pill, floating
   over the page. A paper bar here read as a white strip across the top of
   the hero's plate — the photograph runs to the viewport's top edge and
   the mark sits on it. */
.site-head {
  position: sticky;
  top: 0;
  z-index: 20;
  background: transparent;
  pointer-events: none;
}
/* 44px is the tap-target floor every platform guideline names, and the
   lockup is 42px tall. Two pixels of min-height cost nothing and put the
   one link in the masthead over the line. */
.site-head .mark {
  pointer-events: auto;
  min-height: 44px;
  align-items: center;
}

.site-head__inner {
  display: flex;
  align-items: center;
  gap: var(--gap-lockup);
  flex-wrap: wrap;
  padding: 26px var(--gutter);
}

.nav {
  display: flex;
  flex-wrap: nowrap;
  font-weight: 600;
  font-size: var(--fs-nav);
  letter-spacing: var(--ls-nav);
  text-transform: uppercase;
  color: var(--ink-soft);
}
.nav a { color: var(--ink-soft); white-space: nowrap; }
.nav a:hover { color: var(--red); }


/* ---- Section rhythm ----------------------------------------
   Every section is separated by a full-bleed 1px hairline. */

.section {
  padding: var(--section-y) var(--gutter);
  border-bottom: var(--bd-hair) solid var(--rule-mid);
}

.section--hero {
  position: relative;
  /* A variable rather than a literal so the photograph's plate can undo
     exactly this padding with a negative margin, whatever measured value
     cinematic.css or the breakpoints put in it. */
  --hero-pad-top: 96px;
  padding-top: var(--hero-pad-top);
  /* The hero owns one full viewport: the split row stretches to fill it,
     which is what lets the plate and the seam run edge to edge. */
  display: flex;
  flex-direction: column;
  min-height: 100vh;
}
.section--hero > .split { flex: 1 1 auto; }

.section--closing {
  position: relative;
  padding-bottom: 120px;
  border-bottom: 0;
}


/* ---- The drawn 12-column grid ------------------------------
   One hairline per 8.3333%. The system's only texture — no
   grain, no noise, no photography. Hero and closing only. */

.grid-texture {
  position: absolute;
  inset: 0;
  background: var(--grid-texture);
  pointer-events: none;
}


/* ---- The asymmetric split ----------------------------------
   Argument left, abstract diagram right. Never flips. */

.split {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-5) var(--split-gap);
  align-items: flex-start;
}
.split--stretch { align-items: stretch; }
.split--loose   { gap: 56px var(--split-gap); }

.split__main {
  flex: 1 1 var(--split-main);
  min-width: 0;
}
.split__main--wide { flex: 1 1 560px; }

.split__aside {
  flex: 0 1 var(--split-aside);
  max-width: var(--split-aside);
  min-width: 0;
}
.split__aside--narrow {
  flex: 0 1 340px;
  max-width: 340px;
}


/* ---- Hero --------------------------------------------------
   The 10px ink divider is a pseudo-element so it can draw
   down with reveal-y. */

.hero__aside {
  position: relative;
  /* Reproduces `border-left:10px + padding-left` under border-box. */
  padding-left: calc(var(--bd-frame) + var(--split-gap));
}

.hero__aside::before {
  content: '';
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: var(--bd-frame);
  background: var(--ink);
}

.js .hero__aside::before {
  transform: scaleY(0);
  transform-origin: center top;
  transition: transform var(--dur-y) var(--ease) var(--d, 0ms);
}
.js .hero__aside.in::before { transform: scaleY(1); }

.hero__eyebrow  { display: block; }
.hero__title    { margin-top: 36px; }
.hero__bar      { width: min(340px, 100%); margin-top: 40px; }
.hero__lead     { margin-top: 40px; max-width: 660px; }

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s-3);
  margin-top: 52px;
  align-items: stretch;
}

.hero__aside .caption  { margin-top: var(--s-3); }

/* The photograph fills whatever height the copy column sets (the split
   stretches), with the caption held out of the crop below it. Cover is
   anchored to the top edge so the bar and the thread's anchor stay in
   frame at any column height — what a taller column trims is the empty
   ruled paper at the bottom, never the subject. */
/* The division: the argument keeps the width the words actually need — a
   720px field holds the lead's 660px measure with a constant breath before
   the divider — and the photograph absorbs all viewport growth, still
   bleeding off the right edge. The ratio falls out of the content: ~60/40
   on a laptop, ~45/55 at 1920+. Both earlier rules (a 400px aside, then a
   62.5% grow on the main) left the viewport's extra width in the argument's
   column, where it piled up as a dead field between the lead and the
   divider — the text never grows, so the text column must not either. */
.section--hero .split__main {
  flex: 0 1 720px;
  /* The argument centres itself in the viewport-tall panel: the eyebrow's
     auto margin and the caption's auto margin split the free height, and
     the caption stays pinned at the panel's foot like the meta line of an
     editorial spread. */
  display: flex;
  flex-direction: column;
}
.section--hero .split__main > .eyebrow { margin-top: auto; }
.hero__caption { margin-top: auto; padding-top: var(--s-6); }

.hero__aside--photo {
  display: flex;
  flex-direction: column;
  flex: 1 1 var(--split-aside);
  max-width: none;
  /* The plate. Not an island on the paper — it is bound to the frame:
     negative margins swallow the section's own padding so the photograph
     (and the seam, which spans this box) runs from the masthead's underside
     to the section floor, and on to the right viewport edge via the
     photo's own bleed. A floating rectangle of paper-on-paper read as
     pasted; a plate flush to three edges reads as built. */
  /* The top bleed stops at the masthead's UNDERSIDE, which is what the note
     above claims and what the old value did not do. --hero-pad-top is the
     masthead's height plus a 24px breath, so pulling up by all of it put the
     photograph's first 96px behind a fixed, opaque navigation bar: the lockup
     and the nav pill sat on top of the picture. Pulling up by the breath ALONE
     lands the plate exactly under the chrome.

     --head-h is published by js/cinematic.js from the real measured masthead
     (it grows when the nav wraps), with the same 96px fallback the rest of the
     system uses when the script has not run. */
  margin: calc(-1 * (var(--hero-pad-top) - var(--head-h, 96px)))
          0
          calc(-1 * var(--section-y));
}
.hero__photo {
  position: relative;
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  background: var(--paper);
  /* The photograph runs off the right edge of the viewport — the same move
     the red bar makes inside it. The argument keeps its gutter; the image
     doesn't need one, and stopping it short read as a framed stamp. The
     caption stays inside the aside's box, so it keeps the gutter. */
  margin-right: calc(-1 * var(--gutter));
}
/* The picture is absolutely positioned so the image's intrinsic 4:5 height
   never bids on the flex row's height — the argument column alone sets how
   tall the hero is, and the photograph cover-crops to whatever field that
   leaves it. Without this, a wide viewport hands the aside enough width
   that the intrinsic height inflates the row past the viewport. Stacked,
   there is no row height to inherit, so the band carries its own aspect
   ratio (the wide crop's). */
.hero__photo picture { position: absolute; inset: 0; }
.hero__photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 0;
}

/* The stitch. Its right edge tucks 6px under the photograph (which begins
   after the divider and the gap the aside's padding carries), so the thread
   reads as coming out from behind the sheet; .hero__photo is positioned and
   later in the tree, so it paints over the tuck. The 210px tail clears the
   lead's 660px measure at every width the split survives at ≥1280 — below
   that the field it crosses is too narrow, so it goes entirely. */
.hero__stitch {
  position: absolute;
  right: calc(100% - var(--bd-frame) - var(--split-gap) - 6px);
  /* Entry sits at the marked bar's height, so the thread reads as slipping
     out from under the mark at the sheet's edge. The bar's position inside
     the cover crop drifts with viewport ratio; 30% lands the entry on the
     bar at ~1920 and just beneath it at laptop widths — under the tape
     either way. */
  top: 30%;
  width: 210px;
  height: 120px;
  overflow: visible;
  pointer-events: none;
}
.hero__stitch path   { stroke: var(--red); }
.hero__stitch circle { fill: var(--red); }
@media (max-width: 1399.98px) { .hero__stitch { display: none; } }


/* ---- Problem ----------------------------------------------- */

.section__title { margin-top: 28px; }

/* The evidence board: the scatter pile centred, the four problems around
   it like typed evidence tags, each tied to a lost page by a thread drawn
   into the overlay by js/board.js. The pile spans both rows, so each row
   is ~half its 480px; align-self staggers the tags into a cascade that
   reads 01→02→03→04, never a symmetric table. */
.board {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
  column-gap: clamp(36px, 5vw, 100px);
  align-items: center;
  margin-top: var(--set-lead-in);
}
.board .scatter { grid-column: 2; grid-row: 1 / span 2; justify-self: center; }

.board .beat { max-width: 340px; }
.board .beat[data-thread="1"] { grid-area: 1 / 1; justify-self: end;   text-align: right; align-self: start; }
.board .beat[data-thread="2"] { grid-area: 1 / 3; justify-self: start; align-self: end; }
.board .beat[data-thread="3"] { grid-area: 2 / 1; justify-self: end;   text-align: right; align-self: start; }
.board .beat[data-thread="4"] { grid-area: 2 / 3; justify-self: start; align-self: end; }

/* The strings. Muted red for the thread, full red only at the pin — the
   board stays quieter than the hero. First child of .board, so every
   positioned scrap paints over the far end and the thread reads as pinned
   under the paper (the hero stitch's tuck, repeated). */
.board__threads {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}
.board__threads path   { fill: none; stroke: var(--red-line); stroke-width: 1.5; }
.board__threads circle { fill: var(--red); }

/* Draw-in, keyed off the .in that reveal.js already adds to [data-reveal].
   --len is each path's measured length, written by board.js. */
.js .board__threads path {
  stroke-dasharray: var(--len, 0px);
  stroke-dashoffset: var(--len, 0px);
  transition: stroke-dashoffset .9s var(--ease) var(--d, 0ms);
}
.js .board__threads.in path { stroke-dashoffset: 0; }


/* ---- Concept ----------------------------------------------- */

.concept__bar   { width: min(300px, 100%); margin-top: 44px; }
.concept__lead  { margin-top: 36px; max-width: 700px; }
.concept__aside {
  position: relative;
  padding-left: 28px;
}
.concept__aside .band    { margin-top: 40px; }
.concept__aside .caption {
  margin-top: 22px;
  font-size: 14px;
  letter-spacing: .14em;
}

/* The archive made physical. A framed still composition; the loose
   sheets drift into the slab with scroll. concept-converge.js
   publishes --concept-p (0..1) on the section; each .sheet carries
   its travel in --dx/--dy. At rest (no JS, reduced motion) the
   artwork is simply still — nothing depends on the effect. */
.concept__art {
  box-sizing: border-box;
  border: var(--bd-panel) solid var(--rule-mid);
  background: #f6f4ee;   /* warm paper: the one tone reserved for artwork */
  overflow: hidden;
}
.concept__art svg { display: block; width: 100%; height: auto; }

.concept__art .sheet {
  transform: translate(
    calc(var(--concept-p, 0) * var(--dx, 0px)),
    calc(var(--concept-p, 0) * var(--dy, 0px))
  );
  /* no transition: written from scroll every frame */
}

/* The runner: carries the rail down the artwork as scroll advances.
   The transform lives here, not on .rail — a settled reveal pins the
   rail's own transform. --concept-rail-run is measured by the JS. */
.concept__rail-run {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--rail-w);
  transform: translateY(calc(var(--concept-p, 0) * var(--concept-rail-run, 0px)));
}

/* The caption's pipeline, spelled out under the lead. Same anatomy
   as the how-it-works steps, at the argument's own measure. */
.concept__points {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 44px 40px;
  margin-top: 72px;
  max-width: 780px;
}

/* The pinned beat. While armed (>=1100px, motion allowed), the whole
   split holds still for one short stretch of scroll — the block
   spacer below supplies the travel (a real in-flow box; padding
   would give a sticky child nothing to move through) — and
   concept-converge.js maps that travel onto --concept-p, so the
   sheets fly exactly while the section stands still. */
/* While the beat is pinned, the three steps walk on one by one:
   each fades in over a short left-to-right slide, driven by its own
   window of --concept-p, then stays. --t is the window's 0..1. No
   transitions — scroll is the clock, so scrolling back reverses it.
   Without the attribute (no JS, mobile, reduced motion) the steps
   are simply visible. */
#concept[data-converge-on] .concept__points .step {
  opacity: var(--t);
  transform: translateX(calc((1 - var(--t)) * -36px));
}
#concept[data-converge-on] .concept__points .step:nth-child(1) {
  --t: clamp(0, (var(--concept-p, 0) - 0.10) / 0.22, 1);
}
#concept[data-converge-on] .concept__points .step:nth-child(2) {
  --t: clamp(0, (var(--concept-p, 0) - 0.38) / 0.22, 1);
}
#concept[data-converge-on] .concept__points .step:nth-child(3) {
  --t: clamp(0, (var(--concept-p, 0) - 0.66) / 0.22, 1);
}

@media (min-width: 1100px) {
  #concept[data-converge-on] { --concept-track: 90vh; }
  #concept[data-converge-on]::after {
    content: '';
    display: block;
    height: var(--concept-track);
  }
  #concept[data-converge-on] .split {
    position: sticky;
    top: calc(var(--head-h, 96px) + var(--s-3));
  }
}




/* ---- How it works: the journey ------------------------------
   One ink spine down the centre, nine stations alternating sides.
   Text arrives from its own side, the vignette from the other
   (reveal-once, keyed on the station's .in); the red thread climbs
   the spine with scroll — js/how-journey.js publishes --how-p on
   .journey. Two caps keep the two-pipeline story: ingestion runs
   once per document, retrieval on every question. */

.how__lead { margin-top: 36px; max-width: 720px; }

.journey {
  position: relative;
  max-width: 1060px;
  margin: var(--set-lead-in) auto 0;
}

.journey__spine,
.journey__thread {
  position: absolute;
  left: 50%;
  top: 0;
  width: var(--bd-panel);
  margin-left: -1px;
}
.journey__spine  { bottom: 0; background: var(--rule-mid); }
.journey__thread {
  height: 100%;
  background: var(--red);
  transform: scaleY(var(--how-p, 0));
  transform-origin: center top;
  /* no transition: written from scroll every frame */
}

/* Caps sit on the spine, on their own paper. */
.journey__cap {
  position: relative;
  text-align: center;
  background: var(--paper);
  padding: 12px 0;
  margin: 72px 0 28px;
}
.journey > .journey__cap:first-of-type { margin-top: 0; }

.journey__station {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 120px 1fr;
  align-items: center;
  padding: 36px 0;
}
.station__text {
  grid-column: 1;
  grid-row: 1;
  justify-self: end;
  text-align: right;
  max-width: 470px;
}
.station__text .item-label { font-size: 34px; }
.station__text .item-body  { font-size: 21px; }
.station__node {
  grid-column: 2;
  grid-row: 1;
  justify-self: center;
  width: 18px;
  height: 18px;
  background: var(--red);
}
.station__art {
  grid-column: 3;
  grid-row: 1;
  justify-self: start;
  width: 320px;
}
.station__art svg { display: block; width: 100%; height: auto; }
.station__text .item-label { margin-top: 10px; }
.station__text .item-body  { margin-top: 12px; }

.journey__station--flip .station__text { grid-column: 3; justify-self: start; text-align: left; }
.journey__station--flip .station__art  { grid-column: 1; justify-self: end; }

/* Arrival is scroll-scrubbed, not transitioned: js/how-journey.js
   sets the initial state and writes opacity/translate3d on the
   text, art and node every frame from the station's own viewport
   progress — forward on the way down, backward on the way up, the
   scroll wheel is the only easing. No CSS transitions here: the
   frame writes ARE the motion, and a transition would lag them.
   Elements sit 74% toward their own outer edge mid-flight, so the
   section clips the overhang. Without JS nothing is ever hidden. */
#how { overflow: hidden; }

@media (max-width: 1099.98px) {
  /* Stacked, the spine has no two sides to referee — it just hugs the left
     edge as a stray rule and fights the station arrivals. It goes, thread
     and all; the red node stays as each station's marker and the scrubbed
     walk-ons carry the rhythm on their own. */
  .journey__spine,
  .journey__thread { display: none; }
  .journey__cap { text-align: left; padding-left: 44px; }
  .journey__station { display: block; padding: 30px 0 30px 44px; }
  .station__text,
  .journey__station--flip .station__text { text-align: left; max-width: none; }
  .station__node { position: absolute; left: 0; top: 36px; }
  .station__art,
  .journey__station--flip .station__art { margin-top: 20px; justify-self: auto; }
}


/* ---- Why self-hosted ---------------------------------------
   Sets of four. */

.hosting__lead { margin-top: 36px; max-width: 860px; }

/* The machine in your room. One scrubbed progress (--hood-p, from
   js/hosting-scene.js) carved into four beats: walls draw, papers
   settle, the dashed line reaches, the cut lands. All beat CSS is
   gated behind .js — without the script the scene stands complete. */
.hosting__scene { flex: 1 1 520px; max-width: 660px; }
.hosting__scene svg { display: block; width: 100%; height: auto; }
.hosting__scene .caption { margin-top: 14px; }

.hood__tag {
  font-family: var(--font-body);
  font-weight: 700;
  font-size: 15px;
  letter-spacing: 3px;
  fill: var(--ink-soft);
}
.hood__tag--small { font-size: 13px; letter-spacing: 2.5px; fill: var(--ink-faint); }
.hood__tag--red   { fill: var(--red); }

.hood__wall { stroke: var(--ink); stroke-width: 12; }
.hood__dash { stroke: rgba(17,18,19,.35); stroke-width: 3; stroke-dasharray: 8 8; }
.hood__dash--faint { stroke: rgba(17,18,19,.25); }
.hood__cut line { stroke: var(--red); stroke-width: 7; }

[data-hood] {
  --w:  clamp(0, calc(var(--hood-p, 0) / 0.45), 1);          /* walls   */
  --pp: clamp(0, calc((var(--hood-p, 0) - 0.18) / 0.32), 1); /* papers  */
  --lk: clamp(0, calc((var(--hood-p, 0) - 0.50) / 0.22), 1); /* reach   */
  --cx: clamp(0, calc((var(--hood-p, 0) - 0.78) / 0.12), 1); /* the cut */
}
.js .hood__wall {
  stroke-dasharray: var(--len, 2000px);
  stroke-dashoffset: calc(var(--len, 2000px) * (1 - var(--w)));
}
.js .hood__paper {
  opacity: var(--pp);
  transform: translateY(calc((1 - var(--pp)) * 16px));
}
.js .hood__reach {
  transform: scaleX(var(--lk));
  transform-box: fill-box;
  transform-origin: left center;
}
.js .hood__outside { opacity: var(--lk); }
.js .hood__cut {
  opacity: var(--cx);
  transform: scale(var(--cx));
  transform-box: fill-box;
  transform-origin: center;
}

/* Same reasoning as .steps: 4 → 2+2 → 1, never 3+1. */
.columns {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 56px 40px;
  margin-top: var(--set-lead-in);
}
.columns .ordinal { margin-bottom: 18px; }
.columns .tick    { margin-bottom: 22px; }
.columns .item-label { margin-bottom: var(--s-2); }

/* The boundary diagram sits in the aside slot of the split. */
.boundary .eyebrow { margin-bottom: 18px; }
.boundary .tie     { height: 44px; margin-left: 44px; }
.boundary .caption { margin-top: 22px; }


/* ---- Roadmap ------------------------------------------------
   Ruled rows, no rail. Live pipeline steps ride a 4px red rail;
   what is only planned gets a hairline and a Planned tag. That
   difference is the whole signal. */

.roadmap__lead { margin-top: 36px; max-width: 660px; }

.forward {
  margin-top: var(--set-lead-in);
  border-top: var(--bd-hair) solid var(--rule-mid);
}

.forward__row {
  display: grid;
  /* The item column is capped rather than fluid: at 1440 a full-width
     row runs ~105 characters, well past a readable measure. The tag
     column takes the slack and stays hard right. */
  grid-template-columns: 64px minmax(0, 720px) 1fr;
  gap: var(--s-3) 40px;
  align-items: start;
  padding: 34px 0;
  border-bottom: var(--bd-hair) solid var(--rule);
}

.forward__row .item-label { margin-bottom: 10px; }
.forward__row .ordinal    { padding-top: 6px; }

/* Stays on the caption's --ink-soft. --ink-faint reads as 3.1:1 on paper,
   under the 4.5:1 floor for 12px text; the tag is already quiet enough by
   size, tracking and position. */
.forward__status {
  justify-self: end;
  padding-top: 6px;
  white-space: nowrap;
}


/* ---- Closing ------------------------------------------------ */

.closing__bar  { width: min(260px, 100%); margin-top: 44px; }
.closing__lead { margin-top: 36px; max-width: 700px; }
.closing__cta  { margin-top: 52px; }

.closing__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: var(--s-3);
  margin-top: 52px;
}
/* The secondary takes the wide CTA's height, padding and arrow
   placement, so the pair reads as one block rather than two sizes —
   and when they stack on mobile, labels and arrows line up. */
.closing__actions .cta--secondary {
  height: auto;
  min-height: 96px;
  justify-content: space-between;
  padding: 0 clamp(20px, 3vw, 36px);
}

.closing__aside .bar--cap { margin-top: 20px; }
.closing__aside .caption  { margin-top: var(--s-3); }


/* ---- Footer · the inverted region --------------------------- */

.site-foot {
  background: var(--stage-bg);
  color: var(--on-dark);
  padding: 56px var(--gutter);
}

.site-foot a { color: var(--on-dark-soft); }
.site-foot a:hover { color: var(--red); }

.site-foot__top {
  display: flex;
  align-items: center;
  gap: var(--gap-lockup);
}

.site-foot__note {
  margin-left: auto;
  font-weight: 400;
  font-size: 16px;
  color: var(--on-dark-soft);
}

.site-foot__mid {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: flex-end;
  gap: var(--s-4) var(--s-6);
  margin-top: 30px;
}

.site-foot__desc {
  max-width: 44ch;
  font-size: 16px;
  line-height: 1.5;
  color: var(--on-dark-soft);
}

.site-foot__nav {
  display: flex;
  flex-wrap: wrap;
  gap: 14px clamp(20px, 2.4vw, 36px);
  font-weight: 600;
  font-size: var(--fs-nav);
  letter-spacing: var(--ls-nav);
  text-transform: uppercase;
}

/* ---- Footer sitemap ----------------------------------------
   Four columns. The header carries five items; everything else
   in the site lives down here — the split the Task 2 research
   found in every site that navigates well, and the middle
   ground between Supabase's ~120 links and Appwrite's five.

   Every link here resolves to a page that exists. Docs, API
   reference and changelog are deliberately absent: no public
   URL exists for them yet (content.md §18), and a footer column
   of dead links is worse than a shorter footer. */

.site-foot__cols {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s-4) var(--s-5);
  width: 100%;
  margin-top: var(--s-5);
}

.site-foot__col {
  display: flex;
  flex-direction: column;
  gap: 10px;
  min-width: 0;
}

.site-foot__coltitle {
  margin-bottom: 4px;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--fs-caption);
  letter-spacing: var(--ls-caption);
  text-transform: uppercase;
  color: var(--on-dark);
}

.site-foot__col a {
  font-size: 15px;
  line-height: 1.4;
}

/* The current page is not a link to itself. */
.site-foot__col [aria-current="page"] {
  color: var(--on-dark);
  cursor: default;
}

/* Four columns need ~150px each to hold "Privacy Policy" without
   breaking the word. Below ~1000px they do not have it, so the set
   halves — the same "two across is the floor, then stack" rule the
   content sets follow. Measured, not guessed: at 320px the gutter is
   at its 24px floor, leaving 272px, which two columns and a 24px gap
   split into 124px each. */
@media (max-width: 899.98px) {
  .site-foot__cols { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 399.98px) {
  .site-foot__cols { gap: var(--s-4) var(--s-3); }
}

.site-foot__links {
  display: flex;
  flex-wrap: wrap;
  gap: 10px clamp(20px, 2.4vw, 32px);
}

.site-foot__bottom {
  margin-top: 40px;
  padding-top: 28px;
  border-top: var(--bd-hair) solid var(--on-dark-rule);
  display: flex;
  justify-content: space-between;
  gap: var(--s-2);
  font-size: 15px;
  color: var(--on-dark-soft);
}

.site-foot__index {
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: var(--ls-ordinal);
  white-space: nowrap;
}


/* ============================================================
   RESPONSIVE
   Below ~1100px each split collapses to a single column with
   the diagram after the argument, and the drawn column grid is
   dropped — it reads as noise at narrow widths.
   ============================================================ */

/* Five across needs 5 × 215 + 4 × 44 = 1251px of content, which only
   exists from ~1400 up. Below that the set breaks 3 + 2. */
@media (max-width: 1399.98px) {
  .steps       { grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .steps--four { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 1099.98px) {
  .columns { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 899.98px) {
  .steps { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 1099.98px) {

  .grid-texture { display: none; }

  .split__main,
  .split__main--wide,
  .split__aside,
  .split__aside--narrow {
    flex: 1 1 100%;
    max-width: none;
  }

  /* The signature divider survives — it just draws across
     instead of down. */
  /* The hero main's scoped 720px rule outranks the generic stack reset
     above, so it gets its own. Stacked, the plate's negative margins and
     the viewport-height claim go too — the band flows with the content. */
  .section--hero .split__main { flex: 1 1 100%; }
  .section--hero { min-height: 0; }
  .hero__aside--photo { margin: 0; }
  /* Stacked, the auto margin that pinned the caption to the panel's foot
     resolves to zero and the caption crowds the secondary CTA — give it a
     stated gap instead. */
  .hero__caption { padding-top: 0; margin-top: var(--s-4); }

  .hero__aside {
    padding-left: 0;
    padding-top: calc(var(--bd-frame) + var(--s-5));
  }
  /* Stacked, the photograph becomes a full-bleed band — both gutters go,
     the caption keeps them — at the wide crop's own ratio, since there is
     no copy column beside it to set a height. */
  .hero__photo {
    margin-inline: calc(-1 * var(--gutter));
    flex: none;
    aspect-ratio: 1600 / 893;
  }
  .hero__aside::before {
    left: 0;
    right: 0;
    top: 0;
    bottom: auto;
    width: auto;
    height: var(--bd-frame);
  }
  .js .hero__aside::before {
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform var(--dur-x) var(--ease) var(--d, 0ms);
  }
  .js .hero__aside.in::before { transform: scaleX(1); }

  .scatter { flex: 0 0 auto; }

  /* The board stacks: tags 01–04 in order, then the pile — no strings.
     The pile stays left-aligned; its small-screen scale-down compensates
     with margins that assume a top-left origin. */
  .board { grid-template-columns: minmax(0, 1fr); }
  .board__threads { display: none; }
  .board .beat[data-thread] {
    grid-area: auto;
    justify-self: stretch;
    align-self: auto;
    text-align: left;
    max-width: 560px;
  }
  /* max-width keeps the 400px authored canvas from widening the page on
     narrow screens — the scale rules below 460px shrink the VISUAL, but the
     layout box would still overflow the gutter and hand the page a
     horizontal scrollbar. */
  .board .scatter {
    grid-column: 1;
    grid-row: auto;
    justify-self: start;
    max-width: 100%;
    /* The pile LEADS on a phone rather than trailing.

       On a desktop the board is one picture: four beats arranged around the
       pile they describe, each tied to a scrap by a red thread. Stacked, the
       threads go (they have nothing to span) and the arrangement goes with
       them — so the section became four paragraphs followed, eventually, by an
       unexplained heap of rectangles. That is the "image and text are
       separated" reading, and it is accurate: nothing connected them any more.

       Leading with the pile restores the relationship the threads used to
       carry. The reader meets the mess first — which is what the section is
       ABOUT — and the four beats then read as commentary on something already
       on screen. Same correction, same reason, as the stacked splits in
       pages.css. */
    order: -1;
    /* The gap the reader sees, MINUS the height the scaled-down canvas
       no longer fills. Both parts have to be in one declaration because
       this selector outranks the .scatter tiers that set the scale. */
    margin-bottom: calc(var(--s-5) - 480px * (1 - var(--scatter-s, 1)));
  }

  /* The pile leads, so the board no longer needs the full headline
     lead-in above it — the headline is right there. */
  .board { margin-top: var(--s-5); }
}

@media (max-width: 899.98px) {

  /* The masthead is one line at every width now — the nav pill lives at
     the bottom of the viewport below 1000px — so anchors only need to
     clear the single-line chrome. */
  [id] { scroll-margin-top: 96px; }

  .section { --section-y: 80px; }
  .section--hero { --hero-pad-top: 72px; }
}

@media (max-width: 599.98px) {
  .site-head__inner { padding: 16px var(--gutter); gap: 14px; }
}

@media (max-width: 899.98px) {

  /* Three across needs ~3 × 195 + 80 of gaps; below 760 the columns are
     under 150px and the labels overrun into each other. The steps stack —
     each keeps its own red rail, so the set still reads as three beats. */
  .concept__points {
    grid-template-columns: minmax(0, 1fr);
    gap: 40px;
    margin-top: 56px;
  }

  /* The slab artwork is another text-then-image stack on a phone — a full
     screen of decoration after the three steps have already spelled the
     pipeline out. Mobile carries less: the argument keeps the section, the
     sculpture stays a desktop object (user call). aria-hidden already, so
     nothing leaves the a11y tree. */
  #concept .concept__aside { display: none; }

  /* The roadmap row loses its third column: the ordinal and the
     Planned tag pair up on one line, the item takes the width. */
  .forward__row {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 16px 20px;
  }
  .forward__status { grid-column: 2; grid-row: 1; padding-top: 0; }
  .forward__item   { grid-column: 1 / -1; }
  .forward__row .ordinal { padding-top: 0; }
}

@media (max-width: 599.98px) {

  :root {
    --fs-lead: 20px;
    --fs-item: 17px;
    --fs-label: 24px;
    --set-lead-in: 60px;
  }

  .hero__actions .cta--primary,
  .hero__actions .cta--secondary { flex: 1 1 100%; }

  .closing__actions .cta--wide,
  .closing__actions .cta--secondary { flex: 1 1 100%; }

  /* Two across is the floor for every set; below it they stack. */
  .steps,
  .steps--four,
  .columns { grid-template-columns: minmax(0, 1fr); }

  .rail-set + .rail-set { margin-top: 56px; }

  .site-foot__bottom { flex-direction: column; }
}

@media (max-width: 399.98px) {

  /* "Your infrastructure." is one 15-character word set in the largest
     display voice on the page. At the 48px floor it measures ~320px and
     runs past a 320px screen, and no wrap can save it. Below 460 the
     floor goes fluid instead; 10.5vw lands on 48.3px at the breakpoint
     itself, so the handover is invisible. */
  :root { --fs-h2-xl: clamp(38px, 10.5vw, 92px); }


  .field { flex-wrap: wrap; }
  .field__input  { flex: 1 1 100%; }
  .field__submit { flex: 1 1 100%; text-align: left; }

  /* The frame keeps its 10px weight; only the inset gives way. */
  .boundary__frame { padding: 6px 16px; }
  .boundary .tie   { margin-left: 34px; }
}

/* ---- The scatter canvas -------------------------------------
   Authored at 400x480 in absolute px, so it is SCALED rather than
   reflowed — the disorder is the point and reflowing it tidies it.

   Two tiers, and they had both been collapsed onto one by the
   breakpoint consolidation: the .78 tier was authored at 460px and
   the .66 tier at 380px, and mapping both onto 400 meant every
   phone between 400 and 600 got no scale-down at all. That is why
   the pile was still 480px tall on a 440px screen.

   The figures are also lower than the originals. At full size the
   pile alone filled a phone screen, so the section broke into two
   unrelated views — headline and pile, then the four beats — which
   is exactly the "on PC it views as 1 screen but on phone these
   are 2 different" reading. Smaller, the headline, the pile and
   the first beat share a screen and the section is one thing
   again.

   transform: scale does not change layout height, so the negative
   margin gives back the space the visual no longer occupies. */
@media (max-width: 599.98px) {
  .scatter {
    --scatter-s: .62;
    transform: scale(var(--scatter-s));
    transform-origin: top left;
  }
}

@media (max-width: 399.98px) {
  /* 400px of canvas has to clear a 272px column. */
  .scatter { --scatter-s: .52; }
}


/* Last word: reduced motion drops the divider's draw-down too.
   Must trail the responsive blocks, which redefine the same rule. */
@media (prefers-reduced-motion: reduce) {
  .js .hero__aside::before,
  .js .hero__aside.in::before { transform: none; }

  /* Rest state, not zeroed duration: base.css's global reduced-motion
     transition-duration is !important, so the threads must simply BE
     drawn here. */
  .js .board__threads path {
    stroke-dasharray: none;
    stroke-dashoffset: 0;
    transition: none;
  }
}


/* ---- Tubelight nav ------------------------------------------
   The masthead's pill. Ported from the shadcn tubelight-navbar:
   a rounded container, a lamp that slides under the active item
   (js/nav-tube.js sets left/width; the overshoot curve here is
   the spring), and the red tube riding the pill's upper lip with
   its glow. Placed after every .nav breakpoint rule on purpose —
   equal specificity, later in the file, so these win. */

.nav--tube {
  /* Fixed against the viewport, not laid into the masthead: the masthead
     carries a transform, which would turn it into this pill's containing
     block and pin "bottom" to the chrome instead of the screen.

     The pill used to ride --head-in with the masthead, which meant it was
     off-screen for the first ~4100px of the home page — 45% of the document,
     and the whole of a first-time visitor's landing experience. No Features,
     no Self-hosting, no FAQ, no Contact, no Sign in until the film had been
     scrolled through in full.

     It now rides --nav-in, which nothing sets, so the fallback of 1 keeps the
     pill present from first paint on every page including this one. Only the
     masthead LOCKUP still waits on --head-in, and it waits for a specific
     reason recorded in DESIGN-NOTES: the film's closing beat is a frame that
     is nothing but the lockup, and a second lockup top-left doubles it. The
     pill sits top-right and never collides with that frame. */
  position: fixed;
  top: 23px;
  right: var(--gutter);
  z-index: 30;
  align-items: center;
  gap: 4px;
  padding: 4px;
  border: var(--bd-hair) solid var(--rule-mid);
  border-radius: 999px;
  background: var(--paper);
  transform: translate3d(0, calc((1 - var(--nav-in, 1)) * -250%), 0);
  will-change: transform;
}

.nav--tube a {
  position: relative;
  z-index: 1;
  padding: 10px 22px;
  border-radius: 999px;
  transition: color .2s var(--ease);
}
.nav--tube a.is-active { color: var(--ink); }

/* Sign in: the pill's final segment, past the hairline. The chrome's one
   action keeps the CTA voice — red, flipping to ink on hover. */
.nav__rule { width: 1px; height: 18px; background: var(--rule-mid); margin: 0 6px; flex: none; }
.nav--tube .nav__cta { background: var(--red); color: var(--paper); }
.nav--tube .nav__cta:hover { background: var(--ink); color: var(--paper); }

/* max-width: none escapes base.css's global `img, svg { max-width: 100% }`,
   which resolves to 0 inside a shrink-to-fit link and erased the glyphs. */
.nav__icon { display: none; width: 18px; height: 18px; max-width: none; }

.nav__lamp {
  position: absolute;
  top: 4px;
  bottom: 4px;
  left: 0;
  width: 0;
  border-radius: 999px;
  background: rgba(17, 18, 19, .05);
  opacity: 0;
  transition:
    left  .5s cubic-bezier(.3, 1.4, .45, 1),
    width .5s cubic-bezier(.3, 1.4, .45, 1),
    opacity .25s var(--ease);
}
.nav--tube.has-active .nav__lamp { opacity: 1; }

/* The tube: a red filament on the pill's top edge, and its light. The
   glow is the one soft edge in the chrome — it is the component's name. */
.nav__lamp::before {
  content: '';
  position: absolute;
  top: -5px;
  left: 50%;
  transform: translateX(-50%);
  width: 32px;
  height: 4px;
  border-radius: 0 0 4px 4px;
  background: var(--red);
}
.nav__lamp::after {
  content: '';
  position: absolute;
  top: -12px;
  left: 50%;
  transform: translateX(-50%);
  width: 52px;
  height: 18px;
  border-radius: 999px;
  background: rgba(255, 51, 0, .22);
  filter: blur(8px);
}

/* Below 1000px there is no longer room beside the mark, so the pill pins
   to the bottom of the viewport, icons only — the original component's
   mobile posture. It slides DOWN out of frame while the film runs, the
   mirror of the masthead's slide up. */
@media (max-width: 899.98px) {
  .nav--tube {
    top: auto;
    left: 50%;
    right: auto;
    bottom: calc(16px + env(safe-area-inset-bottom, 0px));
    z-index: 60;
    background: rgba(251, 251, 249, .88);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    box-shadow: 0 10px 30px rgba(17, 18, 19, .14);
    transform: translate3d(-50%, calc((1 - var(--nav-in, 1)) * 250%), 0);
  }

  /* On a phone the gutter is small and body copy runs the full width, so a
     transparent masthead floats the lockup straight over the text scrolling
     under it. The pill's own frosted paper fixes it: same material top and
     bottom, and the hairline gives the chrome an edge. Desktop keeps the
     bare mark — there the photo plate runs to the viewport top and a bar
     would read as a white strip across it. */
  .site-head {
    background: rgba(251, 251, 249, .88);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    border-bottom: var(--bd-hair) solid var(--rule);
  }

  /* The pill is fixed over the page's foot, so the footer's last line needs
     enough ground to clear it — pill height plus its 16px stand-off. */
  .site-foot {
    padding-bottom: calc(56px + 70px + env(safe-area-inset-bottom, 0px));
  }
  .nav--tube .nav__label { display: none; }
  .nav--tube .nav__icon  { display: block; }

  /* Fluid, not fixed. At a flat 18px the six icons plus the Sign in segment
     measured 370px, which overflows BOTH edges of a 320px screen — the pill
     is centred, so it spills symmetrically. Sized against the viewport it
     lands at ~284px on a 320 and relaxes back to the full 18px by the time
     there is room for it. Measured, not guessed: 320 / 360 / 390 all clear
     the edges with margin to spare. */
  /* Tap targets. These six icons ARE the navigation below 1000px, so they
     get the 44px height every platform guideline asks for — vertical space
     is free here. Width is the constrained axis: six targets at 44px plus
     gaps and the divider need ~305px, and a 320px screen does not have it
     once the pill's own margins are paid. So width stays fluid and lands
     at 40px on the narrowest phones — comfortably past WCAG 2.5.8's 24x24
     minimum, short of 2.5.5's 44x44 ideal, and the honest trade for keeping
     all six reachable rather than hiding some behind a menu. */
  .nav--tube a {
    display: flex;
    align-items: center;
    justify-content: center;
    min-height: 44px;
    /* 13px floor, not 8px: the icon is 18px, so 18 + 2x13 = 44 and the
       target finally clears the guideline on BOTH axes. This only became
       affordable when the Home item went — six 44px targets plus gaps,
       the divider and the pill's padding need ~305px, which a 320px
       screen does not have. Five need ~257px, which it does. */
    padding: 0 clamp(13px, 3.4vw, 18px);
  }
  .nav--tube   { gap: clamp(1px, 0.6vw, 4px); }
  .nav__rule   { margin: 0 clamp(2px, 1vw, 6px); }
  /* The tube hangs from the pill's LOWER lip at the bottom of the screen —
     light reads from the edge nearest the frame. */
  .nav__lamp::before { top: auto; bottom: -5px; border-radius: 4px 4px 0 0; }
  .nav__lamp::after  { top: auto; bottom: -12px; }
}

@media (prefers-reduced-motion: reduce) {
  .nav__lamp { transition: opacity .2s var(--ease); }
}


/* ---- Touch targets in the footer ----------------------------
   The footer's links render at ~21px tall — fine for a mouse,
   fiddly for a thumb, and there are twelve of them stacked two
   columns wide on a phone. Below 1000px each becomes a 44px row.

   The explicit gap goes to 0 in exchange: the padding now does
   the separating, so the column reads as a list of rows rather
   than gaining 23px of dead space between every pair. */

@media (max-width: 899.98px) {
  .site-foot__col { gap: 0; }

  .site-foot__col a,
  .site-foot__links a {
    display: flex;
    align-items: center;
    min-height: 44px;
  }

  /* The footer lockup is 26px tall by design — it is the quiet
     rendition. The LINK around it can still be 44px. */
  .site-foot__mark { min-height: 44px; align-items: center; }

  .site-foot__links {
    gap: 0 clamp(20px, 2.4vw, 32px);
    flex-wrap: wrap;
  }
}


/* ---- Nav pill contrast, bottom-pinned rendition -------------
   js/nav-theme.js sets .nav--on-dark when the pill is over the
   footer. The pill takes the OPPOSITE of what is behind it:
   ink over the paper page, paper over the ink footer.

   Frosted paper over a paper page was the problem — the pill
   read as a smudge rather than as chrome, and the icons at
   --ink-soft on near-white had little to hold them. */

@media (max-width: 899.98px) {
  .nav--tube {
    background: rgba(17, 18, 19, .90);
    border-color: rgba(251, 251, 249, .16);
    box-shadow: 0 10px 30px rgba(17, 18, 19, .28);
  }

  .nav--tube a { color: var(--on-dark-soft); }
  .nav--tube a.is-active { color: var(--on-dark); }
  .nav--tube .nav__rule { background: var(--on-dark-rule); }

  /* The lamp is a wash of the surface it sits on, so on ink it
     lifts rather than darkens. The red tube is unchanged — it is
     the one colour that holds on both grounds. */
  .nav__lamp { background: rgba(251, 251, 249, .10); }

  /* Over the footer, flip back to paper. */
  .nav--tube.nav--on-dark {
    background: rgba(251, 251, 249, .92);
    border-color: var(--rule-mid);
    box-shadow: 0 10px 30px rgba(0, 0, 0, .45);
  }
  .nav--on-dark a { color: var(--ink-soft); }
  .nav--on-dark a.is-active { color: var(--ink); }
  .nav--on-dark .nav__rule { background: var(--rule-mid); }
  .nav--on-dark .nav__lamp { background: rgba(17, 18, 19, .06); }

  /* Sign in keeps its red on both grounds — that is the point of
     having exactly one accent. */
  .nav--tube .nav__cta,
  .nav--on-dark .nav__cta { background: var(--red); color: var(--paper); }
}


/* ============================================================
   MOBILE: the home page's stacked sections
   ------------------------------------------------------------
   Two things go wrong when the hero and the problem board
   collapse to one column, and both are about a picture ending
   up next to the WRONG words.

   Each section puts its text first and its visual last. Stacked,
   that visual then sits directly above the NEXT section's
   heading — so the hero's photograph reads as an illustration
   for "Documents are everywhere", and the scatter pile reads as
   one for whatever follows it. The page scans as
   image → text → image → text, with every pairing off by one.

   The fix is to let each visual sit under the heading it belongs
   to, not at the foot of its own section.
   ============================================================ */

@media (max-width: 1099.98px) {

  /* ---- Hero ------------------------------------------------
     The caption lives at the argument's foot because on a desktop
     the plate runs to the section floor and leaves no ground
     beneath itself to stand on. Stacked, that puts it ABOVE the
     photograph — describing a picture the reader has not reached.

     It cannot simply be reordered: it is a child of the argument
     column, not a sibling of the figure, so `order` has nothing
     to act on. Moving it in the markup would break the desktop
     composition DESIGN-NOTES defends, and duplicating it would
     put the same sentence in the DOM twice.

     So on a phone it goes. It is one decorative meta line about a
     photograph that is now directly beneath it — the caption of a
     spread, in a layout that is no longer a spread. Losing it
     costs nothing; showing it in the wrong place cost clarity. */
  .section--hero .hero__caption { display: none; }

  /* ---- Problem board ---------------------------------------
     The pile reads as the section's closing image, not its
     opening one: the four beats are the argument and the pile is
     what they are describing. Putting it first pushed the
     numbered points below a large picture and separated the
     heading from the content it introduces.

     So it stays last, and instead gets the room to be read as
     the section's own closing image rather than as something
     abutting the next heading. */
  .board .scatter { margin-top: var(--s-6); }

  /* The beats ran together: 01's body sat flush against 02's
     ordinal, so four separate problems read as one paragraph. */
  .board .beat[data-thread] + .beat[data-thread] { margin-top: var(--s-5); }
}


/* ============================================================
   THE DARK CHAPTER
   --stage-bg existed in the token set from day one and was used
   in exactly one place: the footer. So a visitor scrolled four
   full-height sections of identical warm paper before the value
   ever changed. That sameness — not the flatness — is what read
   as dull. A Swiss system is allowed to be flat; it is not
   allowed to be monotonous.

   One inverted chapter mid-page fixes it. It costs no new colour
   and breaks no rule: the section simply re-points the existing
   tokens at their on-dark counterparts, so every component inside
   it inverts without knowing anything about the theme.
   ============================================================ */

.section--dark {
  --paper:     var(--stage-bg);
  --ink:       var(--on-dark);
  --ink-soft:  rgba(251, 251, 249, .62);
  --ink-faint: rgba(251, 251, 249, .38);
  --rule:      var(--on-dark-rule);
  --rule-mid:  rgba(251, 251, 249, .28);
  --ink-wash:  rgba(251, 251, 249, .07);
  --paper-veil: rgba(13, 13, 13, .70);

  background: var(--stage-bg);
  color: var(--on-dark);
}

/* The red is the one value that does NOT invert. It was chosen to
   hold against paper and it holds harder against ink — which is
   the point of putting the chapter here, on the section whose
   artwork is already built out of it. */

.section--dark .display,
.section--dark .item-label,
.section--dark h1,
.section--dark h2,
.section--dark h3 { color: var(--on-dark); }

.section--dark .lead,
.section--dark .item-body,
.section--dark .eyebrow,
.section--dark .caption { color: var(--ink-soft); }

.section--dark a { color: var(--on-dark); }
.section--dark a:hover { color: var(--red); }

/* The concept artwork draws its sheets in literal paper white with
   an ink hairline. The fill is right on either ground; the hairline
   disappears, so it is re-pointed at the on-dark rule. */
.section--dark .concept__art svg g[stroke] { stroke: rgba(251, 251, 249, .30); }
.section--dark .concept__art svg rect[fill^="rgba(17"] { fill: rgba(251, 251, 249, .12); }

/* The seam. An inverted band that simply starts is a colour change;
   one that starts on a red hairline is an edit. */
.section--dark::before {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 0;
  height: var(--bd-rail);
  background: var(--red);
}
.section--dark { position: relative; }


/* ============================================================
   THE TRUST STRIP
   An open-source, self-hosted tool has to answer "why would I
   run this" above the fold, and the page had nothing: no licence
   mark, no repository, no statement of what leaves the machine.

   Deliberately NOT a metrics bar. Star counts and install totals
   are claims that need a source; these four are properties of the
   product that the rest of the site already states, so the strip
   asserts nothing new.
   ============================================================ */

/* A strip is not a chapter: it gets a fraction of the section
   rhythm, so it reads as a rule under the hero rather than as
   another thing to scroll past. */
/* The negative top margin is gone. It was pulling the strip up under the
   hero, and the hero paints a full-bleed photograph with overflow:hidden —
   so the top half of every key ("MIT", "Self-hosted", "No telemetry") was
   being covered by the picture. That is what "the below text are not
   visible" was: not clipped text, text behind an image.

   A strip is still not a chapter, so it keeps a fraction of the section
   rhythm — it just takes it as its OWN padding instead of borrowing space
   from the section above. */
.section--strip {
  padding-top: var(--s-5);
  padding-bottom: 0;
  margin-bottom: calc(var(--section-y) * .35);
}

.trust {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 0;
  border-top: var(--bd-hair) solid var(--rule-mid);
  border-bottom: var(--bd-hair) solid var(--rule-mid);
}

.trust__item {
  padding: var(--s-4) var(--s-3);
  border-left: var(--bd-hair) solid var(--rule);
}
.trust__item:first-child { border-left: 0; }

.trust__key {
  display: block;
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 20px;
  letter-spacing: var(--ls-label);
  color: var(--ink);
}
.trust__key .mono { font-size: 18px; }

.trust__val {
  display: block;
  margin-top: 6px;
  font-family: var(--font-body);
  font-weight: 600;
  font-size: var(--fs-caption);
  letter-spacing: var(--ls-caption);
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* The one red mark in the strip: the licence, because it is the
   fact that makes the rest of the strip credible. */
.trust__item--accent .trust__key { color: var(--red); }

@media (max-width: 899.98px) {
  .trust { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .trust__item:nth-child(3) { border-left: 0; }
  .trust__item:nth-child(n + 3) { border-top: var(--bd-hair) solid var(--rule); }
}

/* Stays 2-up all the way down. A single column made the strip 433px
   tall on a 375px phone — four stacked facts is a section, and this is
   supposed to be a rule under the hero. Two-up it is ~200px, and the
   labels wrap to two lines rather than the strip growing. */
@media (max-width: 399.98px) {
  .trust__item { padding: var(--s-3) var(--s-2); }
  .trust__key  { font-size: 17px; }
}


/* ============================================================
   HERO — THE PHOTOGRAPH AS GROUND
   The image is the section's own background, not a plate in a
   column. See the note in index.html for why it moved.

   Legibility is a FLAT veil, not a gradient: the system bans
   gradients outright, and --paper-veil already exists for
   exactly this job (paper at 70%, so overlap reads). The
   photograph is a bright, low-contrast desk, so the veil can
   stay light and the picture still carries.
   ============================================================ */

.section--hero.hero--plate {
  position: relative;
  isolation: isolate;
  overflow: hidden;
}

.hero__plate {
  position: absolute;
  inset: 0;
  z-index: -2;
}

.hero__plate picture,
.hero__plate img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* Top-anchored: what a taller section trims is empty desk at the
     bottom, never the laptop. */
  object-position: 60% 0;
}

/* The veil. One flat fill, no ramp. 82% rather than the token's 70%
   because the argument is ink on this and a 24px lead has to hold at
   AA against the brightest part of the photograph, not the average. */
.hero__plate::after {
  content: '';
  position: absolute;
  inset: 0;
  background: rgba(251, 251, 249, .82);
}

/* The argument. One column, held to the measure the words want —
   the photograph takes all the width the text does not, at every
   size, so there is no stacked rendition to strand. */
.hero__inner {
  position: relative;
  z-index: 1;
  max-width: 720px;
  display: flex;
  flex-direction: column;
}

.section--hero.hero--plate .hero__inner > .eyebrow { margin-top: auto; }
.section--hero.hero--plate .hero__caption {
  margin-top: auto;
  padding-top: var(--s-6);
}

/* The grid texture belongs over the veil, not under the photograph. */
.section--hero.hero--plate .grid-texture { z-index: -1; }

@media (max-width: 899.98px) {
  /* The wide crop lands here, so the veil carries more of the work:
     less picture is visible behind the same amount of text. */
  .hero__plate::after { background: rgba(251, 251, 249, .88); }
  .hero__plate img { object-position: 50% 0; }
  .section--hero.hero--plate .hero__caption { padding-top: 0; margin-top: var(--s-4); }
}


/* ============================================================
   HERO — THE DARK FRAME
   The photograph is a near-black desk (mean luminance 12), so
   the hero inverts rather than veiling. This is the better
   arrangement for the picture AND for the words: a paper veil
   over a bright photo has to be strong enough to carry a 24px
   lead, and at that strength the photograph stops being visible
   at all — which is what "the image not loads fully" was.

   --paper is deliberately NOT remapped here. The primary CTA
   sets its label in --paper on top of --red, and inverting the
   token would put dark text on the red button.
   ============================================================ */

.hero--dark {
  --ink:       var(--on-dark);
  --ink-soft:  rgba(251, 251, 249, .66);
  --ink-faint: rgba(251, 251, 249, .42);
  --rule:      var(--on-dark-rule);
  --rule-mid:  rgba(251, 251, 249, .28);
  --ink-wash:  rgba(251, 251, 249, .10);

  background: var(--stage-bg);
  color: var(--on-dark);
}

/* A scrim, not a veil: one flat fill, sized so the lit laptop screen
   cannot lift the ground under the lead. */
.hero--dark .hero__plate::after { background: rgba(9, 9, 10, .58); }

.hero--dark .display,
.hero--dark h1 { color: var(--on-dark); }
.hero--dark .lead,
.hero--dark .eyebrow,
.hero--dark .caption { color: var(--ink-soft); }
.hero--dark .lead strong { color: var(--on-dark); }

/* The primary CTA keeps red-with-paper. Its hover swapped to --ink,
   which is now light — light on light. Invert the whole swap instead. */
.hero--dark .cta--primary:hover { background: var(--paper); }
.hero--dark .cta--primary:hover .cta__label,
.hero--dark .cta--primary:hover .cta__arrow { color: var(--stage-bg); }

/* The secondary CTA is an ink outline; on dark it becomes a paper one. */
.hero--dark .cta--secondary { border-color: var(--on-dark); }
.hero--dark .cta--secondary .cta__label { color: var(--on-dark); }
.hero--dark .cta--secondary:hover { background: rgba(251, 251, 249, .10); }


/* ============================================================
   MASTHEAD — LEGIBLE OVER WHATEVER IS BEHIND IT
   The masthead is fixed, transparent and has no backdrop, so it
   floated over whatever happened to be under it: the ink lockup
   vanished on the dark hero, and body text ran under the bar on
   the paper sections with nothing to separate the two.

   Two states, both set by js/nav-theme.js:
     --on-dark   over a [data-dark] region: light lockup
     --solid     past the hero: a paper bar with a hairline, so
                 copy scrolls UNDER chrome instead of through it
   ============================================================ */

.mark__lockup--light { display: none; }

.site-head--on-dark .mark__lockup--ink   { display: none; }
.site-head--on-dark .mark__lockup--light { display: block; }

/* Below 900px the masthead already carries a frosted bar at all times — see
   the rule in the phone block above, which solved this for phones and gave the
   reason desktop was left bare: the photo plate runs to the viewport top and a
   permanent white strip across it would read as a seam.

   That reasoning holds for the hero and stops holding the moment the hero is
   behind us, which is where "the lockup sits on top of the paragraph" came
   from. So desktop gets the same material as the phone, on a condition: only
   once the masthead has cleared the hero. Same frosted paper, same hairline,
   so the chrome is one material at every width. */
@media (min-width: 900px) {
  .site-head::before {
    content: '';
    position: absolute;
    inset: 0;
    background: rgba(251, 251, 249, .88);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
    border-bottom: var(--bd-hair) solid var(--rule-mid);
    opacity: 0;
    transition: opacity var(--dur-cta) var(--ease);
    pointer-events: none;
  }
  .site-head--solid::before { opacity: 1; }
}

/* The bar must sit behind the chrome it backs. */
.site-head__inner { position: relative; z-index: 1; }


/* ============================================================
   NAV PILL — THE INK RENDITION, OVER THE FILM
   The pill is fixed at z-30 and the film's stage is z-2, so the
   pill rides ON the footage for the whole scroll-film. That is
   deliberate: it used to hide with the masthead, which left the
   page with no navigation at all for the first several thousand
   pixels — the whole of a first visit.

   What was wrong was not that it was there, it was what it
   looked like. The film's frames are bright white paper and the
   pill's default is paper too, so it read as a white rectangle
   floating on white footage with nothing holding it — a panel
   pasted over the film rather than chrome sitting above it.

   Ink fixes it. Same rendition the phone pill already uses, so
   this introduces no new material — only a third condition for
   an appearance the system already had.
   ============================================================ */

.nav--tube.nav--on-film {
  background: rgba(17, 18, 19, .90);
  border-color: rgba(251, 251, 249, .16);
  box-shadow: 0 10px 30px rgba(17, 18, 19, .28);
}

.nav--on-film a { color: var(--on-dark-soft); }
.nav--on-film a.is-active { color: var(--on-dark); }
.nav--on-film .nav__rule { background: var(--on-dark-rule); }
.nav--on-film .nav__lamp { background: rgba(251, 251, 249, .10); }

/* Sign in keeps its red on every ground — the point of one accent. */
.nav--on-film .nav__cta { background: var(--red); color: var(--paper); }

/* A hard swap mid-scroll reads as a flicker; the film dissolves, so
   the pill should too. */
.nav--tube {
  transition:
    background   var(--dur-cta) var(--ease),
    border-color var(--dur-cta) var(--ease),
    box-shadow   var(--dur-cta) var(--ease);
}
.nav--tube a { transition: color var(--dur-cta) var(--ease); }


/* ============================================================
   PHONE: the board drops its pile
   A phone screen is 956px at its tallest and the board's four
   beats are 750px of it, with 134px of headline above them —
   884px before a single scrap is drawn. The pile cannot join
   them on one screen at any scale worth drawing, so the section
   read as two unrelated views however far it was shrunk.

   So the phone gets the argument alone, on one screen. The pile
   is aria-hidden decoration, nothing is lost in meaning, and the
   title card already sets this precedent — it drops its still
   below 900px for the same reason. Tablets keep it: they have
   the height, and above 600px the two do share a view.
   ============================================================ */

@media (max-width: 599.98px) {
  .board .scatter { display: none; }
  /* With the pile gone the headline is no longer "right there" — the
     board takes its lead-in back. */
  .board { margin-top: var(--s-6); }
}


/* ============================================================
   THE CLOSING SECTION — THE RULED FIELD BEHIND THE WORDS
   Two problems, one motif.

   1. On a phone the home page's closing split put ten black
      rules and a red cap BELOW the argument, as a second block
      with nothing tying it to the first — the same "image and
      text as two separate sections" the board and the splits
      had, in its last remaining place.

   2. Every inner page ends on the same plain headline-lead-two
      buttons, with no visual at all.

   Answering both with the same move: the rules stop being a
   column beside the argument and become the GROUND under it.

   This is the one place the "image as background" idea needed
   adapting rather than copying. The hero's photograph could sit
   behind type at 82% veil because a photograph is continuous
   tone — it goes quiet under a wash. These rules are 5px bars
   of solid --ink; at any opacity that keeps them crisp they
   shred the text, and at any opacity that spares the text they
   are no longer the drawing. So they are not veiled, they are
   RE-WEIGHTED: dropped to the same 10% the grid texture already
   uses, where they read as paper stock rather than as a diagram.
   The red cap keeps its full strength, because one accent at
   full value is what the system does everywhere else.
   ============================================================ */

@media (max-width: 899.98px) {
  .section--closing { position: relative; }

  .section--closing .closing__aside {
    position: absolute;
    inset: 0;
    z-index: 0;
    display: flex;
    flex-direction: column;
    justify-content: center;
    opacity: .10;
    pointer-events: none;
    /* The stack is authored at a fixed pitch; stretched over the section it
       would space out into stripes. Held to its own height and centred. */
    max-width: none;
  }
  .section--closing .closing__aside .caption { display: none; }
  .section--closing .closing__aside .bar--cap { opacity: 0; }

  /* The argument rides above its own ground. */
  .section--closing .split__main { position: relative; z-index: 1; }
}


/* ---- Every page's closing, given the same ground ------------
   .page-closing had no visual at all. It gets the motif as a
   pure gradient rather than as ten more elements: the system
   already draws its grid texture this way, so this introduces a
   repeat of an existing device, not a new one.

   It DRAWS itself in. reveal.js adds .in to [data-reveal], and
   the field wipes left-to-right from nothing — the same gesture
   the stack's own r-x lines make on the home page, so the two
   closings animate identically without sharing any markup. */

.page-closing::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom,
    var(--ink) 0 5px,
    transparent 5px 16px
  );
  opacity: .10;
  /* Right half only: the copy is left-aligned and capped at 1120px, so the
     field fills the space the argument does not use. */
  -webkit-mask-image: linear-gradient(to right, transparent 0 46%, #000 62% 100%);
          mask-image: linear-gradient(to right, transparent 0 46%, #000 62% 100%);
  clip-path: inset(0 100% 0 0);
  transition: clip-path 1.1s var(--ease) 120ms;
}

.page-closing.in::after { clip-path: inset(0 0 0 0); }

/* The red cap the home page's stack ends on, so both closings resolve the
   same way. It draws after the field has arrived. */
.page-closing__inner { z-index: 1; }

.page-closing::before {
  content: '';
  position: absolute;
  right: var(--gutter);
  bottom: var(--s-7);
  width: 38%;
  height: 12px;
  background: var(--red);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform var(--dur-x) var(--ease) 620ms;
  z-index: 0;
}
.page-closing.in::before { transform: scaleX(1); }

@media (max-width: 899.98px) {
  /* On a phone the field is the whole ground, not a right-hand column. */
  .page-closing::after {
    -webkit-mask-image: none;
            mask-image: none;
    opacity: .08;
  }
  .page-closing::before { right: var(--gutter); left: var(--gutter); width: auto; }
}

@media (prefers-reduced-motion: reduce) {
  .page-closing::after  { clip-path: inset(0 0 0 0); transition: none; }
  .page-closing::before { transform: scaleX(1); transition: none; }
}


/* ---- Closing drops -----------------------------------------
   js/closing-drops.js injects the marks; this gives them the
   fall. They live inside the closing section's own box, behind
   the copy, and are clipped by it.

   The mark is 2px — the hairline width — and travels the height
   of the section, fading in over the first fifth and out over
   the last third, so nothing ever appears or vanishes at an
   edge. Opacity peaks at .22 of the rule colour: present when
   you watch for it, gone when you read. */

.drops {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
}

.drops__mark {
  position: absolute;
  top: 0;
  display: block;
  width: 2px;
  height: 18px;
  background: var(--ink);
  opacity: 0;
  will-change: transform, opacity;
  animation-name: drop-fall;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
}

.drops__mark--accent { background: var(--red); }

/* The travel distance is published by the script as --drop-h, measured from
   the section and refreshed on resize.

   It is NOT a container query. `container-type: size` would have given the
   fall a clean 100cqh to travel, and it also applies size containment — the
   section would stop being sized by its own contents and collapse to nothing.
   A decorative animation does not get to put a load-bearing property on the
   element it decorates. */
@keyframes drop-fall {
  0%   { transform: translate3d(0, -24px, 0); opacity: 0; }
  18%  { opacity: .22; }
  66%  { opacity: .22; }
  100% { transform: translate3d(0, var(--drop-h, 480px), 0); opacity: 0; }
}

/* On the inverted footer side nothing changes — the closing sections are all
   paper — but the accent must not thin out on a phone, where there are fewer
   marks in view at once. */
@media (max-width: 599.98px) {
  .drops__mark { height: 14px; }
}

@media (prefers-reduced-motion: reduce) {
  .drops { display: none; }
}
