/* ==========================================================================
   main.css — page components: header, hero, media, prose, footer.
   ========================================================================== */

/* --- Header --------------------------------------------------------------
   A floating pill, centred: the nav is no longer a rail spanning the page but a
   single dark object capped at 400px, holding careers / mark / contact.

   The header element stays full-width and transparent — it is only the
   positioner. The pill inside it is what is seen, which keeps the sticky
   behaviour and the centring independent of the pill's own size. */

.site-header {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  justify-content: center;
  /* The same gutter every section uses, so the mark and the contact link sit on the
     page's own rail rather than a tighter one of the header's — with the pill gone
     there is no box to justify its own inset. */
  padding: var(--space-md) var(--wrap-gutter);
  pointer-events: none;   /* the rail's empty half-width should not swallow clicks */
}

/* The blur lives on a pseudo-element, not on .site-header itself. A mask applies to
   an element AND its children, so masking the header alpha-faded the mark and the
   contact link along with the blur — the 32px icon lost a quarter of its opacity at
   its lower edge. On its own layer behind the content, only the blur is faded.

   Off at the top of the page and faded in once scrolling starts (main.js already sets
   data-scrolled past 8px, for the rail's own state). At rest there is nothing under
   the rail worth softening, and the blur's edge against an untouched sheet is the one
   place the invisible box could show. The mask fades its bottom out rather than
   ending on a line. */
.site-header::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  opacity: 0;
  transition: opacity 220ms var(--ease);
  -webkit-backdrop-filter: blur(4px);
  backdrop-filter: blur(4px);
  -webkit-mask-image: linear-gradient(to bottom, #000 62%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 62%, transparent 100%);
}

.site-header[data-scrolled]::before { opacity: 1; }

.nav__link,
.logo img,
.logo svg {
  transition: color 180ms var(--ease), filter 180ms var(--ease), opacity 180ms var(--ease);
}

/* No pill. The rail is now just two objects on the sheet: the mark at the far left
   and the contact link at the far right, in the page's own ink — so the header adds
   nothing to look at and the globe below it is uninterrupted.

   The two wrappers stay as layout only. .site-header keeps the sticky positioning;
   the frame becomes a full-width row so its children can go to the extremes, which
   is why justify-content lives here rather than on .site-header. */
.site-header__frame {
  width: 100%;
  max-width: var(--wrap-max);
  pointer-events: auto;
}

.site-header__inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
}

/* Over a dark panel the rail's own ink is invisible — the pill used to guarantee
   legibility on both grounds and it is gone. Two declarations do the whole job: the
   link reads var(--ink) and the mark reads filter: invert(var(--mark-invert)), so
   nothing needs a per-element override. main.js toggles the class from the rail's
   midline; the 180ms transition above crossfades it. */
.site-header.is-over-dark {
  --ink: rgb(255 255 255);
  --mark-invert: 1;
}

.logo {
  display: inline-flex;
  align-items: center;
  color: var(--ink);
}
.logo:hover { color: var(--ink); }
.logo img, .logo svg {
  height: var(--logo-h);
  width: auto;
  filter: invert(var(--mark-invert, 0));
}

/* Icon mark. The SVG carries its own grain filter, so it is placed as an
   <img> rather than inlined — no need for currentColor.
   Qualified with .logo to outrank the generic `.logo img` wordmark rule. */
.logo img.logo__icon {
  height: var(--logo-icon-h);
  width: var(--logo-icon-h);
}

.nav {
  display: flex;
  align-items: center;
  gap: clamp(1.5rem, 3vw, 2.5rem);
}

.nav--end { justify-content: flex-end; }

.site-header__inner .nav { gap: var(--space-md); }

.nav__link {
  /* Full white at rest. With one link left in the pill there is nothing for a
     muted state to recede behind — it just read as faint. Hover now dims very
     slightly instead, which is the only direction left. */
  color: var(--ink);
  font-family: var(--font-mono);
  font-size: 14px;
  /* Set like the section labels — 600 weight, wide tracking, uppercased — so the
     nav reads in the same voice as "How we operate" and "Join us" rather than as a
     third type style. The markup keeps its lowercase text; the case is presentation. */
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  opacity: 1;
  transition: color 180ms var(--ease);
}
.nav__link:hover,
.nav__link:focus-visible { color: var(--ink-muted); }

/* --- Scene ---------------------------------------------------------------
   The hero is ONE pinned scene rather than a stage section followed by a copy
   section. The section is several viewports tall and its panel sticks to the
   top of the screen, so a single scroll gesture carries three beats in order:
   the globe holds its place, the manifesto inks in beside it, and only once the
   copy is complete does the tail of the section scatter the globe and release
   the page to the next section. Scroll distance is the transport for all of it,
   which is why the height here and the thresholds in type.js and globe.js are
   read from the same element. */

.scene {
  position: relative;
  z-index: 1;
  /* 190svh. The scene owns the scroll for its whole length, so its height IS how
     long the hero holds before the page moves on.

     It was 240 while the last 42% of the range was reserved for the globe to
     dissolve in. That dissolve is gone, so that stretch became scroll in which
     nothing whatever happened — about a full viewport of it. Unnoticeable on the
     way down the first time, because the ink was still running before it; plainly
     broken on a second pass, where the copy is already permanently inked and the
     reader has to grind through the tail to reach the next section.

     190 with the reveal stretched across nearly all of it (see type.js pinTo)
     keeps the original pacing of the reveal itself and leaves a short beat at the
     end rather than a dead screen. */
  height: 190svh;
  /* Pulled up under the sticky header. The header sits in flow, so without this
     the panel starts a header's height down the page and its bottom edge —
     including the base of the globe — is cut off below the fold at rest. The
     panel's own top padding is what clears the header instead, so nothing is
     ever underneath it. */
  margin-top: calc(var(--header-h) * -1);
}

