/* Graphity styles, extracted verbatim from the storyboard's src/styles.css.
   Only the rules the graphity tree actually renders, plus the custom
   properties those rules reference. src/graphity/ itself is untouched. */
/* scoped to the mount, NOT :root — the storyboard's --ink (#161616) and
   --line differ from ours and would overwrite the whole colour system. */
/* Graphity's own token names, remapped onto THIS site's palette so its type
   reads in the same green ink as every other page. The storyboard shipped a
   near-black (#161616) and warm greys; those are replaced, not overridden per
   rule, so anything downstream that reads --ink or --muted comes along too.
   Still scoped to the mount, so nothing here can reach the rest of the page. */
#graphity-root {
  --ink: #003500;                         /* was #161616 */
  --muted: rgb(0 53 0 / .72);             /* was #6b6b6b */
  --line: rgb(0 53 0 / .14);              /* was #e8e8e4 */
  --line-strong: rgb(0 53 0 / .28);       /* was #d8d6ca */
  --rubix-yellow: #FFE314;
  --rubix-yellow-rgb: 255 227 20;
  --yellow: var(--rubix-yellow);
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ─────────────────────────────────────────────────────────────────────────────
   PLATFORM SHELL (PLAT-01 / PLAT-05)

   Flat, namespaced `site-` rules serving the route shell: skip link, primary nav,
   and the main landmark. Grouped with the shell chrome that already exists above,
   and deliberately placed BEFORE the graph rules that follow.

   ══ GRAPHITY PROTECTION — TWO INDEPENDENT PROHIBITIONS ══

   The graph timeline pins its stage with `pin: stage, pinSpacing: false`, which
   means the stage becomes `position: fixed` for the whole pinned range. Everything
   below follows from that, and the two rules have DIFFERENT causes. Both must stay
   recorded separately, so that disproving one can never justify deleting the other.

   (1) FIXED-POSITIONING CONTAINING BLOCK. On the shell root, on `#main`, or on any
       new ancestor of the graph subtree, each of these creates a containing block
       for `position: fixed` descendants: `transform`, `translate`, `rotate`,
       `scale`, `filter`, `backdrop-filter`, `perspective`, `will-change` naming
       any of those, `contain: layout|paint|strict|content`, and
       `content-visibility` at ANY non-`visible` value (both `auto` AND `hidden`).
       The pinned stage would then position against that ancestor instead of the
       viewport and would SILENTLY DETACH — nothing throws, nothing warns, the
       stage simply stops sticking. IDENTITY VALUES TRAP EXACTLY AS HARD:
       `translate: 0px`, `rotate: 0deg`, `scale: 1`, `filter: opacity(1)` and
       `filter: blur(0px)` were each measured to trap in Chromium, Firefox and
       WebKit. `container-type` measured SAFE in all three engines but stays on the
       prohibition list as a precaution, because css-contain-3 says
       `container-type: size` computes to layout containment and engines may yet
       align with the spec.

   (2) SCROLL CONTAINER. `overflow: hidden|auto|scroll` was measured NOT to create
       a containing block — it does not trap fixed positioning at all. It is
       prohibited for the entirely separate reason that it turns the ancestor into
       a scroll container, which breaks the scroller assumptions the pin is built
       on. Do not delete this prohibition after "disproving" prohibition (1).

   `display: flex` and `display: grid` are additionally prohibited on shell
   ancestors throughout this phase as a precaution around pin measurement. That is
   why the near-bottom footer below is produced by a `min-height` calc rather than
   by the usual flex sticky-footer pattern, and why nothing here introduces a new
   wrapper element.

   THERE IS DELIBERATELY NO RULE FOR THE SHELL ROOT ELEMENT. The AppShell root
   div — the outermost ancestor of the graph subtree — is left entirely unstyled.
   That is the simplest available guarantee that it can never acquire a containing
   block or a scroll container, and it is checked by a source gate.

   Regression net: tests/site/graphity-lifecycle.spec.js "pin stays attached to the
   viewport", which asserts the pinned stage's getBoundingClientRect().top stays
   within 1px of 0 — at desktop AND at 320x640, so an override added inside a media
   query is caught too.

   ══ ZERO MOTION ══

   No `transition` and no `animation` appears in any shell rule in this file — not
   on the skip-link reveal, not on nav hover, not on the active underline, not on
   focus. Shell meaning is therefore identical with and without
   `prefers-reduced-motion: reduce` BY CONSTRUCTION, which is why the
   reduced-motion block below gains no shell rule. Adding one would imply motion
   that does not exist.
   ───────────────────────────────────────────────────────────────────────────── */

