/* ============================================================
   site.css, nonprofit-layout

   STRUCTURE ONLY. Shared by every client on this layout.
   No hex value, no typeface name, no client word lives here.
   Everything brand shaped is a custom property read from the
   theme file. If a change cannot be made in a theme file it
   belongs to EVERY client using this layout, so think twice.
   ============================================================ */

/* ---------- reset ---------- */

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

html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }

/* --ground and --fg exist because --paper and --ink were each doing two
   jobs. --paper was the page background AND the text colour sitting on a
   dark band. --ink was the body text AND the ground of .band--ink. On a
   light theme both readings agree, so nothing showed. On a DARK theme they
   fight, and flipping the values breaks four bands at once. These two names
   carry the page's own ground and text, nothing else. A light theme sets
   them to --paper and --ink and nothing moves. */
body {
  margin: 0;
  background: var(--ground);
  color: var(--fg);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  line-height: 1.65;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; display: block; }

a { color: inherit; }

h1, h2, h3, h4 {
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  line-height: 1.08;
  letter-spacing: var(--display-tracking);
  margin: 0;
  text-wrap: balance;
}

/* Sub-heads sit one voice down from the display weight. Falls back to the
   display weight on a theme that does not set --subhead-weight. */
h3, h4 { font-weight: var(--subhead-weight, var(--display-weight)); }

p { margin: 0; }

/* figure carries a 40px UA margin. It is the single most common reason a
   card image looks inset for no reason. */
figure { margin: 0; }

ul { margin: 0; padding: 0; list-style: none; }

:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

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

/* ---------- rhythm ---------- */

.wrap {
  width: min(100% - 2.5rem, var(--measure-wide));
  margin-inline: auto;
}

.wrap--tight { width: min(100% - 2.5rem, var(--measure-tight)); }

/* ONE NUMBER SETS THE WHOLE PAGE RHYTHM. Every gap between two sections is
   this value twice, because the band above pays half and the band below pays
   half. The hero's bottom and the footer's top are driven off the same
   token, which is the only way the first gap and the last gap match the
   seven in the middle. Measured, not guessed: before this the hero to give
   gap was 264px, the middle was 224px and the footer was 184px. */
.band { padding-block: var(--band-space); position: relative; }

/* THE BAND THAT FOLLOWS A LOGO ROW NEEDS MORE AIR ABOVE IT THAN THE TOKEN GIVES.
   Every other band on this page ends in TEXT, which carries its own settled
   baseline and descender space, so one --band-space either side reads correct.
   The proof band ends in .partners__list, a row of hard edged images flush to
   the bottom of their boxes, and the founder band opens with a large photo.
   Hard edge against hard edge collapses the gap optically while it measures the
   same. Put this on any band that comes straight after a logo row. */
.band--after-logos { padding-block-start: calc(var(--band-space) * 1.9); }

/* The band's content sits LOW in it. Half the space under the content moves
   above it, so the total height of the band does not change and the page
   rhythm below is untouched. For a closing band, where sitting halfway
   between the section above and the footer reads as unfinished. */
.band--sink { padding-block: calc(var(--band-space) * 1.5) calc(var(--band-space) * 0.5); }

.band--ink   { background: var(--ink); color: var(--paper); }
.band--deep  { background: var(--ink-deep); color: var(--paper); }
.band--tint  { background: var(--tint); }
.band--paper { background: var(--ground); }

/* THE HOLE. Reads as paper but is transparent, so the fixed .markdrop layer
   shows through it. Every other ground is solid and covers the mark, so
   which bands get this class is what makes the mark appear, disappear and
   appear again on the way down. A COMPOSITION choice, not a colour one.

   NEVER put it on the first band under the header. A giant mark landing
   directly below the lockup reads as a second, broken logo rather than as a
   backdrop. Start it mid page, after at least one solid band. */
.band--open { background: transparent; }

.band--ink a, .band--deep a { color: var(--paper); }

/* A band head is capped at the text measure, so on a wide screen it hugs one
   side and leaves a hole on the other. --head-inline decides which. `0 auto`
   pins it left, `auto` centres it. The eyebrow's flex line has to follow, or
   a centred heading sits under a label that is still ranged left. */
.band__head {
  max-width: var(--measure-text);
  margin-bottom: clamp(2.25rem, 4vw, 3.25rem);
  margin-inline: var(--head-inline);
  text-align: var(--head-align);
}

/* More air under a head block, for a band whose head is doing more work than
   a label and needs to stop feeling attached to the grid below it. A
   modifier on the layout rather than a one off style in a client file,
   because the next build will want it too. */
.band__head--airy { margin-bottom: clamp(3.5rem, 6.5vw, 5.5rem); }

/* A head with NOTHING after it inside its band needs no grid clearance.
   The margin exists to hold a head off the grid below it, and when the head
   is the band's last element the same margin lands dead on the junction to
   the NEXT band, which then reads wider than every other one on the page.
   Caught by Seth on the solution band 2026-08-09, 256px against the 168px
   every other junction gets. Generic on purpose, the next band this shape
   heals itself. */
.wrap > .band__head:last-child { margin-bottom: 0; }

.band__head .eyebrow { justify-content: var(--head-justify); }

/* The mirrored rule only exists when the eyebrow is centred. Left ranged it
   is capped at 0 and never draws. */
.band__head .eyebrow::before {
  content: "";
  flex: 1;
  height: 1px;
  background: currentColor;
  opacity: 0.35;
  max-width: var(--eyebrow-rule-lead);
}

.band__head .lede { margin-inline: var(--head-inline); }

/* A second paragraph in a band head, following the lede. One step down in
   size and colour, because a head that runs two paragraphs at the same
   weight has no lede, it has two openings. Capped and centred by the same
   tokens as everything else in the head so it cannot drift out of line. */
.head-tail {
  margin-top: 0.85rem;
  margin-inline: var(--head-inline);
  max-width: var(--measure-text);
  font-size: 1.0625rem;
  line-height: 1.65;
  color: var(--ink-soft);
}

.band--ink .head-tail, .band--deep .head-tail { color: var(--paper-soft); }

.eyebrow {
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 0.9rem;
  display: flex;
  align-items: center;
  gap: 0.7rem;
}

/* ---------- the eyebrow AS the heading ----------
   For a band that carries no headline, only its label. Tracking comes DOWN
   as the size goes up: 0.16em is drawn for 12px, and at display size the
   same value pulls the word apart into separate letters. The rule keeps its
   weight rather than scaling with the type, so it stays a mark beside the
   word and does not become a second bar competing with it. */
.eyebrow--head {
  font-size: clamp(1.35rem, 3.2vw, 2.25rem);
  letter-spacing: 0.1em;
  line-height: 1.15;
  margin-bottom: 0;
  gap: 1.1rem;
}

.eyebrow--head::after, .eyebrow--head::before { max-width: 6rem; opacity: 0.3; }

/* The display eyebrow zeroes its own bottom margin because in the band it was
   drawn for it IS the heading and the lede below carries its own top space.
   Put a real headline under it and that zero becomes a collision, measured at
   0px between the two. The space belongs to the pairing, not to either one, so
   it lives on the combinator and no other band is touched. */
.band__head .eyebrow--head + * { margin-top: clamp(1.1rem, 2vw, 1.75rem); }
/* Scoped to .band__head on purpose. As a bare `.eyebrow--head + *` it ties
   on specificity with `.lede`, which is declared later in this file and
   silently won, leaving one band at 18 while the other four sat at 28. */

.eyebrow::after {
  content: "";
  flex: 1;
  height: 1px;
  background: currentColor;
  opacity: 0.35;
  max-width: 4.5rem;
}

.hero .eyebrow, .band--ink .eyebrow, .band--deep .eyebrow { color: var(--accent-on-dark); }

h1 { font-size: clamp(2.5rem, 6.4vw, 4.4rem); }
h2 { font-size: clamp(1.95rem, 4.2vw, 3rem); }
h3 { font-size: clamp(1.15rem, 1.9vw, 1.4rem); line-height: 1.2; }

.lede {
  font-size: clamp(1.05rem, 1.6vw, 1.25rem);
  line-height: 1.6;
  color: var(--ink-soft);
  margin-top: 1.15rem;
  max-width: var(--measure-text);
}

.band--ink .lede, .band--deep .lede { color: var(--paper-soft); }

/* ---------- the mark backdrop ---------- */

/* THE MARK DOES NOT MOVE. One FIXED layer pinned to the viewport, the page
   scrolls over it. Content slides, the mark holds still, so it reads as
   sitting behind the building rather than printed on a band. A repeating
   background or a parallax offset would both cost a scroll listener and
   neither looks like this.

   See .band--open for which bands let it through, and for the one place it
   must never be allowed to show.

   aria-hidden and pointer-events because it carries no information and must
   never eat a click. The mark is announced once, in the header lockup. */
.markdrop {
  position: fixed;
  inset: 0;
  z-index: -1;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  pointer-events: none;
}

.markdrop img {
  width: min(var(--markdrop-vw), var(--markdrop-cap));
  max-width: none;
  opacity: var(--markdrop-opacity);
  /* A logo carries its own brand colour. Raised past about 0.10 that
     colour stops being a hint and starts being a second palette on
     the page, which is exactly what a page with one reserved accent
     cannot have. --markdrop-filter defaults to none, a theme that
     lifts the opacity flattens the mark to one ink instead. */
  filter: var(--markdrop-filter, none);
}

