/* ==========================================================================
   UMARNA Research Institute
   site.css - shared design tokens, base, header, hero
   ========================================================================== */

:root {
  /* Ground and ink */
  --basalt:     #0E0E0D;
  --bone:       #F4F1EA;
  --umber:      #6B4A2F;
  --malachite:  #1E3B36;
  /* Lifted 2026-10-22 (Richard's call): feldspar #9C8B73 -> #b7a38b and
     bone-dim 62% -> 80%, for text and for everything else the two touch —
     rules, strokes, dots, the hairline. 53 literals that bypassed these
     two tokens were rewritten with them so nothing is left behind. */
  --feldspar:   #b7a38b;
  --verdigris:  #3F6B5C;

  /* Seven tones, base depth (2026-11-01, Richard's sheet "03 · COLOUR ·
     SEVEN TONES · SHADES · V4"). The sheet carries three depths — veil,
     base, deep — and only the base is in use for now, so only the base is
     a token here. These replace the feldspar accent section by section,
     the hero first; they are not the twelve systems palette, which stays
     in assets/js/ink.js and belongs to the clusters alone. */
  --benben:     #FBFFB9;   /* 01  first light      */
  --deshret:    #F8D6B9;   /* 02  the red land     */
  --syene:      #F7CBD1;   /* 03  rose granite     */
  --hesmen:     #D2CEFA;   /* 04  amethyst         */
  --nut:        #BEE2F7;   /* 05  the sky          */
  --hapi:       #B0E8DE;   /* 06  the flood        */
  --wadj:       #BBEECA;   /* 07  papyrus green    */

  --bone-dim:   rgba(244, 241, 234, 0.8);

  /* Sub paragraph's size, and the size of running copy across the site:
     every passage on the role reads at it — Our Purpose, Methodologies,
     Our Architecture's intro, the Earth Resonance Map's body and the
     closing pair. Set to the cluster paragraph's own clamp on 2026-10-26
     (Richard's call, site wide), down from clamp(0.82rem, 0.3rem + 1.3vw,
     1.4rem): 22.4 -> 17.63px at 1545, 13.1 -> 12 at 390. Scroll B reads
     --sb-copy off this, so the cluster paragraph and the roles cannot
     drift apart again.

     Subtitle used to share this token and no longer does; it is on a flat
     0.9675rem of its own since the 25th. */
  --sub-size:   clamp(0.75rem, 0.6rem + 0.52vw, 1.2rem);
  --bone-faint: rgba(244, 241, 234, 0.34);
  --hairline:   rgba(183, 163, 139, 0.28);

  /* Type. One family for the whole identity, weights carrying the hierarchy
     that three families used to carry: Regular reads, Medium introduces,
     SemiBold titles. The three tokens stay separate so the intent of each
     rule is still legible, and so a second face could be reintroduced in one
     place if it ever earns its keep. */
  --display: "IBM Plex Mono", ui-monospace, SFMono-Regular, monospace;
  --body:    "IBM Plex Mono", ui-monospace, SFMono-Regular, monospace;
  --mono:    "IBM Plex Mono", ui-monospace, SFMono-Regular, monospace;

  /* Titles alone are Space Mono, set Regular. Both faces are monospaced, and measured
     side by side Space Mono runs 0.612em to Plex's 0.600em, so a title is two
     percent wider than the same string in Plex. That is inside the slack the
     sizes and tracking already carry, which is why nothing needed reflowing.
     Everything else, the wordmark included, stays Plex. */
  --title:   "Space Mono", ui-monospace, SFMono-Regular, monospace;

  /* Every step came down. A monospace sets about a fifth wider per character
     than the serif and the sans it replaces, so holding the old sizes would
     have lengthened every line and coarsened every heading. What a reader
     registers is the length of the line, not the nominal point size. */
  --kicker:   clamp(0.7rem, 0.66rem + 0.2vw, 0.8rem);
  --s0:       clamp(0.94rem, 0.91rem + 0.14vw, 1rem);
  --s1:       clamp(1rem, 0.94rem + 0.3vw, 1.15rem);
  --s2:       clamp(1.2rem, 1.08rem + 0.6vw, 1.55rem);
  --s3:       clamp(1.55rem, 1.3rem + 1.1vw, 2.25rem);
  --wordmark: clamp(2.6rem, 1.6rem + 5.2vw, 7rem);

  /* Measure and rhythm. The prose measure now lives on .prose itself, in ch,
     so it tracks the type size instead of being pinned to a rem value. */
  /* The page's horizontal frame (widened 2026-10-20). --wrap is where the
     content stops growing and --gutter the least space left at the sides;
     between them they put the editorial edge at 44px on a 1440 screen and
     180 on a 1920, where it used to be 60 and 300. Everything that stands
     on that edge — the sections, the header, and the three sequences
     through --edge — reads both from here, so they cannot drift apart. */
  --wrap:    1560px;
  /* The slope is the one it always had; only the ceiling comes down, from
     3.25rem to 2.75rem. Flattening the slope as well took a 1024 screen's
     side margin from 40px to 34 and left Scroll B's opening 6px off the
     page rail, where it had 12. */
  --gutter:  clamp(1.25rem, 0.6rem + 3vw, 2.75rem);

  /* Settled opacity of the emergence field. Lowered on narrow viewports,
     where the veil has far less room to hold the field off the text. */
  --field-opacity: 0.68;

  /* Easing. Strong curves; the built-in keywords are too weak to read as intentional. */
  --settle:   cubic-bezier(0.16, 1, 0.3, 1);   /* long, geological, for entrances */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);  /* short UI responses */
}

/* --------------------------------------------------------------------------
   Base
   -------------------------------------------------------------------------- */

*,
*::before,
*::after { box-sizing: border-box; margin: 0; padding: 0; }

html {
  /* Tells the browser to render its own furniture (scrollbars, form controls)
     dark, so they stop appearing as bright white slabs against the basalt. */
  color-scheme: dark;
  scroll-behavior: smooth;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

body {
  background: var(--basalt);
  color: var(--bone);
  font-family: var(--body);
  font-size: var(--s0);
  line-height: 1.6;
  /* The serif carried a whisper of tracking to open it up. A monospace
     arrives with more than enough of its own. */
  letter-spacing: 0;
  overflow-x: hidden;
}

::selection { background: var(--umber); color: var(--bone); }

a { color: inherit; text-decoration: none; }

a,
button {
  /* Removes the 300ms double-tap zoom delay on touch. */
  touch-action: manipulation;
  -webkit-tap-highlight-color: rgba(244, 241, 234, 0.12);
}

:focus-visible {
  outline: 1px solid var(--bone);
  outline-offset: 4px;
}

/* Full-bleed dark layout, so the gutter has to clear notches and rounded
   corners rather than assuming a rectangular viewport. */
.wrap {
  width: min(100% - (var(--gutter) * 2), var(--wrap));
  margin-inline: auto;
  padding-left: env(safe-area-inset-left);
  padding-right: env(safe-area-inset-right);
}

/* Tracked for Space Mono, which is narrower and quirkier. Plex sits wider,
   so the small caps labels come in a little. */
.mono {
  font-family: var(--mono);
  font-size: var(--kicker);
  font-weight: 400;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--feldspar);
}

