/* ═══ SELF-HOSTED TYPE ══════════════════════════════════════════════════════
   ⭐⭐⭐ THE SITE FINALLY HAS ITS OWN TYPOGRAPHY. Measured 2026-09-10: Poppins and
   Open Sans were named everywhere and **loaded nowhere** -- not installed, and not
   one @font-face rule on the site. Every visitor since day one saw system-ui.
   ⚠ The free/local rule is why webfonts were avoided, and it is a real rule.
   ⭐ Self-hosting keeps it: these two files sit in this webspace. No CDN, no
   third party, no paid dependency, nothing to rate-limit us, and they work
   offline. Total 115 KB for the whole site, latin subset, variable weights.
   ⛔ Do not replace these with a Google Fonts <link>. That is the thing the rule
   forbids and it would also leak every visitor to a third party.
   ⚠ font-display:swap so text is readable immediately and never invisible while
   a font loads. */
/* ⭐⭐⭐ VOICE AND INSTRUMENT, 2026-09-11. Fraunces and Inter are gone.
   ⛔ MEASURED REASON, not a preference: their x-heights were 20% apart, which is
   why headings and body looked like two different projects at the same size.
   Literata and Public Sans measure 2% apart on x-height and 0% on stem width.

   ⭐ THE RULE, which is the whole system in one line:
        LITERATA IS THE VOICE.      Anything a person says.
        PUBLIC SANS IS THE INSTRUMENT. Anything the system states.
   Headline, deck, prose and quotes are the voice. Rates, dates, labels, nav and
   figures are the instrument. Split by WHO IS SPEAKING, never by size.

   ⚠ Both are open licence, which is layer 11: a partner in another city has to
   be able to get them, free, forever, or the commons half of the model fails.
   ⚠ Self-hosted. No CDN, no third party, nothing to rate-limit, works offline. */
@font-face {
  font-family: "Literata";
  src: url("/assets/fonts/literata.woff2") format("woff2-variations"),
       url("/assets/fonts/literata.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "PublicSans";
  src: url("/assets/fonts/publicsans.woff2") format("woff2-variations"),
       url("/assets/fonts/publicsans.woff2") format("woff2");
  font-weight: 400 700;
  font-style: normal;
  font-display: swap;
}

/* SymbioHaven -- SHELL ONLY.
 *
 * ⚠ The page bodies come from `_HTML-PASTE/`, generated by `_build_html.py`, and
 * they carry their own complete `<style>` block scoped to `.sh`. THIS FILE MUST
 * NOT RESTYLE THEM. It dresses the document around them: masthead, nav, footer,
 * and the venture grid that `/impact-we-make/` was missing entirely.
 *
 * ⭐ The palette is not mine. It is read straight off the paste files' own theme
 * fallbacks, so the shell and the body are one design:
 *     gold  #FFC107   (--theme-h1-color)
 *     green #4DAA57   (--theme-button-color)
 *     ink   #151515   paper #f8f8f8   pale #EAE8B7
 *
 * ⚠ No webfonts. The site must run free/local with no CDN, so Poppins/OpenSans
 * degrade to a system stack rather than silently failing to load.
 */
:root {
  /* ⭐⭐⭐ DERIVED, NOT INHERITED. See build_palette.py, which measures every
     pairing and refuses to ship one that fails.
     Codex 2.7 scheme: TWO CONTRASTING SHADES OF ONE HUE + GRAYSCALE.
     ⛔ The previous set came from the IONOS builder theme and matched none of the
     three permitted schemes: gold and green sat 82 degrees apart, gold measured
     1.53:1 on white, and a fourth color had to be invented to patch it. */
  /* ⭐⭐⭐ UMBER, 2026-09-11. Chosen by Brendon from four derived candidates.
     Generated by build_brand.py in OKLCH, which is perceptually uniform, so
     these are real equal steps rather than HSL's lies near white and black.
     ⭐ TWO TONES AND NO GREY. Every neutral below is the umber hue at low
     chroma. There is no #666 anywhere. A grey would be a third colour and the
     codex caps at three.
     ⚠ Verified: every pairing clears WCAG on BOTH grounds, and all sixteen
     survive protanopia, deuteranopia and tritanopia at full severity, because
     the information is carried by lightness rather than by hue. */
  --stock:       #F6EBDD;   /* light page ground, the tinted paper       */
  --ink:         #251600;   /* body text on stock                        */
  --head:        #3F2903;   /* headings and structural rules on stock    */
  --mute:        #6B5E4E;   /* secondary text on stock                   */
  --hairline:    #D8CAB7;   /* row separators on stock                   */
  --accent:      #7A5103;   /* button ground on stock, carries stock text*/
  --accentHover: #614000;   /* that button, pressed                      */
  --focus:       #96660C;   /* focus ring, clears 3:1 on BOTH grounds    */
  --select:      #E4CFAE;   /* text selection on stock                   */
  --deep:        #352000;   /* dark page ground                          */
  --onDeep:      #F6EBDD;   /* text on deep. Same ink as the stock       */
  --headDeep:    #E0B274;   /* headings on deep                          */
  --muteDeep:    #BCA98E;   /* secondary text on deep                    */
  --hairDeep:    #5A4526;   /* row separators on deep                    */
  --accentDeep:  #DCB37E;   /* button ground on deep, carries deep text  */
  --selectDeep:  #5E4720;   /* text selection on deep                    */

  /* ⚠ `--paper` and `--line` are kept as aliases because the generated page
     bodies name them. One source of truth beats two palettes fighting. */
  --paper: var(--stock);
  --leaf:  var(--accent);
  --line:  var(--hairline);

  /* ⚠ Compatibility aliases. The page bodies and older rules still name these.
     Mapping them keeps one source of truth instead of two palettes fighting. */
  --gold:       var(--accent);
  --green:      var(--accent);
  --green-text: var(--head);
  --pale:       var(--hairline);
  --dark:       var(--deep);
  --dark2:      #2A1900;
  --muted:      var(--mute);

  /* ⚠ THE FALLBACK STACK CARRIES THE ROLES, NOT JUST THE LOOK. If both woff2
     files never arrive, Georgia still speaks and the system font still measures,
     so the voice/instrument split survives the fonts failing. Tested. */
  --hfont: "Literata", Georgia, "Times New Roman", serif;
  --bfont: "PublicSans", "Segoe UI Variable Text", "Segoe UI", system-ui,
           -apple-system, "Helvetica Neue", Arial, sans-serif;
  --voice: var(--hfont);
  --inst:  var(--bfont);

  /* ⭐ THE MODULAR SCALE, from build_scale.py. Base 16, ratio 1.2.
     ⚠ The ratio is DERIVED: Literata's own cap-to-x-height proportion measures
     1.373, and the scale sits deliberately below it so the page steps in
     increments smaller than the letterforms already do. That is what keeps a
     reading page calm instead of dramatic.
     ⛔ Do not add a size that is not on this list. The previous system had ten
     sizes with two of them 0.6% apart, which is what happens when each is tuned
     alone and nobody looks at the set. */
  --unit: 6px;   --rhythm: 24px;
  --nano:  9.26px;  --nano-lh:  12px;
  --micro: 11.11px; --micro-lh: 18px;
  --small: 13.33px; --small-lh: 18px;
  --body:  16px;    --body-lh:  24px;
  --lead:  19.2px;  --lead-lh:  30px;
  --h3:    23.04px; --h3-lh:    30px;
  --h2:    27.65px; --h2-lh:    36px;
  --h1:    33.18px; --h1-lh:    42px;
  --space-1: 6px;  --space-2: 12px; --space-3: 18px; --space-4: 24px;
  --space-6: 36px; --space-8: 48px; --space-12: 72px;
}

/* ═══ THE ACCESSIBILITY LAYER ════════════════════════════════════════════════
   ⛔⛔ MEASURED MISSING 2026-09-11, all of it: no focus styles, no focus-visible,
   no skip link, no print stylesheet, no reduced-motion rule. The palette even
   carried a derived `--focus` colour that nothing on the site used.
   ⭐ A focus ring is not decoration. Without it a keyboard user cannot tell
   where they are on the page at all. */
:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: 1px;
}
.sec.dark :focus-visible, .masthead :focus-visible { outline-color: var(--accentDeep); }
::selection { background: var(--select); color: var(--ink); }
.sec.dark ::selection { background: var(--selectDeep); color: var(--onDeep); }

