/* ===========================================================================
   upstream-home.css — /upstream/ home page only
   ---------------------------------------------------------------------------
   Loaded after `upstream.css` by `src/upstream/index.njk`'s own `extraCss`.

   WHY THIS FILE EXISTS. Three builders are working the V1 MVP brief in
   parallel against one 4,661-line `upstream.css`, so no piece deletes from
   or edits that sheet. Everything the home page needed that the shared sheet
   did not already own lives here, and nothing here is general: every rule is
   scoped to a home-page component or to a `.spine-track` construction the
   home page is the only consumer of.

   It is deliberately short. The brief's work on this page is subtraction —
   fifteen fabricated plates, one whole product lane and the in-page subnav
   come out — and subtraction does not need new CSS. What is here is the
   three genuinely new objects (the capability band's attribution row, the
   two-up offer crossing, the prose subsection heading) plus the geometry for
   the two surviving fragment clusters.

   Token purity, per the frozen system: no raw hex, and no bare px for type
   or for anything participating in the page's vertical rhythm. Literal px
   appears nowhere in this file. `rem` is used for drawn-object dimensions —
   a logo's cap height, a plate's width, a frame's height — which is what the
   ADJUDICATED reading of the rule permits: if changing the design system's
   scale should move it, it is a token; a mark is 1.75rem tall because that
   is the right height for a mark.
   =========================================================================== */

/* ── The capability band's attribution row (brief 4.2) ────────────────────
   Four measured capability numbers ride `.bignum--quad .bignum--boxed`,
   which already exists and needs nothing. What is new is the line beneath
   them, because the brief's instruction is to name the two permissioned
   partners rather than describe them generically — and two real marks
   outperform every deleted dashboard on this page.

   The marks and the sentence are ONE object, not a logo strip with a
   caption: the row is a single flex line so the sentence reads as the marks'
   own attribution and cannot be separated from them by a reflow. Below
   560px the marks take their own line above the sentence.                  */
.cap-source {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  text-align: center;
  gap: var(--inf-space-2) var(--inf-space-3);
  /* Centred, so the row straddles the thread the way every other crossing on
     this page does. Left-aligned it put its whole height on one side of the
     line — measured at 1440, 100px of one-sided run directly under a
     full-width band, which is the defect `.spine-head` exists to fix. */
  margin: var(--inf-space-3) auto 0;
  font-family: var(--inf-font-sans);
  font-size: var(--inf-type-label-size);
  line-height: 1.6;
  color: var(--inf-fg-3);
  max-width: 64ch;
}
.cap-source-marks {
  display: flex;
  align-items: center;
  gap: var(--inf-space-3);
  flex: none;
}
/* Sized on cap height, not on box height: the two source files have very
   different aspect ratios (Ecosia 4:1, Heart 2.26:1), so a shared height
   would render one of them half the optical weight of the other. Each is
   capped on BOTH axes and `object-fit: contain` picks whichever constraint
   binds first, which lands the two marks at the same visual weight.

   `heart-browser.svg` is five white wordmark paths PLUS a full-colour
   blue-to-green gradient shield - not a white fill, as round 1's comment
   claimed. It reads on the dark ground with no filter and no plate behind it;
   `heart-browser-light.png` is the inverse treatment, for light grounds, and
   is wrong here. Neither mark is dimmed below the text it sits with - these
   are evidence, and a half-opacity logo reads as a placeholder. Both keep
   their own colour deliberately, which is why the band's NUMERALS give up the
   brand gradient rather than the marks giving up theirs. */
.cap-source-marks img {
  display: block;
  max-height: 1.75rem;
  max-width: 6.5rem;
  width: auto;
  height: auto;
  object-fit: contain;
}
@media (max-width: 560px) {
  .cap-source {
    display: block;
  }
  .cap-source-marks {
    justify-content: center;
    margin-bottom: var(--inf-space-2);
  }
}