.skip-link {
  position: absolute;
  left: var(--gutter);
  top: -100%;
  z-index: 100;
  padding: 0.7rem 1.1rem;
  background: var(--bone);
  color: var(--basalt);
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.skip-link:focus-visible { top: 1rem; outline-offset: 2px; }

/* --------------------------------------------------------------------------
   Header

   V3 was an opaque black bar across the full width. It read the same over
   everything, but it guillotined the picture: the field, the films and the
   diagram all lost their top 75px to a hard horizontal edge.

   Since 2026-10-19 there is no bar. Each of the two things that must stay
   legible — the wordmark and the menu button — sits in its own pool of black
   anchored to its corner and feathered out to nothing, and the middle of the
   screen is given back to whatever is behind it.

   Measured against the brightest top strip on the page (a Scroll B frame,
   mean 39.5 of 255): behind the wordmark the pool reads 16.7 and behind the
   button 10.9, which is what a full-width scrim gives (15.6 and 10.7), while
   the centre comes back at 59.7 against the scrim's 12.3. So the protection
   is a scrim's and the cost is none. Wider pools were tried to 40rem and add
   nothing — both marks sit near their corners, where every width is already
   near-opaque — so these are kept tight.

   No special case for the open menu: the panel paints its own black from the
   top of the screen down, and black pools over it are invisible.
   -------------------------------------------------------------------------- */

.site-header {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 40;
  padding-top: calc(clamp(1rem, 0.6rem + 1.4vw, 1.6rem) + env(safe-area-inset-top));
  padding-bottom: clamp(1rem, 0.6rem + 1.4vw, 1.6rem);
}

/* The two pools. They stand three times the bar's own height so the fade has
   somewhere to go, and sit behind its content but in front of the page. */
.site-header::before,
.site-header::after {
  content: "";
  position: absolute;
  top: 0;
  height: 300%;
  width: min(28rem, 42vw);
  z-index: -1;
  pointer-events: none;
}

.site-header::before {
  left: 0;
  background: radial-gradient(118% 100% at 0% 0%,
    rgba(0, 0, 0, 0.95) 0%,
    rgba(0, 0, 0, 0.78) 30%,
    rgba(0, 0, 0, 0.38) 56%,
    rgba(0, 0, 0, 0) 78%);
}

.site-header::after {
  right: 0;
  background: radial-gradient(118% 100% at 100% 0%,
    rgba(0, 0, 0, 0.95) 0%,
    rgba(0, 0, 0, 0.78) 30%,
    rgba(0, 0, 0, 0.38) 56%,
    rgba(0, 0, 0, 0) 78%);
}

/* With the menu open the toggle sits on the panel's own card, which is the
   site's ground and not black, so the right pool is no longer invisible over
   it — it lays a soft vignette down the card's top 150px. It has nothing left
   to do there, so it stands down. The left pool stays: the wordmark is still
   over the page. (Ignored where :has is not supported, which only leaves the
   vignette as it was.) */
.site-header:has(.nav-toggle[aria-expanded="true"])::after { display: none; }

/* The pools hold below 700px too (2026-10-22, Richard's call), where the
   bar used to go solid. The reason it went solid still stands — a pool is
   only as good as the room it has to feather in, and at 42vw of a phone
   that is 164px, which left the wordmark sitting where the gradient had
   already given up — so the pools are given the room instead of being
   turned off: the fade is held near full across the stretch the type
   actually occupies and only the tail feathers.

   The phone pools are not the desktop shape scaled down. The desktop one
   is an ellipse pinned to the top corner and run down three bar heights,
   which on a phone hangs 165px of fade down the left of the page — a
   curtain over five lines of body text, not a halo. So below 700px the
   gradient is re-centred on the mark itself and given a radius in pixels:
   72 across, 49 down, around a point 34px in from the gutter (the
   wordmark's own centre — it is 68px wide) and 28px down (the bar's).
   That is about 38px of feather on every side of the type, which is what
   reads as a halo. The near-full plateau runs to 48% of the radius, the
   wordmark's own half-width, so the mark sits on black and only the tail
   feathers. The box is sized to contain the gradient and no more. */
@media (max-width: 700px) {
  /* Two devices, because one cannot do it. The pools alone were rejected
     at 0.95 peak over 72x49 — on the page's near-black ground that reads
     as a hole punched in the picture, which is what Richard saw. A shadow
     alone cannot replace them: it follows the glyph outlines, and a line
     of body text passes between and around the letters. Measured over the
     closing paragraph, stacking the shadow three deep at 52px dimmed the
     text behind by 11%; a wash at 0.45 dims it 42%.

     So both, each doing what it is good at. The pools come back at **a
     third of the weight and half again the spread**, which is light
     enough to disappear into the page and still carry a bright line down;
     the shadow does the close work the wash is too soft for, separating
     the stroke from whatever is immediately behind it. */
  .site-header::before,
  .site-header::after { width: min(11rem, 46vw); height: 5rem; }
  .site-header::before {
    background: radial-gradient(116px 66px at calc(var(--gutter) + 34px) 1.75rem,
      rgba(0, 0, 0, 0.45) 0%,
      rgba(0, 0, 0, 0.40) 46%,
      rgba(0, 0, 0, 0.18) 74%,
      rgba(0, 0, 0, 0) 100%);
  }
  .site-header::after {
    background: radial-gradient(116px 66px at calc(100% - var(--gutter) - 20px) 1.75rem,
      rgba(0, 0, 0, 0.45) 0%,
      rgba(0, 0, 0, 0.40) 46%,
      rgba(0, 0, 0, 0.18) 74%,
      rgba(0, 0, 0, 0) 100%);
  }

  .brand {
    text-shadow: 0 0 4px  rgba(14, 14, 13, 1),
                 0 0 14px rgba(14, 14, 13, 0.95),
                 0 0 30px rgba(14, 14, 13, 0.8);
  }
  .nav-toggle {
    /* A filter, not a text-shadow: the bars are a box-shadow on a
       pseudo-element, and text-shadow does not touch those. Two stops
       here, not three — drop-shadows compound, where text-shadow's do
       not, so a third reads heavier on the icon than on the word. */
    filter: drop-shadow(0 0 4px rgba(14, 14, 13, 0.95))
            drop-shadow(0 0 18px rgba(14, 14, 13, 0.88));
  }

  /* The menu panel stays out of the way, as it did with the pools. */
  .nav-panel { pointer-events: none; }
}

/* And for a reader who has asked for a plain interface. */
@media (prefers-contrast: more) {
  .site-header { background: #000; }
  .site-header::before,
  .site-header::after { display: none; }
  .nav-panel { background: #000; border-bottom: 1px solid var(--hairline); pointer-events: auto; }
  .nav-panel::before { background: #000; border-color: var(--bone-faint); }
}

/* Centred rather than on the baseline since V3: the menu button carries no
   visible text, so there is no baseline to share with the wordmark. */
.nav {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

/* UMARNA in the header: SemiBold, uppercase, and given room. */
.brand {
  font-family: var(--display);
  font-weight: 600;
  font-size: 0.88rem;
  text-transform: uppercase;
  letter-spacing: 0.2em;
  color: var(--bone);
}

.nav-links {
  display: flex;
  gap: clamp(0.9rem, 0.4rem + 1.6vw, 2rem);
}

.nav-links a,
.nav-panel a {
  font-family: var(--mono);
  font-size: 0.68rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--bone);
  opacity: 0.8;
  transition: opacity 200ms var(--ease-out);
}

/* Gate hover behind a real pointer. On touch, hover fires on tap and sticks. */
@media (hover: hover) and (pointer: fine) {
  .nav-links a:hover,
  .nav-panel a:hover { opacity: 1; }
}

/* --------------------------------------------------------------------------
   Mobile nav

   The panel lives outside <header> and slides down from behind the bar. It
   shares the bar's black so the two read as one surface when open.
   -------------------------------------------------------------------------- */

/* V3: the menu is collapsed at every width, into this button at the top
   right. The word stays for assistive tech; the eye gets the mark alone. */
.nav-toggle {
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
  /* A comfortable target without moving the mark off the gutter. */
  padding: 0.7rem;
  margin: -0.7rem;
  border: 0;
  background: none;
  color: var(--bone);
  font-family: var(--mono);
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  cursor: pointer;
}

.nav-toggle-label {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.nav-toggle-bars {
  display: grid;
  gap: 4px;
  width: 18px;
}

/* The mark: where a third line would be, three dots, the outer two sitting
   flush with the ends of the lines below. Drawn from one dot and two shadows
   so every page's existing button takes it without a markup change. */
.nav-toggle-bars::before {
  content: "";
  justify-self: start;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: currentColor;
  box-shadow: 7.5px 0 0 currentColor, 15px 0 0 currentColor;
  transition: opacity 240ms var(--ease-out);
}

.nav-toggle-bars i {
  display: block;
  height: 1px;
  background: currentColor;
  transition: transform 240ms var(--ease-out);
}

.nav-toggle[aria-expanded="true"] .nav-toggle-bars::before { opacity: 0.35; }
/* The dots are a pseudo-element, not a child, so the lines are still the
   first and second children. */
.nav-toggle[aria-expanded="true"] .nav-toggle-bars i:nth-child(1) { transform: translateY(2.5px) rotate(13deg); }
.nav-toggle[aria-expanded="true"] .nav-toggle-bars i:nth-child(2) { transform: translateY(-2.5px) rotate(-13deg); }

.nav-panel {
  position: fixed;
  /* Full height since 2026-10-28, so the card behind the links can run the
     whole screen. It also keeps the closed transform honest: translateY(-100%)
     is now a full viewport, so nothing of the card is left showing. */
  inset: 0;
  z-index: 39;
  padding-top: calc(clamp(5.5rem, 16vw, 7rem) + env(safe-area-inset-top));
  padding-bottom: clamp(2rem, 6vw, 3rem);
  /* The panel spans the width so the links keep the page's gutter, but only
     the card behind them is drawn, and only the links take a click. */
  pointer-events: none;
  transform: translateY(-100%);
  overscroll-behavior: contain;
  /* Closing is the system responding, so it snaps. Opening is the deliberate
     act and gets the longer curve, set on .is-open below. */
  transition: transform 200ms var(--ease-out);
}

/* Anchored to the top right, under the button that opens it. A plain opaque
   card rather than the old feathered slab: wide enough for the longest link
   and its gutter, and no wider, so the page is left legible beside it. It
   runs the full height of the panel, which is the full screen. */
.nav-panel::before {
  content: "";
  position: absolute;
  top: 0;
  right: 0;
  /* Widened on 2026-10-29 with the links (Richard's call), 17rem -> 19rem and
     the floor 15rem -> 17rem. The old pair was measured against 13.12px type;
     at 15.12px the longest link, THE RESEARCH ENGINE, is 210px and the card
     left it 18px of air on the left against the 44px gutter on the right, so
     the list sat jammed into the card's edge. 19rem is 304px: 50px and 44px,
     which reads as one gutter either side. */
  width: min(19rem, max(17rem, 62vw));
  height: 100%;
  z-index: -1;
  pointer-events: none;
  background: var(--basalt);
  border-left: 1px solid var(--hairline);
  border-bottom: 1px solid var(--hairline);
}

.nav-panel[hidden] { display: block; visibility: hidden; }

.nav-panel.is-open {
  transform: translateY(0);
  visibility: visible;
  transition-duration: 320ms;
}

.nav-panel nav {
  position: relative;
  pointer-events: auto;
  display: grid;
  gap: 1.15rem;
  /* V3: set flush right, under the menu button that opens it. */
  justify-items: end;
  text-align: right;
}

/* Space Mono since 2026-10-29 (Richard's call), where the shared rule above
   sets IBM Plex Mono for both the header links and these. The panel is the
   one place the menu is read as a list of titles, so it takes the title
   face. Tracking stays at the shared 0.12em: Space Mono needs the air at
   this size in caps, and the longest link still clears the card. */
/* Up 2px on 2026-10-29 (Richard's call): 0.82rem -> 0.945rem, 13.12 -> 15.12px. */
.nav-panel a { font-family: var(--title); opacity: 0.72; font-size: 0.945rem; }

/* V3: the inline links are retired at every width; the menu button opens the
   panel instead. The links stay in the markup until the menu is redesigned. */
.nav-links { display: none; }

/* --------------------------------------------------------------------------
   Hero
   -------------------------------------------------------------------------- */

.hero {
  position: relative;
  height: 100vh; height: 100svh;
  min-height: 620px;
  display: flex;
  align-items: center;
  overflow: hidden;
}

.field-stage {
  position: absolute;
  inset: -8% -6%;
  perspective: 1400px;
}

#field {
  display: block;
  width: 100%;
  height: 100%;
  opacity: 0;
  transform: rotateX(34deg) scale(1.32) translateY(6%);
  transform-origin: center 62%;
  /* transform only: opacity animates once, on entry, and hinting it would pin
     an extra compositor layer for a full-viewport canvas for the whole session. */
  will-change: transform;
}

.field-veil {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background:
    linear-gradient(90deg,
      var(--basalt) 0%,
      rgba(14, 14, 13, 0.86) 20%,
      rgba(14, 14, 13, 0.28) 48%,
      rgba(14, 14, 13, 0) 64%),
    radial-gradient(130% 95% at 52% 44%,
      rgba(14, 14, 13, 0) 32%,
      rgba(14, 14, 13, 0.5) 74%,
      rgba(14, 14, 13, 0.94) 100%),
    linear-gradient(180deg,
      rgba(14, 14, 13, 0.6) 0%,
      rgba(14, 14, 13, 0) 24%,
      rgba(14, 14, 13, 0) 64%,
      var(--basalt) 100%);
}

.hero-inner { position: relative; z-index: 3; }

.hero-meta {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 1rem;
  margin-bottom: clamp(1.5rem, 4vh, 3.5rem);
}

.hero-meta .mono { color: var(--bone-faint); }

.wordmark {
  font-family: var(--display);
  font-weight: 600;
  font-size: var(--wordmark);
  line-height: 0.94;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--bone);
}

.hero-sub {
  margin-top: clamp(0.6rem, 1.5vh, 1.1rem);
  font-family: var(--mono);
  font-size: var(--kicker);
  letter-spacing: 0.42em;
  text-transform: uppercase;
  color: var(--feldspar);
}

/* Italic here was Sentient's italic, which is a subtitle voice. A monospace
   oblique is not: it reads as a code comment. Upright instead, and the only
   italic left on the site is the pull quote, where it is an accent rather
   than a habit. */
/* Set below --s1 since V3: the lead grew from one line to a full sentence, and
   at the old size four lines of monospace read heavy beside the wordmark. */
.hero-tag {
  margin-top: clamp(1.3rem, 3vh, 2.1rem);
  max-width: 34rem;
  font-size: clamp(0.92rem, 0.88rem + 0.12vw, 1rem);
  font-style: normal;
  letter-spacing: -0.015em;
  line-height: 1.6;
  color: var(--bone-dim);
  text-wrap: pretty;
}

/* --------------------------------------------------------------------------
   Learn more (V3)

   The site's quiet call onward, shared because every section of the Final
   Copy ends in one and they should all land with the same weight. Drawn like
   the People page CTA: a hairline box, a small mono label, and an arrow that
   steps forward on hover.
   -------------------------------------------------------------------------- */

.learn-more {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.62rem 1rem;
  /* What used to be the hover state is the resting one since 2026-10-21:
     feldspar type on a 0.28 hairline was close to invisible until the
     pointer found it, and on a touch screen it never was found. Hover now
     adds a light rather than switching one on. */
  /* 2px since 2026-10-21. A hairline around a box this small reads as a
     seam rather than an edge; a whole pixel either side is the smallest
     step that still paints crisply at any pixel ratio. */
  border: 2px solid var(--feldspar);
  font-family: var(--mono);
  font-size: 0.62rem;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--bone);
  text-decoration: none;
  transition: color 300ms var(--ease-out), border-color 300ms var(--ease-out),
              background-color 300ms var(--ease-out);
}

.learn-more .arrow { transition: transform 300ms var(--ease-out); }

/* On touch the box grows to a comfortable finger target; the label stays the
   same size, so it still reads as the same quiet button. */
@media (pointer: coarse) {
  .learn-more { min-height: 44px; padding-inline: 1.15rem; }
}

@media (hover: hover) and (pointer: fine) {
  .learn-more:hover {
    color: var(--bone); border-color: var(--bone);
    background: rgba(244, 241, 234, 0.07);
  }
  .learn-more:hover .arrow { transform: translateX(3px); }
}

.learn-more:focus-visible {
  outline: 1px solid var(--feldspar);
  outline-offset: 3px;
  color: var(--bone);
}

/* --------------------------------------------------------------------------
   Detail panel (V3)

   The popup the Principles & Accords object opens, drawn into the shared
   sheet so any figure can use the same one. It slides in from the right edge
   of its stage, clipped from that edge as it arrives, and places itself
   against its nearest positioned ancestor, which is the stage it serves.
   Values are Principles' own, except the width, a step roomier for the
   longer Field texts, and the body, set to the hero statement's size.
   -------------------------------------------------------------------------- */

.detail-panel {
  position: absolute;
  z-index: 6;
  right: var(--gutter);
  top: 50%;
  width: min(23rem, 38vw);
  transform: translate(26px, -50%);
  /* Tighter on the vertical than the horizontal since 2026-10-22: the
     panel's height is what it has to win on a phone, and padding is the
     cheapest place to find it. */
  padding: clamp(0.9rem, 1.4vw, 1.2rem) clamp(1.2rem, 1.9vw, 1.6rem);
  background: rgba(18, 18, 17, 0.94);
  border: 1px solid var(--hairline);
  opacity: 0;
  visibility: hidden;
  clip-path: inset(0 0 0 100%);
  transition: opacity 190ms var(--ease-out), transform 190ms var(--ease-out),
              clip-path 190ms var(--ease-out), visibility 0s linear 190ms;
}

.detail-panel.is-open {
  opacity: 1;
  visibility: visible;
  transform: translate(0, -50%);
  clip-path: inset(0 0 0 0);
  transition: opacity 380ms var(--settle), transform 460ms var(--settle),
              clip-path 560ms var(--settle), visibility 0s;
}

/* The mirror, for a figure whose subject sits on its left (V3): the panel
   takes the left edge and is revealed from that edge. */
.detail-panel.is-left {
  right: auto;
  left: var(--gutter);
  transform: translate(-26px, -50%);
  clip-path: inset(0 100% 0 0);
}
.detail-panel.is-left.is-open { transform: translate(0, -50%); clip-path: inset(0 0 0 0); }

/* A narrow stage has no room beside its figure, so the panel rises from the
   bottom edge instead. */
.detail-panel.is-bottom {
  right: 50%;
  top: auto;
  bottom: clamp(1rem, 3vh, 2rem);
  width: min(34rem, calc(100% - 2 * var(--gutter)));
  transform: translate(50%, 16px);
  clip-path: inset(100% 0 0 0);
}
.detail-panel.is-bottom.is-open { transform: translate(50%, 0); clip-path: inset(0 0 0 0); }

/* The same panel at the head of the stage, which is where the narrow
   layout has its room: the figure sits low and the space above it is
   empty. It arrives downward, as the bottom one arrives upward. */
.detail-panel.is-top {
  right: 50%;
  /* Tucked up under the fixed header — 55px on a phone, at z-index 40,
     so the panel has to start below it rather than behind it — and no
     further down than that, so it takes as little of the figure as it
     can. */
  top: clamp(3.65rem, 7vh, 4.3rem);
  bottom: auto;
  width: min(34rem, calc(100% - 2 * var(--gutter)));
  transform: translate(50%, -16px);
  clip-path: inset(0 0 100% 0);
}
.detail-panel.is-top.is-open { transform: translate(50%, 0); clip-path: inset(0 0 0 0); }
/* The field's number comes off the narrow panel: it is a line of its own
   for two words, and the height is what the panel has to win here. The
   number is still announced — each title's hit band carries it in its
   own label, which is what a reader reaches the panel through. */
.detail-panel.is-top .dp-idx { display: none; }

.dp-idx {
  margin: 0 0 0.6rem;
  padding-right: 1.8rem;
  font-family: var(--mono);
  font-size: 0.56rem;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--feldspar);
}

.dp-name {
  margin: 0 0 0.55rem;
  padding-right: 1.8rem;
  font-family: var(--title);
  font-weight: 400;
  text-transform: uppercase;
  font-size: clamp(0.95rem, 0.82rem + 0.55vw, 1.2rem);
  letter-spacing: 0.03em;
  line-height: 1.24;
  color: var(--bone);
  text-wrap: balance;
}

/* The same size as the hero statement, so reading text on Home sits at one
   size wherever it appears. */
.dp-body {
  margin: 0;
  font-size: 0.72rem;
  line-height: 1.7;
  color: var(--bone-dim);
  text-wrap: pretty;
}

.dp-close {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  width: 26px;
  height: 26px;
  border: 1px solid var(--hairline);
  background: none;
  color: var(--feldspar);
  cursor: pointer;
  font-family: var(--mono);
  font-size: 0.9rem;
  line-height: 1;
}

/* No rule over the two buttons (2026-10-22), with the other gold lines —
   and it was costing the panel 16px of height for a line nobody needed:
   the buttons are bordered boxes and separate themselves. */
.dp-nav {
  display: flex;
  gap: 0.4rem;
  margin-top: 1rem;
}

.dp-nav button {
  flex: 1;
  padding: 0.45rem 0.4rem;
  border: 1px solid var(--hairline);
  background: none;
  color: var(--feldspar);
  cursor: pointer;
  font-family: var(--mono);
  font-size: 0.53rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

@media (hover: hover) and (pointer: fine) {
  .dp-close:hover,
  .dp-nav button:hover { color: var(--bone); border-color: var(--feldspar); }
}

.dp-close:focus-visible,
.dp-nav button:focus-visible { outline: 1px solid var(--feldspar); outline-offset: 2px; color: var(--bone); }

/* --------------------------------------------------------------------------
   Axes figure (V3)

   An object with four axes run through it. Each axis has a receding half,
   drawn behind the image, and a manifest half, drawn in front; both fade to
   nothing as they reach the centre. The image is screen-blended over the rear
   lines, so the translucent object lets them through where it is dark and
   hides them where it is lit, which is what makes the axes read as passing
   through it rather than lying on top. Everything is static markup.
   -------------------------------------------------------------------------- */

.axes {
  margin: 0;
  /* Room for the pole names, which sit outside the square. The floor was
     4rem, which is less than the names actually need — Imagination runs 88px
     and Knowledge 71 at any figure size, since their type does not scale with
     it. On a 1024 screen that let Knowledge reach a pixel past the page rail
     once the page's frame widened (2026-10-20). The floor now covers the
     longest of them. */
  /* Raised again on 2026-10-21 with the names: at 13.8px Knowledge runs
     97px and Imagination 118, where the floor was 92. The floor now
     covers Knowledge, which is the one on the page rail's side;
     Imagination overhangs to the left, into the column gap. */
  padding-inline: clamp(6.4rem, 6vw, 6.6rem);
}

.axes-stage {
  position: relative;
  width: 100%;
  max-width: 620px;
  margin-inline: auto;
  aspect-ratio: 1;
  isolation: isolate;
  background: #000;
}

.axes-stage svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

.axes-back   { z-index: 0; }
.axes-object { position: absolute; z-index: 1; height: auto; mix-blend-mode: screen; }
.axes-front  { z-index: 2; }

.axis-pole {
  position: absolute;
  z-index: 3;
  font-family: var(--mono);
  /* Up from 0.58rem on 2026-10-21, in two steps: at that size, in a tint,
     on black, the pole names were the first thing to go on a dim screen.
     The tints themselves were lightened with them — at this size a glyph
     reaches its own colour rather than an antialiased fraction of it. */
  font-size: 0.86rem;
  line-height: 1;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  white-space: nowrap;
  /* Each pole wears its axis's tint, set inline as --pole. */
  color: var(--pole, rgba(244, 241, 234, 0.84));
}

/* The receding poles step back with their lines — a step, not a fade. */
.axis-pole.is-back { opacity: 0.95; }

/* The names do not scale with the figure, so on a phone they dwarf it: at
   13.8px Imagination runs 118px against a stage of 143, and the padding
   holding them clear of the page rail takes 205 of a 347 column. Below
   900 there is no rail to clear — it is hidden — so the names come down
   and the padding with them, which is where the figure gets its size
   back. */
@media (max-width: 899px) {
  .axes { padding-inline: clamp(4.4rem, 18.5vw, 5rem); }
  .axis-pole { font-size: 0.62rem; letter-spacing: 0.12em; }
}

.axes-caption {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   Hero statement (V3)

   Back to the layout of the first V3 pass, at the 2026-09-15 review: the lead
   centred on the screen's height at the left, where the veil holds the field
   back, and the 01 · WHAT statement in a column on the right. The statement
   keeps the hairlines of the footer version, one over each paragraph, and a
   soft shade travels with it so it stays legible over the field. Below
   1100px the columns would starve the wordmark, so the statement stacks
   under the lead and the hero grows to fit.
   -------------------------------------------------------------------------- */

.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(2rem, 4.5vh, 2.8rem);
}

.hero-cols {
  position: relative;
  isolation: isolate;
  /* About 60 characters a line at this size. */
  max-width: 27rem;
  display: grid;
  gap: clamp(0.9rem, 2.2vh, 1.3rem);
}

.hero-cols::before {
  content: "";
  position: absolute;
  z-index: -1;
  inset: -3rem -4rem -3rem -3rem;
  pointer-events: none;
  background: radial-gradient(closest-side,
    rgba(14, 14, 13, 0.84) 0%,
    rgba(14, 14, 13, 0.62) 55%,
    rgba(14, 14, 13, 0) 100%);
}

.hero-col {
  padding-top: 0.75rem;
  border-top: 1px solid var(--hairline);
}

/* Rules only between paragraphs: the first needs none above it. */
.hero-col:first-child {
  padding-top: 0;
  border-top: 0;
}

.hero-col p {
  font-size: 0.76rem;
  line-height: 1.72;
  letter-spacing: -0.01em;
  color: var(--bone-dim);
  text-wrap: pretty;
}

@media (min-width: 761px) {
  .hero-col p { font-size: 0.72rem; }
}

@media (min-width: 1100px) {
  .hero-grid {
    grid-template-columns: minmax(0, 1.08fr) minmax(0, 0.92fr);
    column-gap: clamp(3rem, 6vw, 7rem);
    align-items: center;
  }
  /* Held in from the right edge so the page rail in the margin keeps clear
     air beside the text. */
  .hero-cols { justify-self: end; margin-right: clamp(2.5rem, 4vw, 4rem); }
}

@media (max-width: 1099px) {
  .hero {
    height: auto;
    min-height: 100vh; min-height: 100svh;
    padding-block: clamp(6.5rem, 14vh, 8.5rem) clamp(5.5rem, 12vh, 7rem);
  }
}

/* --------------------------------------------------------------------------
   Scroll cue

   Draws itself once, after the hero copy has settled, then holds still. It
   fades out as soon as the visitor starts scrolling, because by then it has
   said everything it has to say.
   -------------------------------------------------------------------------- */

/* Two elements on purpose. The entrance animates .cue-inner, leaving the outer
   element's opacity free for the scroll fade to drive from script: a CSS
   animation with forwards fill outranks inline styles and would block it. */
.scroll-cue {
  position: absolute;
  z-index: 3;
  left: 50%;
  bottom: max(clamp(1.2rem, 3vh, 2rem), env(safe-area-inset-bottom));
  transform: translateX(-50%);
  padding: 0.4rem 0.6rem;
  color: var(--feldspar);
  /* --afford is driven by script as the hero scrolls away; 1 until then. */
  opacity: var(--afford, 1);
}

.cue-inner {
  display: flex;
  align-items: center;
  gap: 0.7rem;
  opacity: 0;
  animation: cue-in 800ms var(--settle) 1200ms forwards;
}

.scroll-cue .mono { color: inherit; transition: color 200ms var(--ease-out); }

.cue-line {
  width: 46px;
  height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left center;
  animation: cue-draw 900ms var(--settle) 1300ms forwards;
}

@media (hover: hover) and (pointer: fine) {
  .scroll-cue:hover .mono { color: var(--bone); }
}

/* Settles at 0.95 rather than 0.75 since 2026-10-21: the cue is the third
   of the marks along the hero's bar, and at 0.75 of feldspar on black it
   was the faintest of them. */
@keyframes cue-in {
  from { opacity: 0; }
  to   { opacity: 0.95; }
}

@keyframes cue-draw {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}

/* --------------------------------------------------------------------------
   Sections
   -------------------------------------------------------------------------- */

.section {
  position: relative;
  padding-block: clamp(5.5rem, 3rem + 11vw, 13rem);
}

.rule {
  height: 1px;
  background: var(--hairline);
  border: 0;
}

.kicker-row {
  display: flex;
  align-items: center;
  gap: 1rem;
  margin-bottom: clamp(2.4rem, 5vw, 4rem);
}

.kicker-row .dot {
  flex: none;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--umber);
}

.kicker-row .mono { font-weight: 400; }
.kicker-row .rule { flex: 1; }

/* --------------------------------------------------------------------------
   2-Intro
   -------------------------------------------------------------------------- */

.intro-grid {
  display: grid;
  gap: clamp(2rem, 4vw, 4.5rem);
  align-items: start;
}

/* Held back to 1100px. Splitting earlier drops the paragraph to roughly 48
   characters per line, well under a comfortable measure; stacked and full
   width reads better than two cramped columns. The statement is capped at
   20ch regardless, so the extra room goes to the prose. */
@media (min-width: 1100px) {
  .intro-grid { grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); }
}