/* ⭐ Skip link. Hidden until focused, then it is the first thing reachable. */
.skip {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--stock); color: var(--ink);
  padding: var(--space-2) var(--space-3); font-family: var(--inst);
  font-weight: 700; text-decoration: none;
}
.skip:focus { left: 0; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important; animation-iteration-count: 1 !important;
    transition-duration: .01ms !important; scroll-behavior: auto !important;
  }
}

/* ⭐⭐ PRINT. For a site whose whole register is a paper of record, having no
   print stylesheet was the joke writing itself. Rates and methods get printed
   and handed to people. */
@media print {
  .masthead, .crumbs, .siblings, .iris-map, .skip, footer nav { display: none !important; }
  body, .sh .sec, .sh .sec.dark { background: #fff !important; color: #000 !important; }
  .sh .sec.dark * { color: #000 !important; }
  a[href^="http"]::after { content: " (" attr(href) ")"; font-size: 10pt; }
  .sh .sec { padding: 0 0 12pt !important; break-inside: avoid; }
  h1, h2, h3 { break-after: avoid; }
  table, figure { break-inside: avoid; }
}

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0; background: var(--paper); color: var(--ink);
  font-family: var(--bfont); overflow-x: hidden;
}

.skip {
  position: absolute; left: -9999px; z-index: 100;
  background: var(--dark); color: #fff; padding: .75rem 1.1rem;
  font-family: var(--hfont); font-size: .9rem; border-radius: 0 0 6px 0;
}
.skip:focus { left: 0; top: 0; }

/* ───────────────────────────────────────────────────────────── masthead ── */
.masthead {
  position: sticky; top: 0; z-index: 50;
  background: var(--dark); color: #fff;
  font-family: var(--hfont);
}
.masthead__row {
  max-width: 1180px; margin: 0 auto; padding: .85rem 1.5rem;
  display: flex; flex-wrap: wrap; align-items: center; gap: .5rem 1.5rem;
}
.masthead__row--two {
  padding-top: 0; padding-bottom: .7rem;
  border-top: 1px solid rgba(255,255,255,.1);
  margin-top: -.15rem;
}
.wordmark {
  display: inline-flex; align-items: center; gap: .6rem; margin-right: auto;
  color: #fff; text-decoration: none;
  font-weight: 700; font-size: 1.24rem; letter-spacing: -.02em;
}
/* ⛔⛔ THE PINWHEEL IS GONE. Brendon, 2026-09-09: "next to symbiohaven at the top
   is some weird logo". ⚠ He was right and it was worse than weird: it was not a
   logo at all, it was a 22px circle drawn in CSS out of a conic gradient and a
   radial gradient. **Nobody designed it.** It was a placeholder invented in a
   stylesheet and then left in the masthead of every one of 71 pages.
   ⭐ The iris exists, it is the one mark he considers good, and its 21 wedges are
   the ventures. ⇒ It goes here, at the top of every page, where the brand is.
   ⚠ Kept as a background-image rather than an <img> so no page markup changes and
   nothing needs rebuilding. */
/* ⭐⭐⭐ THE MARK IS THE SEAL, NOT THE EYE. Rebuilt to the codex 2026-09-10.
   ⛔ The old iris broke three of its own STRICT EXCLUSIONS: a literal eye with a
   pupil ("reads as surveillance, not a haven"), exactly 21 wedges (the venture
   count, which decays), and 21 colors against a 1-to-3 limit.
   ⭐ The seal is a 12-part symbolic frame with an asymmetric growth gap, one
   color, real holes. ⚠ At 26px the 12-part form fills in, so the masthead uses
   the REDUCED 4-part variant the codex requires for small sizes. */
.wordmark::before {
  content: ""; width: 24px; height: 24px; flex: none;
  background: url("/assets/seal-reduced.svg") center / contain no-repeat;
  filter: invert(1);   /* the seal ships black; the masthead is dark */
}
.wordmark:hover { color: var(--gold); }

/* ═══ THE IRIS MAP ═══════════════════════════════════════════════════════════
   ⭐⭐⭐ The mark is the navigation. Brendon drew a lens holding one wedge per
   venture, each its own colour, each already naming itself. ⇒ Every wedge is a
   door: point at it and the venture names itself, click it and you are there.
   ⚠ It answers the real problem, which is that 21 ventures cannot be shown as a
   list without reading like a menu nobody finishes. */
/* ⛔ THESE WIDTHS WERE SET FOR AN ALMOND, AND THE ALMOND IS GONE. The old
   artwork was roughly 3:1, so 620px wide was only ~210px tall. The ring is
   SQUARE, so the same number made it 620px tall and it ran off the first
   screen. ⇒ Capped by height as well as width, which is what actually matters
   for something that has to sit inside the hero. */
.iris-map {
  margin: 2.2rem 0 0; max-width: min(360px, 78vw);
  /* ⚠ Centred on small screens, left-aligned in the hero on large ones. */
  margin-inline: auto;
}
.iris-map__svg { width: 100%; height: auto; display: block; overflow: visible; }

/* ⭐ The dimming is the whole trick. Everything fades back and the one you are
   pointing at stays lit, so the eye is pulled to a single venture at a time
   instead of trying to read 21 colours at once. */
.iris-door { cursor: pointer; }
.iris-door path {
  transition: opacity .28s ease, transform .28s cubic-bezier(.2,.7,.3,1);
  transform-origin: 120px 120px;   /* ⚠ the lens centre, in viewBox units */
}
.iris-map__svg:hover .iris-door path,
.iris-map__svg:focus-within .iris-door path { opacity: .28; }
.iris-door:hover path,
.iris-door:focus-visible path {
  opacity: 1; transform: scale(1.07);
  filter: drop-shadow(0 0 10px rgba(255,193,7,.55));
}
/* ⛔ A focus ring drawn on an arc is a rectangle around the whole lens, which
   tells a keyboard user nothing. The lit wedge IS the focus indicator, and it is
   far clearer. ⚠ The outline is removed only where a visible substitute exists. */
.iris-door:focus-visible { outline: none; }
.iris-door:focus-visible path { filter: drop-shadow(0 0 12px var(--gold)); }

.iris-map__hint, .iris-map__cap {
  text-align: center; margin: 0; font-family: var(--hfont);
}
.iris-map__hint {
  font-size: .78rem; letter-spacing: .09em; text-transform: uppercase;
  color: rgba(255,255,255,.5); margin-bottom: .7rem;
}
/* ⭐ The caption is a live region: it announces the venture as focus moves, so a
   screen-reader user gets the same discovery a sighted user gets from hovering.
   ⚠ min-height reserves the line so the layout cannot jump as names change. */
.iris-map__cap {
  margin-top: .9rem; min-height: 1.6em; font-size: 1.06rem; font-weight: 700;
  color: var(--gold); transition: color .2s ease;
}

@media (min-width: 900px) {
  /* ⭐ Hero becomes two columns: the words on the left, the lens on the right.
     ⚠ Scoped to the FIRST dark section only, so no other section is touched. */
  /* ⛔⛔ `:has(.iris-map)` IS LOAD-BEARING AND IT WAS MISSING. Brendon asked for
     the ring to be pulled out of the hero and it was, but this grid stayed
     behind. With nothing in column two, every hero on the site rendered its
     words and its photograph inside 1.05fr of the band and left the other half
     empty dark, so the most important image on the site came out the size of a
     thumbnail. ⚠ He said the home page did not look updated. It had been
     updated four times; it was the LAYOUT that was wrong, not the picture.
     ⭐ Now the two-column hero happens only when there is a second thing to put
     in the second column. */
  .sh .sec.dark:first-of-type .inner:has(.iris-map--wide) {
    display: grid; grid-template-columns: 1.05fr .95fr;
    gap: clamp(1.5rem, 4vw, 3.5rem);
    /* ⚠ start, not center. Centring the lens against a tall hero pushed it down
       the page, so on a laptop the first thing you saw was body copy. ⭐ It has to
       be in the first view, which means aligned to the top of it. */
    align-items: start;
  }
  .sh .sec.dark:first-of-type .inner:has(.iris-map--wide) > :not(.iris-map) { grid-column: 1; }
  .iris-map {
    grid-column: 2; grid-row: 1 / -1;
    margin: 0; max-width: min(440px, 34vw);
    /* ⛔ NOT justify-self:end. That makes the box shrink-to-fit, and then the
       svg's width:100% has an auto-width parent to resolve against, so the lens
       collapsed to 300px inside a 386px column. ⇒ stretch, and let max-width cap
       it. ⚠ The measured symptom was a mark that looked small for no visible
       reason; the cause was two rules each reasonable on its own. */
    justify-self: stretch; width: 100%;
  }
}

/* ⚠ Motion is an enhancement, never the message. With reduced motion the wedge
   still lights up, it just does not move. */
@media (prefers-reduced-motion: reduce) {
  .iris-door path { transition: opacity .01ms; }
  .iris-door:hover path, .iris-door:focus-visible path { transform: none; }
}

.nav ul {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: .1rem .35rem;
}
.nav a { display: block; text-decoration: none; border-radius: 5px; }

/* ⭐ TIER 1 -- the verb bar. V6 was "I can't find which part is mine", so the
   doors are the loudest thing in the chrome. */
.nav--verbs a {
  color: #fff; font-weight: 600; font-size: .97rem; padding: .45rem .75rem;
}
.nav--verbs a::after {
  content: ""; display: block; height: 2px; margin-top: 4px; border-radius: 2px;
  background: var(--gold); transform: scaleX(0); transform-origin: left;
  transition: transform .16s ease;
}
.nav--verbs a:hover::after,
.nav--verbs a:focus-visible::after,
.nav--verbs a[aria-current="page"]::after { transform: scaleX(1); }

/* TIER 2 -- his own vocabulary, kept intact and quieter. */
.nav--about a {
  color: rgba(255,255,255,.62); font-size: .85rem; padding: .3rem .6rem;
}
.nav--about a:hover, .nav--about a:focus-visible { color: var(--gold); }
.nav--about a[aria-current="page"] { color: var(--gold); }
.nav a:focus-visible { outline: 2px solid var(--gold); outline-offset: 2px; }

/* ─────────────────────────────── the venture grid /impact-we-make/ was missing ── */
/* ⚠⚠ This page linked to ZERO of the 21 ventures. 71 pages and no way to reach
   21 of them. Built from what exists on disk, never from a hardcoded list. */
.vgrid { padding: 3.2rem 1.5rem; background: var(--paper); }
.vgrid.alt { background: #eef1ec; }
.vgrid .inner { max-width: 1180px; margin: 0 auto; }
.vgrid h2 {
  font-family: var(--hfont); font-weight: 700;
  font-size: clamp(1.5rem, 3vw, 2.15rem); line-height: 1.14;
  margin: 0 0 .45rem; color: var(--ink); letter-spacing: -.02em;
}
.vgrid__lede {
  margin: 0 0 1.8rem; color: var(--muted); max-width: 58ch;
  font-size: 1.02rem; line-height: 1.55;
}
.vgrid .cards {
  display: grid; gap: 1.1rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr));
}
.vgrid .card {
  position: relative; background: #fff; border: 1px solid var(--line);
  border-radius: 10px; padding: 1.3rem 1.35rem 1.35rem 1.5rem;
  transition: transform .16s ease, box-shadow .16s ease, border-color .16s ease;
}
.vgrid .card::before {
  content: ""; position: absolute; left: 0; top: 1.2rem; bottom: 1.2rem; width: 3px;
  border-radius: 3px; background: var(--green); opacity: .5;
  transition: opacity .16s ease, top .16s ease, bottom .16s ease;
}
.vgrid .card:hover {
  transform: translateY(-2px); border-color: rgba(21,21,21,.24);
  box-shadow: 0 12px 30px -20px rgba(0,0,0,.5);
}
.vgrid .card:hover::before { opacity: 1; top: .7rem; bottom: .7rem; }
.vgrid .card h3 {
  font-family: var(--hfont); font-weight: 700; font-size: 1.06rem;
  margin: 0 0 .5rem; line-height: 1.25; letter-spacing: -.01em;
}
.vgrid .card h3 a { color: var(--ink); text-decoration: none; }
.vgrid .card h3 a:hover { color: var(--green-text); text-decoration: underline; }
.vgrid .card p {
  margin: 0; color: var(--muted); font-size: .93rem; line-height: 1.55;
}

