/* Galahad, sections.css
   Motion and material for the sections below the fold: the three Capabilities
   diagrams, the Security cards, the FAQ, the demo band and the contact block.

   Served as-is from public/, loaded last so it outranks index.html's <style>
   block on the few classes it takes over. Owned by parts.jsx.

   ONE MECHANISM RUNS EVERY DIAGRAM. motion.jsx's useScrollProgress writes a
   single number, --p, on a scene root. Custom properties inherit, so every
   stroke, pill and label inside the scene reads that same number and derives
   its own window from it with --t0 (where it starts) and --span (how much of
   the scrub it takes). Nothing runs per frame in JavaScript, the whole scene
   scrubs backwards as readily as forwards, and the stylesheet default of --p:1
   is the FINISHED diagram: a page whose script never ran still shows the
   argument rather than an empty rectangle.

   Palette is strictly monochrome: ink, paper, opacity. No hue anywhere. */

/* ============================================================================
   1. THE SCRUB PRIMITIVE
   ========================================================================= */

.cap-scene { --p: 1; }

/* --k is the house curve's shape as arithmetic: fast out, long settle. Cheaper
   than a bezier solve and indistinguishable at this size. */
.cap-el {
  --v: clamp(0, (var(--p, 1) - var(--t0, 0)) / var(--span, 1), 1);
  --k: calc(1 - (1 - var(--v)) * (1 - var(--v)) * (1 - var(--v)));
  /* --k0 is the same curve lifted off zero, and it is what the STRUCTURE reads:
     the board wash, the entities, the minor plane, the markers. They sit at 0.28
     before the scrub has moved, so a panel that has just entered the viewport
     already shows its skeleton and scrolling DEVELOPS it. On --k alone every
     layer was at opacity 0 at --p 0, and each scene spent the first third of its
     travel showing nothing at all. Same principle as scene-problem.css, whose
     defaults are the resolved graph for exactly this reason.
     What ARRIVES stays on --k: the relations, the verbs, the action column, the
     simulated outcomes and the pulses. */
  --k0: calc(0.28 + 0.72 * var(--k));
}

/* ============================================================================
   2. THE SCENE BOX
   ========================================================================= */