/* The one-line definition of the institute, carried at display size. */
.statement {
  max-width: 20ch;
  font-family: var(--display);
  font-weight: 500;
  font-size: var(--s3);
  line-height: 1.3;
  letter-spacing: -0.022em;
  color: var(--bone);
  text-wrap: balance;
}

/* Body copy used to run at --s1, the same token the hero lede and the pull
   quotes use, which put it at 22.4px on a 1280 viewport. Every paragraph read
   as an introduction and nothing sat quietly under a heading. The measure is
   held in ch so the line length follows the type instead of staying pinned to
   42rem as the size changes.

   A monospace wants a shorter line and a taller one than a serif does, so 54
   characters rather than 64 and 1.72 rather than 1.6. Long monospace lines
   are where this kind of setting usually fails. */
.prose {
  max-width: 54ch;
  font-size: clamp(0.95rem, 0.91rem + 0.18vw, 1.04rem);
  line-height: 1.72;
  color: var(--bone-dim);
  text-wrap: pretty;
}

@media (min-width: 1100px) {
  /* Drops the paragraph off the statement's baseline so the two columns read
     as sequence rather than as a pair of equal blocks. */
  .prose { padding-top: 0.55rem; }
}

/* --------------------------------------------------------------------------
   Reveal on scroll

   Transitions rather than keyframes: an element can re-enter the viewport
   mid-flight and retarget from where it is instead of restarting.
   -------------------------------------------------------------------------- */