/* ── Rationing the capability band's colour (bar mechanism 6) ─────────────
   `.bignum b` paints every figure on Upstream in the brand gradient, which is
   right where the band is the only thing on its screen. It is wrong here.
   Measured on the screen this piece added (y 900-1,800): four gradient
   numerals, plus Ecosia's green wordmark, plus the Heart mark's blue-to-green
   gradient shield - three saturated hue families against a bar that allows
   one region per screen.

   The marks are the evidence and they have to look like themselves to work as
   evidence, so the numerals give up the gradient instead. They are already
   the largest type in the band; they do not need colour as well, and the
   reference sets its own stat numerals in plain default text. Scoped to
   `--boxed`, which on this page is the capability band and nothing else, so
   the three market figures in `#numbers` keep the gradient and the page has
   exactly one gradient figure band rather than two.

   Same reset idiom as `.bignum b.pending`, which already had to escape this
   rule for a different reason. */
.bignum--boxed b {
  background: none;
  -webkit-text-fill-color: var(--inf-fg-1);
  color: var(--inf-fg-1);
}

/* The prose-crossing constructions this page introduced — `.prose-h3`, the
   two-column `.doc--two` crossing and the one-paragraph `.doc--mid` — now
   live in `upstream.css`. /fork-cost-model/ grew its own prose subsections
   (the silent-merge and mirror-branch blocks, fix list items 12 and 13), so
   they are shared Upstream components rather than home-page ones and the
   earlier note saying they were deliberately kept out of the shared sheet no
   longer holds. Nothing about their selectors or declarations changed. */

/* ── THE FACING ROW · `.spine-row--face` ──────────────────────────────────
   A third kind of row, added deliberately rather than by accident, so the
   reason is on the record.

   `upstream.css` recognises two: a PAIRING (`--card-left` / `--card-right`, a
   caption card facing product proof, one bead) and a CROSSING (a full-column
   block the thread passes behind, no bead). Round 4 removed a third kind
   because fourteen cards were pointing at headlines and paragraphs at a 286px
   pitch - a decorated list rather than a timeline.

   The MVP brief deleted fifteen of the seventeen plates and the defect
   inverted: two spine-attached cards 1,936px apart, 2.15 viewports against
   the bar's measured 0.55, and no two caption cards ever on screen together.
   There is no proof left in `#problem` or `#services` to hang a card
   opposite, so a pairing is impossible there and a lone card would strand
   itself on a one-sided thread - the failure this loop has already lost a
   verdict to. Two cards facing each other is the one construction that gives
   caption density AND two-sidedness at once: the row is two-sided over the
   whole of its height because both halves are occupied by definition.

   And it keeps the POINTER, which is what the bar's mechanism 2 actually
   checks - "≥4 caption cards; each has a visible pointer to a spine node". It
   does not require a fragment opposite. "A bead is a caption card plus product
   proof" was a previous round's internal doctrine, written when there were
   seventeen plates to point at.

   Two consumers: the pair in `#problem` (brief 4.5's decay paragraph facing
   brief 11.3's closing paragraph) and the two offers in `#services`.       */
.spine-row--face {
  /* Equal-height cards, so the two notches cannot disagree about where the
     node is. `.spine-row`'s own `center` would leave them at different y. */
  align-items: stretch;
}
.spine-row--face > :first-child {
  grid-column: 1;
  justify-self: end;
  --rv-from: var(--rv-from-left-lg);
  --rv-dur: var(--inf-dur-reveal-scale);
}
.spine-row--face > :last-child {
  grid-column: 2;
  justify-self: start;
  --rv-from: var(--rv-from-right-lg);
  --rv-dur: var(--inf-dur-reveal-scale);
  /* One increment, the law's 100ms: the left card lands, the right follows.
     Duration and easing come from the shared `[data-reveal]` transition, so
     nothing here is a new animation and nothing needs its own reduced-motion
     branch - the sheet's blanket rule already forces `--rv-from: none`. */
  --rv-delay: var(--inf-stagger);
}
/* The card takes the row's height; any commercial line stays pinned below it,
   outside the card, because the card spec is a claim and one paragraph and
   nothing else. */