/* A theme that does not want the effect sets --markdrop-opacity to 0. */
@media print { .markdrop { display: none; } }

/* ---------- buttons ---------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.6rem;
  font-family: var(--font-body);
  font-size: 0.9375rem;
  font-weight: 650;
  letter-spacing: 0.01em;
  padding: 0.95rem 1.7rem;
  border-radius: var(--radius-btn);
  border: 2px solid transparent;
  text-decoration: none;
  cursor: pointer;
  transition: transform 0.16s ease, background-color 0.16s ease, border-color 0.16s ease, color 0.16s ease;
}

.btn:hover { transform: translateY(-2px); }

.btn--primary {
  background: var(--accent);
  color: var(--accent-ink);
  border-color: var(--accent);
  box-shadow: var(--shadow-btn);
}

.btn--primary:hover { background: var(--accent-bright); border-color: var(--accent-bright); }

/* A theme whose accent is dark has no working primary button on a dark band.
   These two properties let the theme name a second fill for exactly that case.
   A theme that does not care sets them equal to the accent and nothing moves. */
.site-header .btn--primary,
.hero .btn--primary,
.band--ink .btn--primary,
.band--deep .btn--primary {
  background: var(--btn-on-dark-bg);
  border-color: var(--btn-on-dark-bg);
  color: var(--btn-on-dark-ink);
}

.site-header .btn--primary:hover,
.hero .btn--primary:hover,
.band--ink .btn--primary:hover,
.band--deep .btn--primary:hover {
  background: var(--btn-on-dark-hover);
  border-color: var(--btn-on-dark-hover);
}

.btn--ghost { background: transparent; border-color: var(--rule-strong); color: inherit; }

.btn--ghost:hover { border-color: var(--accent); color: var(--accent); }

.band--ink .btn--ghost, .band--deep .btn--ghost { border-color: var(--rule-on-dark); }

.band--ink .btn--ghost:hover, .band--deep .btn--ghost:hover {
  border-color: var(--accent-on-dark);
  color: var(--accent-on-dark);
}

.btn--lg { padding: 1.1rem 2.1rem; font-size: 1rem; }

.btn-row { display: flex; flex-wrap: wrap; gap: 0.85rem; margin-top: 2rem; }

/* A band's own button row follows the band head. The hero keeps its own
   alignment, which is why this is scoped to .band and not to .btn-row. */
.band > .wrap > .btn-row { justify-content: var(--head-justify); }

/* ---------- header ---------- */

.site-header {
  position: sticky;
  top: 0;
  z-index: 60;
  background: var(--header-bg);
  border-bottom: 1px solid var(--rule-on-dark);
  backdrop-filter: saturate(140%) blur(10px);
}

.site-header__inner {
  width: min(100% - 2.5rem, var(--measure-wide));
  margin-inline: auto;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1.25rem;
  min-height: var(--header-h);
}

.lockup {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  text-decoration: none;
  color: var(--paper);
}

.lockup__mark { width: var(--mark-size); height: var(--mark-size); object-fit: contain; }

.lockup__name {
  display: block;
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-size: 1.0625rem;
  line-height: 1.05;
  letter-spacing: var(--display-tracking);
}

.lockup__sub {
  display: block;
  font-size: 0.6875rem;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--paper-faint);
  margin-top: 0.28rem;
}

.site-nav { display: flex; align-items: center; gap: 1.9rem; }

.site-nav a {
  color: var(--paper-soft);
  text-decoration: none;
  font-size: 0.9375rem;
  font-weight: 500;
  transition: color 0.16s ease;
}

.site-nav a:hover { color: var(--accent-on-dark); }

/* ---------- services dropdown ----------
   Pure CSS: hover or :focus-within opens it, so it works by keyboard with no
   script. The ::before bridge fills the air gap between the link and the
   panel, without it the pointer crosses dead ground on the way down and the
   panel flickers shut. Solid ground on the panel, never translucent: it
   floats over whatever the page scrolled to and type needs a floor. */
.nav-drop { position: relative; }

.nav-drop__panel {
  position: absolute;
  top: calc(100% + 0.85rem);
  left: 50%;
  translate: -50% 0;
  min-width: 17rem;
  padding: 0.5rem;
  background: #0f0f11;
  border: 1px solid var(--rule-on-dark);
  border-radius: var(--radius-card);
  box-shadow: 0 18px 40px -18px rgb(0 0 0 / 0.85);
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.14s ease, visibility 0.14s;
}

.nav-drop__panel::before { content: ""; position: absolute; left: 0; right: 0; top: -0.9rem; height: 0.9rem; }

.nav-drop:hover .nav-drop__panel,
.nav-drop:focus-within .nav-drop__panel { visibility: visible; opacity: 1; }

.nav-drop__panel a,
.nav-drop__panel span {
  display: block;
  padding: 0.55rem 0.8rem;
  border-radius: calc(var(--radius-card) - 6px);
  font-size: 0.9rem;
  white-space: nowrap;
}

.nav-drop__panel a { color: var(--paper-soft); text-decoration: none; }
.nav-drop__panel a:hover { color: var(--accent-on-dark); background: rgb(255 255 255 / 0.04); }

/* CHILDREN RENDER FLAT, Seth's call 2026-08-08. They carried an indent,
   a smaller size and a softer ink for one round and he cut all three:
   every row goes to a page, so every row reads the same. The
   .nav-drop__sub class stays in the markup as the record of the nesting,
   it just draws nothing. */

/* the wide panel, Seth's call 2026-08-08: a square spreading right, not one
   long column. Three PACKED columns, five entries each, children under
   their parents inside the column. The first cut aligned groups on shared
   grid rows and the uneven group heights punched holes through the panel,
   which he called out. Columns pack top down, tops aligned, and a hairline
   between columns carries the structure. The viewport guard keeps the
   centred panel on screen when the header narrows, and nowrap comes off
   inside it so the one long label, AEO, wraps inside its column. */
.nav-drop__panel--grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1.1rem;
  align-items: start;
  width: min(56rem, calc(100vw - 2rem));
  padding: 1rem;
}
.nav-drop__panel--grid a,
.nav-drop__panel--grid span { white-space: normal; }
.nav-drop__col { display: flex; flex-direction: column; gap: 0.1rem; min-width: 0; }
.nav-drop__col + .nav-drop__col { border-left: 1px solid var(--rule); padding-left: 1.1rem; }
.nav-drop__panel span { color: var(--paper-faint); }

@media (max-width: 48rem) {
  /* the wrapper dissolves so the link is a direct flex item of the phone
     menu again, and the panel goes away entirely */
  .nav-drop { display: contents; }
  .nav-drop__panel { display: none; }
}

.site-header .btn { padding: 0.7rem 1.35rem; font-size: 0.875rem; }

.nav-toggle {
  display: none;
  background: none;
  border: 1px solid var(--rule-on-dark);
  border-radius: var(--radius-btn);
  color: var(--paper);
  padding: 0.55rem 0.85rem;
  font-size: 0.8125rem;
  font-weight: 600;
  cursor: pointer;
}

/* ---------- hero ---------- */

.hero { position: relative; background: var(--ink-deep); color: var(--paper); overflow: hidden; }

/* The hero art is usually a photograph, so the default is cover. A theme can
   hand it a LOGO instead, and a logo run through cover gets cropped to a
   meaningless slab, so the fit, the side it stages to and its inset are all
   theme decisions. The mask is what fades its near edge into the ground so
   the headline never has to fight it. */
.hero__media {
  position: absolute;
  inset: 0;
  padding: var(--hero-media-inset);
  -webkit-mask-image: var(--hero-media-mask);
  mask-image: var(--hero-media-mask);
}

.hero__media > img {
  width: 100%;
  height: 100%;
  object-fit: var(--hero-media-fit);
  object-position: var(--hero-media-pos);
}

.hero__scrim { position: absolute; inset: 0; background: var(--hero-scrim); }

.hero__inner {
  position: relative;
  width: min(100% - 2.5rem, var(--measure-wide));
  margin-inline: auto;
  padding-block: var(--hero-space-top) var(--band-space);
  max-width: none;
}

.hero__copy { max-width: 46rem; }

.hero h1 { margin-top: 0.4rem; }

.hero .lede { color: var(--paper-soft); font-size: clamp(1.1rem, 1.8vw, 1.35rem); max-width: 38rem; }

.hero__note {
  margin-top: 1.6rem;
  font-size: 0.875rem;
  color: var(--paper-faint);
  display: flex;
  align-items: center;
  gap: 0.55rem;
}

.hero__note::before {
  content: "";
  width: 1.75rem;
  height: 1px;
  background: var(--accent-on-dark);
  flex: none;
}

/* ---------- pull quote band ---------- */

.pull { display: grid; gap: clamp(2rem, 4vw, 3.5rem); grid-template-columns: 1.15fr 1fr; align-items: center; }

.pull__quote {
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-size: clamp(1.5rem, 3vw, 2.25rem);
  line-height: 1.24;
  letter-spacing: var(--display-tracking);
  text-wrap: balance;
}

.pull__mark {
  display: block;
  font-family: var(--font-display);
  font-size: 3.5rem;
  line-height: 0.6;
  color: var(--accent);
  margin-bottom: 0.6rem;
}