.reveal {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 700ms var(--settle), transform 700ms var(--settle);
}

.reveal[data-d="1"] { transition-delay:  80ms; }
.reveal[data-d="2"] { transition-delay: 160ms; }

.reveal.in {
  opacity: 1;
  transform: none;
}

/* --------------------------------------------------------------------------
   Field pause control

   The field animates indefinitely, which WCAG 2.2.2 requires a stop mechanism
   for. Revealed by script only when the field is actually running, so it is
   never a dead control under reduced motion.
   -------------------------------------------------------------------------- */

.field-toggle {
  position: absolute;
  z-index: 3;
  left: max(var(--gutter), env(safe-area-inset-left));
  bottom: max(clamp(1.2rem, 3vh, 2rem), env(safe-area-inset-bottom));
  display: flex;
  align-items: center;
  gap: 0.55rem;
  padding: 0.4rem 0;
  border: 0;
  background: none;
  color: var(--feldspar);
  font-family: var(--mono);
  font-size: 0.66rem;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  cursor: pointer;
  /* Scaled by --afford so the scroll fade and the hover state compose instead
     of one overriding the other. The base went 0.4 to 0.78 on 2026-10-21:
     at 0.4 the control could not be read until it was hovered. */
  opacity: calc(0.78 * var(--afford, 1));
  transition: opacity 200ms var(--ease-out), color 200ms var(--ease-out);
}