.scene__stage {
  position: sticky;
  top: 0;
  height: 100vh;
  height: 100svh;
  display: flex;
  align-items: center;
  gap: clamp(80px, 6vw, 140px);
  /* The header is sticky and transparent, so the panel keeps its own clearance
     rather than running underneath it — and that clearance has to be the header's
     FULL height plus air.

     It was (--header-h minus --space-lg), about 40px against a 72px rail, on the
     reasoning that a vertically centred column supplies the rest. That holds only
     while the column is short: with the manifesto inked it is nearly as tall as the
     stage, the centring distance goes to nothing, and its first line lands at 40px —
     inside the rail, on the same left edge as the mark. No amount of arithmetic in
     main.js can recover clearance the box does not have, which is why the two
     previous attempts at this both failed. */
  padding-block: calc(var(--header-h) + var(--space-sm)) var(--space-lg);
}

.scene__text {
  position: relative;
  /* Offset written by main.js — see the pinned branch there. */
  will-change: transform;
  order: 1;
  flex: 0 1 42%;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  /* --fit is written by main.js when the column is taller than one screen; every
     vertical measure in here is multiplied by it so the whole column scales as a
     unit rather than the type shrinking away from its own spacing. Defaults to 1,
     so nothing changes on a screen with room. */
  gap: calc(var(--space-md) * var(--fit, 1));
  text-align: left;
}

/* The title heads the column and STAYS there — the manifesto inks in beneath it
   rather than replacing it. In flow, so it reserves its own space and the copy
   below it never shifts. */
.scene__intro { margin-bottom: calc(var(--space-lg) * var(--fit, 1)); }

/* Set by main.js when the column has hit its type floor and STILL does not fit one
   screen — see fitProse(). Top-aligned rather than centred, so the residual hangs
   off the bottom of the screen instead of half of it rising under the sticky nav.

   With its own header clearance: the stage's top padding is (--header-h minus
   --space-lg), which is short of the header on purpose because a centred column
   supplies the difference. Top-aligning gives that up, so the padding is added back
   here — plus a little air, so the first line sits clear of the rail rather than
   against it. */
/* padding-top is written by fitProse() — the distance from the stage's content top,
   which begins above the rail, down to just below it. Declaring it here cannot work:
   the stage's negative top margin means the shortfall is not a constant. */
.scene__text.is-overflowing { align-self: flex-start; }

.scene .media {
  order: 2;
  flex: 1 1 58%;
  align-self: stretch;
  margin-top: 0;
}

/* Running copy set flush left, not centred: it sits in a column beside the globe
   rather than alone in the middle of the screen.

   Sized against viewport HEIGHT rather than stepped through media queries. The
   pinned panel is exactly one screen and now holds the title as well as six
   blocks of copy, so what has to fit is height — and a clamp tracks it
   continuously instead of jumping at three arbitrary thresholds. */
.scene .prose {
  text-align: left;
  /* The clamp tracks viewport height, which gets most of the way there — but a
     clamp cannot know how the copy WRAPPED, so it cannot guarantee the column fits
     one screen. main.js measures the real height and writes --fit; see fitProse(). */
  /* The floor is 15px, not 13. The clamp tracks viewport HEIGHT so the copy can be
     squeezed into a pinned screen — but below the pin threshold there is no screen to
     fit into and the section just scrolls, so the same clamp was setting 13px body
     copy on an ordinary laptop for no gain. --fit still scales it further when the
     scene IS pinned and the column genuinely has to fit. */
  font-size: calc(clamp(15px, 1.85svh, 16px) * var(--fit, 1));
  gap: calc(clamp(8px, 1.5svh, 20px) * var(--fit, 1));
  line-height: 1.5;
}

/* --- Ambient text field ---------------------------------------------------
   Fixed, mirrored, and inked far below the copy so it reads as a watermark in
   the paper rather than as content. Mixed toward the page rather than to
   transparency so it never fights the glyphs of the globe stacked over it, and
   authored against --ink so it follows the page's inversion for free. */

/* --- Character field -------------------------------------------------------
   The background is one uniform grid of characters filling the viewport. Only
   the phrase cells carry ink; the rest of the grid sits at a fraction of a
   percent, or nothing at all. Because a phrase occupies grid CELLS rather than
   a position, every fragment shares one baseline grid and one column rhythm
   and they align with each other by construction.

   Mirrored, so the text reads as an impression in the paper rather than as
   copy — legible enough to recognise as language, not so legible that it
   competes with what is actually being read.

   Inked by mixing toward the page rather than to transparency, so it never
   fights the glyphs of the globe stacked over it, and authored against --ink
   so it follows the page's inversion for free. */

.field {
  position: fixed;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  pointer-events: none;
  user-select: none;
}

.field pre {
  position: absolute;
  inset: 0;
  margin: 0;
  font-family: var(--font-mono);
  /* Must match ambient.js's fontPx / lineHeight exactly — the globe's mask is
     indexed in these cells. */
  font-size: 8px;
  line-height: 1;
  letter-spacing: 0;
  white-space: pre;
  text-transform: uppercase;
}