.pull__attrib {
  margin-top: 1.4rem;
  font-size: 0.875rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

.pull__body p + p { margin-top: 1.1rem; }

/* The prose standing alone, with no quote beside it. Capped at the page's
   text measure and centred, so it reads as the band's body rather than as
   one orphaned column of a two column block. The cap is the point: full
   bleed prose across 1248px loses the eye on the return sweep, which is the
   same reason .band__head carries one. */
.pull__body--solo {
  max-width: var(--measure-text);
  margin-inline: auto;
  text-align: var(--head-align, left);
}

/* ---------- give tiers ---------- */

.tiers {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(13.5rem, 1fr));
  gap: 1px;
  background: var(--rule);
  border: 1px solid var(--rule);
  border-radius: var(--radius-card);
  overflow: hidden;
}

.tier {
  background: var(--card-bg);
  padding: clamp(1.6rem, 2.6vw, 2.15rem);
  text-align: var(--card-align);
}

.tier__amount {
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-size: clamp(1.9rem, 3.4vw, 2.6rem);
  line-height: 1;
  letter-spacing: var(--display-tracking);
  color: var(--accent-deep);
}

.tier__label { margin-top: 0.85rem; font-size: 1rem; line-height: 1.45; color: var(--ink-soft); }

.tiers-note {
  margin-top: 1.5rem;
  font-size: 0.875rem;
  color: var(--ink-faint);
  max-width: var(--measure-text);
  margin-inline: var(--head-inline);
  text-align: var(--head-align);
}

/* ---------- programs ---------- */

/* Five cards on a six column grid. Three spanning two, then two
   spanning three, so the last row fills instead of orphaning a
   card at the end of a row of four. auto-fit cannot do this and
   the 4 + 1 it produces reads like a mistake. */
.cards { display: grid; grid-template-columns: repeat(6, 1fr); gap: 1.25rem; }

.cards > :nth-child(-n+3) { grid-column: span 2; }
.cards > :nth-child(n+4)  { grid-column: span 3; }

/* The two rules above are tuned for FIVE cards, where 3 + 2 is the whole
   point. At eight they produce 3 + 2 + 2 + 1, which orphans a card and
   reads as a mistake. .cards--even puts every card on the same span, so a
   set that is not five falls into clean rows of three.

   :nth-child(n+1) rather than a bare *, because the rules it has to beat
   carry a class AND a pseudo-class. A plain child selector is one step
   lighter and silently loses, which is the specificity trap this layout
   has already been bitten by once. */
.cards--even > :nth-child(n+1) { grid-column: span 2; }

/* When the count leaves TWO cards in the last row, centre the pair: the first
   orphan is pinned to columns 2-3 of the six and the second auto-flows into
   4-5, leaving one empty column each side. Fires only when nth-child(3n+1) is
   also second from the end, so 12 or 15 cards make it inert. The single
   orphan case is deliberately not handled, a lone card has no approved shape
   yet. Seth chose centred over 4-across and over cutting to 12, 2026-08-08. */
.cards--even > :nth-child(3n+1):nth-last-child(2) { grid-column: 2 / span 2; }

@media (max-width: 64rem) {
  .cards { grid-template-columns: 1fr; }
  .cards > :nth-child(-n+3), .cards > :nth-child(n+4) { grid-column: auto; }
  .cards--even > :nth-child(n+1) { grid-column: auto; }
  /* Same selector as the desktop pin so this later rule wins at equal
     specificity, or the pinned card asks a 1fr grid for column 2 and drags
     the whole stack into phantom columns. */
  .cards--even > :nth-child(3n+1):nth-last-child(2) { grid-column: auto; }
}

.card {
  background: var(--card-bg);
  border: 1px solid var(--rule);
  border-radius: var(--radius-card);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  transition: border-color 0.16s ease, box-shadow 0.16s ease;
}

/* Optional image slot. A card without one still works, the body just
   sits at the top of the box. Delete the figure, change nothing else. */
.card__media { aspect-ratio: 16 / 10; background: var(--tint); }

.card__media img,
.card__media video { width: 100%; height: 100%; object-fit: cover; display: block; }

/* A moving card. The clip is a seamless loop with no audio track, so it needs
   no control and never asks for a click. `muted` has to be in the MARKUP and
   not only set in script or Safari refuses to autoplay it.

   TRANSLUCENT, Seth's call 2026-08-06: he wanted the card ground to let the
   page through without washing out the blue artwork. `--card-bg` was ALREADY
   transparent, so the opaque thing was the clip itself, which carries its own
   black ground. `screen` blends each pixel against what is behind it, so pure
   black drops to nothing and the blue glow survives at full strength. That is
   why this is a blend and not an opacity: opacity would fade the blue by
   exactly as much as it fades the black, which is the one thing he asked
   against. Needs the figure's own background gone too, or the video screens
   against #000 and nothing changes. */
.card__media--motion { background: transparent; }

.card__media--motion video,
.card__media--motion .card__still { mix-blend-mode: screen; }

/* A visitor who has asked their machine to stop animating gets a still instead.
   The still is a real img in the markup, NOT a url() in a custom property: a url
   inside a custom property resolves against the STYLESHEET, not the page, which
   is the trap that already cost a round on the hero mask. */
.card__media--motion .card__still { display: none; }

@media (prefers-reduced-motion: reduce) {
  .card__media--motion video { display: none; }
  .card__media--motion .card__still { display: block; }
}

/* --card-align and --card-items move together. text-align handles the type,
   align-items handles the flex children, and a card centred on one but not
   the other looks like a bug rather than a choice. */
.card__body {
  padding: clamp(1.5rem, 2.4vw, 2rem);
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  flex: 1;
  text-align: var(--card-align);
  align-items: var(--card-items);
}

/* NO TRANSFORM ON A CARD THAT CARRIES A BLENDED CLIP. Seth 2026-08-06: the
   clip went black the moment the pointer landed on it. The video was innocent.
   `transform` makes an element a stacking context, and a stacking context is a
   blend GROUP, so `mix-blend-mode: screen` stopped reaching the page behind the
   card and screened against the card's own transparent backdrop instead, which
   hands back the clip untouched, black ground and all. The lift was a 3px
   hover cue and the translucency is the look, so the lift goes.

   Any of transform, filter, opacity below 1, isolation or will-change on the
   card or any ancestor does the same thing. The hover cue has to stay in
   properties that do NOT open a group: colour, border, shadow. */
.card:hover { border-color: var(--accent); box-shadow: 0 0 26px -8px var(--accent); }

/* THE WHOLE CARD IS THE HIT AREA, added 2026-08-08 on Seth's call. The cards
   were pure <article> with no link in them at all, so the accent border on
   hover above was promising a click that never existed.

   Only the TITLE is the real anchor and an ::after on it stretches to the
   card's edges. That way a screen reader announces one destination per card
   instead of two, and the pointer still gets the whole tile. The stretch needs
   `position: relative` on the card, which is safe. It does NOT need transform,
   filter, opacity or will-change, and per the note directly above this block
   any of those would open a blend group and turn the clip black on hover. */
.card { position: relative; }

.card__go { color: inherit; text-decoration: none; }

.card__go::after { content: ""; position: absolute; inset: 0; }

.card:hover .card__go { color: var(--accent-on-dark); }

/* DO NOT position .card__body. The anchor lives inside it, so the overlay
   above resolves against the nearest POSITIONED ancestor. Give card__body a
   position and the hit area silently shrinks to the text block and the
   picture stops being clickable, which looks like nothing at all is wrong. */

.card__idx {
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.16em;
  color: var(--accent);
}

.card p { color: var(--ink-soft); font-size: 0.9875rem; }

/* ---------- the numbered flow, three steps ---------- */

.flow { display: grid; grid-template-columns: 1fr auto 1fr auto 1fr; align-items: start; gap: 1.5rem; }

.step {
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  text-align: var(--card-align);
  align-items: var(--card-items);
}

.step__n {
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-size: 1rem;
  letter-spacing: 0.06em;
  color: var(--accent-on-dark);
}

.step p { color: var(--paper-soft); font-size: 0.9875rem; }

.flow__chev {
  align-self: center;
  color: var(--rule-on-dark);
  font-size: 1.35rem;
  line-height: 1;
  padding-top: 0.4rem;
  user-select: none;
}

/* ---------- proof ---------- */

/* NO BOX. A 1px grid gap over a rule colour draws a table, and a table is
   the one thing a proof band must not look like: it reads as data somebody
   compiled rather than as three things that happened. Open columns with a
   single hairline between them, and no fill, so the backdrop mark runs
   straight through. */
.facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: 0;
}

.fact {
  display: flex;
  flex-direction: column;
  padding: clamp(0.5rem, 1.5vw, 1rem) clamp(1.5rem, 3vw, 2.75rem);
  text-align: var(--card-align);
  border-inline-start: 1px solid var(--rule);
}

/* The first column in each row has nothing to its left to divide it from. */
.fact:first-child { border-inline-start: 0; }

@media (max-width: 62rem) {
  .fact { border-inline-start: 0; border-block-start: 1px solid var(--rule); padding-block: 1.75rem; }
  .fact:first-child { border-block-start: 0; padding-block-start: 0; }
}

/* --fg, not --ink. --ink is a BAND GROUND on this layout, so on a dark theme
   a number painted with it lands near black on near black and disappears.
   This is the same double booking --ground and --fg were split out to fix,
   and the proof band was the last place still reading the wrong one. */
.fact__n {
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-size: clamp(2.6rem, 5.4vw, 4rem);
  line-height: 0.95;
  letter-spacing: -0.03em;
  color: var(--fg);
}