/* Full opacity, not scaled by --afford: the control stays tabbable while the
   field is running, so focusing it must always make it visible. */
.field-toggle:focus-visible {
  opacity: 1;
  color: var(--bone);
}

@media (hover: hover) and (pointer: fine) {
  .field-toggle:hover {
    opacity: var(--afford, 1);
    color: var(--bone);
  }
}

.field-toggle-dot {
  width: 5px;
  height: 5px;
  border: 1px solid currentColor;
  border-radius: 50%;
  background: currentColor;
  transition: background 200ms var(--ease-out);
}

.field-toggle[data-paused="true"] .field-toggle-dot { background: transparent; }

/* The horizontal veil is tuned for a wide viewport, where the field has room
   to sit clear of the text on the right. Narrow screens have no such room, so
   the field steps back and a scrim carries the text block instead. */
@media (max-width: 760px) {
  :root { --field-opacity: 0.46; }

  /* A full-width scrim, since mobile copy runs edge to edge. The band it
     used to be was cut for a hero of one screen: 0.88 to 0.90 from 37% to
     66% of its height. The hero now stacks two screens' worth into 1233px
     on a phone, so that stretch landed over the statement and took the
     field out with it. Even now, and light enough to read the field
     through: 0.44 at its darkest against the 0.90 it was. */
  .field-veil {
    background: linear-gradient(180deg,
      rgba(14, 14, 13, 0.86) 0%,
      rgba(14, 14, 13, 0.30) 12%,
      rgba(14, 14, 13, 0.44) 46%,
      rgba(14, 14, 13, 0.40) 84%,
      var(--basalt) 100%);
  }
}