/* The bypass block (WCAG 2.4.1). It must be reachable AND fully visible from any
   scroll offset — Shift+Tab from mid-page — which an absolutely-positioned
   document-top link would fail, so it is `position: fixed`.

   THE `transform` HERE IS SAFE, AND THE REASON IS SPECIFIC: this link is a SIBLING
   of the graph subtree, not an ancestor of it. The containing block it creates is a
   containing block for nothing that matters — it has no fixed-position descendants
   of its own. The prohibition above applies to ANCESTORS of the pinned stage, and
   this element is not one.

   `z-index: 100` deliberately sits above the pinned stage, which pins at
   `z-index: auto`. Without it a focused skip link could be painted underneath the
   stage on the home route.

   NEVER hide this with `display: none`, `visibility: hidden`, or an `.sr-only`
   clip. The first two remove focusability outright; the clip technique keeps it
   focusable but prevents it from ever becoming fully visible when focused, which is
   the whole point of a bypass block. Off-screen translation is the correct
   mechanism because focus restores it to a real, on-screen, hit-testable box.

   No `transition` on the reveal, by design — see ZERO MOTION above. */
.site-skip-link {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 100;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 8px 16px;
  border: 1px solid var(--line);
  background: #fff;
  color: var(--ink);
  font-size: 15px;
  font-weight: 520;
  letter-spacing: -0.01em;
  text-decoration: none;
  transform: translateY(-120%);
}