.fact__l {
  margin-top: 0.9rem;
  font-size: 1rem;
  line-height: 1.5;
  color: var(--ink-soft);
  /* Pushes every source line to the bottom of its column, so three labels of
     different lengths still leave the sources on one line across the row. */
  flex: 1;
}

/* The source is the point of the band, not a footnote, but it must never
   compete with the number. Small, spaced, and lifted off the label by its
   own rule so it reads as a citation. */
.fact__src {
  margin-top: 1.1rem;
  padding-top: 0.85rem;
  border-top: 1px solid var(--rule);
  font-size: 0.75rem;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  color: var(--ink-faint);
}

/* The sponsor row is a second block inside the proof band, so it gets HALF
   the page rhythm. A full band space here would read as a section break and
   the row would look orphaned from the numbers it belongs to. */
.partners { margin-top: calc(var(--band-space) * 0.6); }

.partners__label {
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint);
  margin-bottom: 1.25rem;
  text-align: var(--head-align);
}

.partners__list {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: var(--head-justify);
  gap: clamp(1.75rem, 4vw, 3.25rem);
}

/* No chip, no box. A partner row is marks sitting on the page. The height
   is the only thing held constant, because logos arrive at every aspect
   ratio and matching their WIDTHS makes the wide ones tiny. */
.partners__list li { display: flex; align-items: center; }

.partners__list img {
  height: var(--partner-h);
  width: auto;
  max-width: 15rem;
  object-fit: contain;
  /* An SVG with no intrinsic pixel size collapses to a token square when
     only its height is set. Naming a minimum width lets a wide mark stay
     wide and a round one stay round. */
  min-width: var(--partner-h);
  filter: var(--partner-filter);
  opacity: var(--partner-opacity);
  transition: opacity 0.16s ease;
}

.partners__list li:hover img { opacity: 1; }

/* ---------- founder ---------- */

/* THE PHOTO IS SIZED BY THE TEXT, NOT BY A RATIO. A fixed 4/5 portrait next to a
   centre aligned text column overhung it by 67px at the top AND 67px at the bottom
   at 1440, so neither edge lined up with anything and the block read lopsided. The
   ratio is dropped on desktop and the media stretches to the grid row instead, so
   its top sits on the eyebrow and its bottom sits on the signature no matter how
   long the copy runs. The mobile override below puts a ratio back once the columns
   stack, because there is no text column left to measure against. */
.founder { display: grid; grid-template-columns: 0.85fr 1.15fr; gap: clamp(2rem, 5vw, 4rem); align-items: stretch; }

.founder__media { border-radius: var(--radius-card); overflow: hidden; background: var(--tint); }

.founder__media img { width: 100%; height: 100%; object-fit: cover; }

.founder__body p + p { margin-top: 1.1rem; }

.founder__body p { color: var(--ink-soft); }

.founder__quote {
  margin-top: 1.75rem;
  padding-left: 1.35rem;
  border-left: 3px solid var(--accent);
  font-family: var(--font-display);
  font-weight: var(--display-weight);
  font-size: clamp(1.15rem, 2vw, 1.5rem);
  line-height: 1.3;
  letter-spacing: var(--display-tracking);
  color: var(--ink);
}

.founder__sig { margin-top: 1.5rem; font-size: 0.875rem; letter-spacing: 0.09em; text-transform: uppercase; color: var(--ink-faint); }

/* ---------- closing call ---------- */

.close { text-align: center; max-width: var(--measure-text); margin-inline: auto; }

.close .lede { margin-inline: auto; }

.close .btn-row { justify-content: center; }

.close__legal { margin-top: 2.25rem; font-size: 0.8125rem; color: var(--paper-faint); line-height: 1.6; }

/* ---------- scope band ---------- */

/* Added 2026-08-08. Restrained on purpose: this band exists to carry
   vocabulary the homepage was missing, so it must read as an index the eye
   can skim, not as another feature section competing with the service cards
   above it. Two columns, no cards, no numbered labels, an accent hairline
   under each heading instead. */
.scope__grid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(2rem, 5vw, 4.5rem);
  margin-top: clamp(2rem, 4vw, 3rem);
}

/* NO `text-align: left` here. The first draft set it and Seth caught the band
   reading as a foreign block: `body.centred` centres every other section on
   this page, so a left aligned one announces itself as bolted on. Measured
   2026-08-08, every sibling band computes headAlign center. Match the page. */

.scope__head {
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--ink-faint, var(--paper-faint));
  padding-bottom: 0.75rem;
  border-bottom: 1px solid var(--accent);
  margin-bottom: 1.25rem;
}

.scope__head--stacked { margin-top: 2.5rem; }

.scope__list { list-style: none; padding: 0; margin: 0; columns: 2; column-gap: 2rem; text-align: center; }

.scope__list li { break-inside: avoid; margin-bottom: 0.6rem; }

.scope__list a {
  text-decoration: none;
  font-size: 0.9375rem;
  line-height: 1.5;
  color: inherit;
  border-bottom: 1px solid transparent;
  transition: color 160ms ease, border-color 160ms ease;
}

.scope__list a:hover, .scope__list a:focus-visible {
  color: var(--accent-deep, var(--accent));
  border-bottom-color: currentColor;
}

.scope__prose { font-size: 0.9375rem; line-height: 1.75; max-width: 34rem; margin-inline: auto; }

@media (max-width: 860px) {
  .scope__grid { grid-template-columns: 1fr; }
  .scope__list { columns: 1; }
}

/* ---------- footer ---------- */

.site-footer { background: var(--ink-deep); color: var(--paper-soft); padding-block: var(--band-space) 2rem; }

.footer-grid { display: grid; grid-template-columns: 1.6fr 1fr 1fr; gap: clamp(2rem, 4vw, 3.5rem); }

.footer-grid h3 {
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--paper-faint);
  margin-bottom: 1rem;
}

.footer-grid p, .footer-grid li { font-size: 0.9375rem; line-height: 1.7; }

.footer-grid li a { text-decoration: none; color: var(--paper-soft); }

.footer-grid li a:hover { color: var(--accent-on-dark); }

.footer-blurb { color: var(--paper-faint); max-width: 26rem; margin-top: 1rem; }

/* The social row under the footer blurb. The links carry `rel="me"`, which is
   how a profile and a domain vouch for each other, so this markup is doing
   identity work as well as navigation and the hrefs must stay real.
   The glyphs are inline SVG on purpose: an icon font or a CDN sprite is a
   network request on every page, and this row ships on all 380. */
.footer-social {
  display: flex;
  gap: 0.5rem;
  margin-top: 1.5rem;
  list-style: none;
  padding: 0;
}

.footer-social a {
  display: grid;
  place-items: center;
  /* 44px is the hit target, the glyph inside is 20px. Anything smaller is a
     miss on a phone, which is where most of this traffic lands. */
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 1px solid rgb(255 255 255 / 0.12);
  transition: border-color 160ms ease, background-color 160ms ease, transform 160ms ease;
}

/* The glyphs carry their OWN brand colour, Seth's call 2026-08-08, so nothing
   here may set `fill`. A `fill: currentColor` on this rule silently flattens
   both marks back to grey and the gradient stops stop mattering. */
.footer-social svg { width: 20px; height: 20px; }

/* `.centred` on body centres the footer column with `text-align`, and a flex
   container does not answer to `text-align`, so without this the row sits hard
   left under a centred wordmark and a centred blurb. Caught in the render
   2026-08-08, not in the markup, which is the only place it is visible. */
.centred .footer-social { justify-content: center; }

/* The chip lifts, the logo does not change colour. Recolouring a brand mark on
   hover is the one thing Meta's usage terms actually forbid. */
.footer-social a:hover,
.footer-social a:focus-visible {
  border-color: rgb(255 255 255 / 0.28);
  background-color: rgb(255 255 255 / 0.06);
  transform: translateY(-1px);
}

@media (prefers-reduced-motion: reduce) {
  .footer-social a { transition: none; }
  .footer-social a:hover, .footer-social a:focus-visible { transform: none; }
}

/* A builder's mark set in the footer line. Sized in `em` so it rides the
   line it sits on rather than a fixed pixel height, which is the only way it
   stays put when the base line resizes. */
.built-by {
  display: inline-block;
  height: 1.05em;
  width: auto;
  vertical-align: -0.2em;
  margin-inline-start: 0.4rem;
  opacity: 0.8;
}

/* The credit line sets the mark against its words. inline-flex rather than
   the base inline-block, because at 13px a baseline aligned logo drifts by
   a pixel or two depending on the descender in the words beside it, and the
   whole point of this line is that it looks set rather than pasted.
   --built-by-h lets a theme size the mark: 1.05em is drawn for a compact
   square mark and a WIDE lockup at that height is a smudge. */
.powered {
  display: inline-flex;
  align-items: center;
  gap: 0.1rem;
  white-space: nowrap;
}

.powered .built-by {
  height: var(--built-by-h, 1.05em);
  vertical-align: baseline;
  margin-inline-start: 0.45rem;
  opacity: 0.9;
}

.site-footer__base {
  margin-top: clamp(2.5rem, 4vw, 3.5rem);
  padding-top: 1.6rem;
  border-top: 1px solid var(--rule-on-dark);
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem 1.5rem;
  justify-content: space-between;
  font-size: 0.8125rem;
  color: var(--paper-faint);
}

/* ---------- narrow ---------- */