@media (max-width: 620px) {
  .hero-meta { justify-content: flex-start; }
  .hero-meta #coord { display: none; }
}

/* --------------------------------------------------------------------------
   Entrance

   Predetermined, plays once on load. CSS animations rather than JS so they
   stay off the main thread while the page is still parsing and painting.
   -------------------------------------------------------------------------- */

/* No JS gate. These fire on load, so a failed script can never leave the hero
   text stuck at zero opacity. */
.hero-meta,
.wordmark,
.hero-sub,
.hero-tag {
  opacity: 0;
  animation: rise 900ms var(--settle) forwards;
}

.hero-meta { animation-delay:  40ms; }
.wordmark  { animation-delay: 110ms; animation-name: rise-wordmark; }
.hero-sub  { animation-delay: 210ms; }
.hero-tag  { animation-delay: 300ms; }

/* The statement follows the lead, one column after another. */
.hero-col {
  opacity: 0;
  animation: rise 900ms var(--settle) forwards;
}
.hero-col:nth-child(1) { animation-delay: 460ms; }
.hero-col:nth-child(2) { animation-delay: 560ms; }
.hero-col:nth-child(3) { animation-delay: 660ms; }

#field {
  animation: field-in 1800ms var(--settle) 120ms forwards;
}

@keyframes rise {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: translateY(0); }
}

/* The wordmark travels slightly further than the rest of the block so it leads
   the entrance. Compositor-only: no filter, since this is the largest element
   on screen and it animates while the page is still parsing. */
@keyframes rise-wordmark {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: translateY(0); }
}

@keyframes field-in {
  from { opacity: 0; }
  to   { opacity: var(--field-opacity); }
}

/* --------------------------------------------------------------------------
   Reduced motion

   Fewer and gentler, not none. Opacity aids comprehension and stays;
   movement, blur and parallax go.
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }

  .hero-meta,
  .wordmark,
  .hero-sub,
  .hero-tag,
  .hero-col {
    animation: fade-in 320ms var(--ease-out) forwards;
  }

  #field { animation: field-in 600ms var(--ease-out) forwards; }

  /* Movement out, opacity kept: the reveal still explains that content has
     arrived, it just stops travelling to say so. */
  .reveal {
    transform: none;
    transition: opacity 300ms var(--ease-out);
  }

  .cue-line { transform: scaleX(1); animation: none; }
  .cue-inner { animation: cue-in 300ms var(--ease-out) 400ms forwards; }

  /* Gentler, not absent. The panel stops travelling and fades instead, so the
     change is still explained rather than snapping into place. */
  .nav-panel {
    transform: none;
    opacity: 0;
    transition: opacity 200ms var(--ease-out);
  }

  .nav-panel.is-open {
    transform: none;
    opacity: 1;
    transition-duration: 240ms;
  }

  @keyframes fade-in {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
}

/* --------------------------------------------------------------------------
   The loading mark

   Conway on a torus, the same figure the hero runs, so a wait reads as the
   site thinking. Mounted over whatever is loading; the field is a fixed square
   rather than a share of the box, so it stays a quiet mark on a full viewport
   and on a single plate alike.
   -------------------------------------------------------------------------- */

.life-loader {
  position: absolute; inset: 0; z-index: 9;
  display: grid; place-items: center;
  background: #000;
  transition: opacity 600ms var(--ease-out);
}
.life-loader.done { opacity: 0; pointer-events: none; }

.life-inner { display: grid; gap: 1rem; justify-items: center; }
.life-inner canvas {
  display: block;
  width: clamp(112px, 24vmin, 200px);
  height: clamp(112px, 24vmin, 200px);
}
.life-pct {
  font-family: var(--mono); font-size: 0.62rem;
  letter-spacing: 0.22em; text-transform: uppercase; color: var(--bone-faint);
}
.life-pct b { color: var(--bone); font-weight: 400; }