/* ───────────────────────────────────────────────────────────────── footer ── */
.foot {
  background: var(--dark2); color: rgba(255,255,255,.82);
  font-family: var(--hfont); font-size: .92rem;
  padding: clamp(2.4rem, 5vw, 3.6rem) 1.5rem clamp(1.8rem, 4vw, 2.6rem);
}
.foot__inner { max-width: 1180px; margin: 0 auto; display: grid; gap: .9rem; }
.foot p { margin: 0; }
.foot a { color: rgba(255,255,255,.82); text-decoration: none; }
.foot a:hover { color: var(--gold); text-decoration: underline; }
.foot__tithe {
  border-top: 1px solid rgba(255,255,255,.12); padding-top: 1.1rem;
  color: var(--gold);
}

@media (max-width: 640px) {
  .masthead { position: static; }
  .nav--verbs a { font-size: .92rem; padding: .4rem .55rem; }
  .nav--about a { font-size: .8rem; padding: .25rem .45rem; }
}

/* ── breadcrumbs + bucket siblings ──────────────────────────────────────── */
/* ⭐ Restored after I dropped them in a converter swap. A spoke reached from a
   search has no orientation otherwise -- V6, one level down. */
/* ⚠ A light crumb bar sat between the dark masthead and the dark hero -- a pale
   stripe sandwiched in the chrome. It belongs to the masthead, so it is dark. */
.crumbs {
  background: var(--dark); border-bottom: 1px solid rgba(255,255,255,.09);
  font-family: var(--hfont); font-size: .82rem;
}
.crumbs__inner {
  max-width: 1180px; margin: 0 auto; padding: .7rem 1.5rem;
  display: flex; flex-wrap: wrap; gap: .25rem .5rem; align-items: baseline;
  color: rgba(255,255,255,.55);
}
.crumbs a { color: rgba(255,255,255,.72); text-decoration: none; }
.crumbs a:hover { color: var(--gold); text-decoration: underline; }
.crumbs__sep { color: rgba(255,255,255,.28); }
.crumbs__bucket { color: var(--pale); }

.siblings { background: var(--paper); border-top: 1px solid var(--line); }
.siblings__inner { max-width: 1180px; margin: 0 auto; padding: 2.2rem 1.5rem; }
.siblings__label {
  font-family: var(--hfont); font-size: .74rem; font-weight: 700;
  letter-spacing: .13em; text-transform: uppercase; color: var(--green-text);
  margin: 0 0 .9rem;
}
.siblings ul {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: .6rem;
}
.siblings li a {
  display: block; padding: .6rem 1rem; border-radius: 999px;
  border: 1px solid var(--line); background: #fff;
  font-family: var(--hfont); font-size: .92rem; font-weight: 600;
  color: var(--ink); text-decoration: none;
  transition: border-color .15s ease, color .15s ease;
}
.siblings li a:hover { border-color: var(--green); color: var(--green-text); }

/* ── photographs ────────────────────────────────────────────────────────── */
/* ⭐ Slots come from the block plans the generator already computed -- 83 of them.
   A slot with no file renders nothing at all, so an unshot page is simply text
   rather than a grid of grey rectangles. */
.shot {
  margin: 0 0 1.8rem; border-radius: 10px; overflow: hidden;
  background: rgba(21,21,21,.06);
}
.shot img {
  display: block; width: 100%; height: auto;
  aspect-ratio: 16 / 9; object-fit: cover;
}
/* In a dark band the image is the brightest thing on screen; a hair of inset
   shadow stops it looking pasted on. */
.sh .sec.dark .shot { box-shadow: inset 0 0 0 1px rgba(255,255,255,.12); }
.sh .sec.lay-two .shot { margin-bottom: 0; }
@media (max-width: 640px) { .shot img { aspect-ratio: 4 / 3; } }