/* Each pair is a fallback then the real value. A browser that cannot parse
   color-mix() drops that declaration and keeps the hex above it — which is the
   light-mode result of the same mix. Without the fallback the property would
   compute to nothing and the field would render at full ink: unreadable, and
   far worse than a page that simply does not invert. */
.field__dim  { color: #e9e9e9; color: color-mix(in oklab, var(--ink) 6%, var(--surface)); }
.field__lit  { color: #dbdbdb; color: color-mix(in oklab, var(--ink) 13%, var(--surface)); }
/* Barely there — for anything that lands over the globe or the copy. */
.field__hush { color: #ececec; color: color-mix(in oklab, var(--ink) 3%, var(--surface)); }

/* The globe's darkness tiers. Same glyphs as the field, overprinted with more
   ink — which is what makes the form read as the text itself rather than as a
   layer above it. Hex fallback first for browsers without color-mix(). */
.field__shape {
  position: absolute;
  inset: 0;
  margin: 0;
}
/* The gap between the field's own ink (6%) and the form's lightest tier is what
   makes the silhouette read at all. The two light tiers are lifted well clear of
   it: at 28% the sphere's lit side dissolved into the background text, so the
   range is compressed upward — the shading still reads, but its lightest step is
   now unmistakably form rather than field. */
.field__s0 { color: #b0b0b0; color: color-mix(in oklab, var(--ink) 40%, var(--surface)); }
.field__s1 { color: #8c8c8c; color: color-mix(in oklab, var(--ink) 57%, var(--surface)); }
.field__s2 { color: #626262; color: color-mix(in oklab, var(--ink) 78%, var(--surface)); }
.field__s3 { color: #2b2b2b; color: color-mix(in oklab, var(--ink) 98%, var(--surface)); }

.ambient-copy { display: none; }

/* --- Wordmark and tagline -------------------------------------------------
   The name and the line under it, heading the column the manifesto fills in. */

/* The wordmark IS the h1 — the asset carries the name, so it is placed rather
   than set. Width, not height: the mark has to hold its column, and its
   intrinsic 966×68 ratio does the rest. */
.stage__title {
  /* Fixed rather than fluid: the mark reads as a logotype, and a logotype that
     resizes with the window looks like a heading instead. min() only so it
     can't overflow a narrow screen. */
  width: min(100%, 420px);
  margin: 0;
  line-height: 0;
}
.stage__title img {
  width: 100%;
  height: auto;
  filter: invert(var(--mark-invert, 0));
}

/* --- Scroll cue -----------------------------------------------------------
   Set like the tagline, not like a control: same muted ink, same size, no box, no
   underline. Its job is to be noticed once and then ignored. */

.cue {
  position: fixed;
  left: 50%;
  bottom: var(--space-lg);
  transform: translateX(-50%);
  z-index: 4;              /* above the field and the globe, below the nav */
  display: inline-flex;
  align-items: center;
  gap: var(--space-xs);
  color: var(--ink-muted);
  font-size: 14px;
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  text-decoration: none;
  transition: color 180ms var(--ease), opacity 320ms var(--ease);
}
.cue:hover,
.cue:focus-visible { color: var(--ink); }

/* Set by main.js once the reader has scrolled. Pointer events go with it, so a
   faded cue cannot be clicked by accident. */
.cue.is-gone {
  opacity: 0;
  pointer-events: none;
}

/* Only the arrow moves. Bouncing the words with it would read as a button
   demanding a press; a drifting arrow reads as a direction. */
.cue__arrow {
  animation: cue-drift 2.4s var(--ease) infinite;
}

@keyframes cue-drift {
  0%, 68%, 100% { transform: translateY(0); }
  /* Asymmetric on purpose: a short fall and a long rest, so it is a periodic hint
     rather than a continuous animation competing with the globe. */
  84%           { transform: translateY(4px); }
}

@media (prefers-reduced-motion: reduce) {
  .cue__arrow { animation: none; }
}

.stage__tagline {
  color: var(--ink);
  font-size: 16px;
  line-height: 1.3;
  letter-spacing: -0.012em;
  margin-top: var(--space-xs);
}

/* --- Hero animation (ASCII) ---------------------------------------------- */

/* The glyph sheet is a viewport-sized layer centred on this box and allowed to
   overflow it, so scattered characters can travel the full width and height of
   the screen. The box itself is the pointer target and the globe's anchor. */
.media {
  position: relative;
  z-index: 1;
  width: 100%;

  /* Fills whatever height the stage has left after the wordmark, rather than
     holding a fixed ratio — the globe is height-bound, so this is what makes
     it scale with the viewport. */
  flex: 1;
  min-height: 0;
  background: transparent;
  cursor: crosshair;
  touch-action: pan-y;
}

.ascii {
  position: absolute;
  left: 0;
  top: 0;
  z-index: 0;
  margin: 0;
  font-family: "Courier New", var(--font-mono);
  font-size: 6px;
  line-height: 1;
  letter-spacing: 0;
  white-space: pre;
  color: var(--ink);
  text-align: left;   /* rows are padded to a fixed width — centering each row shears the form */
  user-select: none;
  -webkit-user-select: none;
  pointer-events: none;
}

/* --- Prose --------------------------------------------------------------- */

.prose {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  max-width: var(--measure);
  font-size: 16px;
  text-align: center;
  line-height: 1.6;
  letter-spacing: 0;
}

.prose__list { color: var(--ink); }

/* Typed reveal (js/type.js). Every character is wrapped up front and switched
   on by class, so the copy holds its final height from the first paint and
   nothing below it reflows while the type arrives. */

/* The copy arrives out of NOTHING, not out of grey. A dim pre-state means the
   whole manifesto is on screen from the first paint and the reveal only changes
   its weight — which is not an arrival at all. Transparent until inked, so the
   column is genuinely empty until the reader scrolls. */
.ty { color: transparent; }
.ty.is-on { color: var(--ink); }

/* --- Join us --------------------------------------------------------------
   Its own section, centred on the page: the closing invitation is a statement
   rather than reference material, so it is set like the manifesto — middle of
   the sheet, one short measure — and not on the left rail the principles use.
   Its top padding is smaller than its bottom because the panel above already
   carries its own; the two stack. */

.join {
  position: relative;
  z-index: 1;
  flex: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  padding-block: var(--space-2xl) calc(var(--space-2xl) + var(--space-2xl));
  text-align: center;
}

.join__label {
  margin-bottom: 16px;
  /* The loudest thing in this section, where every other eyebrow on the page is
     the quietest thing in its own: this one is an invitation, not a label for
     what follows. */
  color: var(--ink);
  font-size: 28px;
  font-weight: 700;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

.join__body {
  /* Three lines: the first sentence wraps to two, and the second is held whole on
     the third by an explicit <br> in the markup rather than by a width that
     happens to break there. */
  max-width: 48ch;
  margin-inline: auto;
  color: var(--ink);
  font-size: 16px;
  line-height: 1.6;
  text-wrap: pretty;
}

/* Filled in the second section's own near-black, so the page's two solid shapes
   are the same object: the panel above and this button. Rounded 8px to match the
   principle boxes inside that panel. */
.join__cta {
  display: inline-flex;
  align-items: center;
  margin-top: var(--space-lg);
  padding: 14px 28px;
  border: 1px solid rgb(38 38 38);
  border-radius: 8px;
  background-color: rgb(38 38 38);
  color: rgb(255 255 255);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  text-decoration: none;
  transition: background-color 180ms ease, color 180ms ease, border-color 180ms ease;
}

.join__cta:hover,
.join__cta:focus-visible {
  border-color: rgb(58 58 58);
  background-color: rgb(58 58 58);
}

.caret {
  animation: caret-blink 1.06s steps(1) infinite;
}

/* The caret sits in the button as a terminal cursor: a small left gap so it
   reads as the next character after the label rather than part of the word. */
.join__cta .caret { margin-left: 2px; }

@keyframes caret-blink {
  0%, 50% { opacity: 1; }
  50.01%, 100% { opacity: 0; }
}

/* --- Document page (about) ------------------------------------------------
   A reading page rather than a composition: the home page centres everything
   against the viewport because it holds five short lines, but this is running
   copy, so it is set left against a measure and flows top to bottom. Type
   scale, color and rhythm are the home page's — only the alignment differs. */

.doc {
  position: relative;
  z-index: 1;
  flex: none;
  /* The reading measure belongs to the BODY, not to the whole article: capping
   the article capped the title with it, so a five-word headline broke across
   three lines. The head is free to run wider; the copy below it is not. */
  max-width: none;
  padding-block: var(--space-xl) var(--space-lg);
}

.doc__body { max-width: 840px; }

/* The block is centred on the page; the type inside it is left-aligned. Centred type
   at this measure makes every line start in a different place, which a reader has to
   hunt for — a left edge gives them one. */
.doc__head {
  max-width: 840px;
  margin-inline: auto;
  margin-bottom: var(--space-xl);
}

.doc__body { margin-inline: auto; }

.doc__eyebrow {
  color: var(--ink-muted);
  font-size: var(--step--1);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  margin-bottom: var(--space-md);
}

.doc__title {
  color: var(--ink);
  font-size: var(--step-2);
  font-weight: 500;
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;
}

.doc__body {
  display: flex;
  flex-direction: column;
  gap: var(--space-md);
  font-size: 16px;
  line-height: 1.6;
}

.doc__lede {
  color: var(--ink);
  font-size: var(--step-1);
  line-height: 1.45;
  letter-spacing: -0.01em;
}

/* Sits closer to the paragraph it introduces than to the one it follows, so
   the section break reads as a break rather than as even spacing. */
.doc__h2 {
  margin-top: var(--space-lg);
  color: var(--ink);
  font-size: 16px;
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

.doc__list { color: var(--ink); }

/* Inline citations. Underlined in the ink colour rather than tinted: the page
   has no accent, and a coloured link would be the only hue on the sheet. The
   offset keeps the rule off the descenders of a monospaced face. */
.doc__body a {
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 3px;
  text-decoration-thickness: 1px;
  transition: color 160ms var(--ease);
}
.doc__body a:hover,
.doc__body a:focus-visible { color: var(--ink-muted); }

.doc__note {
  margin-top: var(--space-lg);
  padding-top: var(--space-md);
  border-top: 1px solid var(--line);
  color: var(--ink-muted);
  font-size: var(--step--1);
}
.doc__note a { color: var(--ink); text-decoration: underline; text-underline-offset: 3px; }
.doc__note a:hover { color: var(--ink-muted); }

@media (max-width: 34rem) {
  .doc { padding-block: var(--space-lg) var(--space-xl); }
  .doc__head { margin-bottom: var(--space-lg); }
  .doc__body { font-size: 15px; }
}

/* --- How we operate ------------------------------------------------------
   Three principles, read across rather than down. Left-aligned to the nav
   container like the stage \u2014 the manifesto above is centred because it is a
   statement; this is reference material and reads as columns. */

.ops {
  position: relative;
  z-index: 1;
  flex: none;
  /* A fixed 24px inset on every screen instead of the rail's fluid gutter, so
     the panel sits the same distance from the edge at every width. The cap is
     restated to match: .wrap's max-width includes a gutter on each side, so
     with a narrower inset the max has to shrink by the same amount or the panel
     would grow past 1440px. Note this section's edges no longer line up with
     the nav links above 24px of gutter — that is the trade. */
  max-width: calc(var(--wrap-max) + 48px);
  padding-inline: 24px;
  padding-block: var(--space-xl) var(--space-2xl);
}

/* The dark ground is now an OBJECT on the page rather than a state of the page.
   The sheet stays white from the first screen to the last — the manifesto is
   read on white throughout — and this section carries its own near-black panel
   instead. Drawn on an inner div rather than on .ops itself: .ops is the
   alignment container, whose box is 1440px PLUS a gutter each side, so a fill
   there would be wider than 1440 and its edges would not meet the nav rail.

   The dark tokens are redeclared on this subtree, so every part inside — type,
   hairlines, the vortex's two glyph colors — resolves against the panel exactly
   as it used to resolve against the inverted sheet, with no second rule. */
.ops__panel {
  --surface:    rgb(38 38 38);
  --surface-2:  rgb(58 58 58);
  --ink:        rgb(255 255 255);
  --ink-soft:   rgb(232 232 232);
  --ink-muted:  rgb(168 168 168);
  --line:       rgb(72 72 72);
  /* Lighter than the panel it sits on, where before it was lighter than the
     page: at the panel's own value the boxes disappeared into it. */
  --panel:      rgb(52 52 52);
  padding: 24px var(--space-lg) var(--space-lg);
  border-radius: 24px;
  background-color: var(--surface);
}

.ops__eyebrow {
  margin-top: 0;
  margin-bottom: 16px;
  color: var(--ink-muted);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

/* The three boxes sit inside a box of their own — same fill, same hairline, a
   4px inset. Radius is 12px rather than 8: a concentric frame's outer corner has
   to be the inner radius plus the gap between them, or the two curves run
   non-parallel through the corner. The grid itself is the container, so this
   costs no extra element. */
.ops__grid {
  display: grid;
  gap: 8px;
  padding: 4px;
  border: 1px dotted var(--line);
  border-radius: 12px;
  /* A third value, between the section's ground and the boxes': the frame reads as
     a tray the boxes sit in rather than as the same surface with lines drawn on it.
     Its own token so the boxes inside still resolve --panel as their fill. */
  background-color: rgb(45 45 45);
}

/* Three abreast only where a third of the container is still a readable
   column; below that they stack rather than shrink toward unreadable. */
@media (min-width: 56rem) {
  /* minmax(0, …), not 1fr on its own: a bare 1fr is minmax(auto, 1fr), so the
     widest mark's min-content width pushed its own track out and the middle
     column's copy ended up on a different measure from its neighbours. */
  .ops__grid {
    grid-template-columns: repeat(3, minmax(0, 1fr));
    /* Two shared rows — heading, then body — that the boxes opt into with
       subgrid below, so every divider rule and every paragraph starts on one
       line across the set. Without it the two-line middle heading pushed its own
       rule and copy 18px below its neighbours', which is exactly the mismatch
       three parallel entries cannot have. Height still comes from the tallest
       heading, not from a hand-set floor, so it holds if the copy changes. */
    grid-template-rows: auto auto;
  }

  /* Child selector, so this outranks the base .ops__item rule that follows it in
     the file: a media query adds no specificity, and the later `display: flex`
     was winning — the boxes spanned both rows but never subgridded them. */
  .ops__grid > .ops__item {
    display: grid;
    grid-row: span 2;
    grid-template-rows: subgrid;
  }
}

/* Each principle sits in its own bordered field. A hairline in --line, not a
   filled card: the sheet inverts light-to-dark on scroll, and a tinted panel
   would have to be re-tinted for both states, whereas a rule reads the same on
   either ground. Padding is the same value the border is offset by on all four
   sides so the type inside stays on the container's measure. */
.ops__item {
  display: flex;
  flex-direction: column;
  padding: 12px;
  border: 1px dotted var(--line);
  border-radius: 8px;
  background-color: var(--panel);
  /* One value owns the space between a name and its paragraph. The two-line
     middle heading made that gap look different from its neighbours' when the
     spacing came from the heading's own leading, so it is set here and nowhere
     else. */
  gap: 16px;
}

/* --- Section figure -------------------------------------------------------
   One drawing for all three principles: a vortex wound around a horizontal axis,
   drawn in characters by js/vortex.js.

   TWO stacked layers on one grid, because colour cannot vary inside a single text
   node: one strand is lit and everything else is grey. The renderer shares a
   z-buffer between them, so the lit strand occludes and is occluded like any
   other — it is genuinely one of the ten, not an overlay tracing them. */
.ops__figure {
  position: relative;
  width: 100%;
  max-width: 100%;
  overflow: hidden;
  min-height: 348px;
  /* The form is centred in its grid, so it carries blank rows above and below
     it. Pulled up by that band rather than shortened: a shorter box means fewer
     rows, and the renderer sizes the figure to the box — the animation itself
     would shrink instead of the dead space. */
  margin: -32px 0 0;
}

.vx-layer {
  position: absolute;
  inset: 0;
  margin: 0;
  font-family: "Courier New", var(--font-mono);
  font-size: 6px;
  line-height: 1;
  letter-spacing: 0;
  white-space: pre;
  user-select: none;
}

/* Lifted, for the same reason the globe's light tiers were: at 58% the body of
   the vortex sank into the panel and only the lit strands read as form. */
.vx-base { color: #979797; color: color-mix(in oklab, var(--ink) 75%, var(--surface)); }
/* The lit strand carries the page's full ink — white once the sheet has
   inverted, which is where this section is read. */
.vx-lit { color: var(--ink); }

/* The name is ruled off from its paragraph, and the rule is FULL BLEED — it
   touches both inner edges of the box rather than stopping at the padding. The
   heading is pulled out by the padding it sits in and given that padding back
   as its own, so the rule spans the box while the text stays on the same measure
   as the paragraph below it. */
.ops__name {
  margin-inline: -12px;
  padding: 0 12px 16px;
  border-bottom: 1px dotted var(--line);
  color: var(--ink);
  font-size: 14px;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.01em;
}

.ops__item p {
  color: var(--ink-soft);
  font-size: 14px;
  line-height: 1.65;
}

/* --- Footer -------------------------------------------------------------- */

.site-footer {
  position: relative;
  z-index: 1;
  padding-block: var(--space-lg) calc(var(--space-lg) + 24px);
}

.site-footer__inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-sm);
  padding-inline: var(--edge-gutter);
  text-align: center;
}

.copyright {
  color: var(--ink-muted);
  font-size: var(--step--1);
  letter-spacing: -0.005em;
}

/* --- Below the desktop breakpoint ----------------------------------------
   The pin is a desktop device and does not survive a narrow screen: the copy
   alone is taller than a phone viewport, so a panel pinned to 100svh would clip
   it with no way to scroll what is clipped. Below the breakpoint the scene is a
   plain document — globe first, then the name and the copy, in normal flow. The
   ink reveal still runs, because it is driven by scroll position rather than by
   the pin. */

@media (max-width: 55.99rem) {
  .scene { height: auto; }

  /* In flow and centred, not sticky. */
  .scene__intro {
    position: static;
    margin-bottom: var(--space-md);
    text-align: center;
  }

  .stage__title { margin-inline: auto; }

  .scene__stage {
    position: static;
    height: auto;
    flex-direction: column;
    align-items: flex-start;
    /* Full header height PLUS a gap: the rail is sticky and the stage is in normal
       flow, so without the header's own height in the stage's padding the globe's
       first rows sit against the pill. */
    padding-block: calc(var(--header-h) + var(--space-md)) 14svh;
    gap: var(--space-lg);
  }

  /* Globe first on a handheld, as it was when this was two sections: the form
     is the arrival, and the copy is what you scroll to. */
  .scene .media {
    order: 1;
    flex: none;
    width: 100%;
    height: 62vh;
    height: 62svh;
    max-height: 520px;
  }
  .scene__text { order: 2; flex: none; width: 100%; }

  /* Full width, no measure. The 34rem cap is a two-column device: beside the
     globe it keeps the copy off the art, but stacked below it there is nothing
     to hold back from and the cap just leaves a third of the screen empty. */
  .prose {
    max-width: none;
    font-size: 15px;
  }

  /* Centred, and the intro with it. Beside the globe on desktop the copy shares the
     form's left edge, which is what makes left-aligned right there; stacked beneath it
     there is no edge to hold to, and the wordmark above is already centred. */
  .scene .prose,
  .scene__text { text-align: center; }
  .scene .prose__list { text-align: center; }

  /* One full-width column, stated rather than left to auto-placement: the boxes
     are the whole content of the panel here, so each spans it end to end. */
  .ops__grid { grid-template-columns: minmax(0, 1fr); }
  .ops__grid > .ops__item { width: 100%; }

  /* No scroll cue: the manifesto is fully inked here, so there is nothing for it to
     announce, and a fixed label over a plain scrolling document is just furniture. */
  .cue { display: none; }

  .cycle { padding-inline: 0; max-width: none; }

  /* The narrow variant draws 34 rows, not 58, so the desktop box left ~140px of
     empty panel beneath it and the figure sat high with dead space under it. Height
     set to the rows it actually draws (34 x 6px), and the pull-up reduced to match
     the smaller band of blank rows a shorter grid carries. */
  .ops__figure { min-height: 208px; margin: -14px 0 0; }

  /* Tighter above and below. The desktop values are set against a pinned scene
     that ends on a held image and needs air before the next thing begins; in a
     plain scrolling document the manifesto simply ends, and --space-2xl reads as a
     gap the reader has to cross rather than a pause.

     Join us takes the same pair, so the three closing sections are spaced alike
     down the page instead of the last one opening up. */
  .ops,
  .join { padding-block: var(--space-lg) var(--space-xl); }

  /* The panel's own inset drops to the boxes' own value, so the boxes reach the
     panel's edges instead of losing 64px of a phone's width to padding. */
  .ops__panel { padding: 16px 12px 12px; }
}

@media (max-width: 34rem) {
  :root { --edge-gutter: 24px; }

  /* Sticky here too — it was static because the old pill was a solid object that
     collided with the pinned wordmark travelling up through it. Both are gone on a
     narrow screen: the pill is transparent now and the wordmark travel is desktop
     only, so there is nothing for the rail to run into. */
  .site-header {
    padding: var(--space-sm) var(--edge-gutter);
  }

  .scene__stage { padding-block: calc(var(--header-h) + var(--space-sm)) 12svh; }
}

/* Very small screens — 320-360px. */

@media (max-width: 22.5rem) {
  /* Sized to the globe rather than to the screen: a viewport-height box on a
     small phone reserves a whole screen for a form that needs far less, and the
     slack reads as the page having failed to load. */
  .scene .media { height: auto; aspect-ratio: 1; max-height: 46svh; }
}

/* --- Short viewports -----------------------------------------------------
   The pinned panel holds the wordmark, the tagline and six blocks of copy in
   ONE screen — nothing below it can be scrolled to while the panel is stuck, so
   anything that does not fit is simply lost. Past these heights the copy
   tightens rather than the composition overflowing. */

@media (min-width: 56rem) and (max-height: 860px) {
  /* Same full-header clearance as the base rule; only the bottom tightens here. */
  .scene__stage { padding-block: calc(var(--header-h) + var(--space-sm)) var(--space-sm); }
  .scene__intro { margin-bottom: var(--space-md); }
  .stage__title { width: min(100%, 360px); }
}

/* --- Too short to pin ----------------------------------------------------
   The pin only works while the copy FITS: the panel is stuck at top:0, so
   scrolling advances the scene rather than the page, and anything past the
   bottom edge can never be reached. A centred column taller than the panel
   overflows both ways too, which puts the first paragraph under the nav.

   Rather than shrink the type until it fits a 500px window, the scene gives up
   the pin below the height its own content needs and becomes an ordinary
   scrolling section — the same layout the narrow breakpoint uses. The scroll
   drivers need no change: each one tests the section against the viewport and
   falls back to measuring the copy's own travel when it is no longer pinned. */

@media (max-height: 700px) {
  .scene { height: auto; margin-top: 0; }

  .scene__stage {
    position: static;
    height: auto;
    align-items: flex-start;
    padding-block: var(--space-lg) var(--space-2xl);
  }

  /* Unpinned, the title would scroll away with everything else — so it sticks to
     the top of the copy instead and stays above the manifesto for the length of
     the read. It carries the page colour and its own padding: a transparent
     sticky element has no backdrop and the copy passing beneath it is simply
     visible through it. */
  /* Not sticky here: the title is centred on the globe beside it and travels
     with the copy, which is the composition either way — pinning it to the top
     would separate the pair the moment the reader scrolls. */

  .scene .media { align-self: auto; height: 62svh; max-height: 460px; }
}

/* Short but still two columns: the panel cannot pin, but the GLOBE can. Without
   this it scrolls off the top while the manifesto is a third read, so the
   dissolve — which now waits for the copy to finish — happens off screen and is
   never seen. Sticky keeps it beside the copy for the length of the read, which
   is the same composition the pinned layout gives, arrived at differently. */
@media (max-height: 700px) and (min-width: 56rem) {
  .scene .media {
    position: sticky;
    top: var(--header-h);
    align-self: flex-start;
  }
}

@media (min-width: 56rem) and (hover: hover) {
  .status { margin-top: 0; }
  .site-footer { padding-block: var(--space-sm) calc(var(--space-sm) + 24px); }
}


/* --- About: image placeholders -------------------------------------------
   Standing in for artwork the client supplies. Deliberately plain and labelled
   rather than illustrated: a drawn stand-in gets mistaken for the design. Each
   is a single element to swap for an <img> — the surrounding layout owns the
   size, so the replacement inherits it. */

.slot {
  display: grid;
  place-items: center;
  border: 1px dotted var(--line);
  border-radius: 8px;
  background-color: var(--surface-2);
  color: var(--ink-muted);
  font-size: var(--step--1);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}


/* --- About: the cycle -----------------------------------------------------
   The mission travels a closed figure eight, continuously. No panel and no
   drawn path: the type IS the figure — a visible track would turn a sentence
   into a diagram, and the loop is legible from the motion alone. */

/* The track is a circle, so the figure's height EQUALS its width — where the
   figure eight was 3:1 and shallow. That is why the cap below is well under the
   600px the eight carried: at 600 a square block would be the tallest object on
   the page for a two-line statement. */
.cycle {
  position: relative;
  z-index: 1;
  width: 100%;
  /* Narrow on desktop: at page width the loop dominates a section that is only a
     two-line statement. Mobile overrides this to edge-to-edge, where the same
     figure has to earn its space in a much smaller frame. */
  max-width: 420px;
  margin-inline: auto;
  padding-inline: var(--edge-gutter);
  padding-block: var(--space-xs) var(--space-lg);
}

.cycle__svg {
  display: block;
  width: 100%;
  height: auto;
  overflow: visible;
}

/* Sized in viewBox units, not px, so the value is chosen against the circuit
   rather than the screen: 20 units on a 420-unit box renders at 20px at the
   desktop cap and near enough the same on a full-width phone, which is why this
   needs no mobile override. */
.cycle__text {
  /* Straddling the path rather than sitting on it, so the ribbon of type is
     centred on the ring instead of hanging off its outside edge. */
  dominant-baseline: central;
  fill: var(--ink);
  font-family: var(--font-mono);
  font-size: 20px;
  font-weight: 500;
  letter-spacing: 0.04em;
}

/* --- About: leadership ----------------------------------------------------- */

.team {
  position: relative;
  z-index: 1;
  padding-block: var(--space-xl) calc(var(--space-2xl) + var(--space-xl));
}

.team__eyebrow {
  margin-top: var(--space-lg);
  margin-bottom: 16px;
  color: var(--ink-muted);
  font-size: var(--step--1);
  font-weight: 600;
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
}

.team__figure { margin: 0 0 var(--space-2xl); }

/* Grey at rest, colour on hover: the page is a sheet of monospaced type, so the
   photograph stays quiet until it is looked at. */
.team__photo {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 8px;
  filter: grayscale(1);
  transition: filter 320ms var(--ease);
}

.team__figure:hover .team__photo,
.team__figure:focus-within .team__photo { filter: grayscale(0); }

/* The one dark object on this page. Its dark tokens are declared here rather than
   each part being restyled, so the cards' fills, hairlines and type all resolve
   light with no second set of rules — the same construction the home page's
   principles use, minus the outer panel. */
.team__grid {
  --surface:    rgb(38 38 38);
  --surface-2:  rgb(58 58 58);
  --ink:        rgb(255 255 255);
  --ink-soft:   rgb(232 232 232);
  --ink-muted:  rgb(168 168 168);
  --line:       rgb(72 72 72);
  /* One step lighter than the box, so a card reads as sitting on it. */
  --panel:      rgb(52 52 52);

  display: grid;
  gap: 8px;
  padding: 8px;
  border: 1px dotted var(--line);
  border-radius: 13px;
  background-color: var(--surface);
}

.team__caption {
  margin-top: var(--space-sm);
  color: var(--ink-muted);
  font-size: var(--step--1);
}

/* --- Leadership: the person cards ---------------------------------------- */

/* The photograph is FULL BLEED on three sides — it touches the card's top, bottom
   and left edges, and only the text is inset. So the card carries no padding of its
   own: the text column pays for its own, and the picture is simply flush.
   overflow: hidden clips the photograph to the card's rounded corners, which it now
   sits in the corner of. */
.person {
  display: grid;
  /* The photo column is FIXED. It used to be `auto` with the picture's width
     derived from the card's height, which is circular: longer text made the card
     taller, a taller card made the photo wider, a wider photo made the text column
     narrower, and narrower text wrapped taller still. At three columns that loop
     settled on a 215px photo beside a 96px text column. A fixed track breaks it.
     min-height is that width at the picture's own 4:5 ratio, so in the common case
     the photo fills the card exactly and nothing is cut. */
  grid-template-columns: 108px minmax(0, 1fr);
  min-height: 135px;
  gap: 0;
  padding: 0;
  overflow: hidden;
  border: 1px dotted var(--line);
  border-radius: 8px;
  background-color: var(--panel);
}

/* Runs the card's full height, uncropped. Two things make that work: the files are
   the whole frame at one 4:5 ratio, and the element is STRETCHED to the row with
   its width derived from that ratio — so the column widens or narrows with the
   card instead of the picture being cut to fit a fixed box.

   Grey at rest, colour on hover, the same behaviour as the group photograph above,
   so the page has one rule about when a photograph is in colour. */
.person__photo {
  align-self: stretch;
  width: 100%;
  height: 100%;
  /* cover, so the picture reaches all three edges with no empty band. The card's
     min-height matches the file's ratio, so at rest there is nothing to crop — only
     a card pushed taller by a long affiliation trims a little from the sides, which
     on a centred head is invisible. */
  object-fit: cover;
  object-position: 50% 28%;
  /* No radius of its own: the card clips it, so the two outer corners follow the
     card's curve and the two inner ones stay square against the text. */
  filter: grayscale(1);
  transition: filter 320ms var(--ease);
}
.person:hover .person__photo { filter: grayscale(0); }

.person__text {
  display: flex;
  flex-direction: column;
  gap: 12px;
  min-width: 0;
  /* No top padding: the name's own rule sits 12px down from the card's top edge, so
     padding here would push the type off the photograph's top line. */
  padding: 12px;
}

/* The rule runs the full width of the text column and touches both the photograph
   and the card's right edge — pulled out by the padding it sits in, then given that
   padding back as its own so the type stays on the paragraph's measure. */
.person__name {
  margin-inline: -12px;
  padding: 0 12px 12px;
  border-bottom: 1px dotted var(--line);
  color: var(--ink);
  font-size: 14px;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.01em;
}

.person__role {
  color: var(--ink);
  font-size: 14px;
  font-weight: 600;
}

.person__note {
  margin-top: -4px;
  color: var(--ink-muted);
  font-size: var(--step--1);
  line-height: 1.55;
  text-wrap: pretty;
}

@media (min-width: 56rem) {
  /* Two abreast from tablet up; below that the cards stack one per row, which the
     base rule already does. Three was tried and rejected: even with the photo
     column fixed, a third column leaves the affiliation lines too narrow to read as
     prose at the widths this page is actually opened at. */
  .team__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  /* The cards do not share rows: each is two columns internally, and a subgrid row
     track cannot line up a portrait against a heading. Their own content keeps them
     close in height. */
  .team__grid > .person { align-content: start; }
}
