/* =============================================================================
   manif.ai — page stylesheet
   -----------------------------------------------------------------------------
   Layered on /tokens.css, which supplies the brand palette and Public Sans and
   is global-safe. This file is the whole of the element and component layer for
   manif.ai and is shared by all three pages; site.css is deliberately not
   loaded, being the element layer for the coming-soon shell this replaced.

   Order: tokens → reset → primitives → layout → sections.
   Type rules follow packages/brand/TYPOGRAPHY.md.
   ========================================================================== */

/* =============================================================================
   manif — stealth site prototype
   Single stylesheet. Order: tokens → reset → primitives → layout → sections.
   No build step, no external dependencies.
   ========================================================================== */

/* ── Fonts ──────────────────────────────────────────────────────────────────
   Public Sans arrives from tokens.css as a variable font served out of
   /fonts/, so there are no @font-face rules here. See the head. */

/* ── Tokens ─────────────────────────────────────────────────────────────── */
:root {
  /* Surface */
  --black:        #08090a;
  --black-deep:   #050506;
  --graphite:     #1f2933;
  --graphite-dim: #14181d;

  /* Ink */
  --off-white:    #f4f3f0;
  --steel:        #8d949b;
  --steel-dim:    #666c73;
  --muted:        #9aa0a6;

  /* Accent — used sparingly */
  --terracotta:   #c65a2e;
  --terracotta-dim: rgba(198, 90, 46, 0.42);

  /* Hairlines */
  --line:         rgba(244, 243, 240, 0.09);
  --line-strong:  rgba(244, 243, 240, 0.18);

  /* Type */
  --font-sans: 'Public Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
  --font-mono: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;

  /* Metrics */
  --gutter: clamp(20px, 5vw, 72px);
  --container: 1280px;
  --header-h: 74px;
  --section-y: clamp(88px, 12vw, 176px);
  --ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

@media (max-width: 700px) {
  :root { --header-h: 62px; }
}

/* ── Reset ──────────────────────────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body, h1, h2, h3, p, ul, figure { margin: 0; }
ul { padding: 0; list-style: none; }
img, svg { display: block; max-width: 100%; }
button, input, textarea { font: inherit; color: inherit; }

html { scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
  }
}

body {
  background: var(--black);
  color: var(--off-white);
  font-family: var(--font-sans);
  font-weight: 400;
  font-size: 16px;
  line-height: 1.55;
  letter-spacing: -0.005em;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

::selection { background: rgba(198, 90, 46, 0.32); color: var(--off-white); }

/* ── Focus ──────────────────────────────────────────────────────────────── */
:focus { outline: none; }
:focus-visible {
  outline: 2px solid var(--terracotta);
  outline-offset: 3px;
  border-radius: 2px;
}

.skip-link {
  position: fixed;
  top: 12px; left: 12px;
  z-index: 90;
  padding: 10px 16px;
  background: var(--off-white);
  color: var(--black);
  font-size: 0.82rem;
  letter-spacing: 0.02em;
  text-decoration: none;
  transform: translateY(-160%);
  transition: transform 0.2s var(--ease);
}
.skip-link:focus-visible { transform: translateY(0); }

/* ── Atmosphere ─────────────────────────────────────────────────────────── */
/* A single soft light over the first screen only.

   This used to be `position: fixed` across the whole viewport, with a graphite
   ellipse anchored below the fold. On a black page that lifted the floor by
   ~10 levels and, being fixed, stayed glued to the viewport while the content
   scrolled past — which read as a glow travelling with the window rather than
   depth belonging to the page. It is now absolute and one screen tall, so it
   scrolls away with the hero and the rest of the page is flat black. */
.atmosphere {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 100vh;
  height: 100svh;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(110% 80% at 50% -12%, rgba(244, 243, 240, 0.028), transparent 60%),
    radial-gradient(90% 70% at 8% 22%, rgba(141, 148, 155, 0.014), transparent 62%);
}

/* Infrastructure spine — continues the hero rail down the page.
   Positioned in the document rather than fixed, so the browser scrolls it
   natively and it stays locked to the content (see spine.js). */
.spine {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 1;
  width: 100%;
  overflow: hidden;
  pointer-events: none;
}

/* Dispatch — the signal a submitted form sends off the page.
   The homepage answers a click with light on the board; the interior pages have
   no board, so the run is drawn on demand into this layer and removed when it
   leaves the frame. A canvas, not an SVG: this is the same signal the netting
   draws, so it is drawn by the same code with the same exponential tail. A
   dashed SVG path gave it a hard-ended bar of flat light, and the signal
   visibly changed character the moment it reached the grid.

   It sits between the wiring and the components, which is the whole point: a
   run passes BEHIND a part, so it reads as going through the board rather than
   across the front of it. The layer is a child of .page-head for exactly this
   reason — see the canvas in contact/index.html.

   The stack inside .page-head, low to high:
     .netting          0   the resting wiring
     .dispatch         1   the signal, over the track it runs on
     .netting--solids  2   the components, which occlude it
     > .container      3   the copy, over everything

   It was z-index 40 on a <body> child, so it painted over the lot. */
.dispatch {
  position: fixed;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* Netting — the board's wiring, continued onto an interior page.
   -----------------------------------------------------------------------------
   Same grammar as the hero field: right angles, 18px fillets, one hairline
   weight, and a resting alpha of about 3% so it is felt before it is read. It
   is not a decoration bolted to the right margin — the runs enter from off the
   right edge and leave through the top, the bottom or each other, exactly as
   the board's do, so nothing begins or ends in open space.

   The triangle is a mask, not a clip. A polygon would cut the runs off along a
   hard edge and read as a crop; a diagonal gradient lets them thin out into the
   page, which is what makes the wedge look like the near corner of something
   larger rather than a shape someone drew. */
.netting {
  position: absolute;
  top: 0;
  right: 0;
  z-index: 0;
  width: 66%;
  height: 100%;
  pointer-events: none;
  /* Two masks, intersected.
     The first is the wedge. The second matters just as much: a canvas clips to
     its own box, and this one's bottom edge is a long way above the bottom of
     the page — so the runs that leave through it were stopping in the middle of
     the page rather than exiting it. Fading the layer out before its own edge
     means they thin away instead of being cut, which is the same thing
     .hero__plate does for the same reason.
     `intersect` is required: layered masks default to `add`, which would union
     the two and undo both. */
  -webkit-mask-image:
    linear-gradient(112deg, transparent 16%, rgba(0, 0, 0, 0.55) 42%, #000 70%),
    linear-gradient(to bottom, #000 76%, transparent 99%);
  -webkit-mask-composite: source-in;
  mask-image:
    linear-gradient(112deg, transparent 16%, rgba(0, 0, 0, 0.55) 42%, #000 70%),
    linear-gradient(to bottom, #000 76%, transparent 99%);
  mask-composite: intersect;
}
@media (max-width: 900px) {
  /* No room beside the copy, and the runs would sit behind it rather than
     beside it. The page is the type at this width. */
  .netting { display: none; }
}

/* Film grain over the whole page — keeps flat black from banding. */
.grain {
  position: fixed;
  inset: -50%;
  z-index: 1;
  pointer-events: none;
  opacity: 0.048;
  mix-blend-mode: screen;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='220' height='220' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* ── Primitives ─────────────────────────────────────────────────────────── */
.container {
  width: min(var(--container), 100% - (var(--gutter) * 2));
  margin-inline: auto;
}

/* The label voice, canonical across every manif surface: 11px, 600, tracked
   0.08em, uppercase by CSS and sentence case in the source. Set in the sans —
   the mono face is kept for identifiers and machine values, which is the whole
   of its job. Matches .doc-nav a and .prose h3 on manif.bot; the guide is
   packages/brand/TYPOGRAPHY.md §3. */
.mono {
  font-family: var(--font-sans);
  font-size: 0.6875rem;               /* 11px */
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--steel-dim);
}
a { color: inherit; }

/* The boxed .button primitive is gone. There was exactly one on the site — the
   contact form's submit — and it was the only control that did not speak the
   bracket grammar every other call to action uses. It is a .link-cta now, and
   the rules below carry the three declarations a <button> needs that an <a>
   does not. */
button.link-cta {
  background: none;
  border: 0;
  cursor: pointer;
}
button.link-cta:disabled {
  opacity: 0.45;
  cursor: default;
}
/* A disabled control should not answer, so the brackets stay away. */
button.link-cta:disabled::before,
button.link-cta:disabled::after { opacity: 0 !important; }

/* Text-only call to action: no box. It takes the same label voice as every
   other piece of UI text on the page — 11px, 600, tracked 0.08em, uppercase
   by CSS — so a CTA, a field label and the copyright line are one treatment
   rather than three. TYPOGRAPHY.md §3. The rank between them is carried by
   colour alone: a CTA sits at --steel and lifts to --off-white when it is
   live, static notes stay at --steel-dim. */
.link-cta {
  position: relative;
  display: inline-block;
  padding: 12px 2px;
  font-family: var(--font-sans);
  /* The label voice a third up: 11px * 1.3. A CTA is the one piece of UI text
     on the page that has to be found rather than read, and at 11px it was
     losing to the copyright line beside it. Tracking is in em, so it rides
     the size up on its own. */
  font-size: 0.894rem;                /* 14.3px */
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--steel);
  /* Same motion as a word in the headline: a small, slow swell. */
  transform: translateZ(0);
  transition: transform 0.42s var(--ease), color 0.42s var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .link-cta:hover { color: var(--off-white); }
}
.link-cta:focus-visible { color: var(--off-white); }
@media (hover: hover) and (pointer: fine) {
  .link-cta:hover { transform: scale(1.024); }
}
.link-cta:focus-visible { transform: scale(1.024); }
@media (prefers-reduced-motion: reduce) {
  .link-cta:hover, .link-cta:focus-visible { transform: none; }
}
/* Brackets, not an underline: on hover the label is framed the way the
   wordmark is. They sit outside the text box and are laid out at rest, so
   nothing reflows when they appear — only their opacity and position move. */
.link-cta::before,
.link-cta::after {
  position: absolute;
  top: 50%;
  color: var(--terracotta);
  /* Heavier than the label, like the brackets in the wordmark. */
  font-weight: 700;
  opacity: 0;
  transition: opacity 0.26s var(--ease), transform 0.42s var(--ease);
  pointer-events: none;
}
/* They arrive by closing in on the label — out and faint at rest, tight to the
   text when lit, so it ends up set as [Get in touch] rather than decorated. */
.link-cta::before {
  content: '[';
  right: 100%;
  transform: translate(-7px, -54%);
}
.link-cta::after {
  content: ']';
  left: 100%;
  transform: translate(7px, -54%);
}
.link-cta:hover::before,
.link-cta:focus-visible::before { opacity: 1; transform: translate(4px, -54%); }
.link-cta:hover::after,
.link-cta:focus-visible::after { opacity: 1; transform: translate(-4px, -54%); }

/* A section heading, in the manif.bot voice. Hierarchy on the interior pages
   comes from colour and case, not size. TYPOGRAPHY.md §1 and §3.

   It takes the same 16px the page title does, which puts the two accent ranks
   on one size — as they are on /problem, where both are 11px. The H1 is still
   distinguished, but by position and by being the only heading with a lede
   under it, not by scale. The third rank (.principle__title, subtle grey) sits
   a rung below at 13px, so the step down is still there where it works. */
.section-title {
  font-size: 1rem;                    /* 16px */
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--terracotta);
}
/* ── Header ─────────────────────────────────────────────────────────────── */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 60;
  height: var(--header-h);
  display: flex;
  align-items: center;
  background: transparent;
}

/* Rather than a bar with an edge, the top of the page dissolves into black:
   a soft scrim sits above the content and below the header, so anything
   scrolling upward is absorbed instead of sliding under a panel. It fades in
   once the page has moved, keeping the hero's light intact at rest. */
.header-scrim {
  position: fixed;
  top: 0; left: 0; right: 0;
  height: calc(var(--header-h) + 78px);
  z-index: 50;
  pointer-events: none;
  /* Always on. The band the header sits in is solid down to the foot of the
     header, then falls away as a gradient — so the header has a floor to sit
     on and no edge to cut across the artwork. It deepens a little further on
     scroll, when there is content moving underneath it.

     The fill is #0E0F10, not the page's own #08090a, and the six levels are
     the point. Two layers lift the rest of the page and neither reaches under
     here: the atmosphere, which is absolute at z-index 0 and puts about four
     levels of warm light across the top of the hero, and the grain, fixed at
     z-index 1 and worth two or three more. The scrim sits at 50, above both.
     Filling it with the raw page colour therefore paints a band that is
     darker than the page it is supposed to disappear into — which read as a
     black bar across the top. This is the lit value, so it matches what is
     actually on screen either side of it rather than what the token says.

     The ramp starts falling at 48% rather than 58% for the same reason: a
     shorter solid section and a longer fade give the band no edge to be seen
     by. */
  opacity: 1;
  transition: height 0.45s var(--ease), opacity 0.45s var(--ease);
  background: linear-gradient(
    to bottom,
    rgb(14, 15, 16) 0%,
    rgb(14, 15, 16) 48%,
    rgba(14, 15, 16, 0.94) 62%,
    rgba(14, 15, 16, 0.74) 74%,
    rgba(14, 15, 16, 0.44) 85%,
    rgba(14, 15, 16, 0.16) 94%,
    rgba(14, 15, 16, 0) 100%
  );
}
.header-scrim.is-visible { height: calc(var(--header-h) + 132px); }
.site-header__inner {
  width: min(var(--container), 100% - (var(--gutter) * 2));
  margin-inline: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 24px;
}
.wordmark { display: inline-block; }
.wordmark img { width: clamp(84px, 8vw, 108px); height: auto; }

/* Clicking the mark answers back with a small hop.

   HEIGHT ONLY, NO DEFORMATION. The squash-and-stretch version reads as more of
   a jump, but the wordmark is fixed brand artwork — scaling it unevenly
   distorts the letterfit and the bracket weight, which is a different thing
   from animating it. This moves it and leaves it alone otherwise.

   The overshoot is in the ease, not the keyframes: cubic-bezier(0.34, 1.56,
   0.64, 1) carries past its endpoint and settles back, so the mark dips a
   fraction below the line on landing. That dip is the whole joke, and at 440ms
   it is over before it asks for attention.

   The <img> moves, not the <a>. The anchor is the hit target and stays put, so
   a second click lands on the same pixels as the first. */
.wordmark.is-jumping img {
  animation: wordmark-jump 440ms cubic-bezier(0.34, 1.56, 0.64, 1);
}
@keyframes wordmark-jump {
  0%   { transform: translateY(0); }
  35%  { transform: translateY(-24%); }
  70%  { transform: translateY(3%); }
  100% { transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  .wordmark.is-jumping img { animation: none; }
}

/* ── Hero ───────────────────────────────────────────────────────────────── */
.hero {
  position: relative;
  /* The board's layers are sized from the hero box. If that box changes after
     they are sized — fonts landing, the opening state being removed, a
     scrollbar appearing — a canvas can briefly extend past the hero and paint
     a run into the section below it. Clip it. */
  overflow: hidden;
  z-index: 2;
  min-height: 100svh;
  padding-top: calc(var(--header-h) + clamp(56px, 12vh, 132px));
  padding-bottom: clamp(96px, 14vh, 148px);
  display: flex;
  flex-direction: column;
  justify-content: center;
}
/* Routing diagram behind the hero content (see hero-field.js). Hidden on
   narrow screens, where the headline fills the viewport. */
.hero__plate {
  position: absolute;
  inset: 0;
  z-index: 0;
  opacity: 0.026;
  mix-blend-mode: screen;
  pointer-events: none;
  /* The plate lifts the hero a fraction above pure black, so it has to fade
     out before the hero ends — otherwise its bottom edge reads as a seam
     across the page. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 62%, transparent 96%);
  mask-image: linear-gradient(to bottom, #000 0%, #000 62%, transparent 96%);
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.62' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='260' height='260' filter='url(%23n)' opacity='0.55'/%3E%3C/svg%3E");
}

/* The board's falloff toward the headline, and the ONLY place it happens.
   /contact learned this the hard way and says so in its own source: the resting
   alpha a segment is painted at and the value the lit solve subtracts from it
   have to be the same number, or a run comes back to rest unevenly along its
   own length. The hero used to compute a shade(x, y) per sample AND carry this
   mask, so the components were attenuated twice over — 0.06 mask x 0.26 core =
   1.6% at the centre, which is why they ghosted through the type instead of
   sitting behind it.

   Now the maths is flat and the falloff is here, once, over both canvases. Both
   must carry the identical gradient: the wires and the solids they punch out of
   are one drawing, and a mask on one but not the other pulls them apart. */
.hero__objects,
.hero__field {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  /* Zero at the centre, not a low value. A mask alpha of 0 removes the board
     outright, so the type sits on the page's own black with nothing behind it
     — at 0.16 the runs were still faintly legible through the lettering, which
     is the one place they must not be. The dead zone reaches 50% of the
     gradient before it starts to open, so the headline and the lede are both
     inside it at every viewport, and it does not reach full strength until the
     rim. */
  -webkit-mask-image: radial-gradient(ellipse 68% 57.5% at 50% 50%,
    rgba(0,0,0,0) 0%, rgba(0,0,0,0) 50%, rgba(0,0,0,0.55) 78%, #000 100%);
  mask-image: radial-gradient(ellipse 68% 57.5% at 50% 50%,
    rgba(0,0,0,0) 0%, rgba(0,0,0,0) 50%, rgba(0,0,0,0.55) 78%, #000 100%);
}
@media (max-width: 760px) { .hero__objects, .hero__plate { display: none; } }
/* The hero's two states, cross-faded in place.

   Stacked in one grid cell, so the box is as tall as the taller of the two and
   nothing below it moves when they swap. That is the whole reason this is a
   grid rather than two absolutely positioned children — absolute would take
   both out of flow and collapse the column.

   The default is state 2 alone: no JS, no scroll and reduced motion all land
   here. `.is-live` is added by the script on load and is what reveals state 1;
   `.is-swapped` takes it away again. Written this way round so the enhancement
   is what can fail, never the content.

   visibility, not just opacity: a faded element is still in the tab order and
   still read out. Transitioning it to hidden AFTER the fade keeps it out of
   both without a flash. */
.hero__swap { display: grid; grid-template-areas: 'state'; }

/* Both states centre themselves in the shared cell.

   They stack in one grid area, so the cell is as tall as the taller of the two
   — and a grid item stretches to fill it. State 2 is now the taller (an eyebrow
   and a gap above the same headline), so state 1 was left sitting at the top of
   a box sized for its neighbour, and the copy stepped up the screen as the
   cross-fade ran. Centring both puts the text on the same line in the hero
   whichever state is showing, and the fade happens in place. */
.hero__state {
  grid-area: state;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* State 2 alone is the default — no JS, reduced motion, or a browser without
   position: sticky all land here, and it is the page's actual claim. */
.hero__state--intro { opacity: 0; visibility: hidden; }
.hero__state--brand { opacity: 1; visibility: visible; }

/* Driven, not timed. The script writes --o1 and --o2 from how far the hero has
   been scrolled through, so the reader moves the cross-fade themselves; there
   is no duration here to fight them. */
.is-live .hero__state--intro { opacity: var(--o1, 1); visibility: visible; }
.is-live .hero__state--brand { opacity: var(--o2, 0); visibility: visible; }

/* Faded is not enough — an element at zero opacity is still tabbable and still
   read out. These flip the finished state out of the tree entirely. */
.is-live[data-at='start'] .hero__state--brand { visibility: hidden; }
.is-live[data-at='end']   .hero__state--intro { visibility: hidden; }

/* The pin. The section is one viewport plus the transition distance; the child
   is one viewport and sticks to the top, so the hero holds still while the page
   scrolls past it. Nothing is prevented and nothing is hijacked — scrollbar,
   keyboard, trackpad and swipe all behave normally, and there is no state in
   which the page cannot be scrolled.

   THE CLIP MOVES TO THE STICKY CHILD. .hero carries overflow: hidden to stop a
   canvas painting into the section below it, and overflow: hidden on an
   ancestor silently disables position: sticky in a descendant — the child just
   scrolls away. The clip belongs on the box that holds the canvases anyway,
   which is now this one.

   The hero's padding moves with it, so the copy sits exactly where it did:
   without that the stick would pin at top 0 and the content would jump up by
   the header's clearance on the first scroll. */
/* justify-content too. .hero is a centring flex column, so once its box grew
   to 100svh + travel it centred the sticky child in that — 260px down — and
   the pin could not engage until the reader had already scrolled past it. The
   child has to start at the top of the box it sticks inside. */
.hero.is-live {
  overflow: visible;
  padding-top: 0;
  padding-bottom: 0;
  justify-content: flex-start;
}
.hero.is-live { min-height: calc(100svh + var(--hero-travel)); }
.hero.is-live .hero__stick {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: hidden;
  /* Symmetric, so justify-content: center lands the block in the TRUE middle
     of the viewport. It was header-height + 132 above against 148 below, which
     centred the content inside an off-centre box and sat it about 70px low.
     That was invisible while the body copy filled the space under the headline;
     with the copy gone the headline is the only thing here, and low reads as a
     mistake rather than as a choice.

     The header's height stays in BOTH halves rather than being removed from the
     top: it is what guarantees the first line clears the header on a short
     viewport, and matching it below is what keeps the centre honest. */
  padding-top: calc(var(--header-h) + clamp(56px, 12vh, 132px));
  padding-bottom: calc(var(--header-h) + clamp(56px, 12vh, 132px));
  display: flex;
  flex-direction: column;
  justify-content: center;
}

/* Colour only, per the token. No weight or size change. */
/* Two lines, the first of them entirely in the accent. Colour only — the size,
   weight and letterfit are the shared .hero__title's. */
.hero__line { display: block; }
.hero__accent {
  color: var(--terracotta);
  /* The accent holds a word and, on some lines, the full stop after it. The
     word is an inline-block, so the browser may break between it and the
     period when the line fills exactly — a full stop alone on the next line.
     Seen at 375px on "you participate." Nothing inside an accent should ever
     split, so the span refuses to. */
  white-space: nowrap;
}
@media (hover: hover) and (pointer: fine) {
  .hero__accent .hero__word:hover { color: var(--terracotta); filter: brightness(1.18); }
}


/* The supporting line under the state 1 headline. Not .hero__lede: that is
   16-19px and reads as a second headline at this scale. */
.hero__body {
  /* Enough air that the line reads as a separate thought under the title
     rather than as a third line of it. */
  margin: clamp(28px, 3.4vh, 40px) auto 0;
  /* A measure, but a wide one. Unbounded it ran about 165 characters, twice a
     comfortable measure, and read as a strapline rather than as copy. */
  max-width: 78ch;
  /* The same font as .page-head__lede on /contact — 17px at 1.6, TYPOGRAPHY.md
     §3's prose rung — but in the primary ink rather than that page's prose
     grey. The accent now sits on two spans in the headline above, so the copy
     has to stay out of its way: grey would rank it below the headline and
     terracotta would compete with the words that carry the accent. */
  /* 21.25px — the 17px prose rung up 25%, the same step the headline took so
     the two keep their proportion. This puts the hero's copy above the 17px
     that TYPOGRAPHY.md §1 sets as the page's largest prose; a deliberate
     exception for the one screen that is a display tier, not a page. */
  font-size: 1.328rem;
  line-height: 1.5;
  color: var(--off-white);
  text-wrap: pretty;
  /* Clicking it sparks the board. The pointer is the only affordance, as it
     is on the keywords; the copy itself does not move. */
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* A hovered word on the accent line keeps its colour. .hero__word lifts to
   #ffffff, which on the off-white line is a small brightening and on the
   terracotta one is a flip to a different colour — the same rule reading as
   two different treatments. The swell is the affordance; this line brightens
   within its own hue instead. */
@media (hover: hover) and (pointer: fine) {
}

/* The cue: a small solid triangle, borders rather than an asset.

   TWO ELEMENTS, and the reason is worth writing down. The outer one carries the
   delayed arrival; the inner one carries the scroll. They cannot share, because
   an animation with fill-mode: forwards pins the property it animated and beats
   a plain declaration — so a single element faded in by @keyframes could not
   then be faded out by opacity: calc(var(--o1) …), and the triangle stayed
   visible in state 2. Nesting multiplies the two opacities instead. */
.hero__cue {
  /* Further down than the 28-44 it had, so it sits clear of the copy rather
     than tucked under it. */
  /* Up from clamp(40px, 5.4vh, 62px). The body copy used to sit between the
     headline and this mark and did the separating; with it gone the arrow sat
     close under the last line and read as attached to it. */
  margin-top: clamp(76px, 10vh, 128px);
  display: block;
  width: 10px;
  /* The column is centred but this is a 10px block, so it sat against the left
     edge of it rather than under the middle of the copy. */
  margin-inline: auto;
  opacity: 0;
  animation: cue-arrive 0.7s var(--ease) 1s forwards;
}
@keyframes cue-arrive { to { opacity: 1 } }

.hero__cue-mark {
  display: block;
  width: 0;
  height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-top: 7px solid var(--terracotta);
  /* 0.8, up from 0.55. At 0.55 it was competing with the board's own runs and
     losing — a 7px mark on busy art needs more than a hairline's worth of
     alpha to register. Still short of full, so it stays a cue rather than
     becoming something to click. */
  opacity: 0.8;
}
/* Gone by state 2: this rides the scroll, the wrapper above does not. */
.is-live .hero__cue-mark {
  opacity: calc(var(--o1, 1) * 0.8);
  animation: cue-breathe 2s var(--ease) infinite;
}
/* Movement only. It drifts down and back — no change of opacity, so it reads
   as a breath rather than a blink. */
@keyframes cue-breathe {
  0%, 100% { translate: 0 0 }
  50%      { translate: 0 6px }
}

@media (prefers-reduced-motion: reduce) {
  /* No pin, no drive, no loop — state 2, standing still. */
  .hero.is-live { min-height: 100svh; overflow: hidden;
    padding-top: calc(var(--header-h) + clamp(56px, 12vh, 132px));
    padding-bottom: clamp(96px, 14vh, 148px); }
  .hero.is-live .hero__stick { position: static; height: auto; padding: 0; overflow: visible; }
  .is-live .hero__state--intro { opacity: 0; visibility: hidden; }
  .is-live .hero__state--brand { opacity: 1; visibility: visible; }
  .hero__cue { animation: none; opacity: 1; }
  .hero__cue-mark, .is-live .hero__cue-mark { animation: none; opacity: 0.8; translate: none; }
}

/* The eyebrow and the headline are one thought — the headline finishes the
   count the eyebrow starts — so they sit closer to each other than to anything
   else.

   Steel, not muted. Both are grey and not interchangeable: muted is the prose
   ink, while steel is what every uppercase label on this site already uses —
   the header's own control and the /about register. The eyebrow is a label, so
   it takes the label grey.

   It was terracotta. With the accent now on "agentic commerce" in the headline
   below it, two terracotta elements stacked split the accent between them and
   left neither carrying it. */
.hero__eyebrow {
  /* 0.625rem, up from 0.5 — the same 25% the type took, so the gap keeps its
     proportion to the line above it rather than shrinking against a bigger
     eyebrow. It is still far tighter than the space below the headline, which
     is what holds the two together as one thought. */
  margin: 0 0 0.78rem;             /* 0.625 x 1.25, riding the type */
  font-size: 1.2695rem;               /* 20.3px — 16.25px x 1.25, with the headline */
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--steel);
  /* Two sentences. Where it must wrap it should break BETWEEN them, so each
     line is a whole thought rather than a run-on cut wherever the width ran
     out. Balance does that; on desktop it is one line and this does nothing. */
  text-wrap: balance;
}

.hero > .container { position: relative; z-index: 1; }
/* All that survived of a thirty-line block ported from the prototype. It
   re-declared .hero__plate and .hero__objects in full inside this query —
   including a second copy of the plate's noise data-URI — and nested another
   @media inside it repeating a `display: none` already set ten lines above.
   The wire canvas going away on a narrow screen was the only thing it did. */
@media (max-width: 760px) { .hero__field { display: none; } }

/* The headline sits in a centred, width-limited column. */
.hero__column {
  width: 100%;
  max-width: 1180px;
  margin-inline: auto;
  text-align: center;
}
/* Sized so the first line holds on one line from tablet upward: two lines
   total, the second in steel grey. Below 760px it wraps naturally. */
.hero__title {
  /* Up 25% from clamp(1.68rem, 3.8vw, 3.44rem) — which lands it back on the
     clamp(2.1rem, 4.75vw, 4.3rem) it had before the earlier 20% cut, since
     0.8 x 1.25 = 1. Both hero states use this rule, so they match by
     construction rather than by two numbers agreeing. */
  font-size: clamp(2.1rem, 4.75vw, 4.3rem);
  font-weight: 500;
  line-height: 1.06;
  letter-spacing: -0.035em;
}
.hero__title-line,
.hero__title-muted { display: block; }
/* Second line stays in the same ink; the hierarchy comes from weight and size,
   not from a colour change. */
.hero__title-muted {
  color: var(--off-white);
  font-weight: 300;
  font-size: 0.87em;
}

/* The only accent in the hero: a short terracotta rule between the headline
   and its supporting line. */
.hero__rule {
  display: block;
  width: 34px;
  height: 1px;
  margin: clamp(26px, 3vw, 38px) auto 0;
  background: var(--terracotta);
}
.hero__bracket { display: none; }   /* shown only by the ?type=c comparison */

/* Key words come forward very slightly on hover. `inline-block` + `transform`
   keeps this off the layout path, so nothing reflows and no neighbouring word
   moves. Precise, not springy: small scale, small brightness step, long ease.
   Fine pointers only; reduced motion drops the movement and keeps the tone. */
/* Clicking one of these answers on the board. The pointer is the only thing
   telling anyone that, so it is not decoration: hovering used to fire the
   spark by itself, which made the affordance unnecessary. Now that the board
   waits to be asked, the word has to look askable. */
.hero__word {
  display: inline-block;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  transform: translateZ(0);
  transition: transform 0.42s var(--ease), color 0.42s var(--ease);
}
@media (hover: hover) and (pointer: fine) {
  .hero__word:hover {
    transform: scale(1.024);
    color: #ffffff;
  }
}
@media (prefers-reduced-motion: reduce) {
  .hero__word:hover { transform: none; }
}

@media (min-width: 761px) {
  .hero__title-line,
  .hero__title-muted { white-space: nowrap; }
}
/* Measure is wide enough to hold one line on desktop rather than orphaning
   the final clause. */
.hero__lede {
  margin-top: clamp(18px, 2vw, 24px);
  margin-inline: auto;
  max-width: 74ch;
  font-size: clamp(1rem, 1.25vw, 1.2rem);
  line-height: 1.6;
  color: var(--muted);
  letter-spacing: -0.005em;
  text-wrap: pretty;
}
@media (max-width: 760px) {
  .hero { min-height: 88svh; padding-bottom: clamp(80px, 16vh, 120px); }

  /* The phone gets its own floor. The desktop clamp's 2.1rem minimum is what a
     375px screen actually renders, since 4.75vw is only 18px there — so the 25%
     desktop lift arrived on the phone as a six-line headline with "built." on
     a line of its own, over a four-line sub-heading. None of that is what the
     lift was for. These hold the desktop sizes exactly (they only apply below
     the board's own breakpoint) and give the phone back the sizes it had before,
     which wrapped in the right places. */
  .hero__title   { font-size: 1.8rem; }        /* 28.8px — two lines per sentence, no orphan */
  .hero__body    { font-size: 1.0625rem; line-height: 1.6; } /* back to the 17px prose rung */
  /* The eyebrow has to stay on one line, and at 20px with 0.12em tracking it
     does not fit 375px; nor did it at 16.25 — that wrap was a known defect. */
  .hero__eyebrow { font-size: 0.875rem; letter-spacing: 0.1em; }   /* 14px */
}

/* ── Sections ───────────────────────────────────────────────────────────── */
/* Sections carry no rules or frames — they are separated by space alone, so
   the page reads as one continuous surface. */
.section {
  position: relative;
  z-index: 2;
  padding-block: var(--section-y);
}

/* Backers — centred, and intentionally not framed as its own section. */
.section--backers {
  padding-top: clamp(72px, 11vw, 168px);
  padding-bottom: clamp(104px, 13vw, 208px);
  text-align: center;
}
.section--backers .section-title { margin-inline: auto; }

.backers__statement {
  position: relative;
  display: inline-block;
  margin-inline: auto;
  /* Wide enough to hold the whole sentence on one line on desktop; it still
     wraps naturally once the container is narrower than the line. */
  max-width: 64ch;
  font-size: clamp(1.25rem, 2.3vw, 1.9rem);
  font-weight: 300;
  line-height: 1.3;
  letter-spacing: -0.018em;
  color: var(--off-white);
}

/* A terracotta tick either side of the line, pointing outward. On hover they
   step further out — the sentence is being opened, not underlined. They are
   the accent in this section: there is no heading above the line, which is
   what keeps this inside the one-mark-per-view rule in TYPOGRAPHY.md §5. */
.backers__statement::before,
.backers__statement::after {
  content: '';
  position: absolute;
  top: 50%;
  width: 0;
  height: 0;
  border-block: 5px solid transparent;
  transition: transform 0.42s var(--ease);
}
/* The scale is carried on the transform rather than taken off the border
   widths: a border resolves to whole pixels, so 5px/8px scaled by hand would
   land on 4px/7px — 20% off one axis and 12% off the other, and the tick would
   change shape rather than just size. Every transform below must keep the
   scale. */
.backers__statement::before {
  right: 100%;
  border-right: 8px solid var(--terracotta);
  transform: translate(-22px, -50%) scale(0.9);
}
.backers__statement::after {
  left: 100%;
  border-left: 8px solid var(--terracotta);
  transform: translate(22px, -50%) scale(0.9);
}
@media (hover: hover) and (pointer: fine) {
  .backers__statement:hover::before { transform: translate(-34px, -50%) scale(0.9); }
  .backers__statement:hover::after  { transform: translate(34px, -50%) scale(0.9); }
}
@media (prefers-reduced-motion: reduce) {
  .backers__statement::before, .backers__statement::after { transition: none; }
}
@media (max-width: 620px) {
  /* No room for them beside the line once it wraps to the full width. */
  .backers__statement::before, .backers__statement::after { display: none; }
}

/* Team.
   The heading used to be centred, which read fine when it was 1.75rem and the
   only thing on its line. At 11px it is identical to every other heading on
   the page, so centring just one of them reads as a mistake. */

/* Same 12px as .principles, and open at the head for the same reason. */
.register { margin-top: 12px; }

.register__link {
  position: relative;                 /* for the lit divider below */
  display: grid;
  grid-template-columns: minmax(0, 26ch) minmax(0, 1fr) auto;
  align-items: baseline;
  gap: clamp(16px, 3vw, 48px);
  padding: clamp(15px, 1.5vw, 21px) 2px;
  border-bottom: 1px solid var(--line);
  text-decoration: none;
  color: inherit;
  transition: border-color 0.28s var(--ease);
}
/* The divider under a picked row, lit the way a run is lit.

   The resting rule is rgba(244,243,240,0.09), within a rounding of the board's
   own IDLE_TRACK of 0.085 — arrived at separately and in agreement, so the line
   under a name is already the weight of track a signal travels on. Lighting it
   to 0.60 is lighting it to MANIF_SIGNAL.LIVE_MAX, which is what the head of a
   signal reaches.

   Static, and the whole bar at once. A travelling head would be a fourth thing
   moving on a page that already carries the netting, the dispatch run and the
   scrim, and it would turn a hover into an animation to sit through. The rule
   simply comes on.

   An overlay rather than a transition on border-bottom-color, so the bloom has
   something to hang on: a 1px border cannot carry a box-shadow without the
   shadow reading as a second line under it. */
.register__link::after {
  content: '';
  position: absolute;
  /* The rule is the bar and stays exactly its width. Only the glow above
     runs past the ends — a light source does not get longer than the thing
     it is. */
  left: 0;
  right: 0;
  bottom: -1px;
  height: 1px;
  background: rgba(244, 243, 240, 0.72);
  box-shadow: 0 0 3px rgba(244, 243, 240, 0.55);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.28s var(--ease);
}
.register__row.is-active .register__link::after { opacity: 1; }

/* The bleed around the lit rule, and it is a GRADIENT rather than a shadow.

   box-shadow was the wrong tool and it took two attempts to see why. A shadow
   is cast by the box it belongs to, and this box is one pixel tall — so
   whatever alpha it has gets spread across the blur radius and arrives at a
   few per cent. Stacking three of them made a slightly wider few per cent.
   What it looked like was a white line slightly out of focus, which is exactly
   what it was: blur, not light.

   A band with a gradient in it has no such constraint. The alpha at every
   distance from the rule is stated outright — 0.22 against the page's own
   0.047 at the centre, which is roughly five times the background rather than
   the fraction above it — and it falls to nothing over 20px either side.

   Two layers, because light has a bright middle and a soft surround, and one
   element cannot be both: ::after is the rule, ::before is what it throws. */
.register__link::before {
  content: '';
  position: absolute;
  /* Past both ends, and faded there by the mask below.

     The band used to stop at exactly the x the rule stops at, which gave the
     glow two hard vertical edges — a lit rule inside a rectangle of haze,
     with square corners at the tips. Light does not end on a straight edge;
     it carries past the thing emitting it and falls off. So the band runs
     22px beyond each end and the mask takes it to nothing over that distance,
     which is what makes the ends read as lit rather than cut. */
  left: -22px;
  right: -22px;
  -webkit-mask-image: linear-gradient(to right,
    transparent 0, #000 22px, #000 calc(100% - 22px), transparent 100%);
  mask-image: linear-gradient(to right,
    transparent 0, #000 22px, #000 calc(100% - 22px), transparent 100%);
  bottom: -21px;
  height: 42px;
  background: linear-gradient(to bottom,
    rgba(244, 243, 240, 0)    0%,
    rgba(244, 243, 240, 0.04) 28%,
    rgba(244, 243, 240, 0.12) 42%,
    rgba(244, 243, 240, 0.22) 50%,
    rgba(244, 243, 240, 0.12) 58%,
    rgba(244, 243, 240, 0.04) 72%,
    rgba(244, 243, 240, 0)    100%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.28s var(--ease);
}
.register__row.is-active .register__link::before { opacity: 1; }

/* A name is not a heading, so it stays in sentence case and at prose size —
   uppercasing people is a different house style. Dropped from ~18px so it
   does not become the one thing on the page above the body. */
.register__name {
  position: relative;
  display: inline-block;
  font-size: 1.0625rem;               /* 17px */
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: -0.012em;
  /* Steel, always. Inverted against the sections above, where the label is the
     grey and the prose is the ink: here the role carries the ink and the name
     sits behind it. Colour only — size, weight and casing are unchanged, so
     the rank still reads off the type.

     No transition, because nothing changes it any more. */
  color: var(--steel);
}
/* The whole row is the target, so the name does not carry a mark of its own —
   it simply comes forward with the row it is in. The brackets that used to
   close around it said "this is a link" a second time, in the same terracotta
   the "read more" beside it now uses, which put two accents in one line. */
/* No lift on the picked row. The name is steel whether the row is picked or
   not — the role is the ink here, and having the name jump to off-white on
   hover put two full-strength values on one line and undid the inversion the
   moment anyone pointed at it.

   The row still answers: the lit divider under it, the mark appearing at the
   end, and the siblings dropping back. Colour is not carrying any of that. */

/* The job title, in the label voice: 13px, 600, tracked, uppercase by CSS and
   sentence case in the roster. It is a label rather than prose — a role, not a
   sentence about one — and setting it as one makes the register read as a
   table of records instead of a list of people with notes after their names.

   Identical to the "read more" beside it in everything but colour, which is
   the §3 model working as intended: one treatment across the UI layer, and
   rank carried by the grey ramp. Steel here, terracotta there. */
.register__role {
  font-size: 0.8125rem;               /* 13px */
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--off-white);
}

.register__id {
  justify-self: end;
  font-family: var(--font-mono);
  font-size: 0.75rem;                 /* 12px */
  letter-spacing: 0.14em;
  color: var(--steel-dim);
  transition: color 0.28s var(--ease);
}
.register__link:hover .register__id { color: var(--terracotta); }

@media (max-width: 720px) {
  /* Three columns will not hold; the row becomes a stacked entry instead. */
  .register__link {
    grid-template-columns: 1fr;
    gap: 5px;
    padding: 16px 2px;
  }
  .register__id { justify-self: start; padding-top: 3px; }
  .register__meta { flex-wrap: wrap; }
}

/* ── Picking a name ────────────────────────────────────────────────────────
   Pointing at a name lifts that row out of the register and takes the rest of
   the list back — a card used to open over the whole page instead, which
   covered the thing you were reading to tell you about it.

   The motion is the page's: 0.42s on the standard ease at scale 1.028, the
   same swell a headline word and a call to action take. The others go to 28%
   and 2.5px of blur rather than out, so the list is still visibly a list and
   the eye has somewhere to go next.

   State is set by class from the script, not by :hover in the sheet, because
   keyboard focus has to produce exactly the same thing and the trigger is the
   name while the effect is the whole row. */
.register__row {
  position: relative;
  transform: translateZ(0);
  transition: transform 0.42s var(--ease),
              opacity 0.42s var(--ease),
              filter 0.42s var(--ease);
}
.register.is-picking .register__row {
  opacity: 0.28;
  filter: blur(2.5px);
}
.register.is-picking .register__row.is-active {
  opacity: 1;
  filter: none;
  transform: scale(1.028);
  z-index: 2;
}

/* Beside the title it belongs to, not stranded at the far end of the line
   where the eye has already stopped reading. Laid out at rest and only faded,
   so the row does not reflow when it appears — the same reason the CTA's
   brackets are always in the box. Terracotta, and now the only accent in the
   row: the brackets that used to sit on the name are gone. */
.register__meta {
  display: flex;
  align-items: baseline;
  gap: clamp(12px, 1.6vw, 22px);
  min-width: 0;
}
/* The only link in the row. Everything else is a record. */
.register__more {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  white-space: nowrap;
  color: var(--terracotta);
  opacity: 0;
  transition: opacity 0.26s var(--ease), color 0.26s var(--ease);
}
.register__more svg { display: block; }

/* On the row: the mark appears. */
.register__row.is-active .register__more { opacity: 1; }

/* On the mark itself: it goes white and the arrow arrives. The arrow is laid
   out at rest and only faded, so nothing shifts when it does — the same reason
   the CTA keeps its brackets in the box. */
.register__out { opacity: 0; transition: opacity 0.22s var(--ease); }
.register__more:hover,
.register__more:focus-visible { color: #ffffff; }
.register__more:hover .register__out,
.register__more:focus-visible .register__out { opacity: 1; }

@media (prefers-reduced-motion: reduce) {
  /* The blur and the fade still say which row is picked; the movement does not
     need to. */
  .register.is-picking .register__row.is-active { transform: none; }
}

.team-fallback { margin-top: 32px; display: grid; gap: 8px; color: var(--steel); }

/* ── Footer ─────────────────────────────────────────────────────────────── */
.site-footer {
  position: relative;
  z-index: 2;
  padding-top: clamp(72px, 9vw, 128px);
  padding-bottom: clamp(56px, 7vw, 96px);
}
/* One row, two ends: links left, copyright right. The mark used to hold the
   left corner and the links sat on the true centre between two equal columns;
   with the mark gone the links take the corner themselves, which keeps the
   footer reading as a base rather than as something floating in the middle.
   No rule on top — the spine's traverse is already the boundary. */
.site-footer__inner {
  /* The container's measure, exactly — the same one the header wordmark and
     every line of copy on the page sit on. The footer used to run on its own
     inset, which put the links on a margin nothing else shared; at a glance
     that reads as a misalignment rather than as a decision, because it is one.
     The links now start on the same vertical as the mark above them. */
  width: min(var(--container), 100% - (var(--gutter) * 2));
  max-width: none;
  margin-inline: auto;
  display: grid;
  grid-template-columns: auto auto;
  justify-content: space-between;
  align-items: center;
  gap: clamp(20px, 4vw, 48px);
}

.site-footer__nav {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  gap: clamp(24px, 3.4vw, 48px);
  /* No inset of its own. The links start on the container's left edge so they
     line up with the wordmark and the copy; the homepage spine's left rail
     runs at 3.2vw, which lands within a couple of pixels of that edge on a
     wide screen — a 1px hairline at 5% behind uppercase type, which is not
     worth breaking the alignment for. */
  margin-left: 0;
}
/* No size step in the footer: the links carry the same 11px label voice they
   do in the header, which is what puts them and the copyright on one line of
   type rather than two. */

.site-footer__note {
  justify-self: end;
  text-align: right;
  color: var(--steel-dim);
}

@media (max-width: 780px) {
  /* Two ends stop fitting side by side, so each takes its own row — and once
     they are stacked they centre.

     They used to keep the left alignment they have on a wide screen, on the
     reasoning that the links already start on the container's left edge. That
     holds while the two ends are pulling apart across a wide footer and stops
     holding the moment they are one under the other: two short rows hugging
     the left of a narrow column leave a column of empty space down the right
     and read as a layout that lost an element rather than as a base. */
  .site-footer__inner {
    grid-template-columns: 1fr;
    row-gap: clamp(20px, 4vw, 32px);
    justify-items: center;
    text-align: center;
  }
  /* Stacked, not side by side. Two CTAs on one row read as a pair to choose
     between; on a narrow screen they also sit close enough that a thumb aiming
     for one can catch the other. One per row gives each its own line and its
     own target, and the column of them lines up with the copyright under it. */
  .site-footer__nav {
    margin-left: 0;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: clamp(14px, 3.5vw, 20px);
  }
  .site-footer__note {
    justify-self: center;
    text-align: center;
    /* 10px against the shared 11px. The copyright is the least important line
       on the page and on a narrow screen it was the widest — 54 characters of
       tracked uppercase running the full column while the two links above it
       took half of one. Dropping a step and easing the tracking lets it sit
       under them rather than underline them. */
    font-size: 0.625rem;              /* 10px */
    letter-spacing: 0.06em;
    line-height: 1.5;
  }
}

/* ── Interior pages ─────────────────────────────────────────────────────── */
/* No hero and no spine: the page opens on type, under the same header. */
/* The header is fixed and 74px tall, so this padding is the whole of the gap
   between it and the first line of the page. 64px put the title just under the
   header's foot and the page opened cramped; 104px lets it clear the header
   band and the scrim's falloff before anything starts. */
.page-head {
  position: relative;
  z-index: 2;
  padding-top: calc(var(--header-h) + clamp(64px, 7vw, 104px));
  /* Nothing below the title: the 12px gap to its content is set by the rule
     under `.page-head +`, so the H1 sits on its list exactly the way an H2
     sits on its own. Splitting the gap across two paddings is what left the
     page title further off its content than every other heading on the page. */
  padding-bottom: 0;
}
/* The interior pages run the §3 scale one rung up from manif.bot's — 13/16/17
   where /problem is 11/14.3/15 — because they are read at arm's length on a
   marketing surface rather than scanned as documentation. The ratios and the
   ordering are identical, which is what keeps it one system at two sizes.

   16px is the ceiling here for the same reason 14.3px was there: prose is
   17px, so a heading still sits under the body and §1 holds — body copy is the
   largest text on the page. A heading at or above prose is a display tier
   under another name.

   The head still carries a lede, and still needs one. Size alone does not make
   a title read as a title at this scale; the pairing does most of the work,
   which is why every .page-head on this site has both. The lede is prose-sized
   and therefore larger than the title above it — the same relationship
   /problem has between its .sub and its h1, and the reason the 13px lede under
   a 14.3px title read as an afterthought.

   No max-width and no balancing: a title that wraps at caps has too many words
   in it. */
.page-head__title {
  font-size: 1rem;                    /* 16px */
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--terracotta);
}
/* The lede is the page summary, one step below prose and in the muted ink.
   12px off the title rather than 32: at 11px the title belongs to the sentence
   under it, and the old gap left the two reading as separate blocks. */
.page-head__lede {
  margin-top: 12px;
  max-width: 66ch;
  font-size: 1.0625rem;               /* 17px — a sentence, so prose */
  line-height: 1.6;
  color: var(--muted);
}

/* The partner categories on /contact.

   Label voice, not prose: these are five category names, and TYPOGRAPHY.md §2
   puts names of that kind in the same uppercase rank the principle titles use.
   They take the ink rather than the grey, because they are the substance of the
   line above them and not an aside to it.

   The marker is a 10px terracotta rule — the site's own hairline at the site's
   own accent, rather than a bullet. `ul` has list-style off globally, so a disc
   would have to be reintroduced to be used, and a disc is a different house
   style. Spacing separates the items; there are no rules between them, for the
   reason the principles list gives for closing on space. */
.partners {
  margin-top: clamp(20px, 2.4vw, 28px);
  display: grid;
  gap: 11px;
}
.partners li {
  position: relative;
  padding-left: 22px;
  font-size: 0.8125rem;               /* 13px — the label rank */
  font-weight: 600;
  line-height: 1.4;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--off-white);
}
.partners li::before {
  content: '';
  position: absolute;
  left: 0;
  /* On the cap height, not the line box: 13px on 1.4 leaves leading above the
     glyphs, and centring the mark in the box sits it visibly high. */
  top: 0.62em;
  width: 10px;
  height: 1px;
  background: var(--terracotta);
}

.section--principles { padding-block: clamp(56px, 7vw, 96px); }
.section--principles + .section--principles { padding-top: clamp(48px, 6vw, 84px); }
/* The head and the first list belong together — no section gap between them. */
/* Zero, so the page title sits on its list at exactly the 12px a section
   heading does. The gap here is `.principles`' own margin and nothing else —
   this padding used to add up to 40px on top of that, which is why the H1 read
   as further off its content than every other heading on the page. */
.page-head + .section--principles { padding-top: 0; }

/* Same register grammar as the founding team: a hairline per entry, the label
   on the left and the substance on the right. */
/* 12px off the heading, not 44. A heading belongs to what follows it, and at
   11px the old gap left the two reading as unrelated blocks. TYPOGRAPHY.md §6.

   No rule at the head either: the heading already marks where the list starts,
   and a hairline directly under it was drawing that boundary twice. What is
   left are the rules between entries, which are the only ones dividing
   anything. */
.principles { margin-top: 12px; }
/* 24ch, and measured rather than reasoned.

   It was 19ch, and the note here said that was sized for "an 11px caps label".
   That was true when it was written and stopped being true when manif.ai moved
   to its own rung of the scale — .principle__title is 13px, not 11, and the
   column never followed. 19ch x 13/11 is 22.5ch, which is roughly where the
   arithmetic lands and close to what the browser reports.

   Measured on the page: the widest label the site has, "Quality Is Our
   Identity", renders at 189px against a 19ch column of 154px, so it wrapped to
   two lines while the other six sat on one — a single ragged item in a list
   that is otherwise a clean ladder. 24ch is 195px: enough for the longest
   label with a little over, and not so much that the gutter opens up again.
   Public Sans is self-hosted, so those metrics are the same everywhere. */
.principle {
  display: grid;
  grid-template-columns: minmax(0, 24ch) minmax(0, 1fr);
  gap: clamp(10px, 3vw, 48px);
  /* Centred against the body, not hung off its first line. The label is 13px
     on 1.2 and the prose is 17px on 1.65, so top-aligning the two cells left
     the label sitting inside the first line's leading rather than level with
     anything — close enough to alignment to read as a mistake. Centring gives
     it a relationship to the whole entry, which is what it labels. */
  align-items: center;
  padding: clamp(20px, 2.2vw, 30px) 2px;
  border-bottom: 1px solid var(--line);
}
/* The entry label is the third heading rank, so it takes the same 11px caps in
   the subtle grey rather than the accent — the section heading above it is
   already the accent, and if everything is terracotta the accent has stopped
   ranking anything. TYPOGRAPHY.md §5. */
.principle__title {
  font-size: 0.8125rem;               /* 13px */
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--steel-dim);
}
/* Prose: the largest text on the page, which is the whole idea.
   76ch, not the canon 66. `ch` is the width of a zero, which in Public Sans is
   wider than the average lowercase letter, so 66ch renders around 72 characters
   — and the longest entry here is 165, which lands it on three lines with four
   words on the last. 76ch takes the same sentence to two. It is at the top of
   the comfortable measure and should not go further; the fix for a longer
   entry is a shorter entry. Recorded in TYPOGRAPHY.md §8. */
.principle__body {
  font-size: 1.0625rem;               /* 17px */
  line-height: 1.65;
  color: var(--off-white);
  max-width: 76ch;
}
/* The principles close on space, not on a rule: every entry carries a hairline
   under it, so the last one would draw a line across the foot of the section,
   and a section ending in a rule reads as cut off rather than finished. The
   rules between entries separate things; the one after the last separates
   nothing.

   The register keeps its last rule, and that is a deliberate exception rather
   than an oversight. Every row there is hoverable and the rule under it is
   what lights — so dropping it left the final name as the one entry in the
   list that could not answer, which is a worse inconsistency than a line at
   the foot of a list. The register is a table of people, not a closing
   statement; it can end on a rule. */
.principle:last-child { border-bottom: 0; }

@media (max-width: 720px) {
  .principle { grid-template-columns: 1fr; gap: 8px; }
}

/* ── Contact page ───────────────────────────────────────────────────────── */
/* The page holds one short block, so it is set in the screen rather than at the
   top of it — but above the true centre, not on it. Optical centre sits higher
   than geometric centre, and a block pinned to the middle of a tall viewport
   reads as having drifted there. The oversized padding-bottom is what does it:
   the content centres in what is left after it, which lands it below the middle
   without sitting on the floor of the screen. */
/* Anchored to the top, not centred.
   Centring reads better on a page whose content never changes size — this one's
   does: the form is replaced by a shorter success block, and with
   `justify-content: center` that made the whole column re-centre, so pressing
   the button walked the title and the sentence above it down the screen. The
   position is set by padding instead, which does not move when the content
   under it changes. */
.section--contact {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  padding-top: calc(var(--header-h) + clamp(140px, 30vh, 340px));
  padding-bottom: clamp(56px, 8vh, 96px);
}

/* One column: the field and the button sit under the sentence that motivates
   them. The measure is on the form, not on the column. */
.contact {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(32px, 5vw, 48px);
  align-items: start;
}
/* Above the netting. */
.section--contact > .container { position: relative; z-index: 3; }

.interest__mail {
  margin-top: 0;

  /* 16.9px, which is the size the guide already had waiting for it.

     TYPOGRAPHY.md §3: manif.ai's pages run manif.bot's scale one rung up —
     labels 13px against 11, prose 17px against 15 — and a CTA takes the same
     1.3 step off the label rank that an accent heading does, because both are
     things that have to be FOUND rather than read. 13 x 1.3 = 16.9px. The
     shared .link-cta is 14.3px, which is 11 x 1.3: manif.bot's number, correct
     on manif.bot and one rung low here.

     It also holds §1's ceiling, and only just. Prose is 17px, so at 16.9 the
     control still sits under body copy and the claim that body copy is the
     largest text on the page survives. Anything past 17px would be a display
     tier under another name, and there is exactly one of those — the hero.

     Only this control moves. The nav and footer CTAs are labels among labels
     and stay where they are; this one is the page's single action. */
  font-size: 1.056rem;                /* 16.9px */

  /* An address, not a label — §2 lists email among the identifiers that are
     never transformed, and .link-cta uppercases, which would ship
     CONNECT@MANIF.AI. Tracking comes down with it: 0.08em is set to open out
     uppercase labels, and on mixed case at this size it pulls the address
     apart until it stops reading as one string. */
  text-transform: none;
  letter-spacing: 0.01em;
  /* The accent goes on the label and the brackets take the ink — the reverse of
     .link-cta, where a steel label is framed in terracotta. Inverting the pair
     rather than adding to it keeps the count at two colours.

     Terracotta at 16.9px is 4.65:1 on the page ground, which clears AA by 0.15.
     That is a real but thin margin, and it is the reason the brackets went
     white rather than the other way round: the address is the thing that has to
     be read and copied, so it holds the larger share of the contrast budget it
     can, and the decoration takes the colour that costs nothing. */
  color: var(--terracotta);

  -webkit-user-select: text;
  user-select: text;
  /* user-select: text alone does NOT make an address inside an anchor
     selectable. Dragging across a link starts a LINK DRAG, so the pointer
     never lays down a selection and the string this control exists to hand
     over cannot be swiped. Measured: drag-select returned an empty string
     before this line was here.

     Turning the drag off costs dragging the link to a bookmark bar or another
     window, which nobody does with a mailto, and buys the one interaction this
     control is actually for. Double-click and triple-click already worked;
     this is what makes the ordinary swipe work too. */
  -webkit-user-drag: none;
}

/* .link-cta's hover lifts the label to --off-white, which on this control would
   drop the accent at the moment of interaction. Hold the colour and brighten
   instead — the same gesture a hovered accent word in the hero uses. */
@media (hover: hover) and (pointer: fine) {
  .interest__mail:hover { color: var(--terracotta); filter: brightness(1.18); }
}
.interest__mail:focus-visible { color: var(--terracotta); filter: brightness(1.18); }

/* White, not terracotta: the brackets are the one part of this control that is
   decoration, so they take the colour with contrast to spare and leave the
   accent to the string that carries the meaning. */
.interest__mail::before,
.interest__mail::after { color: #ffffff; }

[hidden] { display: none !important; }

/* ── Cursor ─────────────────────────────────────────────────────────────── */
/* A plain circle in place of the arrow. Native cursor images rather than a
   JavaScript-driven element, so there is no lag and nothing extra moving on
   the page. 24×24 SVG with the hotspot at its centre; precise pointers only —
   touch devices fall through to the default.
   Kept last in the file so it wins over the component-level `cursor` rules. */
@media (hover: hover) and (pointer: fine) {
  body {
    cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Ccircle cx='12' cy='12' r='7' fill='rgba(198,90,46,0.16)' stroke='rgb(198,90,46)' stroke-width='1.3'/%3E%3C/svg%3E") 12 12, auto;
  }
  /* Everything clickable, including the things that are clickable without
     being links: the hero's keywords and its sub-heading set cursor: pointer
     on themselves, and a class rule on the element beats the circle inherited
     from body — so they showed the native hand. Listed here they take the
     filled circle like every other control. This block is last in the file,
     so at equal specificity it wins. */
  a, button, .button, summary, label, [role='button'],
  .hero__word, .hero__body {
    cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24'%3E%3Ccircle cx='12' cy='12' r='8.8' fill='rgb(198,90,46)' stroke='rgb(198,90,46)' stroke-width='1.4'/%3E%3C/svg%3E") 12 12, pointer;
  }
  input, textarea { cursor: text; }
}

/* ── Print ──────────────────────────────────────────────────────────────── */
@media print {
  .atmosphere, .grain, .site-header { display: none !important; }
  body { background: #fff; color: #000; }
}

/* ── Team card ─────────────────────────────────────────────────────────────
   One person, at /team/<id>. Same shell and the same register grammar as the
   list it came from — a hairline per entry, the label left and the substance
   right — so arriving here reads as opening a row rather than as landing on a
   different kind of page. */

/* The employee id, above the name. Mono because it is an identifier, and the
   one accent on the page apart from the section headings. TYPOGRAPHY.md §3. */

/* A person is not a label, so the page title keeps its casing here and drops
   the caps tracking with it — §2 exempts names and identifiers from the
   uppercase rule. It also takes the display tier, which §4 otherwise reserves
   for the landing hero: the subject of a card should be the largest thing on
   it. Recorded in §8. */

/* The same treatment the role takes in the register. */

/* Where else this person is. A row of text CTAs, not a list of URLs. */

/* Back to the register. Bottom of the page, quiet, and the only way out that
   is not the header. */

/* The solids share the wiring's box and mask; only the compositing differs.
   Above the wiring so a component covers the run it sits on, still under the
   copy. See the netting-solids script. */
.netting--solids { z-index: 2; }

/* ── Arrival ───────────────────────────────────────────────────────────────
   The screen on a first visit. Black, the [m] mark, and a bar built on the
   same signal model as the board — see the script at the foot of index.html.

   Only ever visible under <html class="arriving">, which the gate in the head
   sets before the first paint and only for a reader with no record of having
   been here. Everyone else never renders it at all: display: none, no canvas,
   no work.

   THE FAILSAFE MATTERS MORE THAN THE ANIMATION. A curtain over the page is the
   one component whose failure mode is the whole site being unusable, so it is
   built to leave on its own. `arrival-failsafe` fades it out at 6s and sets
   pointer-events: none, in CSS, with no script involved. If the loader throws,
   if a font never resolves, if window.load never fires behind a hung request —
   the screen still goes. The script's own dismissal is the fast path, not the
   only path. */
.arrival { display: none; }

.arriving .arrival {
  display: flex;
  position: fixed;
  inset: 0;
  z-index: 100;               /* over the skip-link at 90 — nothing outranks it */
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 34px;
  background: var(--black);
  animation: arrival-failsafe 0.4s var(--ease) 6s forwards;
}

/* Dismissed. The class is set by the script; the transition is what the reader
   sees, and removing the node waits for it to finish. */
.arriving .arrival.is-done {
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.34s var(--ease);
}

@keyframes arrival-failsafe {
  to { opacity: 0; pointer-events: none; visibility: hidden; }
}

/* The mark, at the wordmark's own weight and letterfit. Public Sans may not
   have landed yet — this is the first paint of the whole visit — so it is
   drawn in the stack's fallback and swaps when the face arrives. On a black
   field at this size the swap is a reflow of three glyphs, which is why the
   motion below runs long enough to cover it. */
.arriving .arrival__mark {
  font-family: var(--font-sans);
  font-size: clamp(2.6rem, 7vw, 4rem);
  font-weight: 600;
  letter-spacing: -0.02em;
  line-height: 1;
  color: var(--off-white);
  animation: arrival-mark 0.75s var(--ease) both;
}

/* The m arrives on its own and the brackets close on it — they frame it rather
   than appear beside it.

   This was one letter-spacing animation across the whole mark, which is a
   blunter instrument than it looks: letter-spacing adds its track AFTER every
   glyph, so the trailing bracket carried a gap the leading one did not and the
   group drifted left as it settled. Moving the two brackets against each other
   keeps the m fixed on the centre line, which is the thing the eye is actually
   holding on to.

   0.34em each, over 1.3s, starting once the m is already there.

   All three numbers were smaller and it could not be seen. The travel was the
   least of it: the easing was the site's --ease, a cubic-bezier(0.22, 0.61,
   0.36, 1) that is nearly finished in its first fifth. Whatever distance the
   brackets covered, they covered it in about 150ms and then crept. A motion
   that is over before the eye has found it did not need to be bigger so much
   as it needed to be spread out.

   So: a symmetric ease that starts slow and lands slow, and enough delay for
   the m to arrive first — the brackets then close on something already there,
   which is what makes it read as framing rather than as three glyphs fading up
   together. In em, so it holds proportion through the clamp() above. */
.arriving .arrival__bracket {
  display: inline-block;
  color: var(--terracotta);
  animation: arrival-bracket-left 1.3s cubic-bezier(0.4, 0, 0.2, 1) 0.1s both;
}
.arriving .arrival__bracket:last-child {
  animation-name: arrival-bracket-right;
}

@keyframes arrival-mark {
  from { opacity: 0; transform: translateY(6px); }
  to   { opacity: 1; transform: none; }
}
@keyframes arrival-bracket-left {
  from { transform: translateX(-0.34em); opacity: 0.2; }
  to   { transform: none;                opacity: 1; }
}
@keyframes arrival-bracket-right {
  from { transform: translateX(0.34em);  opacity: 0.2; }
  to   { transform: none;                opacity: 1; }
}

.arriving .arrival__bar {
  width: 220px;
  height: 3px;
  animation: arrival-bar 0.5s var(--ease) 0.28s both;
}
@keyframes arrival-bar {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Reduced motion: the screen still happens — it is information, not
   decoration — but nothing travels. The mark is simply there and the bar
   crossfades in place. */
@media (prefers-reduced-motion: reduce) {
  .arriving .arrival__mark,
  .arriving .arrival__bracket,
  .arriving .arrival__bar { animation: none; }
}

@media (max-width: 760px) {
  .arriving .arrival__bar { width: 168px; }
}