.spine-row--face > .spine-stack {
  grid-template-rows: 1fr auto;
}
/* Stretched, the shorter card gains trailing space. Centring its contents
   turns that into padding rather than a gap under the last line. */
.spine-row--face > * > .spine-cap {
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* The notch, both directions. Same geometry as `upstream.css`'s pairing rules
   - the outline triangle in the card's own border colour, the fill clipped
   1px back over that border - reached one level deeper, because these cards
   live inside a stack rather than hanging off the row. */
.spine-row--face > :first-child > .spine-cap::before {
  left: 100%;
  border-left: var(--spine-reach) solid var(--cap-line);
}
.spine-row--face > :first-child > .spine-cap::after {
  left: calc(100% - 1px);
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}
.spine-row--face > :last-child > .spine-cap::before {
  right: 100%;
  border-right: var(--spine-reach) solid var(--cap-line);
}
.spine-row--face > :last-child > .spine-cap::after {
  right: calc(100% - 1px);
  clip-path: polygon(100% 0, 0 50%, 100% 100%);
}
/* ONE bead per row, carried by the left card, landing on the thread exactly
   where both notch tips finish. The right card deliberately has no
   `.spine-node`: two beads at the same point would double the purple bloom,
   and two claims arriving at one node is the gesture. */
.spine-row--face > :first-child > .spine-cap > .spine-node {
  left: 100%;
  margin-left: calc(1px + var(--spine-gutter) / 2 - var(--spine-node-size) / 2);
}

/* The fee credit sits under the price, on its own line, in the muted tier —
   it is a term of the offer, not a second price. `flex-basis: 100%` is the
   idiom `.offer-spec .more` already uses to claim a line in that flex row. */
.offer-credit {
  flex-basis: 100%;
  font-family: var(--inf-font-sans);
  font-size: var(--inf-type-label-size);
  line-height: 1.55;
  color: var(--inf-fg-3);
}

/* ── Source lines under real published data (brief 2.7) ──────────────────
   `.frag-cite` is the cluster's citation slot and its register is right —
   mono, tiny, in the fragments' own chrome voice. But it is uppercase and
   letter-spaced because its only previous job was the single word
   "Illustrative". A source line is a sentence, and a tracked-out uppercase
   sentence is shouting. Same slot, same slot's colour tier, sentence case.

   THIS IS THE OTHER HALF OF THE LABEL RULE. A fragment carrying invented
   figures keeps the label; a fragment carrying real published data must NOT
   carry it, because labelling real data as illustrative throws its value
   away. The release-schedule plate is real Chrome dates, so it gets this
   instead.                                                                 */
.frag-cite--src {
  text-transform: none;
  letter-spacing: normal;
  font-size: var(--inf-type-2xs-size);
  color: var(--inf-fg-3);
}

/* ── The two surviving fragment clusters ──────────────────────────────────
   Cluster geometry follows the positional-preset mechanism documented in
   `upstream.css`: a height, a crop distance, and a `top` plus `width` per
   plate depth, declared as custom properties on the cluster's own rule. No
   fallbacks — an absent depth simply does not set its numbers.

   Both clusters are a SINGLE plate, and both are at `.frag--front` depth
   rather than `.frag--back`. That is not cosmetic: `.frag--back` carries
   `opacity: 0.66`, which exists to push a plate behind the ones in front of
   it. With nothing in front, 66% opacity is just dim — and dimming the one
   piece of real published data on the page is the wrong instinct entirely.

   The crop is still the mechanism. Each plate runs off the frame's side
   edge, so it reads as a window onto something larger rather than a
   screenshot in a device frame — and the two crop distances and the two
   plate widths are different numbers, because a mechanism applied at one
   constant is a template.                                                 */

/* H1 · the published Chromium release schedule, in `#numbers`. Bleeds right.
   Round 4 clipped this table by the frame's TOP edge as well as its side,
   which cut off its own column header — the crop that makes a mock read as
   a window makes real data unreadable. Side crop only here, and the scroll
   box is released so all five published milestones and the header are on
   screen: this plate is evidence, and evidence has to be legible. */
.frag-h1 {
  --frags-h: 11rem;
  /* MEASURED, not chosen. `--frag-out` is exactly how many pixels of the
     plate sit past the viewport edge (the bleed calc resolves to the column
     inset plus this value), and the plate's outermost column here is `cycle`
     — the one that switches from four weeks to two, which is the entire
     reason the table is on the page. At 5.5rem the crop swallowed 88px and
     left 22px of that column on screen. At 2rem it takes 32px, which is the
     cell's own right padding and the slack the auto table layout gives it:
     visibly clipped, every figure legible. */
  --frag-out: 2rem;
  --frag-front-y: 0rem;
  --frag-front-w: 30rem;
}
.frag-h1 .frag-scroll {
  height: auto;
  overflow: visible;
}
/* ANCHORED TO THE THREAD, NOT THE VIEWPORT (V1, 20 Oct). The bleed pinned
   this plate's right edge just past the viewport's, so its distance from the
   thread grew with the screen: 77px at 1024, 205px at 1280, 525px at 1920,
   while the caption card opposite holds ~93px at every width. Detached from
   the branch it sits on, it read as floating off the page. It now hangs off
   its column's thread-side edge at one --inf-space-4 inset, which mirrors the
   caption's distance on the other side, and it is capped at the column so it
   never reaches the viewport edge: the whole table is legible, `cycle`
   column included, so `--frag-out` above no longer does anything here.
   Above the 900px collapse only; below it the plates go static and stack. */
@media (min-width: 901px) {
  .frag-h1.frags--bleed-right .frag--edge {
    right: auto;
    left: var(--inf-space-4);
    width: min(var(--frag-front-w), calc(100% - var(--inf-space-4)));
  }
  .frag-h1 + .frag-cite {
    padding-left: var(--inf-space-4);
  }
}
.frag-h1 .frag-scroll .frag-table {
  margin-top: 0;
}

/* H2 · the one illustrative fragment brief 4.5 permits, in `#problem`.
   Bleeds left. Chosen from the five candidates on grounds of narrow-column
   survival: every row is a short string, where the conflict list, the
   diffstat and both terminals are Chromium file paths and full `git`
   invocations that truncate to ellipses at 375px.

   Offset down inside its frame rather than pinned to the top, so the plate's
   own mid-line lands on the row's centre — which is where the bead is, and
   the bead has to mark the middle of the thing it names. */
.frag-h2 {
  --frags-h: 11.5rem;
  /* Same measurement, other edge. This plate bleeds LEFT, and its left edge
     is where the milestone identifiers are: at 3.25rem the crop took 52px
     and "M153" rendered as "153". At 1rem it takes 16px — the row's own left
     padding — so the plate runs off the frame with every label intact.

     THE OTHER CROP IS VERTICAL, and it is on this plate rather than the
     release schedule's for a reason: the frame has a top and a bottom, the
     bar wants at least one crop that is not a side crop, and this is the ONE
     plate on the page whose figures are invented. Cutting into a mocked
     queue costs nothing; cutting into published Chrome dates costs the thing
     that makes them checkable. `frags--vclip` (in the markup) clips
     vertically only, so the horizontal bleed survives it. The 20px it takes
     off the bottom is the body's closing padding, so the crop is legible as
     a crop without slicing a row through its glyphs. */
  --frag-out: 1rem;
  --frag-front-y: 4rem;
  --frag-front-w: 26.5rem;
}
/* ANCHORED TO THE THREAD, the mirror of `.frag-h1` above. Pinned to the
   viewport's left edge, this plate sat 117px from the thread at 1024, 245px
   at 1280 and 565px at 1920, against the caption's ~93px opposite. It now
   hangs off its column's thread-side (right) edge at one --inf-space-4
   inset, capped at the column, so the side crop and `--frag-out` no longer
   apply here; the vertical crop from `frags--vclip` is untouched. */
@media (min-width: 901px) {
  .frag-h2.frags--bleed-left .frag--edge {
    left: auto;
    right: var(--inf-space-4);
    width: min(var(--frag-front-w), calc(100% - var(--inf-space-4)));
  }
  .frag-h2 + .frag-cite {
    padding-right: var(--inf-space-4);
  }
}

/* ── Below 900px: the thread collapses, and these follow it ───────────────
   `upstream.css` collapses the whole spine at this breakpoint — line, beads,
   notches, alternation and the openers' two-column measure all go, and every
   row becomes one full-width column. The constructions added here collapse
   on the same line and by the same rule, so the page has one breakpoint and
   not two.                                                                 */
@media (max-width: 900px) {
  /* The facing rows collapse on the SHARED rule, not on their own: they are
     `.spine-row`s, so `upstream.css` already gives them one column, releases
     the max-width on their cards, hides the notches and the beads, and sets
     `order`. Three things it cannot know about are theirs alone. */
  .spine-row--face {
    row-gap: var(--inf-space-6);
    /* Stacked, a stretched card would inherit the taller card's height. */
    align-items: start;
  }
  .spine-row--face > :first-child,
  .spine-row--face > :last-child {
    grid-column: 1;
    justify-self: stretch;
    /* No side to enter from once the thread is gone: back to the vertical
       entry the rest of the collapsed page uses. */
    --rv-from: var(--rv-from-below);
  }
  .spine-row--face > * > .spine-cap {
    display: block;
  }
  /* `upstream.css` drops every fragment table's LAST column at this width,
     on the reasoning that a cropped inventory should lose a whole field
     rather than be sliced mid-glyph. Correct for a 612-file patchset
     inventory; wrong here. The release schedule's last column is `cycle`,
     and the switch from four weeks to two inside it is the entire reason the
     table is on the page, so `cycle` stays and a different field goes (see
     the rule below it). Scoped to this one cluster, which is why the shared
     rule is left exactly as it is.

     Brief acceptance criteria flag the fragment components as the likely
     375px failure point; this is that failure, found and fixed. */
  .frag-h1 .frag-table th:last-child,
  .frag-h1 .frag-table td:last-child {
    display: table-cell;
  }
  /* A FIELD STILL HAS TO GO, and it is `branch`, not `cycle`. Measured at
     375: four nowrap mono columns need ~317px of min-content against a 311px
     column, so the plate's own `overflow: hidden` cut `cycle` down to a
     single character — "4", "2", "2" — which is the sliced-mid-glyph defect
     the shared rule exists to prevent, just relocated. Dropping the branch
     date instead leaves milestone, stable date and cycle length, which is
     the whole claim, inside the column with room to spare. */
  .frag-h1 .frag-table th:nth-child(2),
  .frag-h1 .frag-table td:nth-child(2) {
    display: none;
  }
}

/* ── The thread's terminus, re-measured ───────────────────────────────────
   `.spine-line`'s own comment in `upstream.css` says `--spine-end` is the
   measured distance from the bottom of the track to the CTA panel's top
   edge, and to re-measure it if the footnote block or the CTA changes. Both
   changed: the CTA lost its closing paragraph and the footnote block went
   from four notes to three, shorter ones.

   Measured at 1440 after those cuts: track height 4,680px, CTA panel top
   389px above the track's bottom. The inherited 34rem (544px) killed the
   thread 155px short of the panel — a rule trailing off into empty column
   instead of a timeline ending on its call to action. 23rem (368px) lands
   the solid run 21px inside the panel's top edge, with the declaration's own
   1.5rem fade finishing it there.

   Only `--spine-end` is overridden, which is one of exactly two custom
   properties `upstream.css` sanctions a page for setting on this element —
   and it is scoped to `.spine-track--home`, the way that contract's own
   examples scope to `.spine-track--proof` and `.why-track`. Bounding it by
   selector rather than by which sheet happens to load is the difference
   between a page override and a global one that only looks local. */
.spine-track--home .spine-line {
  --spine-end: 23rem;
}

/* ── The terminus panel, with the argument out of it ──────────────────────
   `upstream.css` splits `.cta-panel-inner` into two halves inside the track:
   the closing argument keeps the left, the two calls to action stack in the
   right. That was the right call against a heading plus a 44-word paragraph.
   The paragraph is gone (it said what the two buttons say, and the audit
   page makes the same move at length in `When Not To Buy This`), and a
   heading alone in a half-width column left a 200px void under it inside the
   page's most important panel — the exact defect the split was introduced to
   fix, inverted.

   With one line of type and two buttons there is nothing to split. Back to
   one centred column: the heading straddles the thread, the actions sit
   under it as a row, and the thread runs into the panel's top edge and dies
   inside it with type on both sides of it all the way down. The grid is
   released rather than re-tuned, so nothing here depends on the shared
   rule's column widths.                                                    */
.spine-track .cta-panel-inner {
  /* THE HOME PAGE RELEASES THE SHARED TWO-COLUMN SPLIT. All four of the grid
     declarations the shared rule sets are reset explicitly, not just the
     `display` that makes them inert — otherwise the next builder to re-tune
     `.spine-track .cta-panel-inner`'s split sees no effect here and has to
     bisect three sheets to find out why. */
  display: block;
  grid-template-columns: none;
  text-align: center;
}
/* `.upstream-h2` carries `max-width: 22ch`, which under a released grid means
   a left-anchored box that `text-align` centres its text inside — not on the
   panel. Centre the box too, the same way `.upstream-hero--center` does it
   for the hero title. */
.spine-track .cta-panel-inner > .upstream-h2 {
  margin-inline: auto;
}
/* V1: one line under the heading saying what the call is for, now that the
   call is the panel's only exit. Centred as a box for the same reason as the
   heading above. */
.spine-track .cta-panel-inner > .cta-panel-sub {
  margin-inline: auto;
  color: var(--inf-fg-2);
}
.spine-track .cta-panel-inner > .hero-actions {
  grid-column: auto;
  grid-row: auto;
  align-self: auto;
  flex-direction: row;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  margin: var(--inf-space-3) 0 0;
}

/* ── The posture table below 700px: LEAVE IT ON THE SCROLLER ─────────────
   Brief 11.6 puts the counterweight to the Extended Stable escape hatch in
   this table's fourth column and says it must not be buried, so the obvious
   move at 375px is to take `.dtable`'s `min-width: 620px` floor off and let
   the four columns wrap into the 311px content column. That was tried and
   reverted, and the measurement is why: with the floor removed the table's
   MIN-CONTENT width is still 434px at 375 — the uppercase tracked header
   cells ("MIGRATIONS PER YEAR", "DISTANCE FROM UPSTREAM STABLE") and the
   longest words in the fourth column will not compress past it. All the
   override achieved was to swap `.tw`'s scroller for a silent clip against
   `body { overflow-x: hidden }`, which made the fourth column unreachable
   instead of merely off-screen.

   So the shared component stands: `.tw` scrolls, the reader swipes, and
   nothing is lost. Making it fit would mean re-flowing `.dtable` into
   stacked field/value rows below 700px — a real responsive-table component,
   in `upstream.css`, shared by four pages and three builders. That is a
   central change, not a home-page one, and it is reported rather than
   smuggled in here.                                                        */