@media (max-width: 62rem) {
  .pull { grid-template-columns: 1fr; }
  .founder { grid-template-columns: 1fr; }
  .founder__media { aspect-ratio: 16 / 10; max-height: 22rem; }
  .footer-grid { grid-template-columns: 1fr 1fr; }
  .flow { grid-template-columns: 1fr; gap: 1.75rem; }
  .flow__chev { display: none; }
}

@media (max-width: 48rem) {
  .site-nav {
    display: none;
    position: absolute;
    top: var(--header-h);
    left: 0;
    right: 0;
    flex-direction: column;
    align-items: flex-start;
    gap: 0;
    background: var(--ink-deep);
    border-bottom: 1px solid var(--rule-on-dark);
    padding: 0.5rem 1.25rem 1.25rem;
  }
  .site-nav[data-open="true"] { display: flex; }
  .site-nav a { padding: 0.85rem 0; width: 100%; border-bottom: 1px solid var(--rule-on-dark); }
  .nav-toggle { display: block; }
  .site-header .btn { display: none; }
  .footer-grid { grid-template-columns: 1fr; }
  .btn-row .btn { width: 100%; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  * { transition-duration: 0.01ms !important; }
  .btn:hover, .card:hover { transform: none; }
}

/* ============================================================
   saas-layout additions, added when nonprofit-layout was forked
   on 2026-08-05. Everything above this line is shared with that
   layout and is pure structure. Everything below exists because
   a software company's homepage carries two blocks a charity's
   does not: a long problem list and a process with more than
   three steps.
   ============================================================ */

/* ---------- the problem grid ----------
   Nine items is too many for the .cards grid, which is tuned for
   three across then two across. Nine in a 3 by 3 reads as a list
   the visitor can scan and stop at their own symptom, which is the
   whole job of a problem band. Two columns below 64rem, one below
   52rem, and the count stays nine at every width. */
.probs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1.25rem; }

/* SEPARATE TILES, NOT A TABLE. The 1px gap over a rule colour that
   this grid used to carry drew one solid filled block, and a filled
   block sits on top of the mark backdrop instead of letting it run
   behind. Each cell is now its own hairline card on the same
   --card-bg the service cards below use, so both bands read as the
   same family and the mark shows through the gaps. */
.prob { background: var(--card-bg); border: 1px solid var(--prob-rule, var(--rule)); border-radius: var(--radius-card); padding: clamp(1.25rem, 2.4vw, 1.9rem); display: flex; flex-direction: column; gap: 0.5rem; transition: border-color 0.18s ease, background-color 0.18s ease; }

/* The tile lights on approach. These nine are the only block on the page a
   visitor is meant to SCAN rather than read, stopping at whichever line is
   their own week, and a border that answers the pointer is what tells them
   the scanning is the point. Colour only, no lift and no shadow: nine tiles
   that move on hover is a page that will not sit still. */
.prob:hover, .prob:focus-within { border-color: var(--prob-rule-lit, var(--accent)); background: var(--prob-bg-lit, transparent); }

.prob p.prob__n { font-family: var(--font-body); font-size: 0.75rem; font-weight: 600; letter-spacing: 0.16em; color: var(--accent); }

.prob h3 { font-family: var(--font-body); font-size: 1.0625rem; font-weight: 600; line-height: 1.35; color: var(--fg); }

.prob p { color: var(--ink-soft); font-size: 0.9375rem; }

/* TITLES ONLY. With the paragraph gone the tile is a single sentence, so the
   sentence has to carry the box on its own: bigger type, centred in its own
   space, and every tile the same height so nine of them read as one wall
   rather than nine ragged boxes. The hover stays, because scanning is still
   the job of this band. */
/* Same gap as the service cards below, so the two grids read as one page. */
.probs--bare { gap: 1.25rem; }

.probs--bare .prob {
  min-height: 8.5rem;
  padding: clamp(1.5rem, 2.6vw, 2.1rem) clamp(1.25rem, 2.2vw, 1.75rem);
  justify-content: center;
  gap: 0;
}

.probs--bare .prob h3 {
  font-size: clamp(1.0625rem, 1.35vw, 1.25rem);
  line-height: 1.4;
  font-weight: 500;
  text-wrap: balance;
}

@media (max-width: 52rem) {
  .probs--bare .prob { min-height: 0; padding: 1.35rem 1.25rem; }
}

@media (max-width: 64rem) { .probs { grid-template-columns: 1fr 1fr; } }
@media (max-width: 52rem) { .probs { grid-template-columns: 1fr; } }

/* ---------- the problem as a gallery ----------
   THIRD SHAPE, Seth 2026-08-06. Nine tiles read as a wall to be got through.
   Nine hover rows hid the pictures until the pointer found them, which made
   nine photographs into nine things nobody sees. A gallery shows the work at
   rest and spends the hover on the sentence, which is the right way round: the
   picture is the hook and the line is the payoff.

   Everything happens INSIDE the tile. The grid never re-measures, so opening
   one cell cannot move the eight around it, which is the failure the hover row
   version kept hitting. */
.probs--list {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(0.75rem, 1.4vw, 1.1rem);
  list-style: none;
  margin: 0;
  padding: 0;
}

@media (max-width: 64rem) { .probs--list { grid-template-columns: 1fr 1fr; } }
@media (max-width: 40rem) { .probs--list { grid-template-columns: 1fr; } }

.probs--list .prob {
  position: relative;
  display: block;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  background: var(--tint);
  border: 1px solid var(--rule);
  border-radius: var(--radius-card);
  padding: 0;
  min-height: 0;
  transition: border-color 0.24s ease;
}

/* KEYBOARD. Caught by the codex critic 2026-08-06: `:focus-within` was in every
   selector and could never fire, because a tile is an `li` holding a picture and
   two blocks of text and there was nothing in it to focus. The sentences were
   reachable by pointer and by touch and by nobody else. `tabindex="0"` in the
   markup is what makes the rest of these rules true. */
.probs--list .prob:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.probs--list .prob:hover,
.probs--list .prob:focus-within { border-color: var(--accent); }

/* The picture fills the tile and leans in slightly on hover. Scale rather than
   a size change so nothing around it re-lays out. */
.prob__shot {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 0;
  margin: 0;
  transform: scale(1.001);
  transition: transform 0.7s cubic-bezier(0.16, 1, 0.3, 1);
}

.probs--list .prob:hover .prob__shot,
.probs--list .prob:focus-within .prob__shot { transform: scale(1.045); }

/* The caption is anchored to the BOTTOM, so the sentence opening pushes the cue
   up inside the tile instead of pushing the tile taller. The scrim is a gradient
   and not a flat panel: these photographs are dark at the foot already, so a
   panel would read as a bar stuck on the picture. */