.cap-scene {
  /* ink / paper as raw channels so every alpha below is one rgba() away */
  --cap-ink: 10, 10, 9;
  --cap-paper: 250, 250, 249;

  /* the product's own graph-node material (NetworkGraph.tsx), the same values
     the fragmentation scene carries, so the two read as one system */
  --cap-fill: linear-gradient(180deg, #ffffff 0%, #fafaf9 100%);
  --cap-hair: inset 0 0 0 0.5px rgba(15, 15, 15, 0.13);
  --cap-sheen: inset 0 1px 0 rgba(255, 255, 255, 0.60);
  --cap-lift: 0 1px 2px rgba(15, 15, 15, 0.07), 0 5px 14px rgba(15, 15, 15, 0.09);
  --cap-gloss: linear-gradient(180deg, rgba(255, 255, 255, 0.16) 0%, rgba(255, 255, 255, 0) 48%, rgba(0, 0, 0, 0.05) 100%);

  position: relative;
  width: 100%;
  /* Sized off its own inline size, not the viewport: the panel is half a row on
     a wide screen and the full row on a narrow one, and vw cannot tell those
     apart. */
  container-type: inline-size;
  height: clamp(250px, 64cqi, 336px);
  isolation: isolate;
}

/* Narrow panel: the same diagram, re-authored upright, in a taller box. Shorter
   than it was, because the graph inside it now reaches the frame. */
.cap-scene--compact { height: clamp(330px, 126cqi, 412px); }

.dark .cap-scene {
  --cap-ink: 250, 250, 249;
  --cap-paper: 12, 12, 11;

  --cap-fill: linear-gradient(180deg, #1c1c1e 0%, #161618 100%);
  --cap-hair: inset 0 0 0 0.5px rgba(255, 255, 255, 0.14);
  --cap-sheen: inset 0 1px 0 rgba(255, 255, 255, 0.12);
  --cap-lift: 0 0 0 0 rgba(0, 0, 0, 0);
  --cap-gloss: linear-gradient(180deg, rgba(255, 255, 255, 0.07) 0%, rgba(255, 255, 255, 0) 48%, rgba(0, 0, 0, 0.18) 100%);
}

/* The board wash. Same idea as the fragmentation scene's --sp-field: a resolved
   diagram gains its own ambient depth rather than merely finishing.
   Two layers, not one: a broad wash on the box plus a tighter highlight parked
   on whatever each scene's anchor is (--fx / --fy, written inline by the
   component). One flat radial made every panel read as the same empty board. */
.cap-scene__field {
  position: absolute;
  inset: -10%;
  z-index: 0;
  pointer-events: none;
  opacity: var(--k0);
  background:
    radial-gradient(34% 46% at var(--fx, 50%) var(--fy, 50%),
      rgba(var(--cap-ink), 0.045) 0%, rgba(var(--cap-ink), 0) 74%),
    radial-gradient(56% 72% at 50% 50%,
      rgba(var(--cap-ink), 0.045) 0%, rgba(var(--cap-ink), 0.020) 46%, rgba(var(--cap-ink), 0) 76%);
}
.dark .cap-scene__field {
  background:
    radial-gradient(34% 46% at var(--fx, 50%) var(--fy, 50%),
      rgba(var(--cap-ink), 0.060) 0%, rgba(var(--cap-ink), 0) 72%),
    radial-gradient(56% 72% at 50% 50%,
      rgba(var(--cap-ink), 0.062) 0%, rgba(var(--cap-ink), 0.026) 44%, rgba(var(--cap-ink), 0) 74%);
}

/* ============================================================================
   3. WIRING
   The viewBox is set to the scene's pixel size on every measure, so one SVG
   user unit is one CSS pixel and geometry authored in px lands where it says.
   ========================================================================= */

.cap-scene__wires {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 1;
  pointer-events: none;
  overflow: visible;
}

/* pathLength="1" on every path, so the dash pattern is in normalised units and
   the offset is literally "how much of this edge does not exist yet". 1px is
   one user unit, which pathLength has just declared to be the whole path. */
.cap-edge {
  fill: none;
  stroke: rgb(var(--cap-ink));
  stroke-width: 1;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 1px;
  stroke-dashoffset: calc(1px * (1 - var(--k)));
  opacity: 0.30;
}
.dark .cap-edge { opacity: 0.34; }

/* The link from an entity to the action it can run. Fainter and thinner: it is
   a capability, not a fact about the organisation. */
.cap-edge--soft { opacity: 0.20; }
.dark .cap-edge--soft { opacity: 0.24; }

.cap-edge--strong { opacity: 0.55; }
.dark .cap-edge--strong { opacity: 0.62; }

/* The back plane. Three different things wear it and they are the same claim in
   three scenes: the wiring out to the unnamed entities, the futures the control
   point simulates and throws away, and the declared track a run has not reached
   yet. All of them are structure the reader is meant to see but not read.
   So it is drawn at rest rather than scrubbed on, which is what --k0 does for
   every other structural layer: the third scene is nothing but this plane plus
   its markers, and on the dash it was a blank panel until --p 0.26. The strong
   traversal still draws over it, which is the beat that scene is actually
   about. */
/* Les trois sondes de simulation. Trois points anonymes au bout d'un trait
   plein se lisaient comme de la structure, pas comme des essais. Le pointille
   est celui que le poster met sur ses fils de provenance, et il dit la meme
   chose: ceci est un essai, pas une arete de l'ontologie. */
.cap-edge--faint { opacity: 0.15; stroke-dashoffset: 0; stroke-dasharray: 3 3.5; }
.dark .cap-edge--faint { opacity: 0.19; }

.cap-node-ring {
  fill: none;
  stroke: rgb(var(--cap-ink));
  stroke-width: 1;
  opacity: calc(0.40 * var(--k0));
  transform-box: fill-box;
  transform-origin: center;
  transform: scale(calc(0.6 + 0.4 * var(--k)));
}
.cap-node-core {
  fill: rgb(var(--cap-ink));
  opacity: calc(0.85 * var(--k0));
  transform-box: fill-box;
  transform-origin: center;
  transform: scale(calc(0.4 + 0.6 * var(--k)));
}
/* Ambient depth under the two anchors of the workflow, not a lit node: wide and
   soft enough to read as the board catching light, never as a glowing marker. */
.cap-node-wash { fill: rgb(var(--cap-ink)); opacity: calc(0.05 * var(--k0)); filter: blur(12px); }

/* A simulated outcome, one plane back from the committed spine. Same ring, less
   of it: depth here is opacity, never a second colour. On --k, not --k0: the
   gate running its futures is an event, not the board it runs them on. */
.cap-node-ring.cap-ghost { opacity: calc(0.24 * var(--k)); }
.cap-node-core.cap-ghost { opacity: calc(0.38 * var(--k)); }

/* The relation verbs: advises, holds, signs, governs, contains, exposed to.
   They are words, not texture, so they answer to the 4.5:1 WCAG asks of text
   this size and 0.46 did not. Measured on rendered pixels at the resolved
   scene, over the board's own gradient, 1440x900: 3.71:1 at the bright end of
   the wash and 3.86:1 at the dark end. 0.60 measures 5.31:1 and 5.77:1.
   Still the bottom tier of the scene, well under the pill labels at 10.4:1,
   which is what keeps a verb reading as an annotation on an edge. */
.cap-vlabel {
  font-family: 'Geist', ui-sans-serif, system-ui;
  font-size: 9px;
  letter-spacing: 0.01em;
  text-anchor: middle;
  fill: rgba(var(--cap-ink), 0.60);
  opacity: var(--k);
}

/* ============================================================================
   4. NODES
   Placed by measure() in px (left / top), never animated: all motion here is
   transform and opacity.
   ========================================================================= */

.cap-node {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 2;
  pointer-events: none;
  backface-visibility: hidden;
  transform-origin: 50% 50%;
  /* flex, so the wrapper's box IS the pill's box. As a block containing an
     inline-level child it carried a line box instead: the wrapper measured
     75x24 around a pill that was 69.40x19.53, and the pill sat 0.46px below
     the wrapper's centre on the baseline. useCapLayout routes every edge
     against that box, so every scene was trimming to a node 2.8px too wide,
     2.2px too tall and half a pixel too high. It is the whole reason an
     endpoint on a pill measured 7 to 12px out while an endpoint on a circle,
     which is sized from data and never touches the DOM, measured exactly 7. */
  display: flex;
  transform: translate(-50%, -50%)
             translate3d(0, calc((1 - var(--k)) * 9px), 0)
             scale(calc(0.93 + 0.07 * var(--k)));
  opacity: var(--k0);
}

/* The action column is the payoff of the first scene, so it is the one set of
   nodes that still arrives from nothing. An inverted pill is near-white on a
   near-black board and would have been the loudest mark on a panel that has not
   started yet. Position and scale still come from --k, so it lands rather than
   merely brightening. */
.cap-node:has(.cap-pill--act) { opacity: var(--k); }

/* A minor entity sits one plane behind the named ones. Blur as distance, the
   same device the fragmentation scene uses, held small enough that it reads as
   hierarchy rather than as a rendering fault. */
.cap-node--far {
  opacity: calc(0.58 * var(--k0));
  filter: blur(0.5px);
}
.cap-dot {
  display: block;
  border-radius: 9999px;
  background: rgba(var(--cap-ink), 0.52);
  box-shadow: 0 0 0 3px rgba(var(--cap-paper), 0.50), var(--cap-lift);
}
.dark .cap-dot {
  background: rgba(var(--cap-ink), 0.62);
  box-shadow: 0 0 0 3px rgba(var(--cap-paper), 0.70), 0 1px 3px rgba(0, 0, 0, 0.6);
}

/* A word on the floor, not a chip. The fill, gloss, hairline and lift are
   exactly what made these read as a screenshot of a diagram tool. */
.cap-pill {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 0 7px;
  white-space: nowrap;
}

.cap-pill__l {
  font-family: 'Space Grotesk', 'Geist', ui-sans-serif, system-ui, sans-serif;
  font-size: 11px;
  font-weight: 400;
  letter-spacing: -0.01em;
  line-height: 1;
  color: rgba(var(--cap-ink), 0.88);
}

/* Entity: a hollow ring, the same glyph the diagram used before. */
.cap-pill__g {
  flex: none;
  width: 9px;
  height: 9px;
  border-radius: 9999px;
  box-shadow: inset 0 0 0 1.2px rgba(var(--cap-ink), 0.45);
}

/* Action: the one node that executes is the one at full ink. Contrast says
   it, not a hue: this page is black and white end to end. */
.cap-pill--act .cap-pill__l { color: rgb(var(--cap-ink)); font-weight: 500; }
.cap-pill--act .cap-pill__g {
  width: 8px;
  height: 9px;
  border-radius: 0;
  box-shadow: none;
  background: rgb(var(--cap-ink));
  clip-path: polygon(0 0, 100% 50%, 0 100%);
}

/* The terminal box of the control flow: a traced container, not a pill, so it
   reads as a destination rather than another step. */
/* La boite a coche muette du point de controle est partie: elle etait la seule
   forme pleine de la scene et ne portait aucun mot. Ce qu'elle voulait dire est
   maintenant une pastille d'action, comme dans les deux autres scenes et comme
   dans le poster. */

/* REVERSIBLE, the caption under the control loop. Same alpha as the verbs and
   the same reason for moving it: 9.5px uppercase on 0.14em tracking measured
   4.17:1, under the 4.5:1 floor. 0.60 measures 6.30:1. */
.cap-note {
  font-family: 'Geist', ui-sans-serif, system-ui;
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.01em;
  line-height: 1;
  white-space: nowrap;
  color: rgba(var(--cap-ink), 0.60);
}

/* ============================================================================
   5. FLOW
   One pulse per capability link, on the resolved diagram only. offset-path is
   written by measure() from the very path string the edge draws, so the pulse
   is on the wire and not near it.
   ========================================================================= */

.cap-flows {
  position: absolute;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  opacity: var(--k);
}
.cap-flow {
  position: absolute;
  left: 0;
  top: 0;
  width: 5px;
  height: 5px;
  /* No centring margin. offset-anchor defaults to auto, which resolves to
     transform-origin, so offset-path already puts the element's CENTRE on the
     curve. The -2.5px pair on top of that put every pulse 3.54px off its own
     wire, on the diagonal, in all three scenes. */
  border-radius: 9999px;
  background: rgb(var(--cap-ink));
  box-shadow: 0 0 9px rgba(var(--cap-ink), 0.45);
  offset-rotate: 0deg;
  offset-distance: 0%;
  /* --dur is per pulse where a scene carries several: the ontology's nine
     share no divisor, so the set never re-synchronises into a single beat. */
  animation: cap-run var(--dur, 2.8s) cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
@keyframes cap-run {
  0%   { offset-distance: 0%;   opacity: 0; }
  16%  { opacity: 0.9; }
  84%  { opacity: 0.9; }
  100% { offset-distance: 100%; opacity: 0; }
}

/* ============================================================================
   6. REDUCED MOTION
   useScrollProgress pins --p at 1 under this query, so every scene is already
   at its finished state. All that is left is to stop the pulses, which have
   nothing to say once the diagram is complete.
   ========================================================================= */

@media (prefers-reduced-motion: reduce) {
  .cap-flow { display: none; }
}

/* ============================================================================
   7. SECURITY CARDS
   Four containers, and none of them is a link. theme-dark.css is explicit that
   .card-link announces itself at rest and .card stays calm, and these have no
   href, no role, no tabindex and cursor: auto.

   So there is NO :hover here. The card used to step its surface from
   rgb(28,28,30) to rgb(36,36,40), lift and brighten the ghost numeral, fill the
   icon chip and run a bar across its foot. Those are a control's signals, and
   wearing them on a static card is what makes an interface lie. Anything added
   back here has to come with a real click target.
   ========================================================================= */

.trust-card__ghost {
  /* --ink-faint is the token for a decorative numeral: non-text, never body
     copy. On the floor the old 0.04 of --fg was invisible, which is why these
     read as empty cards. 0.16 did not finish the job: it composites to rgb 41
     on a card at rgb 28, a 13-level step, 1.17:1 measured on rendered pixels,
     which is still an empty card at arm's length.
     0.46 lands rgb 65, 1.69:1. The ceiling is what it must not become: this
     numeral has no meaning to carry, so it stays under 2:1 and never competes
     with the 20px title at 16.5:1 whose baseline it stops exactly at. */
  color: var(--ink-faint, #6e6e73);
  opacity: 0.46;
}
/* The specular from useCursorGlow. Overlay against a near-black card roughly
   doubles what it touches, so the alpha carries the whole effect: 0.42 lands
   the hotspot at about +45% of the surface, which reads as a light passing
   over the material and never as a lit control. */
.trust-card { --glow-size: 340px; --glow-strength: 0.42; }

/* Card 05 runs full width and its right column reaches the top-right corner,
   where the ghost numeral lives on the other four. It moves to the dead space
   under the left column instead of striking through the first row of evidence.
   Below the two-column breakpoint the card stacks and has no dead space left,
   so the numeral goes rather than collide again. */
.trust-card--wide .trust-card__ghost {
  top: auto; right: auto;
  bottom: -20px; left: 12px;
}
@media (max-width: 1023px) {
  .trust-card--wide .trust-card__ghost { display: none; }
}

/* The evidence list in card 05. Hairline-separated rows, a mono micro-label
   over a line of body copy: the section's existing vocabulary, no new colour
   and no accent. The rows are what makes it read as evidence rather than as a
   second paragraph, which is the whole reason the card runs full width. */
.sov-proof {
  display: flex; flex-direction: column;
  margin: 0; padding: 0; list-style: none;
}
.sov-proof li {
  display: flex; flex-direction: column; gap: 5px;
  padding: 13px 0;
  border-top: 1px solid var(--rule, rgba(255,255,255,0.10));
}
/* No rule above the first row and no dead space under the last: the card's own
   padding already states both edges. */
.sov-proof li:first-child { border-top: 0; padding-top: 0; }
.sov-proof li:last-child  { padding-bottom: 0; }
.sov-proof__t {
  font-family: 'Geist', ui-sans-serif, system-ui;
  font-size: 9.5px;
  font-weight: 500;
  letter-spacing: 0.01em;
  line-height: 1.4;
  color: var(--ink, #fafafa);
}
.sov-proof__d {
  font-size: 12.5px;
  line-height: 1.6;
  color: var(--ink-muted, #a3a3a3);
}
/* Under lg the card is one column, so the list follows the paragraph and needs
   the gap the two-column layout was giving it. */
@media (max-width: 1023px) {
  .sov-proof li:first-child { border-top: 1px solid var(--rule, rgba(255,255,255,0.10)); padding-top: 13px; }
}

/* ============================================================================
   8. FAQ
   theme-dark.css owns the accordion mechanics (.faq / .faq-q / .faq-a, the
   0fr -> 1fr row, the chevron rotation, hover, press and focus). Everything
   here is the page's own typography sitting inside them: the mono index and
   the display-face question the section already carried.
   ========================================================================= */

/* theme-dark pairs `width: 100%` with `margin-inline: -16px`, so the row solves
   to the container's width and then sits 16px to the left of it: the chevron
   stops 32px short of the right edge, which is very visible in a 375px column.
   `auto` is not the fix either, because a <button> shrink-to-fits even when it
   is a block-level flex container, and a short question would pull its chevron
   inward. The width has to state the bleed. */
.faq-q { width: calc(100% + 32px); }

.faq-q__n {
  flex: none;
  width: 34px;
  font-family: 'Geist', ui-sans-serif, system-ui;
  font-size: 10.5px;
  line-height: 1;
  letter-spacing: 0.01em;
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint, #6e6e73);
  transition: color var(--dur-state, 150ms) var(--ease, cubic-bezier(0.16, 1, 0.3, 1));
}
.faq-q:hover .faq-q__n,
.faq-q[aria-expanded="true"] .faq-q__n { color: var(--ink-muted, #a3a3a3); }

.faq-q__t {
  flex: 1 1 auto;
  font-family: var(--font-display, 'Fraunces', Georgia, serif);
  font-weight: 400;
  font-optical-sizing: auto;
  font-size: 19px;
  line-height: 1.28;
  letter-spacing: -0.012em;
  color: var(--ink, #fafafa);
  text-wrap: balance;
}
@media (min-width: 640px) { .faq-q__t { font-size: 22px; } }

.faq-q > .chev {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border-radius: 9999px;
  box-shadow: inset 0 0 0 1px var(--rule, rgba(255, 255, 255, 0.10));
  transition: transform var(--dur-state, 150ms) var(--ease, cubic-bezier(0.16, 1, 0.3, 1)),
              color var(--dur-state, 150ms) var(--ease, cubic-bezier(0.16, 1, 0.3, 1)),
              box-shadow var(--dur-state, 150ms) var(--ease, cubic-bezier(0.16, 1, 0.3, 1));
}
.faq-q:hover > .chev { box-shadow: inset 0 0 0 1px var(--ctl-edge, rgba(255, 255, 255, 0.34)); }

/* 34px of index plus the button's own 20px gap: the answer starts under the
   question, not under its number. */
.faq-a__p {
  padding: 2px 8px 26px 54px;
  max-width: 700px;
  font-size: 14.5px;
  line-height: 1.7;
  color: var(--ink-muted, #a3a3a3);
}
@media (min-width: 640px) { .faq-a__p { padding-right: 48px; } }

/* Narrow: the question needs the room more than the index and the gaps do. */
@media (max-width: 480px) {
  .faq-q { width: calc(100% + 24px); gap: 13px; padding-inline: 12px; margin-inline: -12px; }
  .faq-q__n { width: 24px; letter-spacing: 0.14em; }
  .faq-a__p { padding-left: 37px; }
}

/* 9. DEMO STAGE — the claim in one row, then the recording at the full width of
   the page. It arrives turned away and pushed back, then squares to the camera. */

/* clip, never hidden: hidden on an ancestor makes it a scroll container */
.demo-band { overflow-x: clip; }

.demo-stage {
  --p: 1;
  /* smoothstep: --p stays linear, the arrival does not read linear */
  --e: calc(var(--p) * var(--p) * (3 - 2 * var(--p)));
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  gap: 34px;
  padding: 92px 0 48px;
  /* camera on the stage, not off to one side: an off-centre vanishing point
     turned the card into a collapsing trapezoid */
  perspective: 2400px;
}

/* one row above the strip: the claim on the left, the reading of it on the right */
.demo-stage__copy {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 27rem);
  align-items: end;
  gap: 56px;
  padding-inline: max(40px, calc((100vw - 1280px) / 2));
}
.demo-stage__eyebrow {
  display: block;
  font-family: var(--font-sans);
  font-size: 12px;
  letter-spacing: 0.02em;
  color: var(--ink-faint, #78716c);
  font-variant-numeric: tabular-nums;
}
.demo-stage__title { margin: 12px 0 0; color: var(--ink, #fafaf9); }
.demo-stage__body {
  margin: 0;
  font-size: 14.5px;
  line-height: 1.7;
  color: var(--ink-muted, #a8a29e);
}

/* the full width of the page. The source is a screen recording, so it keeps its
   own ratio: any crop cuts words out of the product UI. */
.demo-stage__card {
  position: relative;
  width: 100%;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  will-change: transform;
  /* it turns on its own centre. No vertical tilt: that read as falling over. */
  transform:
    translate3d(0, 0, calc((1 - var(--e)) * -560px))
    rotateY(calc((1 - var(--e)) * 12deg));
  /* rule 26: no shadow on a dark ground, so the edge is a hairline that firms
     up as the card faces you */
  box-shadow: 0 0 0 1px rgba(250, 250, 249, calc(0.05 + 0.09 * var(--e)));
}

.demo-stage__media {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
/* the video lands on the poster and crossfades in on its first frame */
.demo-stage__video { opacity: 0; transition: opacity 360ms var(--ease, cubic-bezier(0.16, 1, 0.3, 1)); }
.demo-stage__video.is-on { opacity: 1; }

/* white, small, and nothing else: no disc, no glass, no label */
.demo-stage__full {
  position: absolute;
  right: 16px;
  bottom: 16px;
  width: 34px;
  height: 34px;
  display: grid;
  place-items: center;
  color: #fafaf9;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition:
    opacity var(--dur-arrive, 400ms) var(--ease, cubic-bezier(0.16, 1, 0.3, 1)),
    transform var(--dur-state, 150ms) var(--ease, cubic-bezier(0.16, 1, 0.3, 1));
  filter: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.7));
}
.demo-stage.is-live .demo-stage__full { opacity: 0.78; pointer-events: auto; }
.demo-stage.is-live .demo-stage__full:hover { opacity: 1; }
.demo-stage__full:active { transform: scale(0.94); }
.demo-stage__full:focus-visible {
  outline: var(--focus-w, 2px) solid var(--focus-color, #fafaf9);
  outline-offset: 2px;
  opacity: 1;
}

/* reduced motion holds --p at 1, so the transform is already the identity */
@media (max-width: 900px), (prefers-reduced-motion: reduce) {
  .demo-stage {
    min-height: 0;
    gap: 26px;
    padding: 0;
    perspective: none;
  }
  .demo-stage__copy {
    grid-template-columns: minmax(0, 1fr);
    gap: 16px;
    padding-inline: 24px;
  }
}

/* ============================================================================
   10. CONTACT
   The submit control is theme-dark.css's .btn-primary now: same three states,
   drawn once, in the file that owns every other control on the page. The
   .contact-send block that used to live here went with the class.
   ========================================================================= */

/* ============================================================================
   13. THE SECTION HEADING
   theme-dark.css's .section-head composition takes two children in .section-meta
   and has no slot for a separator. This page's eyebrows carry one, "01 · Label",
   and the copy is frozen, so it renders as a third child and the 14px gap comes
   down to fit three items instead of two. Nothing else about the component
   changes.
   ========================================================================= */

.section-meta { gap: 9px; }

/* ============================================================================
   11. HEADER, OVER THE HERO PHOTO
   theme-dark.css owns .nav-link: the resting --ink-muted, the underline wipe,
   the [aria-current] bar. index.html:197 paints EVERY anchor in the over-hero
   header `color: #fafaf9` at (0,2,2), which outranks .nav-link at (0,1,0) and
   .nav-link:hover at (0,2,0): over the photo the item sits at full ink at rest
   and the hover has nowhere left to travel. Three classes plus an element
   restate the same two-step ramp against the photo instead of the floor. The
   underline wipe is currentColor and needs nothing here.
   ========================================================================= */

header.glass.over-hero .nav-link { color: rgba(250, 250, 249, 0.76); }
header.glass.over-hero .nav-link:hover,
header.glass.over-hero .nav-link.is-active,
header.glass.over-hero .nav-link[aria-current="page"] { color: #fafaf9; }
header.glass.over-hero .nav-link:active { color: rgba(250, 250, 249, 0.76); }

/* The header CTA needs nothing here. It is .cta-glass, and index.html:197's
   `header.glass.over-hero a { color: #fafaf9 }` already paints its label at
   full ink over the photo, which is what the lens wants against a bright
   backdrop. Measured worst case over the five hero slides: 16.2:1. */

/* ============================================================================
   12. THE HEADER CTA
   NOTHING HERE, DELIBERATELY. This section used to restate the CTA's whole rim
   band, and both reasons it existed are gone:

   1. THE UNIT. useCursorGlow publishes --mx / --my as a unitless 0..1 ratio and
      also --mx-px / --my-px in raw px. The rim's radial-gradient needs a
      <length-percentage>, so reading the ratio pair invalidated the whole
      `background` shorthand and the band painted nothing. tokens.css reads the
      px pair itself now, so there is nothing left to substitute.

   2. THE COLLISION. useCursorGlow also puts .cursor-glow on the element, and
      motion.css gave .cursor-glow::after a generic overlay specular that
      replaced the rim. motion.css scopes that rule `:not(.cta-glass)` now, so
      there is nothing left to reset either.

   What the restatement cost while it was still here: it was (0,2,1) in the last
   sheet loaded against tokens.css's (0,1,1), so it WON, and it froze the rim at
   values from the 44px control. --glass-bevel and --glass-spec-r were retuned
   for the 37px one and reached every consumer except the header CTA, the single
   control the retune was for. Its specular stayed at a hardcoded 58px radius,
   wider than the button is tall, which lights the entire band and reads as
   metal trim rather than a highlight; its bevel still ended on
   rgba(0,0,0,0.13), a shaded underside, the one thing glass cannot have.
   The rim comes from the primitive. Retune it in public/tokens.css.
   ========================================================================= */

/* ============================================================================
   13. THE MONARCH MARK
   parts.jsx serves the light cut of the monogram, which is the right asset on
   the floor this page actually ships. The light path is still reachable (the
   `dark` tweak drops the class off <html>), and the same mark there would be
   #fafaf9 on paper: invisible again, the other way round. Inverting is exact on
   a flat single-colour vector whose anti-aliasing lives in the alpha channel,
   so this stays crisp rather than muddy.
   ========================================================================= */

html:not(.dark) .monarch-mark-lockup { filter: invert(1); }

/* ============================================================================
   14. HIT TARGETS
   The baseline is docs/product-ui/08-design-audit.md: 44x44 on touch, 36 on
   desktop. Six controls sat under it, all of them for the same reason, that
   their box is exactly as big as a short word: FAQ was 32.7 x 40, the footer
   links 31 to 45 wide by 18.8 tall, the burger 36 x 36, the hero dots 16 x 2.

   Every one is fixed the same way, and NOT with padding: padding grows the box,
   which moves text, shifts a flex row, repaints a background over the bleed and,
   on .nav-link, drags the underline wipe out from under the word. An empty
   absolutely positioned ::before is outside layout entirely. It paints nothing,
   it belongs to the control so hover, focus and activation are unchanged, and
   the numbers below are the rendered box.
   ========================================================================= */

/* Nav item: 32.7 x 40 -> 56.7 x 44 on the shortest label. The 28px gap between
   items absorbs 12px a side and still leaves 4px of dead ground between two
   boxes. .nav-link is already position: relative and its ::after is the
   underline, which stays exactly where it is, under the word. */
header .nav-link::before {
  content: '';
  position: absolute;
  inset: -2px -12px;
}

/* Footer links: 18.8 tall -> 44.8, and 20px wider. The row's gap-y is 28px in
   parts.jsx so that two wrapped lines cannot overlap each other's hit box;
   above 480px the row is one line and nothing about it moves. */
.footer-links .link { position: relative; }
.footer-links .link::before,
.contact-mail::before {
  content: '';
  position: absolute;
  inset: -13px -10px;
}
.contact-mail { position: relative; }

/* The hero's slide dots are a 2px bar each, the smallest target on the page.
   The 8px gap between them caps the horizontal bleed at 3px a side, so this
   buys the height: 16 x 2 -> 22 x 44, and the active bar 48 x 2 -> 54 x 44. */
.hero-dot { position: relative; cursor: pointer; }
.hero-dot::before {
  content: '';
  position: absolute;
  inset: -21px -3px;
}

/* The splash skip control: 27.3 x 15.8 -> 59.3 x 45.8. It is on screen for
   three seconds and it is the only way out of a locked page.
   No position declaration here, unlike its neighbours above: the control is
   already absolute, pinned bottom-right by its own utilities, and that already
   makes it the containing block this ::before needs. Adding position: relative
   dropped it out of that pin and into normal flow, because this stylesheet is
   unlayered and so outranks Tailwind's layered utilities whatever the source
   order. */
.splash-skip { cursor: pointer; }
.splash-skip::before {
  content: '';
  position: absolute;
  inset: -15px -16px;
}

/* The hero's text CTA sits at 34 tall, under the 36 desktop floor by two
   pixels. Vertical only: its neighbour is a pill 10px to the left. */
.cta-link::before {
  content: '';
  position: absolute;
  inset: -5px 0;
}

/* La barre, opaque. Elle etait a rgba(18,18,20,0.64), donc 36% de ce qui defile
   dessous passait au travers et un titre de section se lisait sous la nav. Une
   barre n'a rien a montrer de ce qu'elle recouvre.

   Le bord est adouci plutot que franc: un filet net pose sur un fond net, ce
   sont deux coupures collees (regle 25). Un court degrade sous le filet fait la
   transition, et la barre garde son arete du haut cote hero. */
header.glass:not(.over-hero) {
  background: #0e0e10;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom-color: transparent;
  box-shadow:
    inset 0 -1px 0 0 rgba(255, 255, 255, 0.07),
    0 10px 20px -14px rgba(0, 0, 0, 0.95);
}
header.glass:not(.over-hero)::after {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 100%;
  height: 14px;
  pointer-events: none;
  background: linear-gradient(180deg, rgba(14, 14, 16, 0.55) 0%, rgba(14, 14, 16, 0) 100%);
}


/* 14. THE JOURNAL — a plate that opens from a held box to the full page as it
/* 14. THE JOURNAL — a plate that opens from a held box to the full page as it
   passes, then four square picks under it. */

.mf-plate {
  --p: 1;
  --e: calc(var(--p) * var(--p) * (3 - 2 * var(--p)));
  position: relative;
  display: block;
  /* full bleed in the layout at all times: only the clip moves, so nothing
     reflows and the opening costs one composited property */
  width: calc(100% + 2 * var(--bleed, 40px));
  margin: 76px calc(-1 * var(--bleed, 40px)) 0;
  height: min(78vh, 660px);
  overflow: hidden;
  background: #0a0a09;
  clip-path: inset(calc((1 - var(--e)) * 9%) calc((1 - var(--e)) * 20%));
}

/* the ground is the cover blurred past legibility, pooled behind the pair so
   the composition stands on a stage instead of in a void */
.mf-plate::before {
  content: '';
  position: absolute;
  inset: -12%;
  background: url('/assets/manifesto-cover.webp') center / cover no-repeat;
  filter: blur(56px) saturate(0) brightness(1.25);
  opacity: 0.5;
  -webkit-mask-image: radial-gradient(58% 78% at 50% 48%, #000 0%, transparent 76%);
  mask-image: radial-gradient(58% 78% at 50% 48%, #000 0%, transparent 76%);
}

/* The title and the book are ONE object, centred as a pair. Held apart by a
   1fr/auto grid they read as two things exiled to opposite walls. */
.mf-plate__inner {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: clamp(40px, 4.4vw, 84px);
  padding-inline: 40px;
}

.mf-plate__ink { display: block; flex: 0 0 auto; text-align: right; }

.mf-plate__eyebrow {
  display: block;
  font-family: var(--font-sans);
  font-size: 12px;
  letter-spacing: 0.02em;
  color: var(--ink-muted, #a8a29e);
}

.mf-plate__title {
  display: block;
  margin-top: 18px;
  font-size: clamp(38px, 5.2vw, 88px);
  color: var(--ink, #fafaf9);
  text-wrap: nowrap;
}

.mf-plate__meta {
  display: block;
  margin-top: 22px;
  font-family: var(--font-sans);
  font-size: 12px;
  color: var(--ink-faint, #78716c);
  font-variant-numeric: tabular-nums;
}

/* the book stands as an object: its own portrait ratio, never cropped */
.mf-plate__cover {
  display: block;
  flex: 0 0 auto;
  height: min(80%, 520px);
  width: auto;
  /* rule 26: no shadow on a dark ground, so the edge is a hairline */
  box-shadow: 0 0 0 1px rgba(250, 250, 249, 0.10);
  transform: translateY(calc((1 - var(--e)) * 24px)) scale(calc(0.95 + 0.05 * var(--e)));
  transition: box-shadow var(--dur-arrive, 400ms) var(--ease, cubic-bezier(0.16, 1, 0.3, 1));
}

/* the book lights up on hover: the plate is clickable and the object is what
   the eye is on, so the glow goes on the object, not only the frame hairline */
.mf-plate:hover .mf-plate__cover {
  box-shadow:
    0 0 0 1px rgba(250, 250, 249, 0.30),
    0 0 48px rgba(250, 250, 249, 0.22);
}

/* one hairline that arrives on hover, which is the whole affordance */
.mf-plate::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 2;
  box-shadow: inset 0 0 0 1px rgba(250, 250, 249, 0.08);
  transition: box-shadow var(--dur-state, 150ms) var(--ease, cubic-bezier(0.16, 1, 0.3, 1));
  pointer-events: none;
}
.mf-plate:hover::after { box-shadow: inset 0 0 0 1px rgba(250, 250, 249, 0.28); }
.jr-grid {
  margin-top: 84px;
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 28px;
}

.jr-card { display: block; }
/* square, because the box is the point: the photograph is cropped to it and
   grows inside it on hover, the box itself never moves */
.jr-card__box {
  display: block;
  position: relative;
  aspect-ratio: 1 / 1;
  overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(250, 250, 249, 0.07);
}
.jr-card__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: grayscale(1) contrast(1.06) brightness(0.6);
  transform: scale(1.02);
  transition:
    transform 700ms var(--ease, cubic-bezier(0.16, 1, 0.3, 1)),
    filter var(--dur-arrive, 400ms) var(--ease, cubic-bezier(0.16, 1, 0.3, 1));
}
.jr-card:hover .jr-card__img { transform: scale(1.08); filter: grayscale(1) contrast(1.06) brightness(0.78); }

.jr-card__kicker {
  display: block;
  margin-top: 16px;
  font-family: var(--font-sans);
  font-size: 11px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-faint, #78716c);
}
.jr-card__title {
  display: block;
  margin-top: 8px;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: 17px;
  line-height: 1.24;
  letter-spacing: -0.03em;
  color: var(--ink, #fafaf9);
  text-wrap: balance;
}
.jr-card__date {
  display: block;
  margin-top: 10px;
  font-family: var(--font-sans);
  font-size: 12px;
  color: var(--ink-faint, #78716c);
}

.jr-all {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-top: 56px;
  font-size: 13px;
}

@media (max-width: 900px) {
  .jr-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; }
  /* the cover leads on a narrow screen and the type follows under it */
  .mf-plate { height: auto; }
  .mf-plate__inner {
    position: static;
    flex-direction: column;
    gap: 34px;
    padding: 56px 24px;
  }
  .mf-plate__ink { order: 2; text-align: center; }
  .mf-plate__cover { order: 1; height: auto; width: min(62vw, 260px); }
  .mf-plate__title { text-wrap: balance; }
}
@media (max-width: 520px) {
  .jr-grid { grid-template-columns: minmax(0, 1fr); }
}


/* 15. THE DIFFUSE FIELD — the three capability scenes sit in no box, so each
   one ends by dissolving rather than by hitting an edge. */

.cap-scene {
  -webkit-mask-image: radial-gradient(78% 84% at 50% 50%, #000 34%, transparent 92%);
  mask-image: radial-gradient(78% 84% at 50% 50%, #000 34%, transparent 92%);
}

/* the board wash was the panel's floor light; with no panel it is just a smudge */
.cap-scene__field { opacity: 0; }


/* 16. THE REGISTER — six systems holding one asset under six names, flush at a
   column when the section arrives and sheared out of it by the time it leaves. */

.drift {
  --p: 1;
  --e: calc(var(--p) * var(--p) * (3 - 2 * var(--p)));
  --gutter: 128px;
  --gap: 34px;
  position: relative;
  margin-top: 76px;
  /* bleeds right so a record can run off the page; the left padding puts the
     system names back on the text column */
  width: calc(100% + 2 * var(--bleed, 40px));
  margin-inline: calc(-1 * var(--bleed, 40px));
  padding: 4px 0 6px var(--bleed, 40px);
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, #000 62%, transparent 99%);
  mask-image: linear-gradient(90deg, #000 62%, transparent 99%);
}

/* the column the six were flush against, left standing once they have gone */
.drift__rule {
  position: absolute;
  top: 54px;
  bottom: 10px;
  left: calc(var(--bleed, 40px) + var(--gutter) + var(--gap));
  width: 1px;
  background: linear-gradient(180deg,
    transparent, rgba(250, 250, 249, 0.30) 14%, rgba(250, 250, 249, 0.30) 86%, transparent);
}

.drift__lane {
  display: grid;
  grid-template-columns: var(--gutter) minmax(0, 1fr);
  column-gap: var(--gap);
  align-items: baseline;
  padding: 13px 0;
  white-space: nowrap;
}

.drift__sys {
  font-family: var(--font-sans);
  font-size: 10.5px;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  text-align: right;
  color: var(--ink-faint, #78716c);
}

/* the one moving part on the page: each lane leaves the column by its own
   distance, so the register shears instead of sliding */
.drift__run {
  display: block;
  transform: translate3d(calc(var(--dx, 0) * var(--e) * var(--k, 1) * 1px), calc(var(--dy, 0) * var(--e) * 1px), 0);
  will-change: transform;
}

.drift__key {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 15px;
  letter-spacing: -0.01em;
  color: var(--ink, #fafaf9);
}

.drift__tail {
  margin-left: 24px;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 15px;
  color: rgba(250, 250, 249, 0.52);
  opacity: calc(1 - 0.45 * var(--e));
}

/* the register's own title: it names the column, so it never drifts */
.drift__lane--head { padding-bottom: 24px; }
.drift__lane--head .drift__run {
  transform: none;
  font-family: var(--font-sans);
  font-size: 10.5px;
  letter-spacing: 0.11em;
  text-transform: uppercase;
  color: rgba(250, 250, 249, 0.58);
}

@media (max-width: 720px) {
  .drift {
    --gutter: 60px;
    --gap: 16px;
    --k: 0.38;
    -webkit-mask-image: linear-gradient(90deg, #000 80%, transparent 100%);
    mask-image: linear-gradient(90deg, #000 80%, transparent 100%);
  }
  .drift__lane--head .drift__run { font-size: 9.5px; letter-spacing: 0.07em; }
  .drift__rule { top: 46px; }
  .drift__lane { padding: 10px 0; }
  .drift__key, .drift__tail { font-size: 12px; }
  .drift__tail { margin-left: 14px; }
}

@media (prefers-reduced-motion: reduce) {
  .drift__run { transform: none; }
  .drift__tail { opacity: 1; }
}


/* px-6 below 640, px-10 above: the bleed is the section padding or it overflows */
@media (max-width: 639px) {
  .mf-plate, .drift { --bleed: 24px; }
}