/* ── spokes of a hub ────────────────────────────────────────────────────── */
/* ⚠ Four spokes were unreachable because their hub's prose never mentioned them.
   Prose is not a navigation guarantee. This block lists only what the copy has
   NOT already linked, so a hub that does the job in its own words is untouched. */
.spokes { background: #eef1ec; border-top: 1px solid var(--line); }
.spokes__inner { max-width: 1180px; margin: 0 auto; padding: 2.2rem 1.5rem; }
.spokes__label {
  font-family: var(--hfont); font-size: .74rem; font-weight: 700;
  letter-spacing: .13em; text-transform: uppercase; color: var(--muted);
  margin: 0 0 .9rem;
}
.spokes ul {
  list-style: none; margin: 0; padding: 0;
  display: grid; gap: .5rem;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
}
.spokes li a {
  display: block; padding: .8rem 1rem; background: #fff;
  border: 1px solid var(--line); border-left: 3px solid var(--green);
  border-radius: 6px; text-decoration: none; color: var(--ink);
  font-family: var(--hfont); font-size: .95rem; font-weight: 600;
  transition: border-color .15s ease, color .15s ease;
}
.spokes li a:hover { border-color: var(--green); color: var(--green-text); }


/* ⭐ /hire-us/ services directory. Plain-language service names grouped by the
   venture that does them -- the route a buyer actually needs, which the concept
   nav ("The Grove", "Our Flow") cannot give them. */
.svc { border-top: 1px solid var(--line); }
.svc .inner { max-width: 1180px; margin: 0 auto; padding: 2rem 1.5rem; }
.svc h2 { font-family: var(--hfont); font-size: 1.15rem; margin: 0 0 .9rem; }
.svc h2 a { color: var(--head, #1B3A24); text-decoration: none; }
.svc h2 a:hover { text-decoration: underline; }
.svc__list { list-style: none; margin: 0; padding: 0;
  display: grid; gap: .6rem; grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr)); }
.svc__list a { display: block; padding: .8rem .9rem; border: 1px solid var(--line);
  border-left: 3px solid var(--green); border-radius: 6px; text-decoration: none;
  color: var(--ink); background: #fff; }
.svc__list a:hover { border-left-color: var(--gold); }
.svc__list strong { display: block; font-family: var(--hfont); font-size: .98rem;
  color: var(--head, #1B3A24); margin-bottom: .15rem; }
.svc__list span { display: block; font-size: .82rem; color: var(--muted); line-height: 1.45; }

/* ⚠⚠ TAP TARGETS ON A PHONE. Measured at 375px: the tier-2 nav links were 25px
   tall and the wordmark 26px, against the 44px both Apple and Google publish as
   the minimum a thumb can hit reliably. On a desktop that is invisible; on the
   device most people will actually read this on, it is a nav you fumble.
   ⭐ Only the touch case is changed -- pointer:coarse -- so the desktop chrome
   keeps its density and nothing else moves. */
@media (pointer: coarse) {
  .nav a { min-height: 44px; display: flex; align-items: center; }
  .nav--verbs a { padding: .45rem .8rem; }
  .nav--about a { padding: .3rem .7rem; font-size: .9rem; }
  .wordmark { min-height: 44px; display: inline-flex; align-items: center; }
  /* the service directory and venture cards are the other thumb surfaces */
  .svc__list a, .vgrid .card h3 a, .spokes li a, .siblings li a { min-height: 44px; }
  .foot a { display: inline-block; padding: .25rem 0; }
}

/* ⭐⭐ /hire-us/ "What do you need?" -- the flat A-Z that LEADS the page.
   The venture-grouped .svc sections below it are our org chart; a buyer looking
   for a dog sitter does not know the words "Woof, Wag & Purr". This list is the
   same links ordered by the reader's vocabulary instead of ours, so it has to
   read as a dense scannable index, not as cards -- cards would push it to three
   screens and defeat the point. */
.needs { border-top: 1px solid var(--line); background: var(--sand, #F7F5EF); }
.needs .inner { max-width: 1180px; margin: 0 auto; padding: 2.2rem 1.5rem; }
.needs h2 { font-family: var(--hfont); font-size: 1.4rem; margin: 0 0 .3rem;
  color: var(--head, #1B3A24); }
.needs__lede { margin: 0 0 1.1rem; color: var(--muted); font-size: .9rem; }
.needs__list { list-style: none; margin: 0; padding: 0;
  display: grid; gap: .1rem .9rem;
  grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); }
/* ⚠⚠ SPECIFICITY, NOT !important. The page carries an INLINE <style> block with
   `.sh a { color: var(--gold) }`. Inline <style> sits after the external sheet, so
   at equal specificity (0,1,1) it wins -- and it rendered these links amber on
   cream at a MEASURED 1.5:1 contrast, against the 4.5:1 WCAG AA needs. Nearly
   invisible, on the one section whose entire job is being readable.
   ⭐ `.needs .needs__list a` is (0,2,1) and wins on merit, in either order. */
.needs .needs__list a { display: block; padding: .55rem .2rem; text-decoration: none;
  color: var(--ink); font-size: .95rem; border-bottom: 1px solid transparent; }
.needs .needs__list a:hover, .needs .needs__list a:focus-visible {
  color: var(--head, #1B3A24); border-bottom-color: var(--gold); }
/* ⚠ 44px minimum tap target on a phone -- same rule the nav already follows. */
@media (pointer: coarse) {
  .needs .needs__list a { padding: .8rem .2rem; min-height: 44px; }
}

/* ⭐ /get-help/ "Written out in full" - the free door's deeper pages.
   ⛔ Deliberately QUIETER than .needs: no count, no dense grid, larger line
   spacing. Someone arriving here may be having a bad week; a catalogue is the
   wrong shape for that, and the moral grammar is not decoration. */
.helpmore { border-top: 1px solid var(--line); }
.helpmore .inner { max-width: 42rem; margin: 0 auto; padding: 2rem 1.5rem; }
.helpmore h2 { font-family: var(--hfont); font-size: 1.25rem; margin: 0 0 .3rem;
  color: var(--head, #1B3A24); }
.helpmore__lede { margin: 0 0 1rem; color: var(--muted); font-size: .92rem; }
.helpmore__list { list-style: none; margin: 0; padding: 0; }
/* ⚠ Specificity beats the page's inline `.sh a { color: var(--gold) }`, which
   rendered the /hire-us/ index at 1.5:1 on cream. Same trap, same fix. */
.helpmore .helpmore__list a { display: block; padding: .7rem 0; font-size: 1rem;
  color: var(--ink); text-decoration: none;
  border-bottom: 1px solid var(--line); }
.helpmore .helpmore__list a:hover,
.helpmore .helpmore__list a:focus-visible {
  color: var(--head, #1B3A24); text-decoration: underline; }
@media (pointer: coarse) { .helpmore .helpmore__list a { min-height: 44px; } }

/* ⭐ VENTURE MARKS. Each venture's print, beside its name.
   ⚠ Sized in em so it tracks the heading rather than fighting it, and given a
   fixed flex basis so a 21-card grid cannot go ragged when one mark is wider. */
.vmark { width: 1.5em; height: 1.5em; flex: 0 0 auto; display: block; }
.card h3 { display: flex; align-items: center; gap: .5rem; }
.vmark--card { opacity: .9; }
/* ⚠ The mark is decorative next to a heading that already names the venture, so
   it must never be the only thing carrying meaning. alt="" is set in the markup. */
@media (prefers-reduced-motion: no-preference) {
  .card:hover .vmark--card { opacity: 1; }
}

/* ⛔⛔ MARKS HIDDEN ON THE VENTURE GRID ONLY. Brendon, 2026-09-09.
   ⚠ The 21 venture marks are placeholders right now: every slot holds the
   SymbioHaven iris until the real marks are drawn. That reads as deliberate
   anywhere a mark appears on its own, and reads as a BUG on this one page, where
   all 21 sit together in a grid and would be 21 identical images in a row.
   ⭐ On a page whose whole job is to tell 21 ventures apart, repeating one image
   21 times actively works against the page. The venture NAMES carry it instead.

   ⭐⭐ THE LAYOUT IS LEFT INTACT AND READY. Nothing is removed from the markup:
   the <img> is still in every card, `.card h3` is still the flex row that will
   hold it, and the gap and sizing rules above are untouched. ⇒ When real marks
   exist, DELETE THIS ONE BLOCK and all 21 return to a layout that never changed.

   ⚠ Scoped to `.vgrid` on purpose. `.vgrid` and `.vmark--card` appear on exactly
   one page, `/impact-we-make/`, so this cannot reach a mark anywhere else. */
.vgrid .vmark--card { display: none; }

/* ⭐ The venture mark leading its breadcrumb trail.
   ⚠⚠ I FIRST WROTE A `.crumbs__inner` RULE HERE AND IT WAS A REAL BUG. The
   existing rule 120 lines up already sets max-width, margin:0 auto and padding;
   mine redeclared display/gap/align only, and being later at equal specificity it
   WON - silently dropping the max-width and the padding, which would have made
   the breadcrumb bar full-bleed and flush to the edge on every venture page.
   ⛔ Never restate a shared layout selector to adjust one child. Target the child.

   ⚠⚠ AND THE CRUMBS BAR IS DARK (`background: var(--dark)`), so the mark's own
   #2E5C55 ink was nearly invisible on it. An <img> cannot be recoloured by CSS
   the way an inlined SVG can, so it is filtered to white here. Measured, not
   assumed: dark green on that ground is roughly 1.3:1. */
/* ⛔⛔ HIDDEN, LIKE THE CARD MARKS ALREADY ARE. Brendon, 2026-09-09: *"none of
   the logos are even good enough to put on"*, and again 2026-09-10 about the
   marks sitting in venture logo slots: **"isnt right"**.
   ⚠ The card marks were hidden then and this one was missed, so every venture
   page still carried a work-in-progress mark flattened to a white silhouette,
   which throws away whatever detail the drawing had.
   ⭐ The rule stays here rather than being deleted, so the slot is ready the day
   he has finished marks: change this one line back. */
.vmark--crumb {
  display: none;
  width: 1.15em; height: 1.15em; flex: 0 0 auto;
  align-self: center;
  filter: brightness(0) invert(1);
  opacity: .78;
}

/* ⚠⚠ THE ONLY PAGE ON THE SITE THAT SCROLLED SIDEWAYS ON A PHONE.
   `/papers/reclaiming-the-commons/` - the references list. Academic citations
   carry long DOIs and URLs with no spaces, and a browser will not break inside an
   unbreakable token: the content measured 483px inside a 338px column, which
   pushed the whole document to 502px against a 375px screen.
   ⭐ Found by hit-testing the rendered page, NOT by reading the CSS - no element
   was wider than the viewport, so every "find the wide box" query returned zero.
   The overflow was text escaping a correctly-sized box.
   ⛔ Scoped to citations. `overflow-wrap:anywhere` globally would break ordinary
   prose at ugly points for no reason. */
.refs { overflow-wrap: anywhere; word-break: break-word; }
.refs a { overflow-wrap: anywhere; }

/* ⭐ THE IRIS ON /impact-we-make/. Brendon, 2026-09-10: "impact we make is weak
   looking." ⚠ It was: the 21 venture marks are hidden on that page, so the hero
   had nothing in it but two lines of text above a long list of cards.
   ⇒ The lens goes at the top, wide and centred. It IS the index of that page:
   every wedge is a venture, and the grid below spells out the same set. */
.iris-map--wide {
  /* ⚠ Same almond-to-ring correction as above: 640px wide used to be ~215px
     tall and is now 640px tall. */
  max-width: min(400px, 72vw); margin: 1.6rem auto .4rem;
}
.iris-map--wide .iris-map__cap {
  color: var(--gold); min-height: 1.6em;
}

/* ═══ TYPOGRAPHY PASS ════════════════════════════════════════════════════════
   Brendon, 2026-09-10: "fix the fucking website its ugly."

   ⭐ MEASURED, NOT GUESSED. On a 1440px screen:
       two-column prose  = **86 characters per line**  (comfortable is 65 to 75)
       card body text    = **34 characters per line**  (cramped, same page)
       section padding   = 3.2rem at every screen size
   ⇒ Long lines make text read as a wall; short ones make cards read as scraps.
   Both were wrong on the same page, which is why it felt flat rather than dense.

   ⚠⚠ THIS FILE IS NORMALLY FORBIDDEN FROM RESTYLING `.sh` BODIES, and the rule is
   at the top of this stylesheet. ⭐ Deliberate exception, and here is why it is
   safe: the alternative is editing the inline <style> block in all 71 pages,
   which is the very thing a rebuild would overwrite. ⇒ One file, `body`-prefixed
   for the specificity to beat the inline block, and nothing here changes colour,
   layout structure, or anything a page author chose. Only measure and rhythm. */

/* ⛔⛔ THESE WERE THE RULE THAT BEAT THE NEW CAP, and they are mine. 39rem and
   41rem were tuned on 2026-09-10 when body text was 18px; body is 16px now, so
   39rem measures 93 characters and 41rem measures 98. They carry `body` plus
   four classes, so they outrank `.sh p` everywhere and quietly held 345
   paragraphs above the readable band while the new cap looked applied.
   ⭐ The lesson is the specificity, not the number: a high-specificity rule
   written for old values does not announce itself when the values change.
   ⇒ Stated in `ch` against the voice face, so it cannot drift out of the band
   again when a size changes. */
body .sh .sec.lay-two .col-body > p,
body .sh .sec.lay-two .col-body > ul,
body .sh .sec:not(.lay-two):not(.lay-cards) .inner > p,
body .sh .sec:not(.lay-two):not(.lay-cards) .inner > ul { max-width: 49ch; }

/* ⚠ The hero is the exception: it is already 49 characters and short lines are
   correct there, so it is left alone. */

@media (min-width: 900px) {
  /* ⭐ Vertical rhythm. A section needs room to read as a section. */
  body .sh .sec { padding: 4.6rem 1.5rem; }

  /* ⭐ Headings scale with the screen instead of sitting at one fixed size.
     ⚠ clamp keeps the small end exactly where it was, so nothing shrinks. */
  /* ⛔ clamp(45,4.4vw,62) is three sizes, none of them on the scale. The
     system announces with a RULE, not with a huge headline, so h1 stays at
     its scale step and the weight of the page comes from structure. */
  body .sh h1 { font-size: 33.18px; line-height: 42px; letter-spacing: -.022em; }
  body .sh h2 { font-size: 27.65px; line-height: 36px; letter-spacing: -.018em; }

  /* ⭐ Cards get room. 34 characters is a caption, not a paragraph. */
  body .sh .cards { gap: 1.4rem; }
  body .sh .card { padding: 1.6rem 1.7rem; }
}

/* ⭐ Body text a touch larger and looser. The site sets 18px and renders 16.96;
   the line height was 1.65 which is tight for a long measure. */
body .sh { line-height: 1.72; }
body .sh .standfirst { max-width: 32rem; }

/* ⛔ THE SECOND NAV ROW READ AS A TOOLBAR. Brendon, 2026-09-10: "why does impact
   we make have that weird toolbar at the top."
   ⚠ It is on every page, not just that one. Two stacked dark strips with a rule
   between them is the shape of browser chrome, so the eye files it as furniture
   instead of navigation.
   ⭐ The two-tier structure itself is right and stays: tier 1 is the verbs someone
   arrives wanting (Hire Us, Get Help, Co-Grow), tier 2 is Brendon's own vocabulary.
   That separation is the V6 fix and removing it would undo the thing it solved.
   ⇒ Only the BAR is removed: no divider rule, tighter, quieter, so the second row
   reads as a subtitle under the wordmark rather than a second toolbar. */
.masthead__row--two {
  border-top: 0;
  padding-top: 0;
  padding-bottom: .55rem;
  margin-top: -.45rem;
}
.nav--about a {
  font-size: .8rem;
  padding: .22rem .5rem;
  color: rgba(255,255,255,.5);
}
.nav--about a:hover,
.nav--about a[aria-current="page"] { color: rgba(255,255,255,.9); }

/* ═══ THE SYSTEM, APPLIED ════════════════════════════════════════════════════
   ⭐⭐⭐ VOICE AND INSTRUMENT. Brendon, 2026-09-11. Everything a PERSON says is
   Literata. Everything the SYSTEM states is Public Sans. Split by who is
   speaking, never by size, because splitting by size means nothing.
   ⚠ These rules sit at the end on purpose: they must win over the older
   declarations above without any of them being deleted, so nothing that
   referenced the old names breaks. */
body { font-family: var(--voice); font-size: var(--body); line-height: var(--body-lh);
       background: var(--stock); color: var(--ink); }
.sh p, .sh li, .sh blockquote { font-family: var(--voice); }
h1, h2, h3, h4, .sh h1, .sh h2, .sh h3 { font-family: var(--voice); }

/* the instrument: anything the system states rather than says */
.masthead, .nav, .crumbs, .siblings, .iris-map__hint, .iris-map__cap,
.vgrid .card .meta, .rate, .rate *, table th, .evt__when, .evt__where,
.evt__basis, .btn, .cta, button, input, select, textarea, footer .meta,
.badge, .label, .dateline { font-family: var(--inst); }

/* ⭐ Figures line up because they are told to, not because they happen to. */
table td, .rate td, .evt__basis, .price, .figure { font-variant-numeric: tabular-nums; }

/* ⛔⛔ THE MOBILE MASTHEAD WAS 280px TALL AND THE NAV BAND ALONE WAS 208px.
   Measured at 375px inside an iframe, because the preview pane kept reporting
   980px no matter what width it was set to and I believed it for hours.
   ⭐ That is 42% of a phone screen spent on navigation before a stranger reads
   one word about the work. The descriptors I added to fix information scent are
   what caused it. ⇒ The descriptors are a WIDE-SCREEN affordance: they explain
   the invented names when there is room, and get out of the way when there is
   not. The names still carry, because a phone user is one tap from the page. */
.nav i, .nav .desc { display: none; }
@media (min-width: 900px) {
  .nav i, .nav .desc {
    display: block; font-style: normal; font-size: var(--nano);
    line-height: var(--nano-lh); color: var(--muteDeep); letter-spacing: .04em;
  }
}
@media (max-width: 640px) {
  .masthead__row--two .nav ul { gap: 0 var(--space-2); }
  .masthead { padding-bottom: var(--space-1); }
}

/* ⛔⛔ THE MASTHEAD AND THE DARK HERO WERE THE IDENTICAL COLOUR (#352000), so
   they merged into one brown field and the toolbar stopped reading as a
   toolbar. Measured, not noticed by eye.
   ⭐ The masthead is the darkest thing on the site now. A masthead is a bar
   across the top of a paper; it has to be a bar. */
.masthead { background: var(--dark2); }
.masthead__row--two { border-top: 1px solid var(--hairDeep); }

/* ⛔⛔ STILL 248px ON A PHONE AFTER HIDING THE DESCRIPTORS, which is 31% of the
   screen. The second row wraps to three lines because eleven links cannot sit
   on 375px however small the type gets.
   ⭐ A newspaper's section bar on a phone is ONE scrollable line, not a block.
   ⚠ `flex-wrap:nowrap` plus horizontal overflow on the LIST only. The page body
   still must not scroll sideways; the strip does, and that is the difference
   between a scrollable element and a broken layout. */
/* ⛔⛔ REVERSED 2026-09-13, and the reason outranks the vertical-space argument
   above. The scrolling strip held 726px of links in a 364px window, so on a
   phone "Papers" sat at x=609 and "About Us" at x=679: both off-screen, with no
   arrow, no fade and no scrollbar to say they were there.
   ⚠ A link a first-time visitor cannot SEE is not a link. The newspaper analogy
   fails on a phone because a reader swipes a section bar they already know
   exists; nobody swipes a strip that looks finished.
   ⭐ Brendon, 2026-09-13, about to demo the site live on a call: "it needs to be
   a clickable link on both computers and phones just by landing on the site."
   The cost is a taller masthead. That is the correct trade: height is a
   scroll away, a hidden link is gone. */
@media (max-width: 780px) {
  .masthead__row--two .nav ul {
    flex-wrap: wrap; overflow: visible;
    gap: .15rem .3rem; padding-bottom: 2px;
  }
  .masthead__row--two .nav li { flex: 0 0 auto; }
  .masthead__row--two .nav a { white-space: nowrap; }
  /* ⚠ The verbs row is the one that must never be reached by scrolling: it is
     the two doors. It wraps, and it stays. */
  .masthead__row .nav--verbs ul { flex-wrap: wrap; }
}

/* ⛔ AND THAT FIX BROKE THE PAGE SIDEWAYS. The strip went to one line but
   overflowed the document instead of scrolling inside itself, because a flex
   item will not shrink below its own content unless it is told it may.
   ⚠ `min-width:0` is the whole fix, and without it `overflow-x:auto` on a flex
   child is decoration. */
@media (max-width: 780px) {
  .masthead__row--two { min-width: 0; max-width: 100%; }
  .masthead__row--two .nav { min-width: 0; max-width: 100%; }
  .masthead__row--two .nav ul { max-width: 100%; }
}

/* ═══ THE CRAFT PASS ═════════════════════════════════════════════════════════
   Brendon, 2026-09-11: it has to look **"professionally made by an expert human
   bean not AI at all."**
   ⚠ The machine tells are not the tooling, they are machine-PERFECT geometry and
   repeated shapes, which is Marshall Peck's note after two hours on the phone:
   *"stop making it look like AI does anything, even if it does."*
   ⭐ What follows is the handwork a typesetter does and a generator never
   bothers with. None of it is decoration; every line has a reason. */

/* ⭐ 1. LIGATURES AND KERNING, ON. Browsers disable discretionary shaping by
   default at small sizes. A human-set page has fi and ffi joined and real
   kerning pairs; a generated one has neither, and the difference is visible in
   any word with an f in it. */
body, .sh {
  font-kerning: normal;
  font-variant-ligatures: common-ligatures contextual;
  text-rendering: optimizeLegibility;
}

/* ⭐ 2. OLD-STYLE FIGURES IN PROSE, LINING TABULAR IN TABLES. This is the
   single most expert-looking thing on a page and almost nobody does it. In
   running text, numbers should sit on the baseline with ascenders and
   descenders like lowercase letters, so "10 percent" stops shouting inside a
   sentence. In a column of prices they must be uniform width and full height.
   ⛔ Do not apply oldstyle to the rate card. A ledger needs lining figures. */
.sh p, .sh li, .sh blockquote { font-variant-numeric: oldstyle-nums proportional-nums; }
table td, table th, .rate, .rate *, .evt__basis, .price,
.lg td:last-child { font-variant-numeric: lining-nums tabular-nums; }

/* ⭐ 3. NO WIDOWS, NO RAGGED HEADINGS. `balance` distributes a heading evenly
   across its lines instead of leaving one word stranded; `pretty` does the same
   for the last line of a paragraph. A typesetter fixes these by hand, one at a
   time, and it is the most common giveaway that nobody did. */
h1, h2, h3, .sh h1, .sh h2, .sh h3, .card h3 { text-wrap: balance; }
.sh p, .sh li { text-wrap: pretty; }

/* ⭐ 4. HANGING PUNCTUATION. An opening quote sitting inside the margin makes
   the text edge look bent. Hanging it outside makes the column edge read
   straight, which is what the eye wants rather than what the maths says. */
.sh { hanging-punctuation: first last; }
.sh blockquote { text-indent: -.42em; }

/* ⭐ 5. OPTICAL CORRECTION ON THE UPPERCASE LABELS. Letterspacing uppercase is
   correct; letterspacing it evenly is not, because the final letter gets a
   trailing space that pushes the label off its left-aligned neighbours. Pull it
   back by the tracking amount so the block aligns optically rather than
   mathematically. ⚠ This is exactly the kind of correction a generator never
   makes, and the reason a hand-set page looks squarer. */
.kick, .crumbs__bucket, .iris-map__hint, table th, .mlabel {
  letter-spacing: .14em; margin-right: -.14em;
}

/* ⭐ 6. BREAK THE RHYTHM ON PURPOSE. Every section having identical padding is
   a template signature. The first section after the masthead carries more air
   than the ones below it, because the top of a page needs room and the middle
   needs momentum. ⚠ Deliberate asymmetry, not a random number: the values are
   still on the 6px grid. */
.sh .sec { padding: var(--space-8) var(--space-4); }
.sh .sec:first-of-type { padding-top: var(--space-12); padding-bottom: var(--space-8); }
.sh .sec + .sec { padding-top: var(--space-6); }
.sh .sec:last-of-type { padding-bottom: var(--space-12); }

/* ⭐ 7. THE RING IS NO LONGER THE FIRST IMPRESSION, so it stops behaving like a
   hero object and behaves like a figure: smaller, captioned, with air around it
   the way a plate sits in a book. */
.iris-map { max-width: min(300px, 62vw); margin: var(--space-6) auto var(--space-4); }
.iris-map__hint { color: var(--muteDeep); }

/* ═══ THE PAGES THAT WERE NEVER STYLED ═══════════════════════════════════════
   ⛔⛔ MEASURED 2026-09-11 by auditing all 72 pages instead of looking at the
   home page: `/privacy-policy/`, `/legal-notice/` and `/events/` carry NONE of
   the system. They are generated by `build_legal.py` and `build_events.py`,
   which emit `.band`/`.wrap`/`.prose` and `.sec` markup that no rule in this
   stylesheet ever matched. They shipped with text against the left edge, no
   section rhythm, and a dark hero with no background.
   ⚠ Events is fixed at source by wrapping its body in `.sh`. The legal pages
   use their own structure, so they get their own rules, built from the SAME
   tokens rather than from new values. */
.band { padding: var(--space-8) var(--space-4); }
.band--lead { padding-top: var(--space-12); background: var(--deep); color: var(--onDeep); }
.band + .band { padding-top: var(--space-6); }
.band:last-of-type { padding-bottom: var(--space-12); }
.wrap { max-width: 54rem; margin: 0 auto; }
.prose { font-family: var(--voice); font-size: var(--body); line-height: var(--body-lh); }
.prose h1 { font-family: var(--voice); font-size: var(--h1); line-height: var(--h1-lh);
            color: var(--head); margin: 0 0 var(--space-3); letter-spacing: -.022em; }
.prose h2 { font-family: var(--voice); font-size: var(--h2); line-height: var(--h2-lh);
            color: var(--head); margin: var(--space-6) 0 var(--space-2); letter-spacing: -.018em; }
.prose h3 { font-family: var(--voice); font-size: var(--h3); line-height: var(--h3-lh);
            color: var(--head); margin: var(--space-4) 0 var(--space-2); }
.prose p, .prose li { max-width: 54ch; margin: 0 0 var(--space-3);
                      font-variant-numeric: oldstyle-nums proportional-nums; }
.prose ul, .prose ol { padding-left: var(--space-4); margin: 0 0 var(--space-3); }
.prose a { color: var(--accent); text-underline-offset: 2px; }
.prose a:hover { color: var(--accentHover); }
.band--lead .prose a { color: var(--accentDeep); }
.prose strong, .prose b { font-weight: 700; }
.prose hr { border: 0; border-top: 2px solid var(--hairline); margin: var(--space-6) 0; }
/* ⛔⛔ CASCADE COLLISION, and it shipped. `.band--lead h1` and `.prose h1` have
   IDENTICAL specificity (one class, one element), so source order decided it and
   `.prose h1` came later. The result was --head on --deep: dark brown heading on
   dark brown ground, measured at roughly 1.2:1, which is invisible.
   ⚠ Two rules each reasonable alone, and nobody wrote the collision. This is why
   the override now sits AFTER what it overrides and names both classes. */
.band--lead .prose h1, .band--lead .prose h2, .band--lead .prose h3 { color: var(--headDeep); }
.band--lead .prose, .band--lead .prose p, .band--lead .prose li { color: var(--onDeep); }

/* ⛔⛔⛔ ONE ALIAS CANNOT SERVE TWO GROUNDS, and this shipped on all 72 pages.
   Found 2026-09-11 by measuring what actually RENDERS rather than by checking
   the palette: the footer tithe line came out at 2.43:1 against a 4.5 floor,
   identically on every page.
   ⚠ `--gold` is the old name and it is used in BOTH contexts: `.sh a` on light
   sections, and dark-section headings, masthead nav, crumbs and footer on dark.
   I aliased it to `--accent`, which is the DARK register built for buttons on a
   LIGHT ground. Correct for the links, invisible everywhere else.
   ⭐ Custom properties inherit, so redefining the alias INSIDE the dark regions
   fixes every descendant at once, without touching a single component rule.
   This is the same two-register principle the palette already encodes; the
   compatibility alias had quietly thrown it away. */
.masthead, .foot, .crumbs, .sh .sec.dark, .band--lead { --gold: var(--accentDeep); }

/* ⛔ MEASURE RAN TO 104 CHARACTERS on five pages. `.sh .inner` is capped at
   54rem, which is 864px, and at 16px body that is about 104 characters. The
   readable band is 45 to 75 and the whole type system was tuned at 65.
   ⚠ The fix constrains the PROSE, not the container: cards, grids and the rate
   tables still want the full 54rem, and capping the container would squash them.
   ⭐ `ch` here resolves against Literata because these elements are set in it,
   which is the trap I fell into earlier when the container was set in the sans
   and 66ch measured 80 characters. */
.sh p, .sh li, .sh blockquote, .prose p, .prose li {
  max-width: 49ch;
}
/* ⚠ A paragraph inside a card or a column is already narrow; capping it again
   leaves a ragged half-empty box. */
.sh .card p, .sh .card li, .vgrid p, .svc__list p, .siblings p,
.evt p, .sh .cols p { max-width: none; }

/* == PEOPLE ================================================================
   Cards rendered from openclaw/people.json by build_people.py.
   ⭐ Every rule here has to hold at ONE card and at forty, which is why nothing
   below names a number. The grid is auto-fit with a min width, so one person
   does not stretch into a poster and eight are not crammed into a fixed three
   across; and the one-card case is handled by `:only-child`, so the LAYOUT
   reacts to the real count without any code ever asking what it is.
   ⚠ The portrait is 4/5, not the 16/9 the venture photos use. A headshot cropped
   to 16/9 is a band across somebody's eyes. */
.sh .cards.people { grid-template-columns: repeat(auto-fit, minmax(17rem, 22rem)); }
.sh .card.person { padding: 0; overflow: hidden; }
/* ⛔⛔ `height:100%` WAS HERE AND IT BEAT THE ASPECT RATIO. In the two-column
   card the image stretched to the full height of the TEXT column beside it, so
   the slot became whatever the bio happened to need. Measured on the live site
   2026-09-13: 272 x 1092, a ratio of 0.25. `object-fit:cover` then threw away
   the sides of a 0.75 portrait and left a narrow vertical strip through the
   middle of a face.
   ⚠ THE TRAP: it looked fine for weeks because the column was short. Adding a
   third image to the card made the text taller, which made the portrait
   NARROWER. A rule whose output depends on the length of unrelated content is
   a bug waiting for content. Brendon found it with the page open on a call.
   ⭐ `align-self:start` is the fix that keeps the original intent: the portrait
   holds its own 4/5 shape and sits at the top of its column instead of being
   stretched to match a neighbour. ⛔ Do not put a height back on this. */
/* ⭐ THE MEDIA RAIL. One column, one width, one left edge, and every picture at
   its own natural shape inside it. See the note in build_people.py.
   ⚠ `align-content:start` so the rail does NOT stretch its children to fill the
   height of the prose beside it. That stretch is exactly what turned the
   portrait into a 272 x 1092 strip. */
/* ⭐⭐ THE MAT. Three pictures at 0.75, 0.84 and 1.22 cannot share a crop without
   one of them being butchered, and they cannot share a row at natural height
   without a ragged bottom edge. A gallery solves this the same way every time:
   identical frames, and the picture sits INSIDE its frame at its own shape with
   a mat around it. `object-fit:contain` is the mat.
   ⛔ Do not "fix" the mats by switching to cover. That is the crop, and the crop
   is what decapitated the certificate. */
.sh .person__media {
  display: grid; gap: .9rem; align-items: start;
  grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
  padding: 1.3rem 1.4rem 0;
}
/* ⛔⛔ THE MAT IS GONE, 2026-09-13. Squaring every frame and using `contain` did
   stop the cropping, but it paid for that with grey bars: side bars on the two
   upright pictures and bars top and bottom on the landscape one. Brendon saw
   exactly that and named all three. ⚠ A mat is a real technique and it was the
   wrong one here, because these sit on a card rather than in a gallery and the
   bars read as a rendering fault, not as a frame.
   ⭐ So: no forced shape at all. Each picture keeps its own ratio and they are
   tied together by equal WIDTH and a shared top edge. Uneven bottoms are what a
   row of real photographs looks like.
   ⛔ Do not add aspect-ratio back. `cover` crops the certificate, `contain` puts
   the bars back, and those are the only two things a fixed ratio can do here. */
.sh .person__frame {
  margin: 0; border-radius: 6px; overflow: hidden;
}
.sh .person__frame img { display: block; width: 100%; height: auto; }
.sh .person__body { padding: 1.2rem 1.4rem 1.4rem; }
/* ⚠ "Recognition", set as a label rather than a heading: it is small structure
   inside a card, and an h3-sized step here would outrank the person's name. */
.sh .person__ch {
  font-family: var(--bfont); text-transform: uppercase; letter-spacing: .11em;
  font-size: .72rem; font-weight: 700; color: var(--green-text);
  margin: 1.5rem 0 .6rem;
}
.sh .sec.dark .person__ch { color: var(--pale); }
/* A credential is a document, so it gets a frame and a wider band than a face.
   ⛔ Its focal point comes from people.json, because the lines that carry a
   certificate sit in its middle third and a blind centre crop loses the name. */
/* ⛔⛔ THE CROP WAS THE WHOLE PROBLEM. This forced `aspect-ratio:16/10` onto a
   certificate that is 0.84, a TALL document, so `cover` threw away its top and
   bottom and left an unreadable band. Measured 2026-09-13: 414 x 259 holding a
   1005 x 1200 image.
   ⭐ THE RULE FOR THIS CARD: nothing here gets cropped. These three pictures do
   different jobs and come in three shapes (headshot 0.75, certificate 0.84,
   stage 1.22). Forcing a shared shape butchers at least one of them every time.
   What makes them a SET is the shared width, frame and rhythm, not a shared
   aspect ratio. ⛔ Do not put an aspect-ratio back on any of these. */
/* .person__cred and .person__shot inherit the frame above; they exist only
   so a stylesheet can still tell a document from a photograph. */
/* ⭐ PHOTOGRAPHS OF THE PERSON AT WORK, which is a third kind of image on this
   card: not the portrait (who they are) and not the credential (what somebody
   else said). It is evidence.
   ⚠ SCALE INVARIANT BY CONSTRUCTION. `auto-fit` with a minimum means one photo
   fills the band and nine reflow into rows, and nothing in the HTML or the CSS
   ever states which. ⛔ Do not replace this with a fixed column count. */
.sh .person__shot figcaption {
  padding: .25rem .5rem .4rem;
  font-family: var(--inst); font-size: var(--micro); line-height: var(--micro-lh);
  color: var(--mute); margin-top: .3rem;
}
.sh .sec.dark .person__shot figcaption { color: var(--muteDeep); }
/* ⭐⭐ THE ONE-CARD CASE. A lone card in an auto-fit grid sits top left with two
   thirds of the band empty, which reads as a company apologising for its size.
   ⛔ The wrong fix is code that asks how many people there are and branches:
   that is a quantity baked into the build. `:only-child` gets the same result
   with nothing to maintain, and the card is two boxes rather than a portrait
   plus loose siblings, so auto-placement has nothing to get wrong. */
.sh .cards.people:has(> .person:only-child) { grid-template-columns: 1fr; }
.sh .card.person:only-child {
  display: block;
}
@media (max-width: 640px) {
  .sh .card.person:only-child { grid-template-columns: 1fr; }
}


/* == HOME HERO: PHOTOGRAPH AND RING, SIDE BY SIDE =========================
   Brendon, 2026-09-13: the full-width hero spent half its width on a car park
   that carried nothing. So the half that means something sits next to the ring,
   and the hero says both halves of the H1 at once: the work, and the network it
   belongs to.
   ⛔ An earlier attempt did this with grid columns on the hero's own children
   and the ring landed beside the HEADLINE, because the H1 spans both columns
   and auto-placement is free to fill row one. The pair is wrapped now, so there
   is no row to get wrong.
   ⚠ Nothing here touches /impact-we-make/, which puts the ring after the H1
   with no photograph between. Two arrangements, two rules, on purpose. */
.hero-pair { margin: 0 0 1.4rem; }
.hero-pair .shot { margin: 0; }
@media (min-width: 820px) {
  .hero-pair {
    display: grid; grid-template-columns: 1fr 1fr;
    column-gap: clamp(1.4rem, 3.5vw, 2.8rem);
    /* ⛔ WAS `center`, which left the photograph floating in its half with dead
       space above and below it while the ring set the height. The photograph
       should FILL its half. */
    align-items: stretch;
  }
  /* ⚠ No fixed ratio here on purpose. A 4/3 box inside a taller column leaves a
     gap; letting it take the row's height and cover means the picture is the
     full half whatever the ring's height turns out to be. */
  .hero-pair .shot { height: 100%; }
  .hero-pair .shot img { height: 100%; aspect-ratio: auto; object-fit: cover; }
  .hero-pair .iris-map--beside { margin: 0; max-width: 100%; align-self: center; }
}
/* ⛔ Below 820 they stack, photograph first, which is the order a reader
   expects: the thing, then the diagram of the thing. */
@media (max-width: 819px) {
  .hero-pair .iris-map--beside { margin: 1.1rem 0 0; }
  /* ⛔ The site-wide 4/3 box is right for a landscape photograph and wrong for
     this one. The hero is an UPRIGHT frame, and forcing it into 4/3 on a phone
     cropped away the open truck at the top and the goods in the foreground,
     which between them are the whole reason the picture is there. Letting it
     keep its own shape costs some scroll and keeps the subject. */
  .hero-pair .shot img { aspect-ratio: auto; height: auto; }
}

/* ═══ THE CONTACT FORM ═══════════════════════════════════════════════════════
   ⭐ Built 2026-09-13. /contact-us/ previously carried a heading reading "The
   form" and a Send button linking to the page it was already on, while three
   other pages told the reader to use a contact form that did not exist.
   ⚠ Inputs inherit the page's own type deliberately. A form set in the
   browser default while the page around it is set in Literata reads as a
   third-party widget bolted on, which is exactly what this is not. */
.sh .cform { max-width: 34rem; margin: 1rem 0 0; }
.sh .cform__row { display: grid; gap: .3rem; margin: 0 0 .9rem; }
.sh .cform label {
  font-family: var(--inst); font-size: var(--small); line-height: var(--small-lh);
  font-weight: 600; color: var(--head);
}
.sh .sec.dark .cform label { color: var(--headDeep); }
.sh .cform__opt { font-weight: 400; color: var(--mute); }
.sh .cform input[type="text"], .sh .cform textarea {
  font: inherit; font-size: var(--body); line-height: var(--body-lh);
  color: var(--ink); background: #fff;
  border: 1px solid var(--hairline); border-radius: 6px;
  padding: .55rem .7rem; width: 100%;
}
.sh .cform textarea { resize: vertical; min-height: 8rem; }
.sh .cform input:focus-visible, .sh .cform textarea:focus-visible {
  outline: 3px solid var(--focus); outline-offset: 2px; border-color: var(--focus);
}
/* ⛔ THE HONEYPOT. `display:none` is the one thing that does NOT work here:
   many bots skip hidden fields, and some browsers skip them on autofill too.
   Moved off-canvas instead, so it is in the document and never on the screen. */
.sh .cform__pot {
  position: absolute; left: -9999px; width: 1px; height: 1px;
  overflow: hidden;
}
.sh .cform button { border: 0; cursor: pointer; font: inherit; }
/* ⭐ The answer after a send. Hidden until contact.php redirects back with the
   state in the query string, so the page carries no stale message. */
.sh .cform-said { display: none; margin: 1rem 0 0; padding: .9rem 1.1rem;
  border-radius: 6px; border: 1px solid var(--hairline);
  background: rgba(21,21,21,.04); }
.sh .cform-said p { margin: 0; }