.prob__cap {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 1;
  padding: clamp(0.9rem, 1.6vw, 1.15rem);
  background: linear-gradient(to top, #000000d9 0%, #000000a6 45%, transparent 100%);
}

.prob__cue {
  margin: 0;
  font-size: clamp(1rem, 1.15vw, 1.0625rem);
  font-weight: 600;
  line-height: 1.3;
  color: var(--paper);
}

/* Same 0fr to 1fr reveal as before, which is still the only way to animate to a
   height nobody measured. The child keeps overflow hidden or the sentence spills
   out of the closed row. */
.probs--list .prob__full {
  display: grid;
  grid-template-rows: 0fr;
  opacity: 0;
  margin: 0;
  transition: grid-template-rows 0.52s cubic-bezier(0.16, 1, 0.3, 1),
              opacity 0.42s ease 0.08s;
}

.probs--list .prob__full > span {
  overflow: hidden;
  display: block;
  line-height: 1.5;
  font-size: 0.9375rem;
  color: var(--paper-soft);
}

.probs--list .prob:hover .prob__full,
.probs--list .prob:focus-within .prob__full {
  grid-template-rows: 1fr;
  opacity: 1;
  padding-top: 0.45rem;
}

/* No pointer, no hover, so nothing would ever open. Every sentence shows. */
@media (hover: none), (pointer: coarse) {
  .probs--list .prob__full { grid-template-rows: 1fr; opacity: 1; padding-top: 0.45rem; }
}

@media (prefers-reduced-motion: reduce) {
  .probs--list .prob__full,
  .prob__shot { transition: none; }
  .probs--list .prob:hover .prob__shot { transform: none; }
}

/* The founder band with no photograph. A face is the strongest thing
   that band can carry, so the media half stays in the template and
   this modifier exists for the honest case where no photograph has
   been supplied yet. A stock face is never the answer. */
.founder--nophoto { grid-template-columns: 1fr; max-width: var(--measure-tight); margin-inline: auto; }

/* ---------- five step flow ----------
   The shared .flow is a fixed three step grid. This modifier takes
   five, which is the real length of this client's process. Below
   64rem both drop to a stack and the chevrons rotate, handled by
   the shared rule. */
.flow--5 { grid-template-columns: 1fr auto 1fr auto 1fr auto 1fr auto 1fr; gap: 1rem; }

@media (max-width: 64rem) { .flow--5 { grid-template-columns: 1fr; } }

/* ---------- the close, with the form beside it ----------
   The one action on this page is the assessment. The email is the
   quiet second door for somebody who will not fill a form in. */
.close__alt { margin-top: 1.25rem; font-size: 0.9375rem; color: var(--ink-faint); }

.close__alt a { color: var(--accent-on-dark); }

/* The outcome line on a service card. It is the only place ember
   appears inside a card, because the outcome is what the buyer is
   actually shopping for and the card title is only the label on it. */
.card p.card__out { margin-top: 0.9rem; padding-top: 0.75rem; border-top: 1px solid var(--rule); font-size: 0.8125rem; font-weight: 600; letter-spacing: 0.04em; color: var(--accent-on-dark); }

/* ---------- narrow viewport floors ----------
   Measured on a real 390px render, not assumed. Banked from the
   Northern Horizons run: a 14px text floor and a 44px tap target
   floor are the two things that fail silently on a phone because
   they still look fine on a laptop.
   The header sub label is HIDDEN rather than enlarged: at 11px it
   was the smallest string on the page, and the wordmark beside it
   already says the same thing. */
@media (max-width: 52rem) {
  .lockup__sub { display: none; }
  .eyebrow { font-size: 0.8125rem; }
  .site-footer__base { font-size: 0.875rem; }
  .site-footer li a,
  .site-footer .footer-blurb + p a,
  .close__alt a { display: inline-flex; align-items: center; min-height: 44px; }
  .nav-toggle { min-height: 44px; font-size: 0.875rem; }
  .lockup { min-height: 44px; }
  .card p.card__out { font-size: 0.875rem; }
  .site-footer h3 { font-size: 0.875rem; }
}

/* ---------- the tier row carrying WORDS instead of amounts ----------
   .tier__amount is display type sized for "$40". Drop a phrase into it
   at that size and it overflows its own cell: measured at 1440, the
   fifth label was clipped at the right edge and the second collided
   with its own sentence. This modifier makes the top line a label and
   lets the sentence below carry the weight. */
/* SEPARATE BOXES, not a joined strip, Seth's call 2026-08-05, so this band
   matches the nine problem tiles above it. The base .tiers is one bordered
   container whose 1px grid gap IS the divider between cells, which is why
   the background and the border both have to come off here before the cells
   can carry their own. Scoped to the words variant so a money tier row on
   another client keeps the strip it was drawn as. */
.tiers--words {
  gap: 1.25rem;
  background: transparent;
  border: 0;
  border-radius: 0;
  overflow: visible;
}

.tiers--words .tier {
  border: 1px solid var(--prob-rule, var(--rule));
  border-radius: var(--radius-card);
  transition: border-color 0.18s ease, background-color 0.18s ease;
}

.tiers--words .tier:hover, .tiers--words .tier:focus-within {
  border-color: var(--prob-rule-lit, var(--accent));
  background: var(--prob-bg-lit, transparent);
}

.tiers--words .tier__amount { font-size: clamp(1rem, 1.5vw, 1.1875rem); line-height: 1.25; letter-spacing: 0.01em; }
.tiers--words .tier__label { font-size: 1rem; color: var(--ink-soft); }

/* ---------- a WIDE wordmark in the header ----------
   .lockup__mark is sized square for a compact mark. A 1067 by 289
   wordmark in that slot renders as an illegible smudge, measured on
   a 1440 render. This one is sized by HEIGHT with width free, and it
   replaces the text name rather than sitting beside it, because a
   wordmark plus the same word set in type is the same word twice. */
.lockup__wordmark { height: 1.55rem; width: auto; }

/* The same wordmark standing in for a footer name. Sized off the line it
   replaces rather than off the header's, because the header lockup is the
   page's primary mark and the footer's is a signature. Centred by the align
   tokens, so it follows whichever way the theme is set. */
.lockup__wordmark--footer {
  height: 1.35rem;
  margin-inline: var(--head-inline, 0);
  margin-bottom: 0.15rem;
}

.centred .lockup__wordmark--footer { margin-inline: auto; }
@media (max-width: 52rem) { .lockup__wordmark { height: 1.3rem; } }

/* ---------- centred variant of the blocks the tokens do not reach ----------
   The six align tokens carry the band heads and the cards. These four
   blocks set their own alignment in structure because their layout is a
   grid rather than a stack, so a centred theme has to say so here.
   Applied by the .centred class on <body>. */
.centred .hero__copy { text-align: center; margin-inline: auto; }
.centred .hero__copy .eyebrow { justify-content: center; }
.centred .hero__copy .lede { margin-inline: auto; }
.centred .hero__note { justify-content: center; }
.centred .pull { text-align: center; }
.centred .pull__body { margin-inline: auto; max-width: var(--measure-text); }
.centred .founder__body { text-align: center; }
.centred .founder__body .lede,
.centred .founder__body p { margin-inline: auto; max-width: var(--measure-text); }
.centred .prob { align-items: center; text-align: center; }
.centred .tier { text-align: center; }
.centred .btn-row { justify-content: center; }
/* A CENTRED FOOTER NEEDS EQUAL COLUMNS. The base grid runs 1.6fr 1fr 1fr,
   which is correct left ranged: the wide first column gives the blurb room
   and every block still starts on the same left edge. Centre the text and
   that stops being true, because each block now centres inside its OWN
   column, so three groups sit at 348, 815 and 1186 across a 1248 row and
   the first one reads as knocked out of line.
   Equal thirds put the three centres on even ground, and the blurb loses
   its own measure cap for the same reason: a 26rem cap inside a 416px
   column re wraps the last line short and leans the block again. */
.centred .footer-grid { text-align: center; grid-template-columns: repeat(3, 1fr); }
.centred .footer-blurb { max-width: none; margin-inline: auto; }

/* A centred page reads as centred only if the LAST band is too. The base
   .close already centres, so nothing is needed for it. */

/* ---------- the hero stage ----------
   A boxed surface inside the hero that holds moving art and the mark on top
   of it. Two separate pieces on purpose: the page's own backdrop runs
   underneath and around this card and is never interrupted by it, which is
   only true because the card carries an OPAQUE ground. A translucent one
   would let the backdrop through and the two would read as one confused
   layer instead of a picture sitting on a page.

   overflow hidden is load bearing. The canvas inside is sized to this box
   and a shader that spills past a rounded corner announces the rectangle
   the corner was hiding. */
.hero__stage {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  border-radius: var(--radius-card);
  background: var(--stage-ground);
  margin: 0 var(--hero-media-inset) clamp(1.5rem, 3.2vw, 2.5rem);
  min-height: clamp(14rem, 30vw, 21rem);
  display: flex;
  align-items: center;
  justify-content: center;
}

/* FULL BLEED, Seth's call 2026-08-05. It runs edge to edge and butts
   straight up under the sticky header. The radius goes with the margins:
   a rounded corner sitting on the viewport edge has nothing to be rounded
   away from, so it reads as a rendering fault rather than a card. The
   bottom edge keeps its own hairline instead, which is the only thing left
   telling the eye where the stage stops and the page starts. */
.hero__stage--bleed {
  margin: 0 0 clamp(1.75rem, 3.6vw, 2.75rem);
  border-radius: 0;
  border-bottom: 1px solid var(--rule-on-dark);
  min-height: clamp(24rem, 48vw, 38rem);
}

.hero--stage .hero__media {
  position: relative;
  z-index: 1;
  inset: auto;
  margin: 0;
  border: 0;
  min-height: 0;
  width: 100%;
  padding: clamp(2rem, 5.5vw, 4rem) clamp(1.25rem, 4vw, 3rem);
}

.hero--stage .hero__scrim { z-index: 2; pointer-events: none; }

/* The moving layer. Opacity is the only thing that animates, because the
   canvas is torn down the moment it reaches zero and a transform mid
   teardown is a repaint nobody asked for. */
.hero__fx {
  position: absolute;
  inset: 0;
  opacity: 1;
  transition: opacity var(--intro-fade, 1600ms) cubic-bezier(0.4, 0, 0.2, 1);
}

.hero__fx.is-out { opacity: 0; }

.hero__fx canvas { display: block; width: 100%; height: 100%; }

/* ---------- the flow reveals in order ----------
   A process shown all at once is a list. Shown one step at a time it is a
   sequence, which is the only reason the band exists.

   .is-armed is added by the script, never written in the markup. That
   ordering is the whole safety story: the steps are visible by default, and
   the ONLY thing that can hide them is a script that has already proved it
   can bring them back. No script, no observer, no class, no hiding. */
.flow.is-armed > * { opacity: 0; }

.flow.is-in > * {
  animation: step-in 700ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

/* FIFTH SETTING, Seth's call 2026-08-06, and the four before it bracket it
   tightly: 1.4s read as one event, 4.7s left the visitor waiting, 4.0s and
   3.0s were both "a little too slow". This runs 2.4s end to end.

   The whole ladder was scaled by 0.75 rather than re-picked, so the SHAPE
   survives the speed change: the gap between steps is 420ms and the chevron
   before each one lands 160ms ahead of it, a lead in and then the step, five
   times over. Even spacing would be a metronome. This is the difference
   between counting and walking. */
.flow.is-in > *:nth-child(1) { animation-delay: 0ms; }
.flow.is-in > *:nth-child(2) { animation-delay: 260ms; }
.flow.is-in > *:nth-child(3) { animation-delay: 420ms; }
.flow.is-in > *:nth-child(4) { animation-delay: 680ms; }
.flow.is-in > *:nth-child(5) { animation-delay: 840ms; }
.flow.is-in > *:nth-child(6) { animation-delay: 1100ms; }
.flow.is-in > *:nth-child(7) { animation-delay: 1260ms; }
.flow.is-in > *:nth-child(8) { animation-delay: 1520ms; }
.flow.is-in > *:nth-child(9) { animation-delay: 1680ms; }

@keyframes step-in {
  from { opacity: 0; transform: translateX(-14px); }
  to   { opacity: 1; transform: none; }
}

/* ---------- the problem cards populate one at a time ----------
   Seth's ask 2026-08-08. The script arms and reveals each CARD, not the
   grid. The beat between cards lives in the SCRIPT, one global queue with
   one clock, because a per batch CSS delay re-races the order on reload
   when Safari restores the scroll position. Here there is only the rise,
   it starts the moment the script says so. The animation ends at
   transform none so the hover reveal inside the tile is untouched.

   TIMING HISTORY, because it has moved four times and the reason matters.
   220/450, then 450, then 800 on Seth's 2026-08-09 story beat call, now
   110 gap and 380 rise on 2026-08-11: "I like it, but the delay is taking
   too long." THE LESSON: the felt wait is the gap AND the rise stacked,
   not the gap alone. At 800/900 the ninth card finished 7.3s after the
   first, so the last three cards landed after most visitors had already
   scrolled past and the band read as broken. Cutting only the gap would
   have left a 900ms rise on the last card and still felt sluggish. */
.prob.is-armed { opacity: 0; }

.prob.is-in {
  animation: card-in 380ms cubic-bezier(0.16, 1, 0.3, 1) both;
}

@keyframes card-in {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .flow.is-armed > * { opacity: 1; }
  .flow.is-in > * { animation: none; }
  .prob.is-armed { opacity: 1; }
  .prob.is-in { animation: none; }
}

/* ---------- the mark, assembled from pieces ----------
   A box held at the artwork's own ratio with every piece absolutely placed
   at the percentage it sat at in the original file. Percentages rather than
   pixels because the box is fluid, and a piece pinned in pixels drifts off
   the drawing at every other viewport.

   No mask on these. The single image version carried one, and a mask
   stacked with a filter on the same element is what drew a rectangle around
   the ember E over the red. The drop shadows stay, they trace the alpha. */
.mark {
  position: relative;
  width: min(86vw, 1020px);
  aspect-ratio: 1200 / 325;
  margin-inline: auto;
}

/* The letters only cut. Its own ratio, because the box that held the full
   lockup is mostly empty once the swoosh is out of it. */
.mark--letters { aspect-ratio: 845 / 92; width: min(82vw, 900px); }

/* The hero wordmark became the page's h1 on 2026-08-08. A heading brings the
   user agent's own `font-size: 2em` and `margin-block: 0.67em`, and this box
   is measured artwork, so both are cancelled here rather than in `.mark`,
   which other, non-heading marks on the site still use. */
h1.mark { margin-block: 0; font-size: inherit; font-weight: inherit; }

/* NO FILTER ON THESE, EVER, while the canvas underneath is permanent. This
   is the rectangle Seth caught around the ember E. A drop-shadow gives an
   element a filter REGION, and a filter region composited over a live WebGL
   canvas is painted as its own opaque rectangle in WebKit rather than
   blended. It is not in the artwork: the source PNG's alpha was measured
   and its corners and letter gaps are all zero.

   Shadows exist to lift a mark off a FLAT ground. This ground is never flat
   again, so there is nothing to lift it off and nothing lost. */
/* THE ARTWORK'S OWN PIXELS, back on Seth's call 2026-08-05 after one round
   as masked spans filled with a live silver gradient. The gradient was
   cleaner metal and it was the wrong call: these letters already carry
   their own shading, drawn once, and a mask throws that away and repaints
   flat. The tokens for the gradient are still in the theme if it is ever
   wanted, and nothing on the page reads them. */
.mark__p {
  position: absolute;
  left: var(--l);
  top: var(--t);
  width: var(--w);
  height: auto;
}

/* ---------- the greeting ----------
   Sits exactly where the mark sits and hands over to it. Hidden by default
   so a page with no script, no WebGL or a reduced motion request never
   shows a word that was only ever there to leave. */
.stage-word {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  margin: 0;
  font-family: var(--font-display);
  /* CAPS, WIDE, CINEMATIC. This is not the wordmark's face and cannot be:
     the wordmark is artwork, not a font on this machine. What it CAN do is
     borrow the wordmark's proportions, which are Roman capitals set far
     apart, so the greeting and the name that follows it read as the same
     voice even though they are different objects.

     Tracking goes POSITIVE here, against the display tracking every other
     heading uses. Negative tracking is for a headline you read as words.
     A single line of widely spaced capitals is read as an image, and the
     air between the letters is the whole effect.

     padding-left cancels the trailing space letter-spacing adds after the
     last letter, which otherwise pushes a centred line off centre by half
     a space. */
  text-transform: uppercase;
  font-weight: 500;
  font-variation-settings: "SOFT" 0, "WONK" 0, "opsz" 90;
  /* Tracking comes DOWN as the line gets longer. 0.36em was drawn for two
     short words. Three words at that value runs past the stage on anything
     under about 1600px, and a title that touches both edges stops reading
     as a title. */
  /* Tracking pays for the size. Going from 2.4rem to 3.1rem at 0.24em would
     have run the 28 character line to about 1230px inside a 1325px box,
     which is a title touching both walls. 0.2em buys back the room. */
  letter-spacing: 0.2em;
  padding-left: 0.2em;
  font-size: clamp(1.25rem, 4.2vw, 3.1rem);
  line-height: 1.25;
  /* Plain ivory, back on Seth's call. It wore the metal ramp clipped to the
     glyphs for one round and lost more than it gained: at 38px the line
     samples only a slice of the ramp, so it read as tarnish rather than as
     silver, and the page's brightest ink is what this line is for. */
  color: var(--paper);
  /* margin-inline auto is doing real work here, not tidying. An absolutely
     positioned box with left and right both 0 AND a max-width resolves its
     leftover space to the right edge, so the line silently parks 199px left
     of centre. auto splits the leftover instead. */
  /* ONE LINE at desktop. 60rem capped the box at 960px and broke a 28
     character line that wants about 1080, so it wrapped to two and the
     title stopped reading as a single held breath. 78rem is the page's own
     wide measure, so the line still agrees with everything under it, and
     narrow viewports wrap on their own where wrapping is correct. */
  max-width: min(94%, 78rem);
  margin-inline: auto;
  text-align: center;
  text-wrap: balance;
  color: var(--paper);
  opacity: 0;
  pointer-events: none;
  /* text-shadow, never filter. A filter on anything sitting over the live
     canvas paints its region as a rectangle, which is the bug that cost a
     round on the ember E. text-shadow carries no filter region. */
  text-shadow: 0 1px 2px #000000a6, 0 4px 40px #00000099;
}

/* ---------- the intro ----------
   .intro is added by the script ONLY once a WebGL context is in hand, and
   removed when the intro finishes. That ordering is the whole safety story:
   no context means no class, which means the mark and the capture band are
   never hidden waiting for an animation that will not run. */
@keyframes mark-arrive {
  from { opacity: 0; transform: translateX(-13%); }
  to   { opacity: 1; transform: none; }
}

@keyframes band-arrive {
  from { opacity: 0; transform: translateY(14px); }
  to   { opacity: 1; transform: none; }
}

/* The greeting comes up, holds, and leaves. One keyframe rather than two
   animations, because the exit has to be certain: an element that fades in
   on one rule and out on another leaves a word on the page forever the
   moment the second one is mistyped. */
/* A film title, not a UI transition. It comes up from slightly too large
   and settles, holds long enough to be read twice, then lifts away. The
   scale is tiny on purpose, 1.055 to 1, because a title that visibly zooms
   reads as a slideshow. Everything here is transform and opacity, so it
   stays on the compositor and never touches layout. */
@keyframes word-through {
  0%   { opacity: 0; transform: scale(1.055); }
  22%  { opacity: 1; transform: scale(1.006); }
  66%  { opacity: 1; transform: scale(1); }
  100% { opacity: 0; transform: scale(0.99) translateY(-10px); }
}

.intro .stage-word {
  animation: word-through 2600ms cubic-bezier(0.33, 0, 0.12, 1) 300ms both;
}

/* Each letter carries its own index, so one rule staggers all eight. They
   start only after the greeting has cleared. */
.intro .mark__p {
  animation: mark-arrive 900ms cubic-bezier(0.16, 1, 0.3, 1) both;
  animation-delay: calc(2600ms + var(--i) * 140ms);
}

/* THE CAPTURE HAS NO ENTRANCE AT ALL. Seth's call 2026-08-08: the form is
   on the page from first paint, no delay and no arrival animation. It went
   4.6s, then 0.7s, then immediate in one evening, and he plans to redesign
   the band later, so do not reintroduce a timed arrival without his word.
   The letters keep their own clock, the form never had one after this. */

@media (prefers-reduced-motion: reduce) {
  .intro .mark__p, .intro .hero__capture, .intro .stage-word { animation: none; }
  .hero__fx, .stage-word { display: none; }
}

/* ---------- the hero email capture ----------
   A hero that carries a form cannot keep its art on an absolute layer.
   Type over artwork is a contrast measurement that changes with every
   viewport, and a form is the one block on a page that must never be
   guessed at. So .hero--capture drops the media back into flow: the logo
   takes the space it needs, the panel sits under it, nothing overlaps and
   the panel has its own opaque ground to measure against.

   Same shape as the hero scrim learning: moving type across artwork
   re-opens the measurement, so this does not move type across artwork. */
.hero--capture { padding-top: var(--hero-space-top); }

/* A stage that bleeds has no gap above it either. The header is sticky, so
   the stage starts exactly where the header's own rule ends. */
.hero--stage.hero--capture { padding-top: 0; }

/* THE LOGO IS A PICTURE, SO IT GETS A FRAME. Floating on bare ground with
   no edge it reads as a sticker dropped on the page, and the eye has
   nothing to measure it against. A hairline rectangle gives the hero a
   subsection of its own: picture above, capture band below, two blocks
   instead of one drift. The frame stays open, no fill, so the line field
   still runs through it. */
.hero--capture .hero__media {
  position: relative;
  inset: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0 var(--hero-media-inset) clamp(1.5rem, 3.2vw, 2.5rem);
  padding: clamp(2rem, 5.5vw, 4rem) clamp(1.25rem, 4vw, 3rem);
  border: 1px solid var(--hero-frame-rule);
  border-radius: var(--radius-card);
  min-height: clamp(14rem, 30vw, 21rem);
}

/* THE BAND RUNS THE FULL WIDTH, the wrap is inside it. A capture boxed to
   the text measure floats in the hero as a second surface. Edge to edge it
   reads as the floor of the hero, which is what it is. */
.hero__capture {
  position: relative;
  background: var(--capture-ground);
  border-top: 1px solid var(--capture-rule);
}

.capture {
  width: min(100% - 2.5rem, var(--measure-wide));
  margin-inline: auto;
  padding-block: clamp(1.15rem, 2.4vw, 1.7rem);
  display: grid;
  grid-template-columns: minmax(12rem, 17rem) 1fr;
  gap: clamp(1.25rem, 3vw, 2.5rem);
  align-items: center;
  text-align: left;
}

/* The head keeps its rule lead, the same mark the eyebrow carries, so the
   band belongs to the page rather than to the form library it came from. */
.capture__label {
  font-size: 0.8125rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--accent-on-dark);
  display: flex;
  align-items: center;
  gap: 0.65rem;
}

.capture__label::before {
  content: "";
  width: 1.75rem;
  height: 1px;
  background: var(--accent-on-dark);
  flex: none;
}

.capture__note { margin-top: 0.5rem; font-size: 0.9375rem; line-height: 1.5; color: var(--paper-soft); }

.capture__form {
  display: grid;
  gap: 0.75rem 1rem;
  grid-template-columns: repeat(3, minmax(0, 1fr)) auto;
  align-items: end;
}

.capture__form label { display: grid; gap: 0.35rem; }

.capture__form label span { font-size: 0.8125rem; font-weight: 600; color: var(--paper); }

/* 16px on the input itself, never smaller: iOS Safari zooms the whole page
   on focus for anything under 16px, and a hero that jumps on first tap
   reads as broken. min-height carries the 44px target floor. */
.capture__form input {
  font: inherit;
  font-size: 1rem;
  color: var(--paper);
  background: var(--field-ground);
  border: 1px solid var(--field-rule);
  border-radius: var(--radius-btn);
  padding: 0.75rem 0.9rem;
  min-height: 44px;
  width: 100%;
}

.capture__form input:focus-visible {
  outline: 2px solid var(--accent-on-dark);
  outline-offset: 2px;
  border-color: var(--accent-on-dark);
}

.capture__form .btn { min-height: 44px; white-space: nowrap; }

.capture__form .btn[disabled] { opacity: 0.6; cursor: default; }

.capture__status { grid-column: 1 / -1; font-size: 0.875rem; color: var(--paper-soft); }

.capture__status:empty { display: none; }

/* The honeypot is for a robot's eyes only. Off canvas rather than
   display:none, because some bots skip anything that is not rendered. */
.capture__honey {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.capture__done { grid-column: 1 / -1; }
.capture__done .capture__note { margin-top: 0.5rem; }

/* THE BAND STAYS LEFT ON A CENTRED PAGE. Every other block on this build is
   centred by the six align tokens, and this one is not, because a form is
   read left to right down a column of labels. Centring the fields costs
   the eye the return sweep it needs to fill them. */
.centred .capture { text-align: left; }

@media (max-width: 68rem) {
  .capture { grid-template-columns: 1fr; gap: 1.1rem; }
}

@media (max-width: 52rem) {
  .capture__form { grid-template-columns: 1fr; }
  .capture__form .btn { justify-self: stretch; }
}

/* ---------- the line field ----------
   Ported 2026-08-05 from a React component that ran on framer-motion.
   This site is hand coded, so the geometry is static SVG generated once
   and the motion is CSS. 72 bezier curves, two mirrored sets of 36, the
   same curve maths the original used.

   pathLength="1" on every path is what makes it work with no script. It
   normalises each curve to a length of 1 whatever its real geometry, so
   one dasharray value draws the same PROPORTION of a short curve and a
   long one. Without it each path needs its own measured length, and
   measuring a path is a getTotalLength call, which is JavaScript.

   Each path carries its own duration and a NEGATIVE delay, so the field
   is already mid flow on the first frame. All 72 starting together reads
   as one pulsing object instead of a field. */
/* ---------- the page's weather ----------
   The hero's shader again, run slow and held down to a fraction of its
   opacity, filling the fixed backdrop layer. The page then scrolls over a
   ground that is moving rather than over flat black.

   Its opacity is the ONLY control, and it is a contrast decision, so it
   lives in the theme beside the measurement that set it. Anything drawn
   behind every word on a page is governed by the smallest ink on that page,
   never by how it looks on its own. */
.markdrop .pagefx {
  position: absolute;
  inset: 0;
  opacity: var(--pagefx-opacity, 0.1);
}

.markdrop .pagefx canvas { display: block; width: 100%; height: 100%; }

.markdrop .paths { width: 100%; height: 100%; color: var(--field-line, var(--fg)); }

/* THE FIELD IS STILL BY DEFAULT. Every one of the 72 curves is a solid
   drawn line and nothing more. Before this they ALL carried a travelling
   dash, which is 72 things moving at once behind a page of body copy, and
   the eye reads that as static rather than as motion.

   Motion is now the exception: a handful of curves get a single lit segment
   that runs the length of the path and then goes out. Everything about that
   is in the two rules below. */
/* MOST OF THE FIELD IS OFF, Seth's call 2026-08-05. 72 curves at a low
   opacity is not a field, it is a texture, and a texture behind body copy
   is just noise. Four survive: two that simply sit there so the page still
   has a drawn line in it, and the travellers below.

   The geometry stays in the markup rather than being deleted. It cost a
   generation pass to make, the browser skips a display none path before it
   ever rasterises, and turning the field back up is one selector. */
.markdrop .paths path {
  display: none;
  stroke: currentColor;
  stroke-opacity: var(--o);
}

/* ALL 72 ARE OFF, Seth's call 2026-08-05. The cloud above replaced them
   outright: he asked for the weather instead of the lasers. The svg and its
   geometry stay in the markup, and one selector here brings any number of
   them back. */

/* EVERY 24th PATH, which is three of the 72, and the modulus was SOLVED
   rather than picked. Duration and delay are not set here and cannot be:
   each path carries its own pair inline, and an inline style beats every
   selector in this file. So the phase of each travelling line is decided
   entirely by which paths get selected.

   Reading the 72 inline pairs and testing every modulus from 9 to 24, this
   one puts its three at phases 0.00, 0.35 and 0.70 of their cycles, which
   is as close to evenly spread as the existing numbers allow. One in six
   made four lines that all ran within two seconds of each other and read
   as a single event.

   nth-of-type, not nth-child: the first sibling anybody adds to that svg
   would silently shift the whole selection. */
.markdrop .paths path:nth-of-type(24n) {
  stroke-opacity: calc(var(--o) * 2.4);
  stroke-dasharray: 0.05 0.95;
  animation-name: path-travel;
  animation-timing-function: cubic-bezier(0.5, 0, 0.5, 1);
  animation-iteration-count: infinite;
}

/* The segment draws from the head of the curve to its tail and then stops
   being lit at all, so most of each cycle is quiet. pathLength 1 on every
   path is what lets one offset value mean the same proportion of a short
   curve and a long one. */
@keyframes path-travel {
  0%   { stroke-dashoffset: 0;  opacity: 0; }
  8%   { opacity: 1; }
  46%  { opacity: 1; }
  54%  { stroke-dashoffset: -1; opacity: 0; }
  100% { stroke-dashoffset: -1; opacity: 0; }
}

/* ---------- the field arrives ----------
   On load the backdrop comes forward from further back and settles
   on exactly the resting values the theme already sets, so the end frame
   is the page as it was. It runs ONCE and never again, because the layer
   is fixed and the page scrolls over it: re-running it on scroll would
   turn a backdrop into a moving object competing with the copy.

   forwards, not infinite, and the final keyframe repeats the token
   rather than hard coding a number, so a theme that changes
   --markdrop-opacity changes where the animation lands too. */
@keyframes markdrop-arrive {
  from { opacity: 0; transform: scale(1.08); }
  to   { opacity: var(--markdrop-opacity); transform: scale(1); }
}

.markdrop img,
.markdrop .paths {
  animation: markdrop-arrive 1500ms cubic-bezier(0.22, 1, 0.36, 1) 120ms both;
}

/* A visitor who asked the system for less motion gets the end frame and
   nothing else. `both` above means the from frame holds before the delay,
   so this needs the animation off entirely, not just shortened. */
@media (prefers-reduced-motion: reduce) {
  .markdrop img,
  .markdrop .paths { animation: none; opacity: var(--markdrop-opacity); }
  .markdrop .paths path { animation: none; stroke-dasharray: none; }
}