.graphity { position: relative; background: #fff; }

/* `.graphity-entry p` was deleted 2026-08-06 with the standfirst it styled
   (GraphityExperience.jsx). The entry frame now carries its h1 alone. */
.graphity-scroll { position: relative; height: var(--desktop-scroll); }

.graphity-stage {
  position: relative;
  width: 100%;
  height: 100svh;
  overflow: hidden;
  background: #fff;
  isolation: isolate;
}

.stage-wash {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(circle at 48% 48%, rgb(var(--rubix-yellow-rgb) / 0.055), transparent 28%),
    linear-gradient(180deg, #fff 0%, #fffefa 50%, #fff 100%);
}

.graph-scene { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }

.camera { transform-box: fill-box; }

.graph-scene line,
.graph-scene path { vector-effect: non-scaling-stroke; }

.latent-relationship {
  stroke: var(--rubix-yellow);
  stroke-width: 0.8;
  fill: none;
}

.latent-cell rect {
  fill: var(--rubix-yellow);
  stroke: var(--rubix-yellow);
  stroke-width: 0.85;
}

.latent-cell--connected rect {
  fill: var(--rubix-yellow);
  stroke: var(--rubix-yellow);
  stroke-width: 1.2;
}

.opening-cell rect {
  fill: var(--rubix-yellow);
  stroke: var(--rubix-yellow);
  stroke-width: 0.9;
}

rect.opening-cell--bridge {
  fill: var(--rubix-yellow);
  stroke: none;
}

.active-relationship,
.programmable-link,
.ecosystem-relationship {
  fill: none;
  stroke: url(#activeYellow);
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* Single activation resting tier (D-07). The provenance tier (stroke-width 1.35,
   stroke-opacity 0.5) is written as GSAP inline styles by the Plan-04 demotion
   tweens — inline-style-wins cascade, never a class swap. */
.active-relationship { stroke: var(--rubix-yellow); stroke-width: 2.5; }

/* Ecosystem resting tier clones .active-relationship (D-07). Provenance/context tier
   arrives at runtime via demotePath inline styles (Plan 12-04/05), never a class swap. */
.ecosystem-relationship { stroke: var(--rubix-yellow); stroke-width: 2.5; }

.annotation-stem {
  fill: none;
  stroke: rgb(0 53 0 / .28);
  stroke-width: 0.9;
}

.token-annotation text { fill: rgb(0 53 0 / .68); font-size: 10px; }

.token-annotation .annotation-title { fill: var(--ink); font-size: 12px; font-weight: 700; letter-spacing: 0.02em; }

.structure-label,
.ecosystem-destination text,
.mark-label {
  fill: var(--ink);
  font-family: inherit;
}

.programmable-link { stroke: rgb(var(--rubix-yellow-rgb) / 0.82); stroke-width: 1.65; }

/* Full canonical-yellow default fill shared by every programmable structure cell
   (UI-SPEC Delta 3 / §9 — the pale translucent grid treatment is prohibited). */
.structure-cell rect { fill: var(--rubix-yellow); stroke: rgb(var(--rubix-yellow-rgb) / 0.88); stroke-width: 0.9; }

.structure-cell--identity rect { fill: #fff; stroke: var(--rubix-yellow); stroke-width: 1.35; }

/* The SC bridge cell deliberately shares the .structure-cell default treatment (the identity
   cell is the ONLY differentiated role, D-07/§9) — no .structure-cell--bridge override exists
   (IN-05 review fix: the old rule duplicated the default fill and read as a distinct treatment). */
.structure-label { font-size: 12px; font-weight: 700; }

.graphity-debug__bounds { fill: none; stroke: #ef3e8d; stroke-width: 0.7; stroke-dasharray: 2 2; }

.graphity-debug__anchor { fill: #00a3ff; stroke: #fff; stroke-width: 0.5; }

/* IN-03: boundary-trimmed emitted path endpoints (D-13) — distinct from the blue reference anchors. */
.graphity-debug__endpoint { fill: #17c964; stroke: #fff; stroke-width: 0.5; }

.graphity-debug__label { fill: #8a1450; font: 7px/1 ui-monospace, monospace; }

.destination-formation rect { fill: var(--yellow); }

.ecosystem-destination text,
.mark-label { font-size: 14px; font-weight: 660; }

.graphity-entry {
  position: absolute;
  z-index: 3;
  top: 13vh;
  left: 6vw;
  /* wide enough for "The Graph of Everything" on one line under the wordmark
     at the type scale's 76px ceiling; 410px forced it into three broken lines
     once Rubix took its own row. The title sits above the mark at every
     viewport, so the extra width crosses empty field, not content. */
  width: min(940px, calc(100vw - 48px));
  opacity: 1;
  pointer-events: none;
  transform: translateY(0);
}

/* The lockup: the wordmark leads, big and heavy; the tagline runs beneath at
   natural width in a lighter weight, and its first line — "The Graph" — sits
   in a solid yellow cell with green type: the one pairing where yellow is
   allowed to lead (10.8:1, the nav CTA's treatment), squared to the
   modular-cell radius. The graph itself is the thing made of yellow material,
   so it is the phrase that earns the cell. */
.graphity-entry h1 {
  margin: 0;
  color: var(--ink);
  /* base scale = the wordmark; the tagline steps down from it */
  font-size: clamp(42px, 5.3vw, 76px);
  font-weight: 640;
  letter-spacing: -0.052em;
  line-height: 0.98;
}
/* Squared: each line's size is solved so all three — the wordmark, the yellow
   cell's OUTER edge, and "of Everything" — run to the same width and the
   lockup reads as one block. Ratios are measured from Inter's rendered widths
   (box padding included); re-measure if any line's text changes. */
.gt-l1, .gt-l2, .gt-l3 { display: block; width: fit-content; }
/* letter-spacing restated here: an em letter-spacing computes to fixed px at
   the level it is declared, so the h1's value would not scale with this line */
.gt-l1 { font-size: 1.517em; letter-spacing: -0.052em; }
.gt-l2, .gt-l3 { font-weight: 480; letter-spacing: -0.03em; }
.gt-l2 { font-size: 0.909em; margin-top: 0.24em; }
.gt-l3 { font-size: 0.78em; margin-top: 0.14em; line-height: 1.04; }
.graphity-title-box {
  display: inline-block;
  padding: 0.04em 0.2em 0.1em;
  border-radius: 0.12em;
  background: var(--yellow);
  color: var(--ink);
}


.narration-layer { position: absolute; inset: 0; pointer-events: none; }

.narration {
  position: absolute;
  z-index: 2;
  width: min(490px, calc(100vw - 48px));
  opacity: 0;
}

.narration p {
  margin: 0;
  color: var(--ink);
  font-size: clamp(34px, 4.25vw, 68px);
  font-weight: 520;
  line-height: 0.98;
  letter-spacing: -0.052em;
}

/* Each narration sentence renders on its own line (split after every period). */
.narration-line { display: block; }

.narration--potential { left: 6vw; top: 13vh; width: 410px; }

.narration--activity { right: 7vw; top: 15vh; width: 370px; }

.narration--provenance { left: 6vw; bottom: 12vh; width: 460px; }

.narration--ownership { right: 5.5vw; top: 11vh; width: 440px; }

.narration--structures { right: 6vw; top: 13vh; width: 400px; }

.narration--graphity { left: 6vw; top: 11vh; width: 450px; }

.narration--ecosystem { left: 50%; bottom: 7.5vh; width: 760px; transform: translateX(-50%); text-align: center; }

.narration--ecosystem p { font-size: clamp(42px, 5.3vw, 82px); }

.scroll-cue {
  position: absolute;
  left: 50%;
  bottom: 28px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  transform: translateX(-50%);
  color: rgb(0 53 0 / .55);
  font-size: 10px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}

.scroll-cue i { position: relative; width: 1px; height: 30px; background: rgb(0 53 0 / .22); overflow: hidden; }

.scroll-cue i::after { content: ""; position: absolute; top: 0; left: 0; width: 1px; height: 12px; background: var(--yellow); animation: cue 1.8s ease-in-out infinite; }

@media (max-width: 1024px) {
  /* ══ NO `.site-header`/`.site-nav` OVERRIDE LIVES IN THIS BLOCK ANY MORE (quick task 260803-1xk,
     Task 6) ═══════════════════════════════════════════════════════════════════════════════════════
     Every override this block used to carry for the header/nav (the 102px tablet height, the
     `display: contents` nav-dissolving technique, the disclosed-list flex-basis wrap) governed the
     Variant A grouped header this task retires outright. `SiteHeader` is now a bare `position: sticky`
     shell with no explicit height of its own (`styles.css`'s `.site-header` base rule) — it is a plain
     block box whose height is simply its content's own intrinsic height, so it grows to fit the
     capsule automatically at every width, with nothing to override here. `SiteNav`'s own capsule
     carries its ENTIRE responsive contract in its own `@media (max-width: 800px)` block, immediately
     following its base rules above — a width band deliberately independent of this 1024px shell
     breakpoint (this component's own locked value, transcribed from the sketch). */
  .site-footer { flex-wrap: wrap; }
  .site-footer__list { justify-content: flex-start; }
  .graphity-scroll { height: var(--tablet-scroll); }
  .density-desktop { display: none; }
  .narration p { font-size: clamp(32px, 5.6vw, 54px); }
  .narration--ownership { top: 9vh; right: 5vw; }
  .narration--ecosystem { bottom: 8vh; }
  /* §19: "Offices drop to 1 column" at tablet, ahead of the mobile breakpoint. */
  .office-list { grid-template-columns: 1fr; }
  /* §17's tablet row for Sections 1-4: `min-height` drops to `auto` — never `height`, never
     `overflow`, never `aspect-ratio`. Neither section carries a `grid-template-columns` override any
     more (quick task 260803-1xk, Task 7) — both are plain flow blocks at every width now, so there is
     no column split left to collapse here. */
  .home-graph { min-height: auto; }
  .home-enable { min-height: auto; }
  .home-domains { min-height: auto; }
  .home-domains__intro { grid-template-columns: 1fr; }
  /* STACKED, THE TWO ITEMS ARE NO LONGER SIDE BY SIDE, SO THE SHARED TOP EDGE STOPS
     BEING THE GOAL AND `margin-top: 0` BECOMES A DEFECT. The base rule zeroes the
     lead's margin so it can start level with the title in the two-column row above
     1024px; in one column that same zero puts the paragraph flush against the
     descenders of a heading set at `line-height: 0.98`, which reads as overlap even
     where the boxes do not actually intersect. `.home-lead`'s own 24px is restored
     here — the DOM order is already title-then-lead and is unchanged, so this is the
     only thing the stacked case needs. */
  .home-domains__intro .home-lead { justify-self: start; margin-top: 24px; }
  .home-domains__grid { grid-template-columns: 1fr; }
  /* The vertical rules become horizontal ones, which is the whole reason they are
     `border-left` on the article rather than an absolutely-positioned spine. */
  .home-domain { min-height: 400px; }
  .home-domain + .home-domain { border-left: 0; border-top: 1px solid var(--line); }
  .post-story { min-height: auto; }
  .home-proof__row--stats,
  .post-paths,
  .home-proof__row--universe { flex: none; min-height: auto; }
  /* The band's two columns stack, and the four KPI cells become 2x2. All four cells
     still RENDER in every arm — the layout changes, the cell count never does. `.post-paths`
     collapses the same way its retired `.home-proof__row--actions` predecessor did: kicker/heading
     above, the Xell/Build/Learn action cluster below. */
  .home-proof__row--stats,
  .post-paths,
  .home-proof__row--universe { grid-template-columns: 1fr; }
  .home-stats__row { grid-template-columns: repeat(2, 1fr); }
  .home-stats__cell:nth-child(2) { border-right: 0; }
  .home-stats__cell:nth-child(-n+2) { border-bottom: 1px solid var(--line); }

}

@media (max-width: 640px) {
  /* RE-MEASURED 2026-08-02 against the built site, Chromium, /about, after the
     single-row closed header and the 2x2 footer navigation landed. Measured at
     640x800, 640x256, 500x800, 390x844, 320x640 and 320x256 — both values are FLAT
     across the entire ≤640px band:

       header  62px, DOWN from 102px. The 102px stood for a two-row closed header:
               a brand row, then a wrapped row holding only the Menu button. With
               `.site-nav { display: contents }` at tablet the brand and the Menu
               control share row 1 and the closed header is one row, so the 62px
               `min-height` below is now BOTH the floor and the rendered height.
       footer  227px, UP from 182.08px. The four links used to fit on ONE wrapped
               row at these widths — measured, they occupy ~234px of a 346px content
               box at 390px — so the old 183px was the height of a one-row footer
               that merely looked like a deliberate composition. The 2x2 grid states
               the composition and costs a second 32px row plus its 10px gap.

     THE TWO TOKENS MOVED IN OPPOSITE DIRECTIONS, AND ONLY ONE OF THEM WAS EVER
     DANGEROUS. A token that UNDERSTATES its region makes `#main`'s min-height
     overshoot and gives every short route dead scroll below its own footer; a token
     that overstates merely seats the footer a little high. So the footer's 183px →
     227px is the correction that had to happen in this edit or ship a 44px defect,
     while the header's 102px → 62px is only recovering 40px of slack that stopped
     being real when the second row went away. Both are re-measured rather than
     reasoned, because the note on the `:root` declarations requires it.

     62px IS NOW A FLOOR THAT IS ALSO THE HEIGHT, and that coincidence is worth
     naming: reading the floor as the height is what shipped 40px of dead scroll here
     once already. They agree today because the closed header's natural content is
     ~45px and the floor is what makes up the difference — if the brand or the toggle
     ever grows past 62px, these two quantities separate again and the token must be
     re-measured, not inferred from the `min-height` below. */
  :root { --shell-footer-h: 227px; }
  /* ══ `--shell-header-h` NO LONGER CHANGES HERE (quick task 260803-1xk, Task 6) ══════════════════
     It used to drop to 62px at this breakpoint, for a two-row-then-one-row closed header whose
     total height genuinely shrank at narrower widths. That is no longer true of anything: the
     Sliding Library capsule's own closed head row stays a fixed 56px at EVERY width (the sketch's
     own locked value, unchanged by its `@media (max-width: 800px)` rule), and `SiteHeader`'s own
     16px top padding is likewise constant — so the closed header's total height (16 + 56 = 72px) is
     breakpoint-invariant, and the root's 72px token already covers every width without an override
     here. `SiteHeader` itself also needs no override in this block: it is a bare, unstyled-height
     block box (see its own base rule) whose real height already tracks its content exactly, at
     every width, with nothing to re-measure. */
  .site-header__note { display: none; }
  /* THE MOBILE WORDMARK — 120x35, the owner's specified mobile size, against the
     header's 137x40. `object-fit: contain` is inherited from the base rule and is
     what makes this safe: 120/35 is 3.4286:1 while the asset is 1829:535 = 3.4187:1,
     so setting both dimensions without it would distort the artwork by a third of a
     percent horizontally. It letterboxes instead — the rendered mark is 119.65px
     wide inside a 120px box. Both dimensions are set rather than `height: auto`
     because the 62px header height above is measured against a KNOWN box; an
     intrinsic height would resolve to 35.10px and put a token measurement at the
     mercy of the asset's aspect ratio. */
  .site-wordmark { width: 120px; height: 35px; }
  /* 22px, NOT the scale's 24px. This is the shipped `.site-footer` mobile gutter
     (see `.site-footer`'s mobile `padding: 34px 22px`), so adopting it keeps the main landmark
     edge-aligned with the footer on every mobile page. A 24px page gutter against a
     22px footer gutter would ship a visible 2px misalignment on every mobile page.
     Recorded in the approved contract's exceptions ledger — and note the direction
     of the fix: the page primitives adopt the shipped value, rather than the shipped
     footer being retro-edited to 24px outside this phase's remit. */
  .page-intro { padding: 64px 22px 64px; }
  .page-recovery { padding: 48px 22px 64px; }
  /* §10.3's rendered-size table, second row: at ≤640px the domain mark drops its 420px ceiling and
     fills the gutter-constrained band width instead. */
  .relmap--domain { max-width: 100%; }
  .graphity-scroll { height: var(--mobile-scroll); }
  .graphity-stage { height: 100svh; }
  .density-tablet,
  .density-desktop { display: none; }
  .graph-scene { width: 148%; left: -24%; }
  .graphity-entry { top: 9vh; left: 18px; width: calc(100vw - 36px); }
  .graphity-entry h1 { font-size: clamp(34px, 10.5vw, 48px); line-height: 1.02; }
  .narration { width: calc(100vw - 36px); left: 18px; right: auto; top: 9vh; bottom: auto; transform: none; text-align: left; }
  .narration p { font-size: clamp(34px, 10.5vw, 48px); line-height: 1.02; }
  .narration--provenance { top: auto; bottom: 9vh; }
  .narration--ownership { top: 7vh; width: calc(100vw - 36px); }
  .narration--ecosystem { top: auto; bottom: 5.5vh; text-align: center; }
  .narration--ecosystem p { font-size: clamp(37px, 11.4vw, 50px); }
  .scroll-cue { bottom: 18px; width: calc(100vw - 36px); }
  .scroll-cue span { display: block; white-space: nowrap; }
  .site-footer { align-items: flex-start; flex-direction: column; min-height: 145px; padding: 34px 22px; }
  /* THE FOOTER NAVIGATION GOES FULL WIDTH AND ITS FOUR LINKS BECOME A 2x2 GRID.
     `.site-footer` is `flex-direction: column` here, so the `<nav>` is a column item and its
     default `align-items: flex-start` shrinks it to its content — hence the explicit `width: 100%`
     rather than relying on a stretch that this block has already turned off.

     A GRID, NOT THE INHERITED WRAPPING FLEX. `.site-footer__list`'s base `flex-wrap: wrap` would
     also produce two rows at this width, but only by accident of the four labels' measured widths:
     "Insights · About · Contact · Careers" happen to break 2/2 at 390px and would break 3/1 or 1/3
     the moment a label changed length or the gutter moved. `repeat(2, 1fr)` states the composition
     instead of arriving at it, and the two columns are equal so the second column's links align
     with each other rather than sitting wherever the first column's text ended. The link set is
     derived from the route manifest (see `SiteFooter.jsx`) — this rule must keep working for four
     entries whatever they are called. */
  .site-footer__nav { width: 100%; }
  .site-footer__list { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px 22px; }
  .ecosystem-destination[data-destination="learn"] { transform: translate(600px, 125px); }
  .ecosystem-destination[data-destination="explorer"] { transform: translate(835px, 290px); }
  .ecosystem-destination[data-destination="universe"] { transform: translate(365px, 430px); }
  .ecosystem-destination[data-destination="build"] { transform: translate(600px, 590px); }
  /* §19: controls reflow to full width; the office line wraps rather than truncating (no
     `text-overflow`, no `nowrap` anywhere in this contract). */
  .contact-fields { max-width: 100%; }
  .contact-field__control { max-width: 100%; }
  .office-item__line { max-width: 100%; }
  /* The shipped `.site-footer` mobile gutter (see the `.page-main` mobile comment above),
     inherited so the six post-Graphity chapters stay edge-aligned with the footer. */
  .home-graph,
  .home-enable,
  .home-domains,
  .home-proof__row--stats,
  .post-paths,
  .home-proof__row--universe { padding-inline: 22px; }
  /* ══ VARIANT A'S MOBILE TITLE OVERRIDE, RESTORED FOR BOTH ROLES ═════════════════════
     The reference collapses `.display-title` and `.section-title` to ONE size below this
     breakpoint — the three-deep desktop step is a desktop device, and at 390px there is
     not enough width for three distinguishable heading sizes to read as a hierarchy
     rather than as inconsistency.

     THE VIEWPORT TERM IS THE WHOLE POINT, AND IT IS WHY THE DESKTOP CLAMPS CANNOT JUST BE
     LEFT TO RUN DOWN. `14vw` is more than double the display role's own `6.5vw`, so at
     390px this resolves to 54.6px where the unoverridden Section 1 clamp would give 48px
     and Section 2/3 would give 44.8px — the mobile heading is LARGER than what the
     desktop rule degrades to, not smaller, and the two roles land on one value instead of
     two. Reading this as a shrink and dropping it is how the mobile frames lose their
     opening statement.

     ONLY THE CEILING DOES WORK INSIDE THE SUPPORTED RANGE. `4.4rem` (70.4px) binds from
     503px up (70.4 / 0.14), so the type stops climbing well before the breakpoint hands
     over at 640px — without it a 640px viewport would render an 89.6px title. The
     `2.6rem` floor (41.6px) does NOT bind anywhere this site supports: `14vw` alone
     reaches 41.6px at 297px, and the narrow floor is 320px, where `14vw` gives 44.8px.
     It is transcribed from the reference and kept as the backstop it is — not deleted as
     dead, and not described as load-bearing when it is measured slack.

     BOTH ROLES, ONE DECLARATION — and they are listed as two selectors rather than
     merged into the desktop rules because the SPLIT between them is a desktop contract
     (see the note on `.home-display-title` above) that this block deliberately suspends
     rather than deletes. `max-width` is untouched here: `920px` and "uncapped" are both
     wider than any viewport in this block, so neither binds. */
  .home-display-title,
  .home-section-title { font-size: clamp(2.6rem, 14vw, 4.4rem); }
  /* The concept row's third column moves under the first two rather than squeezing. */
  .home-concept { grid-template-columns: 28px 1fr; gap: 12px; }
  .home-concept__body { grid-column: 2; }
  .home-panel--grid,
  .home-panel--causal { min-height: 440px; }
  /* §10.3: context cells carry no unique meaning and are hidden here to reduce density — this
     override lives only inside this ≤640px block, never at base. */
  .relmap__cell--context { display: none; }
  /* THE KPI CELLS STAY 2x2 HERE — they fall to one column only at 320px, in the
     narrow block below. `va-mobile-5.png` shows a 2x2 grid at 390px, so collapsing
     at this breakpoint would diverge from the approved mobile reference. */
  /* THE TILE PAIR STACKS AND KEEPS ITS SHARED BORDER BOX — the inner divider becomes
     a horizontal rule. Both `<a>` stay direct children of the tile group inside the
     actions band, so the two-CTA count holds and the stacked pair satisfies the
     separation assertion on its non-overlapping-vertically branch. */
  .home-tiles { grid-template-columns: 1fr; }
  .home-tile + .home-tile { border-left: 0; border-top: 1px solid var(--line-strong); }
  /* THE CTA STAYS COMPACT HERE — `width: 100%` is DELETED, not overridden elsewhere.
     Variant A's mobile frame draws this as a small yellow button sitting at the left edge
     of the stacked band, not as a full-bleed bar. Stretched to the gutter it became the
     single heaviest object on the mobile page and outweighed the band heading above it,
     which inverts the band's own hierarchy.

     `justify-self: start` IS WHAT REPLACES IT, AND IT IS NOT THE SAME AS SIMPLY DROPPING
     THE WIDTH. The band's `grid-template-columns` collapses to `1fr` in the ≤1024px block
     above, so every child is a grid item in a full-width track; the default `stretch`
     would pull the CTA back to full width on its own. The base rule's `inline-flex` only
     governs the CTA's INTERNAL layout, not how the grid sizes it. */
  .home-universe-cta { justify-self: start; }
  .home-stats__cell { border-right: 0; border-bottom: 1px solid var(--line); }
  .home-stats__cell:last-child { border-bottom: 0; }
}

@media (max-width: 640px) and (max-height: 620px) {
  .graphity-entry { top: 18px; left: 18px; width: calc(100vw - 36px); }
  .graphity-entry h1 { font-size: clamp(30px, 9vw, 40px); }
  .scroll-cue { bottom: max(12px, env(safe-area-inset-bottom)); }
}

@media (prefers-reduced-motion: reduce) {
  /* THE SHELL INTRODUCES EXACTLY ONE TRANSITION, AND IT IS DECLARED IN THIS BLOCK.
     This note previously read "NO SHELL RULE BELONGS HERE ... the shell introduces zero
     animation and zero transition". That was true through Phase 21 and stopped being true
     when the grouped header landed: Variant A's nav group buttons carry a 2px yellow
     underline affordance drawn on a `::after` and revealed with `transform: scaleX()`,
     which is a transition on a shell control.

     The underline is NOT removed under reduced motion — it is the non-colour-only
     affordance `tests/site/a11y-shell.spec.js` requires, so removing it would trade a
     motion concern for an accessibility regression. It is made INSTANTANEOUS instead.

     THE RULE FOR ANY FUTURE SHELL TRANSITION IS THE SAME: declare it here, in the same
     edit that introduces it. There is no gate on this — `check:site` cannot see a missing
     reduced-motion override — so an omission fails silently, which is why the requirement
     is written down rather than assumed. Assert it by reading the PSEUDO-ELEMENT
     (`getComputedStyle(el, "::after").transitionDuration`), never its owner: the owner's
     own duration is `0s` whether or not the override exists, so an assertion on the owner
     proves nothing. */
  .site-nav__group::after { transition: none; }
  .scroll-cue i::after { animation: none; transform: translateY(12px); }
  .direction-pulse { display: none; }
  /* THE CAPSULE'S CONTAINER/PANEL TRANSITIONS (quick task 260803-1xk, Task 6 — supersedes the
     `.post-nav` rule this block used to carry). Every state change becomes instantaneous while the
     SAME expanded/collapsed end-state (`grid-template-rows`/`border-radius`/`box-shadow`, and the
     panel's own `opacity`/`transform`/`padding`) is preserved — `transition: none` jumps straight to
     whatever value the state selector already resolves to, it does not change what that value is. */
  .site-nav,
  .site-nav__panel { transition: none; }
  /* THE SLIDING LIBRARY'S ONE SLIDE-SWITCH ENTRANCE ANIMATION — declared in the same edit that
     introduces it, per this block's own stated rule above. `animation: none` jumps straight to the
     animation's own end-state (`opacity: 1; transform: none`), since `@keyframes site-nav-slide-in`'s
     `to` block is already the element's fully-settled resting appearance — it does not change what
     that state is, only removes the motion getting there. */
  .site-nav__slide--active { animation: none; }
  /* Plan 21.1-03 (§11.3): the desktop scene and the mobile/tablet figures never carry a CSS
     `transition` of their own — every state change GSAP ever makes is set as an inline style, and
     `usePostGraphityMotion.js`/`PostGraphityFigure.jsx` both check this SAME media feature in
     JavaScript and skip building any GSAP timeline entirely under reduced motion, so no interpolation
     ever runs to make instantaneous. This rule is the defensive, explicit guarantee anyway — matching
     every other override in this block — so a future CSS transition added to either selector is
     instantaneous by construction rather than by an implicit absence. The deep-green Section 6 field
     is covered by the same rule: with no GSAP timeline built, `.post-scene__field` never receives an
     inline `fill` tween in the first place, so its CSS-default resting colour renders with no tween
     of any kind. */
  .post-scene__group,
  .post-scene__field,
  .post-figure { transition: none; }
}

/* ═══ MOBILE FIT ADJUSTMENTS ═══════════════════════════════════════════════
   Appended as overrides rather than edited into the extracted rules above, so
   re-extracting from the storyboard cannot silently drop them.

   The storyboard positions this text against its own fixed header. Ours is a
   floating capsule that sits lower (bottom edge ~70px), and we also park a
   Skip pill at the bottom-right — so the top text needs more clearance than
   9vh gives, and the bottom text needs to sit above the pill and scroll cue. */
@media (max-width: 640px) {
  /* clear the capsule: 9vh is 76px at 844 tall, and the nav ends at ~70px */
  .graphity-entry,
  .narration { top: max(14vh, 112px); }
  .narration--ownership { top: max(12vh, 100px); }

  /* clear the scroll cue AND the Skip pill, so nothing is cut off at the base */
  .narration--provenance { top: auto; bottom: max(16vh, 128px); }
  .narration--ecosystem { top: auto; bottom: max(14vh, 116px); }
  .scroll-cue { bottom: 78px; }
}