/* ==========================================================================
   Contact
   One dialog, injected by assets/js/contact.js on every page, opened by the
   Contact link in the nav. The link keeps its href, so without script it
   still goes to a page; with script the click is intercepted.
   ========================================================================== */
  /* ---- the dialog -----------------------------------------------------
     Native <dialog>. showModal() gives focus trapping, Escape to close, an
     inert background and focus return to the trigger, which are exactly the
     parts hand-rolled modals get wrong. */
  dialog.contact-dialog {
    padding: 0; border: 0; color: var(--bone); background: transparent;
    max-width: none; max-height: none;
    margin: auto; width: min(34rem, calc(100vw - 2rem));
  }
  dialog.contact-dialog::backdrop { background: rgba(0,0,0,0.72); backdrop-filter: blur(6px); }
  dialog.contact-dialog:not([open]) { display: none; }
  .sheet {
    position: relative;
    background: #0B0B0A; border: 1px solid var(--hairline);
    display: grid; gap: clamp(1.4rem, 3vw, 2.2rem);
    padding: clamp(1.6rem, 3.4vw, 2.6rem);
  }
  .d-head { display: grid; gap: 0.7rem; }
  .d-kicker {
    display: flex; align-items: center; gap: 0.8rem;
    font-family: var(--mono); font-size: 0.58rem;
    letter-spacing: 0.2em; text-transform: uppercase; color: var(--feldspar);
  }
  .d-kicker .rule { flex: 1; height: 1px; background: var(--hairline); }
  .d-title {
    font-family: var(--title); font-weight: 400; text-transform: uppercase;
    font-size: clamp(1.2rem, 0.95rem + 0.95vw, 1.65rem);
    line-height: 1.18; letter-spacing: 0.02em; color: var(--bone);
  }
  .d-lede { color: var(--bone-dim); font-size: 0.95rem; line-height: 1.55; max-width: 40ch; }
  .d-close {
    position: absolute; top: clamp(0.9rem, 2vw, 1.3rem); right: clamp(0.9rem, 2vw, 1.3rem);
    width: 30px; height: 30px; display: grid; place-items: center;
    border: 1px solid var(--hairline); background: none; color: var(--feldspar);
    cursor: pointer; font-family: var(--mono); font-size: 0.9rem; line-height: 1;
  }
  .d-close:hover { color: var(--bone); border-color: var(--feldspar); }
  /* ---- the form -------------------------------------------------------- */
  .form { display: grid; gap: clamp(1rem, 2vw, 1.4rem); }
  @media (min-width: 560px) { .form .pair { display: grid; grid-template-columns: 1fr 1fr; gap: inherit; } }
  .field { display: grid; gap: 0.4rem; }
  .field > span {
    font-family: var(--mono); font-size: 0.54rem;
    letter-spacing: 0.18em; text-transform: uppercase; color: var(--feldspar);
  }
  /* Ruled rather than boxed. A row of filled boxes is the one thing on this
     site that would look like somebody else's form. */
  .field input, .field select, .field textarea {
    width: 100%; background: transparent; color: var(--bone);
    border: 0; border-bottom: 1px solid var(--hairline);
    font-family: var(--body); font-size: 0.98rem; line-height: 1.4;
    padding: 0.5rem 0; border-radius: 0;
    transition: border-color 300ms var(--ease-out);
  }
  .field textarea { resize: vertical; min-height: 5.5rem; }
  .field select { font-family: var(--mono); font-size: 0.72rem; letter-spacing: 0.06em; }
  .field select option { background: #0B0B0A; }
  .field input:focus, .field select:focus, .field textarea:focus {
    outline: none; border-color: var(--bone);
  }
  /* :user-invalid rather than :invalid, so an untouched empty field is never
     marked wrong before anyone has had the chance to fill it. */
  .field input:user-invalid, .field textarea:user-invalid { border-color: #A6553F; }
  /* Off screen rather than display:none, which some bots test for. Out of the
     tab order and hidden from assistive tech, so nobody using the form
     properly ever meets it. */
  .hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
  .form-foot {
    display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between;
    gap: 1rem; padding-top: 0.4rem;
  }
  .send {
    display: inline-flex; align-items: center; gap: 0.7rem;
    padding: 0.75rem 1.3rem; border: 1px solid var(--feldspar);
    background: none; color: var(--bone); cursor: pointer;
    font-family: var(--mono); font-size: 0.6rem;
    letter-spacing: 0.18em; text-transform: uppercase;
    transition: background 300ms var(--ease-out), color 300ms var(--ease-out);
  }
  .send:hover { background: var(--bone); color: #000; }
  .send[disabled] { opacity: 0.45; cursor: default; }
  .send[disabled]:hover { background: none; color: var(--bone); }
  .fine {
    font-family: var(--mono); font-size: 0.52rem;
    letter-spacing: 0.12em; text-transform: uppercase; color: var(--bone-faint);
    max-width: 30ch; line-height: 1.7;
  }
  /* ---- reply states ---------------------------------------------------- */
  .reply { display: none; gap: 0.9rem; }
  /* Scoped to .d-head on 2026-10-31. It read .sheet.is-sent .d-lede, which
     also matched the reply's own paragraph — the one thing the reader is
     meant to see — so a sent message left the kicker and the form's title
     standing over an empty panel and nothing else. The title goes with the
     lede now, and each reply carries its own; the kicker stays, so the
     panel still says what it is. */
  .sheet.is-sent .form,
  .sheet.is-sent .d-head .d-title, .sheet.is-sent .d-head .d-lede,
  .sheet.is-failed .form,
  .sheet.is-failed .d-head .d-title, .sheet.is-failed .d-head .d-lede { display: none; }
  .sheet.is-sent .reply.sent { display: grid; }
  .sheet.is-failed .reply.failed { display: grid; }
  .reply .tick {
    width: 34px; height: 34px; display: grid; place-items: center;
    border: 1px solid var(--feldspar); color: var(--bone);
    font-family: var(--mono); font-size: 0.8rem;
  }
  .reply.failed .tick { border-color: #A6553F; }
  .again {
    justify-self: start; margin-top: 0.3rem;
    border: 1px solid var(--hairline); background: none; color: var(--feldspar);
    cursor: pointer; padding: 0.5rem 0.9rem;
    font-family: var(--mono); font-size: 0.55rem;
    letter-spacing: 0.16em; text-transform: uppercase;
  }
  .again:hover { color: var(--bone); border-color: var(--feldspar); }

/* ==========================================================================
   Type roles (2026-10-21)

   Richard's own names for the levels a section is built from, so the same
   hierarchy can be set once and adopted section by section rather than
   re-tuned each time. Tried on Our Purpose first.

     .t-title       the section's name
     .t-subtitle    the line under it
     .t-main        the one passage the section is built around
     .t-sub         everything that supports it
     .t-conclusion  the line it closes on

   A role carries type, colour and measure only. Where it sits, and the
   space around it, belong to the section.
   ========================================================================== */

/* Was a 12px tracked gold label; a third again as large, with the tracking
   opened to match. Bone and SemiBold since 2026-10-21 — the gold moved to
   the subtitle, which now carries the weight of the two. */
/* Space Mono since 2026-10-22 (Richard's call), taking The Research Engine
   (.pair-title) as its base so the two read as one voice. The face came
   first and the two still looked unalike, because the tracking carried over
   from the old setting: this was IBM Plex Mono **600** at +0.2em, where wide
   tracking suits a small heavy caps label. At 400 in a face with this much
   contrast, 4.32px of air between letters reads as a spaced-out label rather
   than a heading — 17x .pair-title's 0.26px. So size, tracking and leading
   all come from .pair-title now, and only the role differs.

   Weight stays 400: the site requests Space Mono at 400 only, so a 600 here
   would be the browser's synthesised bold, which smears this face. */
.t-title {
  margin: 0;
  font-family: var(--title); font-weight: 400; text-transform: uppercase;
  /* Ceiling raised 1.6rem -> 2.6rem on 2026-10-24, to meet the cluster
     titles in Scroll B, which are a flat 2.6rem at every width. The whole
     clamp is scaled by that 1.625 rather than pinned, so the two are
     identical from 1300px up and the role still comes down on a phone:
     pinned, OUR METHODOLOGIES would be 41.6px on a 390 screen and wrap. */
  font-size: clamp(1.87rem, 1.544rem + 1.3vw, 2.6rem);
  letter-spacing: 0.01em; line-height: 1.12;
  color: var(--bone);
}

/* The two swap on 2026-10-21: the gold comes down here and the subtitle
   becomes the larger of the pair, set lighter than the title above it —
   30px Regular against the title's 22px SemiBold on a 1545 screen. 40px
   was tried and put back. Caps,
   with the tracking opened to carry them: at 0.03em capitals set this size
   close up. */
/* Out of uppercase and down a step on 2026-10-24 (Richard's call), after
   .t-title went to 2.6rem: at 30px in caps under a 41.6px title it read as
   a second heading rather than a subtitle. The clamp is scaled by 0.798 to
   a 1.5rem ceiling, 24px at 1545.

   The tracking comes down with the caps. +0.08em was there because the line
   was uppercase, where letters need the air; on lowercase it reads as
   artificially spaced — the same fault that made .t-title look like a
   different typeface before its tracking was fixed. */
.t-subtitle {
  margin: 0;
  /* Down again on 2026-10-24, from a 1.5rem ceiling to Sub paragraph's own
     --sub-size, and bold with it. 600 is the heaviest IBM Plex Mono the
     site loads and it is a real cut — Plex's 700 is not requested, so
     asking for it would get the browser's synthesised bold instead. */
  font-family: var(--mono); font-weight: 600;
  /* The cluster categories' own size (2026-10-25, Richard's call): a flat
     0.9675rem, 15.48px, as .scrollB .keys carries. This breaks the pairing
     with .t-sub made the day before — the two shared --sub-size so they
     could not drift — so Subtitle has its own size again and Sub paragraph
     keeps the token. It is also flat rather than a clamp, because the line
     it is matching is. */
  font-size: 0.9675rem;
  letter-spacing: 0.01em; line-height: 1.4;
  /* Bone at 80% since 2026-10-27 (Richard's call), where it was feldspar.
     The role carried the gold from the day it was written; it now reads in
     the same tone as the running copy and the gold is left to the phrases
     lifted out of a statement, which is the one place it still means
     something. */
  color: var(--bone-dim);
}

/* The display voice, in two sizes. Neither has a measure of its own, so
   each fills whatever column it is given, and an em inside either takes the
   gold.

   .t-tagline is the loud one — a 51px face on a 1545 screen. It is what a
   section opens on when the line is short enough to carry a screen.

   .t-main is the same voice for a passage rather than a line. The split
   came on 2026-10-21, when Purpose's opening paragraph ran to seven lines
   at the larger size; it went to 36px, then to 24px with a measure of its
   own, so it stands as a column rather than a slab across the whole width,
   and then back up, to 32px and then 36, once the supporting text had
   settled at 22. The tagline keeps no measure — it is meant to
   reach the far edge. */
.t-tagline {
  margin: 0; max-width: none;
  font-family: var(--display); font-weight: 400;
  font-size: clamp(1.3rem, 0.4rem + 3.1vw, 3.2rem);
  line-height: 1.26; letter-spacing: -0.015em;
  color: var(--bone); text-wrap: wrap;
}
.t-main {
  margin: 0; max-width: 50ch;
  font-family: var(--display); font-weight: 400;
  /* Up to 32px on 2026-10-21, where it had been 24: it is the passage the
     section is built around and sat too near the text supporting it; 32
     first, then 36.

     The floor is what a phone gets, since the slope falls well under it
     there, and it has to clear the supporting text below — at 0.7rem the
     passage computed to 11px against that text's 13, which inverts the
     hierarchy. Tried at 1.1rem to shorten it on a phone and put back on
     Richard's call: 1.4rem reads better there even at fourteen lines. */
  font-size: clamp(1.4rem, 0.34rem + 2.25vw, 2.25rem);
  line-height: 1.32; letter-spacing: -0.015em;
  color: var(--bone); text-wrap: pretty;
}
.t-tagline em,
.t-main em { font-style: normal; color: var(--feldspar); }

/* The underline mark (2026-11-01). The second way a section marks a phrase:
   the words stay bone and the section's own tone runs under them, where an
   em colours the words themselves. --mark is set per section; feldspar is
   the fallback, so a section not yet onto the seven-tone sheet still
   underlines in the page's own gold.

   Measured off Richard's references rather than chosen: the rule is 1.3px
   of ink at a 35.6px face (0.03em) sitting 5px under the baseline (0.14em).
   The thickness is floored at 1px so it cannot fall under a device pixel on
   a phone, and 0.17em is the offset that renders that 0.14em gap. */
.hero-tag u,
.hero-statement u,
.t-main u,
.t-tagline u,
.t-conclusion u,
.t-sub u,
.ci-text u,
.closing-line u {
  text-decoration: underline;
  text-decoration-color: var(--mark, var(--feldspar));
  text-decoration-thickness: max(1px, 0.03em);
  text-underline-offset: 0.17em;
}


/* Reading size, two levels, measured off Richard's second reference: the
   base is the **gold**, not a grey, and the phrases a sentence turns on are
   lifted out of it into bone. Sampled from that image: base #8F877A at the
   peak of the glyphs, which is feldspar; lifted #E5E4E1, which is bone.

   A run of them steps down in tone rather than in size, which is what the
   reference does across its three: gold, then bone, then grey. Sampled off
   it: #8E867A, #E2E2DF and #737372, which are feldspar, bone and bone at
   0.47. The grey is carried at 0.62 rather than the reference's 0.47 —
   Richard found 0.47 too dark on some screens, and against plain black,
   where it stands in Our Methodologies, it had no field to lift it. They set solid, with no space between them, so the leading runs
   unbroken across the whole run and the tone is the only thing that marks
   one from the next.

     .t-sub           feldspar
     .t-sub.is-bone   bone
     .t-sub.is-dim    bone at 0.47
     <b>, <em>        a phrase lifted to bone within a feldspar passage

   Sized off the same image: its line runs 67 characters, and the face that
   gives that here is 22px, under .t-main's. Full width at this size would
   run 110 characters a line, so the measure is what the reference's own
   setting needs. 76 rather than the reference's 67, because at 68 the one
   passage set in bone broke after its 68th character and left two words
   hanging on a line of their own. */
.t-sub {
  margin: 0; max-width: 76ch;
  font-size: var(--sub-size);
  line-height: 1.72;
  color: var(--feldspar);
  text-align: left; hyphens: none; text-wrap: pretty;
}
.t-sub.is-bone { color: var(--bone); }
.t-sub.is-dim { color: rgba(244, 241, 234, 0.8); }
.t-sub b { font-weight: 400; color: var(--bone); }
.t-sub em { font-style: normal; color: var(--bone); }

/* The line a section closes on. Set in the
   display voice since 2026-10-21 — at reading size it was the last thing
   on the screen and the easiest to miss. A step under .t-main rather than
   a step over .t-sub. It briefly collided with .t-main at 36px; the
   passage then came down to 24 and the two separated again on their own.
   54px against a 36px passage and 22px of supporting text, on a 1545
   screen — the closing line is a declaration, not reading matter, so it
   stands well above the passage rather than under it. It is the largest
   thing in a section that has no tagline, and at this size its 64ch
   measure no longer binds: it takes the content width and sets to three
   lines. */
.t-conclusion {
  margin: 0; max-width: 64ch;
  /* The feldspar rule down its left is gone (2026-10-21), with the other
     gold lines. The line stands on its size and its place alone. */
  font-family: var(--display); font-weight: 400;
  font-size: clamp(1.7rem, 0.6rem + 2.9vw, 3.4rem);
  line-height: 1.22; letter-spacing: -0.018em;
  color: var(--bone); text-wrap: pretty;
}
.t-conclusion em { font-style: normal; color: var(--feldspar); }

