/*
 * The marketing site's styles, lifted from eifen-eighth-project-mockup.html.
 *
 * Taken from the approved mockup rather than rewritten, so the built site is the
 * design that was signed off rather than an interpretation of it. The ops app
 * has its own stylesheet and shares nothing with this one -- the two are
 * different products on different hostnames, and a rule changed for one must not
 * quietly move the other.
 */
/* The application's palette, verbatim from static/foundation/app.css.
   ------------------------------------------------------------------------
   The mockup was drawn in green, gold and terracotta. The application moved
   to the blue, flame and amber taken off the logo, and that is the pair of
   products people will see side by side -- so the site follows the app
   rather than the drawing.

   Copied rather than shared: one stylesheet per site, on separate hostnames,
   and a change made for one must not silently move the other. Worth knowing
   that this also carries a fix the mockup never had -- its --text-muted was
   #8B8E85, which fails AA at 3.33:1. */
:root {
  /* Brand ramp, taken from the logo. The mark is two gradient ribbons in
     almost equal measure: blue #0036D9 -> #2693FF, warm #FF4000 -> #FF8000.
     Those two are the source of everything below.

     The warm values are 100% saturation at 50% lightness, which is right for
     a logo and too light for small text on white -- #FF8000 manages only
     3.2:1. So the text-bearing warm tokens are darkened, and the logo's own
     values are kept for fills, where contrast does not apply. */
  --blue-900: #00124A;      /* sidebar, drawer */
  --blue-700: #0029A6;      /* links, positive figures */
  --blue-600: #0036D9;      /* primary -- straight off the logo */
  --blue-500: #2693FF;      /* azure accent -- light end of the ribbon */
  --blue-100: #DCE4FA;

  --flame-600: #B32C00;     /* text */
  --flame-500: #d83a0a;
  --flame-400: #FF4000;     /* fill -- straight off the logo */
  --flame-100: #FBE1D7;

  --amber-600: #9A5200;     /* text */
  --amber-500: #FF8000;     /* fill -- straight off the logo */
  --amber-100: #FAEAD5;

  /* Traceability-chain stage ramp — each stage coloured by its position along
     the logo's own two ribbons.

     Not the ribbon values themselves. Those are the -500 band this file
     already rules out for anything that has to be read, and as a card accent
     they fail even the 3:1 asked of non-text: #FF8000 manages 2.13:1 on its
     own card tint. Tinting them instead does not work either -- 12% of
     #FF4000 and 12% of #FF8000 over white are 1.02:1 apart, which is nothing.
     The warm ribbon is simply too narrow a gradient to survive dilution.

     So these walk each ribbon's HUE path in Lab with lightness pinned where
     the darkened tokens already sit: warm at L*41 (flame-600 -> amber-600),
     blue at L*25 (blue-700 -> the light end brought down to match). Adjacent
     stages are 9.1 apart in deltaE on the warm run and 11+ on the blue, so
     the step is visible; the seam between stage 4 and 5 is 120, so the two
     halves still read as two halves. Worst pairing on its card tint: 5.27:1. */
  --stage-1: #B52E02;   /* Donor        ·                          */
  --stage-2: #AB3C00;   /* Donation     ·  warm ribbon,            */
  --stage-3: #A04600;   /* Fund         ·  flame-600 -> amber-600  */
  --stage-4: #954E00;   /* Expense      ·                          */
  --stage-5: #032AA7;   /* Cohort       ·  blue ribbon,            */
  --stage-6: #0036A1;   /* Beneficiary  ·  blue-700 -> darkened    */
  --stage-7: #003F9B;   /* Outcome      ·  light end               */

  /* Semantic aliases. Meaning lives here; hue lives above. */
  --danger: var(--flame-600);
  --danger-100: var(--flame-100);
  --success: #0F7A52;       /* stays green: it carries meaning, and as a small
                               accent it no longer competes with the mark */
  --success-100: #DDF0E7;

  /* Neutrals, and they are COOL. The comment here used to say the cream stayed
     warm and tied back to the orange ribbon; --cream has been #eef1f7 -- a
     blue-grey -- since this stylesheet was written, so that was never true of
     the marketing site, and the greys are now keyed to it rather than to a
     warmth nothing on the page had.

     THESE DIVERGE FROM app.css ON PURPOSE. The staff sheet keeps the warm
     values (#20241F / #5C6058 / #70746B) and is untouched by this. Two sheets,
     two audiences, and no shared file -- but the same token NAME now resolves
     to a different colour in each, which is exactly the shape of the
     --flame-500 bug: it meant #FF4000 in one and #d83a0a in the other, and the
     Publish button shipped at 3.51:1 because of it. So do not read a value out
     of one sheet and assume the other. Check the file you are in.

     Every text pairing was measured after this change; see --text-muted. */
  --cream: #eef1f7;
  --card: #ffffff;
  --border: #d6dfe4;
  --border-strong: #bcbfd2;
  --text-primary: #1c1c22;
  --text-secondary: #585960;
  /* 4.52:1 on --cream, 5.11:1 on --card. The warm #70746B this replaced was
     4.22:1 on this stylesheet's cool cream -- BELOW AA, and live: .pipeline-note
     sets muted text directly on the cream band of the programme page. It was
     measured at 4.51 against the warm cream the app still uses and carried over
     here unchecked when the ground changed. The second time this token has been
     the one that failed; #8B8E85 before it was 3.33:1. */
  --text-muted: #6b6e74;
  --radius: 10px;
  --radius-lg: 16px;
  --shadow: 0 1px 2px rgba(20, 30, 20, 0.05), 0 4px 16px rgba(20, 30, 20, 0.04);

  /* Type scale. Replaces the thirteen ad-hoc sizes this file used to carry,
     eleven of which sat inside a five-pixel band. Comments give the px range. */
  --text-2xs: clamp(0.65rem, 0.63rem + 0.08vw, 0.72rem);   /* 10.4 – 11.5 */
  --text-xs:  clamp(0.70rem, 0.68rem + 0.10vw, 0.78rem);   /* 11.2 – 12.5 */
  --text-sm:  clamp(0.76rem, 0.73rem + 0.13vw, 0.84rem);   /* 12.2 – 13.4 */
  --text-md:  clamp(0.83rem, 0.79rem + 0.16vw, 0.92rem);   /* 13.3 – 14.7 */
  --text-lg:  clamp(0.92rem, 0.87rem + 0.20vw, 1.05rem);   /* 14.7 – 16.8 */
  --text-xl:  clamp(1.30rem, 0.90rem + 0.55vw, 1.90rem);   /* 20.8 – 30.4 */
  --text-2xl: clamp(1.50rem, 1.00rem + 0.65vw, 2.30rem);   /* 24.0 – 36.8 */

  /* Display sizes, for the marketing site's headlines.
     ---------------------------------------------------------------------------
     The scale above tops out at 36.8px, which is a heading inside a page. The
     hero headline is 85px on a desktop and had no business being 85px on a
     360px phone -- it set "to real jobs — for youth" at roughly four characters
     a line and pushed everything else below the fold.

     Ceilings are the sizes this site already used, so a wide screen is
     unchanged. Floors are where the line stops being a headline and starts
     being a paragraph.

     Built from two endpoints rather than guessed: 22px at a 360px viewport,
     85px at 1920px. That gives a slope of (85-22)/(1920-360) = 4.04vw and an
     intercept of 22 - 0.0404*360 = 7.46px = 0.466rem. Below 360 and above 1920
     the clamp pins it, so neither end can run away.

     THE rem TERM IS NOT DECORATION. A preferred value of pure vw ignores the
     reader's own font-size setting entirely -- text that will not grow when
     somebody has asked for larger text. Keeping a rem in the sum is what lets
     it still respond, and it is why every token above is written the same way. */
  --text-display: clamp(1.375rem, 0.466rem + 4.04vw, 5.3125rem);  /* 22 – 85 */
  --text-title:   clamp(1.375rem, 0.826rem + 2.44vw, 3.75rem);    /* 22 – 60 */
  --text-heading: clamp(1.25rem, 0.788rem + 2.05vw, 3.25rem);     /* 20 – 52 */
  --text-lead:    clamp(0.9375rem, 0.865rem + 0.32vw, 1.25rem);   /* 15 – 20 */
  --text-stat:    clamp(1.25rem, 1.034rem + 0.96vw, 2.1875rem);   /* 20 – 35 */

  /* Spacing scale, same principle. */
  --space-3xs: clamp(0.19rem, 0.18rem + 0.03vw, 0.22rem);  /*  3   –  3.5 */
  --space-2xs: clamp(0.25rem, 0.24rem + 0.05vw, 0.31rem);  /*  4   –  5   */
  --space-xs:  clamp(0.40rem, 0.37rem + 0.10vw, 0.50rem);  /*  6.4 –  8   */
  --space-sm:  clamp(0.62rem, 0.58rem + 0.18vw, 0.81rem);  /* 10   – 13   */
  --space-md:  clamp(0.87rem, 0.80rem + 0.30vw, 1.25rem);  /* 14   – 20   */
  --space-lg:  clamp(1.10rem, 1.00rem + 0.45vw, 1.60rem);  /* 17.6 – 25.6 */
  --space-xl:  clamp(1.40rem, 1.20rem + 0.80vw, 2.40rem);  /* 22.4 – 38.4 */

  /* Layout. The sidebar breathes with the viewport but stays a nav rail;
     the content column stops widening past the point where a table row
     becomes hard to track across. */
  --sidebar-w: clamp(15rem, 3.75vw + 13.2rem, 20rem);         /* 240 – 320 */
  --sidebar-rail: 4.5rem;                                  /* 72 — icon only */
  --topbar-h: 3.5rem;                                      /* 56 — fits the logo */
  --content-max: 90%;
}

/* Skip to content.
   ---------------------------------------------------------------------------
   First thing in the tab order, and off-screen until it has focus. Someone
   navigating by keyboard or screen reader otherwise walks the whole header --
   seven links, on every page, before a word of the content. WCAG 2.4.1.

   Positioned off-screen rather than display:none or visibility:hidden, because
   both of those remove it from the tab order and the link then cannot be reached
   by the only people it is for.

   The markup came over from the staff app's base template and this rule did not,
   because the rest of this stylesheet came from the mockup -- which has no skip
   link. The result was a stray visible link at the top of every marketing page. */
.skip-link {
  position: absolute; left: -9999px; top: 0; z-index: 100;
  background: var(--blue-900); color: #fff;
  padding: 10px 16px; border-radius: 0 0 8px 0;
  text-decoration: none;
}
.skip-link:focus { left: 0; color: #fff; }

/* SCREEN-READER-ONLY TEXT, and this sheet had none.
   ---------------------------------------------------------------------------
   `.visually-hidden` lives in app.css, which the public site never loads, so
   every use of it here rendered as ordinary visible text. home.html carries a
   comment about hitting exactly this once before -- and the blog rail hit it
   again, printing "— showing only these; select to clear" inside a filter chip.
   The comment was not enough; the rule is.

   Defined rather than the uses deleted, because a public site needs this
   repeatedly: a control whose meaning is carried by a symbol has to say that
   meaning to somebody who cannot see the symbol. The same declaration as the
   staff sheet's, so the two cannot drift into meaning different things.

   clip-path rather than display:none or visibility:hidden -- both of those take
   the text out of the accessibility tree as well as off the screen, which is
   the opposite of the point. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; white-space: nowrap; clip-path: inset(50%);
}

  *{box-sizing:border-box;}
  html{scroll-behavior:smooth;}
  body{
    margin:0;
    font-family:'Raleway',sans-serif;
    background:var(--cream);
    color:var(--text-primary);
    -webkit-font-smoothing:antialiased;
  }
  a{color:inherit;text-decoration:none;}
  code{font-family:inherit;background:rgba(0,0,0,0.05);padding:1px 5px;border-radius:5px;font-size:0.95em;}

  /* What the ops-app section left behind, and the only part of it this site
     ever used.
     ---------------------------------------------------------------------------
     This stylesheet was lifted whole from the mockup, which previewed the staff
     tool alongside the marketing pages -- so ~150 lines of sidebar, KPI cards,
     nav items, badges, a traceability chain and a mode switch came with it, for
     markup that does not exist on any page here. 94 of the 97 classes in that
     block appeared nowhere in public/templates; the three that looked live were
     artefacts (.main matched an id, .active only ever compounded with dead
     classes like .navitem.active).

     These stayed because they are element selectors and the marketing site does
     use them: the impact page has a table, and contact and get-involved have
     forms. The staff app styles its own copies in app.css -- the two files share
     no rules, by design. */
  table{width:100%;border-collapse:collapse;font-size:var(--text-sm);}
  thead th{text-align:left;font-size:var(--text-2xs);font-weight:700;text-transform:uppercase;letter-spacing:.03em;color:var(--text-muted);padding:0 12px 10px;border-bottom:1px solid var(--border);}
  tbody td{padding:12px;border-bottom:1px solid var(--border);vertical-align:middle;}
  tbody tr:last-child td{border-bottom:none;}
  td.num{text-align:right;font-variant-numeric:tabular-nums;font-weight:600;}

  select, input[type=text], input[type=email], textarea{
    font-family:inherit;font-size:var(--text-sm);padding:8px 12px;border-radius:8px;
    border:1px solid var(--border-strong);background:#fff;color:var(--text-primary);
  }
  textarea{width:100%;resize:vertical;}

  /* ============================================================ */
  /* ===================  PUBLIC MARKETING SITE  ================= */
  /* ============================================================ */
  .container{max-width:70%;margin:0 auto;padding:0 32px;}
  /* Reading measure for the pages that are prose or a form rather than a grid.
     Four templates already asked for `narrow` and nothing defined it, so they
     ran full width. */
  .container.narrow{max-width:760px;}

  /* The translucent fill is --cream at 94%, written out because a custom
     property cannot be used inside rgba(). It was the MOCKUP's cream --
     rgba(247,244,238,..), a warm sand -- while --cream became the cool
     #eef1f7 when the palette moved to the logo. The nav therefore sat as a
     warm band over a cool page, most visible where it overlapped a section
     edge on scroll. Change both together or neither. */
  .site-nav{position:sticky;top:0;left:0;z-index:900;background:rgba(238,241,247,0.94);backdrop-filter:blur(6px);border-bottom:1px solid var(--border);}

  /* 70%, matching .container: the nav bar itself spans the window, but its
     contents have to line up with the page beneath it or the logo sits off the
     left edge of the copy it belongs to. Same padding as .container for the
     same reason. Change the two together. */
  /* Three tracks, so the links land in the TRUE centre of the bar. The outer
     two are 1fr each and therefore equal, which centres the middle one however
     wide the logo and the button happen to be.

     Auto margins on a flex row would only centre the links in the gap between
     those two, and the logo here is much the wider of the pair -- two lines of
     text against a small button -- so the links would sit visibly right of
     centre. This is the case flex cannot do without a spacer element. */
  .site-nav-inner{
    max-width:70%;margin:0 auto;padding:14px 32px;
    display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:28px;
  }
  .site-logo{display:flex;align-items:center;gap:10px;cursor:pointer;}
  .site-navlinks{justify-content:center;}
  .cta-donate{justify-self:end;}
  /* The real mark, in place of the "E8" square that stood in for it. Its own
     class rather than an <img> under .brand-mark: that rule paints a gradient
     and centres a letter, both of which would show through and around a
     transparent PNG. */
  .brand-logo{display:block;object-fit:contain;flex-shrink:0;}
  /* 32px, just under the Donate button's ~34px: 9px of padding top and bottom
     around 13px text at normal line-height. The button is the tallest thing in
     the bar, so anything above it makes the mark look oversized rather than
     making the bar look taller.

     Square because the source is 256x256, and object-fit:contain on .brand-logo
     means the proportions hold even if it is ever replaced with a wider mark --
     it would letterbox inside the box rather than stretch. */
  .site-logo .brand-logo{width:32px;height:32px;}
  .site-logo .sl-name{font-size:var(--text-md);font-weight:700;line-height:1.2;}
  .site-logo .sl-sub{font-size:var(--text-2xs);color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;}
  .site-navlinks{display:flex;gap:20px;flex-wrap:wrap;}
  .site-navlinks a{font-size:var(--text-sm);font-weight:600;color:var(--text-secondary);cursor:pointer;padding:6px 2px;border-bottom:2px solid transparent;}
  .site-navlinks a:hover{color:var(--blue-700);}
  .site-navlinks a.active{color:var(--blue-700);border-bottom-color:var(--amber-600);}
  .cta-donate{background:var(--flame-500);color:#fff;padding:9px 18px;border-radius:8px;font-size:var(--text-sm);font-weight:700;cursor:pointer;border:none;font-family:inherit;}

  /* The mobile menu, and above 900px there is no such thing.
     ---------------------------------------------------------------------------
     display:contents is what lets the links and the button be wrapped at all.
     The bar is a three-track grid whose middle track centres the links, and a
     wrapper with a box of its own would have made the three items into one.
     With contents the wrapper lays out nothing and the grid is untouched.

     The hamburger and the close link do not exist here either. Both are shown
     only inside the 900px block, so nothing has to be hidden on the desktop
     that was never drawn. */
  .nav-toggle{display:none;}
  .nav-close{display:none;}
  .site-menu{display:contents;}
  .cta-donate:hover{background:var(--flame-600);opacity:.92;}

  /* A band is a full-bleed <section class="section-public"> wrapping a
     .container. Both classes on one element -- which every page did -- caps the
     background at the container's 1120px, so an alternating colour renders as a
     floating box rather than a band across the window. */
  .section-public{padding:64px 0;}
  .section-public.tight{padding:44px 0;}

  /* Alternating grounds: --cream from the body, --card on every second band.
     Counted by the browser rather than written into each template, because
     these sections are conditional -- a page with no spotlights or no published
     posts simply omits one, and hand-assigned classes would then put two
     identical grounds against each other with no seam between them. The hero is
     a band too, so it takes the first turn and the rhythm starts under it.

     nth-of-type counts <section> elements, so a band must BE a section for the
     count to hold. .alt stays as a manual override. */
  main > section.section-public:nth-of-type(even){
    background:var(--card);
    border-top:1px solid var(--border);
    border-bottom:1px solid var(--border);
  }
  .section-public.alt{background:var(--card);border-top:1px solid var(--border);border-bottom:1px solid var(--border);}
  /* The eyebrow as a badge: a tinted pill that hugs its own text.
     ---------------------------------------------------------------------------
     inline-block is what makes it hug. As a block-level <div> a border would
     have run the full width of the band and read as a rule rather than a badge.

     text-align does not centre an inline-block, its parent does -- .eyebrow
     .center still works because the band it sits in is centred. */
  .eyebrow{
    display:inline-block;
    font-size:var(--text-xs);font-weight:700;text-transform:uppercase;
    letter-spacing:.08em;color:var(--flame-600);
    background:var(--flame-100);border:1px solid var(--flame-500);
    border-radius:99px;padding:5px 12px;
    margin-bottom:25px;
  }
  .h-public{font-size:var(--text-heading);font-weight:400;line-height:1.25;margin:0 0 14px;max-width:100%;}
  /* A page title outranks a section heading at both ends -- 22 against 20 at
     the floor, 60 against 52 at the ceiling -- and the element is what says
     which is which. Every page title in these templates is an <h1> and every
     band heading an <h2>, so no new class was needed for a distinction the
     markup was already making.

     They shared a 52px ceiling briefly, and it did not work: at 1920px the two
     came out 51.9 and 52.0, the heading fractionally the larger of the pair
     because its slope is steeper. Nobody would see 0.06px, but a title that is
     merely equal to the headings under it is not a title.

     THE MARKUP CARRIES THIS AND NOTHING CHECKS IT. A page title written as an
     <h2>, or a band heading as an <h1>, silently takes the wrong size. Nine
     <h1> and thirteen <h2> are right today.

     Specificity keeps the hero out of it: `.hero .h-public` is (0,2,0) against
     this rule's (0,1,1), so the hero headline goes on taking --text-display. */
  h1.h-public{font-size:var(--text-title);}
  /* NO h3.h-public. One existed for a few minutes, for two sub-headings inside
     About's "Financial Integrity in Action"; both were dropped, one of them
     because it repeated a Core Value by name two columns away. A rule with
     nothing using it is the same smell .trust-band was removed for -- add it
     back when a page actually needs a third level, sized by its tag like the
     h1 above. */
  .h-public.center{margin-left:auto;margin-right:auto;text-align:center;}
  /* The mockup centred these with inline styles. As classes instead, so the
     rules stay in one file and a page can be restyled without editing markup.
     `.centered` on the band is for a band whose every child is centred -- do not
     put it on one holding cards, or their body copy centres with it. */
  /* Centring an element that hugs its own text takes auto margins, not
     text-align. This was `text-align:center` and that was right while the
     eyebrow was a full-width block -- it centred the words inside it. Now the
     badge is only as wide as its label, so text-align centres the text within a
     badge still sitting at the left of the band.

     display:table, NOT width:fit-content. A table box shrink-to-fits in every
     engine and has done for twenty years; fit-content is newer, and where it is
     read as min-content the badge collapses to its longest WORD and the label
     wraps at every space. That is what these looked like on a phone -- reported
     from one, and not reproducible here, where each of them has 100-300px of
     room to spare. Auto margins centre a table box exactly as they would a
     block, including inside the hero's flex column. */
  .eyebrow.center{
    display:table;
    margin-left:auto;margin-right:auto;
    text-align:center;
  }
  .section-public.centered{text-align:center;}
  .p-public{font-size:var(--text-lg);line-height:1.75;color:var(--text-secondary);max-width:100%;margin:0 0 18px;}
  .p-public.center{margin-left:auto;margin-right:auto;text-align:center;}

  /* 60% of the viewport, as a floor rather than a fixed height: a headline that
     wraps to three lines on a narrow screen, or a stat strip that wraps to two
     rows, grows the band instead of spilling out of it. `height` would clip.

     NO justify-content here, deliberately. It moves every child together --
     `center` re-centred the whole group whenever anything below it grew, and
     `flex-end` dragged the headline down along with the strip. The slack is
     handed out by auto margins instead, which move one element and leave the
     rest where they were. See .hero > .container below. */
  .hero{
    min-height:60vh;
    display:flex;
    flex-direction:column;
    padding:64px 0 50px;
  }

  /* The container fills the band and centres its contents in it.

     Every gap inside the hero is now a fixed margin -- 33px above the CTAs, 50px
     above the stat strip -- so there is no elastic element left for spare height
     to flow into, and justify-content is what decides where the block sits.
     That is safe here precisely BECAUSE the gaps are fixed: it moves the whole
     group together, which was wrong while the strip was meant to pin to the foot
     of the band and is right when nothing is pinned.

     The eyebrow's `margin-top:auto` is gone with it. Left in place as the only
     auto margin, it would have absorbed all the slack on its own and pushed the
     entire group to the bottom of the hero. */
  .hero > .container{flex:1;display:flex;flex-direction:column;justify-content:center;}
  /* Centred hero. The headline and paragraph carry max-widths, so centring the
     text alone would leave them measured from the left edge -- they need auto
     margins to sit in the middle of the band. The CTA row and the stat row are
     both flex, where text-align does nothing and justify-content is the lever. */
  /* The hero headline, in CSS rather than the inline font-size the template
     carried. Inline styles beat any selector short of !important, so the rule
     below would have been dead while that attribute stayed on the h1. */
  /* The full container, not the 920px .h-public caps at -- that is a measure
     chosen for 32px type, and at 85px it is four or five words a line, stacking
     the headline into a narrow column down the middle of the band.

     100% and not 75%. The proportion held while the type was fixed at 85px;
     once the size scales with the viewport the line has already shortened by
     the time the screen is narrow, and holding it to three quarters of the
     container on top of that wasted a quarter of a phone's width for nothing.

     Scoped to .hero: 920px is still right for the h1 and h2 on every other
     page, which are set at 52px. */
  /* 85px at the top and 22px at the bottom -- see --text-display. The ceiling
     is the size this line has always been, so a desktop is unchanged; what
     moves is everything below it. */
  .hero .h-public{font-size:var(--text-display);max-width:100%;font-weight:500;}
  /* 920px rather than the 640px .p-public caps at: that measure was set for
     15px body text, and at 20px it holds only about 52 characters a line --
     a narrow column under a headline running to 75% of the container. */
  .hero .p-public{font-size:var(--text-lead);max-width:920px;}

  .hero.centered{text-align:center;}
  .hero.centered .h-public,
  .hero.centered .p-public{margin-left:auto;margin-right:auto;}
  .hero.centered .hero-ctas,
  .hero.centered .hero-stats{justify-content:center;}

  /* The stat strip: the full width of the hero's column, on the same ground as
     the staff app's side nav. --blue-900 is the shared token, defined the same
     in app.css and here, so the marketing site and the tool read as one product
     rather than two that happen to be nearby.

     auto-fit rather than a fixed three columns: the strip divides evenly for
     however many figures it is given, and wraps instead of crushing them on a
     narrow screen.

     White on #00124A is 17.7:1, so the 11.5px label clears AAA -- worth stating
     because the README's rule is that every pairing is checked before it ships.

     Scoped to .hero on purpose. impact.html reuses .hero-stats for its own KPI
     row, which belongs on the page ground and should not become a dark strip. */
  .hero .hero-stats{
    display:grid;
    grid-template-columns:repeat(auto-fit,minmax(150px,1fr));
    gap:18px;
    /* A fixed gap, not `auto`. auto ate whatever space the band had left, which
       pinned the strip to the foot of the hero; 50px keeps it a set distance
       below the CTAs at every viewport height. */
    margin-top:50px;
    padding:15px 18px;
    background:var(--blue-900);
    border-radius:var(--radius-lg);
  }
  .hero .hero-stat-val{color:#fff;font-size:var(--text-stat);font-weight:600;}
  .hero .hero-stat-label{color:#fff;}
  .hero-ctas{display:flex;gap:12px;margin-top:33px;flex-wrap:wrap;}
  .btn-lg{font-family:inherit;font-size:var(--text-md);font-weight:700;padding:13px 24px;border-radius:9px;cursor:pointer;border:1px solid transparent;}
  .btn-lg.solid{background:var(--flame-500);color:#fff;}
  /* The secondary action in the primary's colour: border and label both.
     ---------------------------------------------------------------------------
     KNOWN AND ACCEPTED: this is the one pairing on the site below AA. #d83a0a
     on the hero's cream measures 4.10:1 against the 4.5 asked of 14.7px bold,
     and it is the ground that tips it -- the same label on a white band is
     4.64:1 and passes. The button only ever appears in the hero.

     --flame-600 would pass on both grounds, at 5.66:1 on cream and 6.41:1 on
     white, and was offered. The brighter label was chosen deliberately.

     Recorded here rather than left to be rediscovered, because this file's own
     header claims every text pairing passes AA -- and from this rule onward
     that is no longer true. */
  .btn-lg.outline{background:transparent;border-color:var(--flame-500);color:var(--flame-500);}
  .btn-lg.outline:hover{border-color:var(--blue-700);color:var(--blue-700);}

  .hero-stats{display:flex;gap:26px;margin-top:34px;flex-wrap:wrap;}
  .hero-stat-val{font-size:var(--text-xl);font-weight:800;color:var(--blue-700);}
  .hero-stat-label{font-size:var(--text-xs);color:var(--text-secondary);text-transform:uppercase;letter-spacing:.03em;}

  /* TWO COLUMNS, DOWN TO 900 WHERE THEY BECOME ONE -- the fund grid's ladder,
     adopted here at the founder's call. Four steps read as 2x2 the whole way,
     which is the arrangement .fund-public-grid already gets from the same two
     rules for the same reason: four cards and no width at which three or four
     across is an improvement.

     It used to be 4 / 3 / 2 / 1, stepping at 1920, 1440 and 900. Those two
     middle steps are gone rather than kept as a compromise: at 3 across, a
     four-step sequence reads as 3 + 1, with the last step alone on a row --
     the orphan the old comment about "exactly four steps" was written to
     avoid, and the reason the ladder had a 4 at the top at all. Two columns
     never produce it, so the steps in between stopped being worth having. */
  .steps-row{display:grid;grid-template-columns:repeat(2,1fr);gap:20px;}
  .step-card{background:#fff;border:1px solid var(--border);border-radius:var(--radius-lg);padding:22px 20px;}
  .step-num{width:30px;height:30px;border-radius:50%;background:var(--blue-100);color:var(--blue-700);font-weight:800;font-size:var(--text-sm);display:flex;align-items:center;justify-content:center;margin-bottom:14px;}
  /* THE CORE VALUES CARD'S PAIRING, and deliberately the same one: --text-lg
     over --text-md at 1.35 and 1.6. Both are a bold title above a short
     paragraph inside a bordered card, so a reader meeting them on two pages
     should not find the second set smaller than the first.

     Raised from md/sm because two columns give the step cards more width than
     the four-column arrangement ever did, and the old sizes left them looking
     underfilled at it. The type carries the extra room instead of the cards
     standing half empty. */
  .step-title{
    font-size:var(--text-lg);line-height:1.35;font-weight:700;
    margin-bottom:6px;
  }
  .step-desc{font-size:var(--text-md);line-height:1.6;color:var(--text-secondary);}

  /* THE GENERIC PUBLIC CARD, and it had no rules at all until now.
     ---------------------------------------------------------------------
     .card-grid, .pub-card, .pub-card-name and .pub-card-meta were written
     into four templates -- programs, blog, blog_post -- and defined in
     neither stylesheet. .card-grid exists in app.css, which is the STAFF
     sheet and is never loaded on a public page, so the markup looked
     plausible while the browser computed `display:block` and a transparent
     background: the cards rendered as flat stacked text on the live site.

     Measured on academy.localhost before this rule existed: .card-grid
     `display:block`, .pub-card `background:rgba(0,0,0,0)`, border `0px`.

     auto-fit rather than a fixed repeat(3,1fr) like the mockup's grids,
     because these grids hold a different number of things on each page --
     four on the programme page, however many posts on the blog. At 220px
     the container fits exactly four across at 1440, and auto-fit (not
     auto-fill) collapses the empty tracks so three cards stretch to thirds
     instead of leaving a gap where a fourth would go.

     THE CAP IS ON THE CARD, NOT THE TRACK, and the difference is not
     cosmetic. auto-fit counts repetitions from the MAX when the max is a
     definite length, so minmax(220px,340px) computed three tracks at 1440
     and wrapped the programme page's fourth card onto a row of its own.
     With 1fr the count comes from the 220px min and four fit, which is what
     that row needs.

     But 1fr also means auto-fit's collapse hands a lone item the whole row:
     the blog has one published post today, and that card measured 1088px
     wide -- a title and a date stretched across the full container. Capping
     the CARD leaves the track arithmetic alone and still gives a row of one
     a card's proportions, with the slack left at the end of the row. */
  .card-grid{
    display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
    gap:20px;margin-top:26px;
  }
  .card-grid > *{max-width:340px;}
  .pub-card{
    background:var(--card);border:1px solid var(--border);
    border-radius:var(--radius-lg);padding:22px 20px;
  }
  .pub-card-name{
    font-size:var(--text-md);font-weight:700;line-height:1.35;
    color:var(--text-primary);margin:0 0 6px;
  }
  .pub-card-name a{color:inherit;text-decoration:none;}
  .pub-card-name a:hover{color:var(--blue-700);}
  .pub-card-meta{font-size:var(--text-xs);color:var(--text-muted);margin:0;}
  /* --text-lg is the measure for a column of prose and far too large inside
     a 220px card, so card copy drops to the size .step-desc already uses for
     exactly this. Scoped to .pub-card so the page's own paragraphs are
     untouched. */
  .pub-card .p-public{
    font-size:var(--text-sm);line-height:1.6;margin:0;
  }

  /* THE PIPELINE — four joined cells, one per stage of the journey.
     ---------------------------------------------------------------------
     An <ol> because these are ordered stages and the order is the meaning:
     prospective, then active, then placed. A <div> row would say the same
     thing visually and nothing at all to a screen reader.

     The cells share borders rather than sitting apart, so the row reads as
     one object -- a path -- instead of four unrelated tiles. That is what
     `border-left:none` on the adjacent sibling buys, and why only the first
     and last carry a radius.

     flex-wrap with a 45% basis below 1100: at four across the labels start
     breaking mid-word, and two rows of two is a better shape than four
     columns of stacked type. Every cell gets its radius and its left border
     back there, because a wrapped cell has no neighbour to share with. */
  .pipeline-row{
    display:flex;align-items:stretch;gap:0;
    list-style:none;margin:30px 0 0;padding:0;
  }
  .pipeline-step{
    flex:1;background:var(--card);border:1px solid var(--border);
    padding:20px 14px;text-align:center;
  }
  .pipeline-step:first-child{border-radius:var(--radius-lg) 0 0 var(--radius-lg);}
  .pipeline-step:last-child{border-radius:0 var(--radius-lg) var(--radius-lg) 0;}
  .pipeline-step + .pipeline-step{border-left:none;}
  .pipeline-count{font-size:var(--text-xl);font-weight:800;color:var(--blue-700);}
  .pipeline-label{
    font-size:var(--text-xs);color:var(--text-secondary);
    text-transform:uppercase;letter-spacing:.03em;margin-top:4px;
  }
  .pipeline-note{
    font-size:var(--text-xs);color:var(--text-muted);margin:10px 0 0;
  }

  /* THE TRAININGS — cards on the spotlights' ladder.
     ---------------------------------------------------------------------
     Same column counts as .spotlight-grid and the same steps down (3 / 2 / 1
     at 1680 and 900), so the two grids read as the same kind of row on two
     different pages. Written as its own selector rather than added to the
     spotlight rule: these cards are a different shape and only the LADDER is
     shared, so joining them would tie the columns of one to any future change
     in the other.

     --card, like every other card here. These were transparent for one commit,
     drawn by the border alone so the band showed through; the founder changed
     them back. The constraint that argument rested on is still true and worth
     keeping in view: this section is band 3, which is CREAM, so a white card
     reads. Move it to an even-numbered <section> and it lands on white, where a
     white card is its own ground and only the hairline border separates them.
     If that ever happens, transparent is the fix.

     No shadow. The cards on this page are flat, and .pub-card in the section
     above sets that expectation two bands earlier.

     A <ul>, because the trainings are a list of things and the grid is only
     how they are laid out. */
  .track-grid{
    display:grid;grid-template-columns:repeat(3,1fr);gap:18px;
    list-style:none;margin:26px 0 0;padding:0;
  }
  .track-card{
    background:var(--card);
    border:1px solid var(--border);
    border-radius:var(--radius-lg);
    padding:22px 20px;
    /* The count sits at the foot however tall the blurb runs, so a row of
       cards has its figures on one line rather than each floating under its
       own paragraph. Cards in a grid row already stretch to the tallest. */
    display:flex;flex-direction:column;
  }
  .track-name{
    font-size:var(--text-md);font-weight:700;line-height:1.35;
    color:var(--text-primary);margin:0;
  }
  .track-blurb{
    font-size:var(--text-sm);line-height:1.6;color:var(--text-secondary);
    margin:8px 0 0;
  }
  /* margin-top:auto is what pushes it down; the 18px floor keeps it off the
     name on a card with no blurb, where auto alone would leave them touching. */
  .track-count{
    font-size:var(--text-xs);font-weight:700;color:var(--text-muted);
    margin-top:auto;padding-top:18px;
  }

  /* COHORT CARDS. auto-fit like .card-grid and for the same reason -- the
     number of cohorts grows -- but with its own 240px minimum, because a
     cohort card carries two stat columns side by side and squeezes below
     that where a prose card does not. */
  /* gap and margin match .card-grid exactly. Two grids of cards on one page
     set at 16px and 20px read as a misalignment rather than a distinction --
     the mockup's 16px was for a three-card row inside a narrower container. */
  .cohort-cards{
    display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));
    gap:20px;margin-top:26px;
  }
  /* .cohort-cards > *{max-width:360px;} */
  .cohort-card{
    background:var(--card);border:1px solid var(--border);
    border-radius:var(--radius-lg);padding:18px 20px;
  }
  .cohort-name{font-size:var(--text-md);font-weight:700;color:var(--text-primary);}
  .cohort-dates{font-size:var(--text-xs);color:var(--text-muted);margin-top:2px;}
  .cohort-stats{display:flex;gap:24px;margin-top:14px;}
  .cohort-stat-val{font-size:var(--text-lg);font-weight:800;color:var(--blue-700);}
  .cohort-stat-label{
    font-size:var(--text-2xs);color:var(--text-secondary);
    text-transform:uppercase;letter-spacing:.03em;
  }

  /* THE IMPACT PAGE.
     ---------------------------------------------------------------------
     Four of these -- .table-wrap, .pub-table, .note and .tag -- were written
     into impact.html and defined in neither stylesheet, the same gap the
     public card classes had. Measured before this block existed: .table-wrap
     computed `overflow:visible`, so the cohort table could not scroll on a
     narrow screen, and .tag was `display:inline` with no ground and no
     padding -- which is why "hosts interns hires graduates" ran together as
     plain text under a partner's name.

     The table itself is gone; cohorts are cards now. .table-wrap and
     .pub-table are kept because impact.html is not the only page that may
     want a table, and a wrapper that cannot scroll is the bug worth fixing
     once rather than rediscovering. */
  .table-wrap{overflow-x:auto;}
  .pub-table{width:100%;border-collapse:collapse;font-size:var(--text-sm);}
  .pub-table th{
    text-align:left;font-size:var(--text-2xs);font-weight:700;
    text-transform:uppercase;letter-spacing:.03em;color:var(--text-muted);
    padding:0 12px 10px;border-bottom:1px solid var(--border);white-space:nowrap;
  }
  .pub-table td{padding:12px;border-bottom:1px solid var(--border);}
  .pub-table td.num,.pub-table th.num{
    text-align:right;font-variant-numeric:tabular-nums;
  }
  /* A footnote under a figure, not body copy. --text-lg is the measure for
     prose and made these read as another paragraph of argument. */
  .note{
    font-size:var(--text-sm);line-height:1.6;color:var(--text-muted);
    margin:18px 0 0;
  }
  /* The neutral label, for tags that only name a thing -- "hosts interns",
     "hires graduates". No colour distinction because there is none to make. */
  .tag{
    display:inline-block;font-size:var(--text-2xs);font-weight:700;
    text-transform:uppercase;letter-spacing:.03em;
    padding:3px 9px;border-radius:99px;
    background:var(--blue-100);color:var(--blue-700);
    margin-left:6px;white-space:nowrap;
  }
  /* A FUND'S DESIGNATION IS NOT A NEUTRAL LABEL, and it shipped as one: both
     badges were the plain .tag, so "Restricted" and "Unrestricted" -- opposite
     facts about what a donor's money can be used for -- rendered in identical
     blue. A badge that reads the same for both values is decoration; the word
     was doing all the work and the colour none of it.

     The mockup made the same distinction in green and terracotta. Neither
     survives this palette -- green went when the logo did -- so the split is
     blue against amber: unrestricted money is usable anywhere and takes the
     primary, restricted money is tied and takes the warm tone.

     AMBER, NOT FLAME. Flame is --danger here and is already on this page,
     colouring a negative balance and the "needs support" line. Restricted is
     not a problem, it is a donor's instruction being honoured, and painting it
     in the error colour would say otherwise two lines above a real warning.

     8.85:1 and 4.97:1. */
  .tag.unrestricted{background:var(--blue-100);color:var(--blue-700);}
  .tag.restricted{background:var(--amber-100);color:var(--amber-600);}

  /* The four headline figures. Its own grid rather than .hero-stats, which is
     a flex row sized by its content: two of these are money and run to eight
     characters where the other two are single digits, and a flex row lets the
     widest one push the rest out of alignment. Equal columns keep the four
     baselines and the four labels in line. */
  .impact-kpi-row{
    display:grid;grid-template-columns:repeat(4,1fr);gap:18px;margin-top:30px;
  }
  .impact-kpi{
    background:var(--card);border:1px solid var(--border);
    border-radius:var(--radius-lg);padding:20px;
  }
  .impact-kpi-val{
    font-size:var(--text-xl);font-weight:800;color:var(--blue-700);
    font-variant-numeric:tabular-nums;
  }
  .impact-kpi-label{
    font-size:var(--text-xs);color:var(--text-secondary);
    text-transform:uppercase;letter-spacing:.03em;margin-top:4px;
  }

  /* "Read straight from operations data". A claim, so it is given a mark that
     reads as a status rather than as a sentence of copy. */
  .sync-note{
    display:inline-flex;align-items:center;gap:8px;
    font-size:var(--text-xs);color:var(--text-secondary);
    margin:6px 0 0;
  }
  .sync-dot{
    width:8px;height:8px;border-radius:50%;
    background:var(--success);flex:0 0 auto;
  }

  /* FUND BY FUND — two columns, down to 900 where they become one.
     ---------------------------------------------------------------------
     The mockup's arrangement, with the collapse moved from 1100 to 900 so the
     pair holds across tablet widths. With four funds that reads as 2x2.

     A stated count, not auto-fit. auto-fit derives its columns from a minimum
     width, which would give three or four across on a wide screen; a fund card
     carries three labelled money rows and a description, and it wants the
     width. Two is the arrangement, at every size that can hold two.

     A single row across the top was tried first, at 1680 and then at 1920.
     Both were dropped: four funds at 1680 came to 263px each, and the count
     that looks right depends on how many funds exist, which is not something
     a stylesheet should have an opinion about. */
  /* WHAT A GIFT BUYS, on the donate page.
     -------------------------------------------------------------------------
     A list and not the card grid the funds use, because these are a menu of
     one-line choices rather than records to compare. Cards would give six
     small things the visual weight of six funds, and the ask is the sentence:
     "350,000 — laptop".

     The cost leads. It is what a donor is deciding about, and putting the
     figure first means the column of them lines up down the page and can be
     read without reading the words beside it. Tabular numerals so it actually
     does line up: proportional digits make 350,000 and 30,000 ragged. */
  .need-list{list-style:none;margin:22px 0 0;padding:0;}
  .need-line{
    display:flex;align-items:baseline;gap:14px;
    padding:12px 0;border-bottom:1px solid var(--border);
  }
  .need-line:first-child{border-top:1px solid var(--border);}
  .need-cost{
    flex:0 0 auto;min-width:7.5em;
    font-weight:700;color:var(--text-primary);
    font-variant-numeric:tabular-nums;
  }
  /* Quieter than the figure it qualifies, and not part of the column width. */
  .need-per{font-weight:400;color:var(--text-muted);font-size:var(--text-sm);}
  .need-what{flex:1 1 auto;color:var(--text-secondary);}
  /* The headcount, where the threshold allows one at all. Deliberately the
     least prominent thing on the line: it is context, the cost is the ask, and
     below the threshold the line has to read as a complete sentence without
     it. See projection.funding_needs. */
  .need-count{color:var(--text-muted);font-size:var(--text-sm);}
  .need-total{margin-top:20px;}

  @media (max-width:600px){
    /* Stacked, because at 360px a 7.5em cost column leaves the words a strip
       too narrow to read. The figure keeps its prominence by being first. */
    .need-line{flex-direction:column;gap:2px;}
    .need-cost{min-width:0;}
  }

  .fund-public-grid{
    display:grid;grid-template-columns:repeat(2,1fr);
    gap:20px;margin-top:26px;
  }
  .fund-public-card{
    background:var(--card);border:1px solid var(--border);
    border-radius:var(--radius-lg);padding:22px 20px;
    display:flex;flex-direction:column;
  }
  .fund-public-top{
    display:flex;align-items:flex-start;justify-content:space-between;gap:10px;
  }
  .fund-public-name{
    font-size:var(--text-md);font-weight:700;line-height:1.35;
    color:var(--text-primary);margin:0;
  }
  .fund-public-top .tag{margin-left:0;}
  .fund-public-desc{
    font-size:var(--text-sm);line-height:1.6;color:var(--text-secondary);
    margin:10px 0 0;
  }
  .fund-public-tie{
    font-size:var(--text-xs);color:var(--text-muted);margin:6px 0 0;
  }
  /* A <dl>, because each figure is a term and its value. Grid rather than the
     browser's default dt/dd indent, so the three amounts line up on one right
     edge and can be read down as a column. margin-top:auto pins the block to
     the foot, so cards in a row have their figures on the same line whatever
     their descriptions run to. */
  .fund-public-nums{
    display:grid;grid-template-columns:1fr auto;gap:6px 16px;
    /* auto, not a fixed top margin. "Power & internet fund" wraps to two lines
       where the other three names do not, and with a fixed margin its figures
       sat a line lower than the rest of the row -- three cards agreeing and
       one not, which reads as a mistake rather than as a longer name. The
       card is a column flex container so auto pushes this to the foot; the
       16px floor keeps it off the text above on the shortest card. */
    margin:16px 0 0;margin-top:auto;
    padding-top:14px;border-top:1px solid var(--border);
    font-size:var(--text-sm);
  }
  .fund-public-nums dt{color:var(--text-secondary);}
  .fund-public-nums dd{
    margin:0;text-align:right;font-weight:700;color:var(--text-primary);
    font-variant-numeric:tabular-nums;
  }
  .fund-public-nums dd.is-short{color:var(--danger);}
  .fund-public-nums dd.is-clear{color:var(--blue-700);}
  .fund-needs{
    font-size:var(--text-xs);font-weight:700;color:var(--danger);
    margin:12px 0 0;
  }

  /* Outcome cards. A <ul> carrying .card-grid, so the list semantics stay and
     the layout is the generic one; only the two lines inside need rules.
     list-style is reset here rather than on .card-grid, which is a <div>
     everywhere else and should not have to carry a list's baggage. */
  .outcome-cards{list-style:none;padding:0;}
  .outcome-count{
    font-size:var(--text-xl);font-weight:800;color:var(--blue-700);
    line-height:1.1;font-variant-numeric:tabular-nums;
  }
  .outcome-label{
    font-size:var(--text-sm);color:var(--text-secondary);
    margin-top:6px;
  }

  /* PARTNER CARDS — their own grid, and the tracks are allowed to stretch.
     ---------------------------------------------------------------------
     This began on .card-grid, which was wrong for two reasons that only show
     up with a short list. That grid caps every child at 340px, so with two
     partners the tracks came out ~494px wide and the cards sat at 340 inside
     them, leaving a dead gap beside each -- a full row that looked broken
     rather than a short list that looked deliberate. And its 220px minimum is
     tuned for four cards of prose, where a partner card holds a logo of
     unknown width and a name that can run to "Paystack-adjacent Fintech Ltd".

     So: no cap, and a 260px minimum. auto-fit shares the whole row between
     however many partners there are, which is the right behaviour for a list
     that is two today and will not stay two. Two partners get half the row
     each; five get three and then two.

     A ragged last row is left ragged. The alternative is stretching the
     remainder to fill it, which makes the final card wider than its
     neighbours and reads as emphasis nobody intended. */
  .partner-cards{
    display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));
    gap:20px;margin-top:26px;
    list-style:none;padding:0;
  }
  .partner-card{display:flex;flex-direction:column;}
  /* Contained, not cropped: a partner's mark is their property and must not be
     trimmed to fit a box. The height is fixed so a row of cards has its names
     on one line whatever shape the artwork is, and object-position:left keeps
     the logo on the same edge as the text below it. */
  .partner-logo{
    height:44px;width:auto;max-width:100%;
    object-fit:contain;object-position:left center;
    margin-bottom:14px;
  }
  /* No wordmark rule. There was one, standing in for a missing logo, and it
     duplicated the name on every card that had no artwork -- the name is
     printed unconditionally below.

     THE NAME AND TAGS ARE PINNED TO THE FOOT instead, which is what lines a
     row up. Cards stretch to the tallest in the row, so a card with a logo put
     its name a logo's height down while a card without one had it at the very
     top: one row, headings on two lines. Pushing the pair down aligns them
     from the bottom and costs nothing, where reserving an empty logo slot
     would have put 58px of nothing on every card that has no artwork. */
  .partner-foot{margin-top:auto;}
  .partner-name{
    font-size:var(--text-md);font-weight:700;color:var(--text-primary);
  }
  /* The tags carry their own left margin for use inline after a name; in a
     block of their own that pushes the first one off the card's left edge. */
  .partner-tags{
    display:flex;flex-wrap:wrap;gap:6px;margin-top:10px;
  }
  .partner-tags .tag{margin-left:0;}

  /* PUBLIC FORMS — five classes that three templates used and no stylesheet
     defined.
     ---------------------------------------------------------------------
     Measured before this block existed: labels computed `display:inline`, so
     every label sat BESIDE its input and the three inputs lined up at three
     different left edges; rows had `margin-bottom:0`, so the fields touched;
     and .form-help rendered at 16px in --text-primary, indistinguishable from
     a paragraph of body copy. The Get Involved and Contact forms have looked
     broken since they were written.

     A grid rather than margins on the label, so the label, the control, the
     help and the error stack in one place and a row with three of them spaces
     the same as a row with one. */
  .public-form{display:grid;gap:20px;}
  .form-row-public{display:grid;gap:6px;}
  .form-row-public label{
    font-size:var(--text-xs);font-weight:700;
    text-transform:uppercase;letter-spacing:.03em;
    color:var(--text-secondary);
  }
  /* The inputs already have a base rule further up this file, which is why they
     were the one part that looked deliberate. It sizes them but does not make
     them fill the row. */
  .form-row-public input,
  .form-row-public select,
  .form-row-public textarea{width:100%;}
  /* Below the control, not above it: help that appears before the field is
     read as part of the label and lengthens it. --text-xs and muted so it
     cannot be mistaken for the page's own prose again. */
  .form-help{
    font-size:var(--text-xs);line-height:1.5;color:var(--text-muted);margin:0;
  }
  .form-error{
    font-size:var(--text-xs);font-weight:700;color:var(--danger);margin:0;
  }
  .form-errors-public{
    background:var(--flame-100);border:1px solid var(--danger);
    border-radius:var(--radius);padding:12px 14px;
    font-size:var(--text-sm);color:var(--flame-600);
  }
  .form-errors-public ul{margin:0;padding-left:18px;}
  /* Full width, because it is the only action on the page and a button sized
     to its own text leaves the card looking unfinished under a stack of
     full-width fields. */
  .form-submit{width:100%;}

  /* The card the form sits in. max-width lives on .container.narrow rather
     than here, so the note underneath shares the same measure. */
  .form-card{
    background:var(--card);border:1px solid var(--border);
    border-radius:var(--radius-lg);padding:26px;
    margin-top:26px;
  }

  .notice-public{
    background:var(--success-100);border:1px solid var(--success);
    border-radius:var(--radius);padding:14px 16px;margin-top:22px;
    font-size:var(--text-sm);line-height:1.6;color:var(--text-primary);
  }
  .notice-public strong{display:block;color:var(--success);}

  /* GET INVOLVED — four ways to help, each a link into the form.
     ---------------------------------------------------------------------
     The same ladder as .track-grid and .spotlight-grid (3 / 2 / 1 at 1680 and
     900) would leave four cards as 3 + 1. These are four fixed offers, so the
     row is four across and halves rather than stepping down one at a time.

     The LINK wraps the icon and the title only, not the description. A link
     whose text is three sentences long is one a screen reader reads out in
     full before saying where it goes, and the accessible name here should be
     "Become a mentor". */
  .involve-grid{
    display:grid;grid-template-columns:repeat(4,1fr);gap:20px;
    list-style:none;margin:26px 0 0;padding:0;
  }
  /* THE WHOLE CARD IS CLICKABLE. Same pattern as .blog-card, and for the same
     reason it is a pattern rather than an <a> around everything: the anchor
     wraps the icon and the title, so that is the accessible name, and a
     stretched pseudo-element makes the card the target. Wrapping the
     description too would have a screen reader read three sentences before
     saying where the link goes.

     position:relative is what the overlay is measured against. */
  .involve-card{
    position:relative;
    background:var(--card);border:1px solid var(--border);
    border-radius:var(--radius-lg);padding:22px 20px;
  }
  .involve-link{
    display:block;text-decoration:none;color:inherit;
  }
  .involve-link::after{
    content:"";position:absolute;inset:0;
  }
  /* Keyed to the CARD now, not the anchor -- the card is what responds to a
     pointer, so hovering the description has to light the title too. The flame
     on the title is kept exactly as it was. focus-within gives somebody tabbing
     the same signal, which a focus ring on the title alone does not. */
  .involve-card:hover .involve-title,
  .involve-card:focus-within .involve-title{color:var(--flame-600);}
  .involve-card:hover,
  .involve-card:focus-within{border-color:var(--flame-600);}
  .icon-badge{
    width:38px;height:38px;border-radius:10px;
    background:var(--blue-100);color:var(--blue-700);
    display:flex;align-items:center;justify-content:center;
    margin-bottom:12px;
  }
  .icon-badge svg{width:18px;height:18px;}
  .involve-title{
    display:block;font-size:var(--text-md);font-weight:700;
    color:var(--text-primary);margin-bottom:6px;
  }
  .involve-desc{
    font-size:var(--text-sm);line-height:1.6;color:var(--text-secondary);
    margin:0;
  }

  /* THE BLOG INDEX.
     ---------------------------------------------------------------------
     The mockup's arrangement: a card of a picture, a tag, a title, an excerpt
     and a date. Three across, and it steps down on the spotlights' ladder
     rather than one of its own -- these are the same shape of object, a
     picture over a block of text, and they should break at the same widths.

     THE WHOLE CARD IS ONE LINK. A title-only link inside a clickable-looking
     card is a target most people miss on a phone, and two links to the same
     place -- one on the picture, one on the title -- make a screen reader
     announce the post twice. So the anchor wraps everything and the elements
     inside are spans: an <a> may not contain a <div>, and nesting block
     elements in it is the quiet way to invalid markup. */
  /* THE RAIL IS A ROW ABOVE THE POSTS UNTIL THERE IS ROOM FOR A COLUMN.
     ---------------------------------------------------------------------
     Beside the posts the rail costs its own width plus a 40px gap, and that
     comes out of the cards. Above them it costs one line and the grid gets
     the whole container back, so the cards are WIDER below this breakpoint
     than above it -- which is why the rail moving out of the way is a gain
     rather than a compromise.

     Written as a min-width so the rail is beside the posts AT 1680 rather
     than one pixel above it. This file otherwise counts downwards; the
     exceptions are the rules stated as "and above", here and .about-split.

     The nesting survives the change because it lives in the markup rather
     than in the columns: the subcategories are a list inside their parent's
     item, so as a row they wrap onto their own line beneath it. */
  .blog-layout{
    display:grid;grid-template-columns:1fr;
    gap:24px;align-items:start;margin-top:26px;
  }
  .blog-rail{position:static;}
  .blog-rail-list,
  .blog-rail-sub{list-style:none;margin:0;padding:0;}
  .blog-rail-list{display:flex;flex-wrap:wrap;gap:8px;}
  .blog-rail-sub{
    width:100%;display:flex;flex-wrap:wrap;gap:8px;
  }

  @media (min-width:1680px){
    /* max-content, not a fixed 220px. With the counts gone the rail is exactly
       as wide as its longest label -- "Internship started", indented under its
       parent -- and every pixel it stops taking goes to the posts. Safe to let
       the content decide because the labels are a closed set: four categories
       and five outcome types, none of them free text. */
    .blog-layout{grid-template-columns:max-content minmax(0,1fr);gap:40px;}
    /* It sticks, like the policy page's chapter list and for the same reason:
       scrolling a long feed should not mean scrolling back up to change what
       you are looking at. top clears the nav from its own variable rather than
       restating a number.

       Sticky only as a column. A row pinned to the top of a viewport spends
       scarce height on navigation while somebody is trying to read, which is
       why the policy rail stops sticking on a phone too. */
    .blog-rail{position:sticky;top:calc(var(--nav-h) + 24px);}
    .blog-rail-list{display:block;}
    /* Indented and ruled, so the subcategories read as belonging to the
       category above them rather than as four more categories. */
    .blog-rail-sub{
      display:block;width:auto;
      margin:2px 0 6px 12px;padding-left:10px;
      border-left:1px solid var(--border);
    }
  }
  /* `block`, not a space-between flex row -- there is nothing to push apart
     now that the count has gone, and a flex row would still stretch each item
     to the widest label rather than letting the column size itself. */
  .blog-rail-item{
    display:block;
    padding:8px 12px;border-radius:8px;
    font-size:var(--text-sm);font-weight:600;
    color:var(--text-secondary);text-decoration:none;
    white-space:nowrap;
  }
  .blog-rail-item:hover{background:var(--blue-100);color:var(--blue-700);}
  /* Filled, not merely tinted. A border or a colour shift alone is the kind of
     difference that disappears on a bright phone screen outdoors, which is
     where a good part of this audience reads. */
  .blog-rail-item.is-active{background:var(--blue-700);color:#fff;}
  .blog-rail-item.is-sub{
    font-weight:500;font-size:var(--text-xs);padding:6px 10px;
  }
  /* No counts and no × any more. The numbers made every row as wide as its
     label plus a figure that changes, and the × was a second mark saying what
     selecting the row already does. Both gone, and the rail shrank to its
     labels.

     A caret in their place, but ONLY on a row that has children -- it is a
     disclosure marker, not decoration, and it points down when the list is
     open. Rows with nothing to disclose show none, which is what keeps it
     meaning something. */
  .blog-rail-caret{
    display:inline-block;margin-left:6px;
    border-left:4px solid transparent;border-right:4px solid transparent;
    border-top:5px solid currentColor;
    transform:rotate(-90deg);transition:transform .15s ease;
    vertical-align:middle;opacity:.85;
  }
  .blog-rail-caret.is-open{transform:none;}

  /* TWO COLUMNS UNTIL 2200, THEN THREE. The spotlight ladder steps to three
     at 1680, but the spotlights have the whole container to divide and these
     cards do not -- above 1680 the rail takes its width out of the grid
     first. Three columns there came to 313px each, which put the picture on
     its 110px floor and left 201px for the words: less than a 360px phone
     gets. So the shape is the spotlights' and the steps are not.

     min-width for the same reason the rail above uses one: "2200 and beyond"
     should include 2200. */
  .blog-grid{
    display:grid;grid-template-columns:repeat(2,1fr);gap:18px;
    list-style:none;margin:0;padding:0;
  }
  @media (min-width:2200px){
    .blog-grid{grid-template-columns:repeat(3,1fr);}
  }
  .blog-pager{
    display:flex;align-items:center;justify-content:space-between;
    gap:14px;flex-wrap:wrap;
    margin-top:28px;padding-top:18px;border-top:1px solid var(--border);
    font-size:var(--text-sm);
  }
  .blog-pager-step{
    display:inline-flex;align-items:center;gap:6px;
    padding:8px 16px;border-radius:8px;
    border:1px solid var(--border);color:var(--text-primary);
    text-decoration:none;font-weight:600;white-space:nowrap;
  }
  a.blog-pager-step:hover{border-color:var(--blue-700);color:var(--blue-700);}
  .blog-pager-step.is-off{color:var(--text-muted);border-color:#EDF0F5;}
  .blog-pager-state{
    color:var(--text-secondary);text-align:center;flex:1 1 auto;min-width:0;
  }
  .blog-pager-total{color:var(--text-muted);}
  .blog-pager-total::before{content:"·";margin:0 6px;}
  /* THE WHOLE CARD IS CLICKABLE, AND THE LINK IS STILL ONLY THE TITLE.
     ---------------------------------------------------------------------
     Two ways to do this and both are wrong on their own. An anchor wrapping
     the entire card gives a good tap target and an accessible name three
     sentences long -- a screen reader reads the tag, the title, the excerpt
     and the date before saying where it goes. An anchor on the title alone
     names itself properly and leaves a target most people miss on a phone.

     So: the anchor wraps the title, and a pseudo-element stretches it across
     the card. The name is the title, the target is the card, and there is one
     link rather than two pointing at the same place.

     `position:relative` here is what the ::after is measured against -- without
     it the overlay escapes to the nearest positioned ancestor and covers the
     whole grid.

     Known cost: dragging to select text inside the card is awkward, because
     the overlay is above it. That is the accepted trade of this pattern and it
     is why the excerpt is a summary rather than anything anybody would want to
     copy. */
  /* HORIZONTAL, like the spotlight cards. A vertical card made the picture's
     shape the card's problem: at `height:110px` a square headshot was cropped
     to a letterbox, and at `height:auto` the thumbnail became as tall as the
     column was wide -- 826px in a single column at 900. Neither is a size
     anybody chose; both are the aspect ratio of whatever file was uploaded.

     Side by side, the TEXT sets the height and the picture fills the strip it
     is given. A blog card's text is a tag, a title, four clamped lines and a
     date, which is a predictable height -- so the cards in a row match, and
     nothing is decided by an upload. */
  .blog-card{
    position:relative;
    display:flex;align-items:stretch;
    background:var(--card);border:1px solid var(--border);
    border-radius:var(--radius-lg);overflow:hidden;
  }
  /* Hover moves to the CARD, since the card is what is clickable now. And
     focus-within, so somebody tabbing gets the same signal a mouse gets --
     the focus ring lands on the title text, which is easy to miss against a
     card that has not changed at all. */
  .blog-card:hover,
  .blog-card:focus-within{border-color:var(--blue-700);}
  .blog-title-link{
    color:inherit;text-decoration:none;
  }
  .blog-title-link::after{
    content:"";position:absolute;inset:0;
  }
  /* THE STRIP THE PICTURE LIVES IN, and it has no size of its own on purpose.
     An <img> carries its intrinsic dimensions and that is what a flex line
     measures, so a picture placed directly in the card would set the card's
     height -- the spotlight cards learned this the hard way at 1770px. The
     wrapper stretches to whatever the text beside it needs; the picture is
     positioned inside it and fills what it is given.

     A percentage rather than a fixed width, so the strip keeps its proportion
     as the card narrows -- the card stays side by side at every width, down to
     a 360px phone, where 110px of picture and 219px of text still works.
     max-width so a wide card does not hand a third of itself to a thumbnail;
     min-width so a narrow one shows something recognisable rather than a
     sliver. There is deliberately no stacked fallback: a full-width picture on
     a one-column card is the 464px-tall thumbnail this whole change removed. */
  .blog-thumb-wrap{
    flex:0 0 auto;align-self:stretch;position:relative;
    width:32%;max-width:200px;min-width:110px;
  }
  /* Absolutely positioned, which is what stops the picture having a say in the
     height -- see the wrapper's comment in blog.html. cover crops rather than
     distorts: a face squashed to fit is worse than one trimmed. */
  .blog-thumb{
    position:absolute;inset:0;width:100%;height:100%;
    display:flex;align-items:center;justify-content:center;
    object-fit:cover;object-position:center;
    background:var(--blue-100);color:var(--blue-700);
  }
  .blog-thumb svg{width:28px;height:28px;}
  /* TINTS, NOT FILLS, and the difference is the whole point of this block.
     Saturated -600 and -700 grounds behind a white mark made the placeholder
     the loudest thing on the page -- louder than the headline, and on a post
     that has no picture, which is exactly backwards. The -100 tint with its
     -600 mark is the pairing every badge and icon-badge on this site already
     uses, and it says "there is no photograph here" quietly.

     Keyed to the category so a blog with few pictures still reads as sorted
     rather than as a wall of identical boxes. */
  .blog-thumb.is-milestone{background:var(--blue-100);color:var(--blue-700);}
  .blog-thumb.is-programme{background:var(--amber-100);color:var(--amber-600);}
  .blog-thumb.is-giving{background:var(--flame-100);color:var(--flame-600);}
  .blog-thumb.is-stories{background:var(--success-100);color:var(--success);}

  /* min-width:0 so a long word in a title cannot push the body wider than its
     column and drag the picture off with it -- the same guard the spotlight
     body carries, and for the same reason. */
  .blog-body{
    display:flex;flex-direction:column;flex:1 1 auto;min-width:0;
    padding:16px 18px 20px;
  }
  .blog-tag{
    font-size:var(--text-2xs);font-weight:700;
    text-transform:uppercase;letter-spacing:.04em;
    color:var(--amber-600);margin-bottom:8px;
  }
  .blog-title{
    font-size:var(--text-md);font-weight:700;line-height:1.35;
    color:var(--text-primary);margin:0 0 8px;
  }
  .blog-card:hover .blog-title,
  .blog-card:focus-within .blog-title{color:var(--blue-700);}
  /* Clamped, not truncated by the layout. The summary is 200 words -- sized
     for the About card, which stretches -- and a grid cell would simply run to
     whatever length the longest one dictates, leaving every other card padded
     with white space. Four lines is the same for all of them. */
  .blog-excerpt{
    font-size:var(--text-sm);line-height:1.6;color:var(--text-secondary);
    margin-bottom:12px;
    display:-webkit-box;-webkit-line-clamp:4;-webkit-box-orient:vertical;
    overflow:hidden;
  }
  .blog-date{
    font-size:var(--text-xs);color:var(--text-muted);margin-top:auto;
  }

  /* A count and its label, one per line -- "2 internships started". Used by
     the programme page and twice by impact, and also undefined until now:
     it was rendering as a browser-default bulleted list, indent and all. */
  .stat-rows{list-style:none;margin:18px 0 0;padding:0;}
  .stat-rows li{
    font-size:var(--text-md);color:var(--text-secondary);
    padding:10px 0;border-bottom:1px solid var(--border);
  }
  .stat-rows li:first-child{border-top:1px solid var(--border);}
  .stat-rows strong{
    color:var(--blue-700);font-weight:800;font-size:var(--text-lg);
    margin-right:8px;
  }

  .spotlight-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;}
  /* ONE column by default, and the text fills it. The picture column exists
     only on a card that has a picture -- hence the modifier rather than a fixed
     `auto 1fr`, which would leave the copy in a narrow first track beside an
     empty one on every card without a photograph.

     text-align:left of its own, because the band's heading is centred and the
     cards would otherwise centre with it. */
  /* NO PADDING ON THE CARD. The picture has to reach the card's own edges, so
     the inset moves inward to .spotlight-body and the picture gets none at all.
     A padded card cannot do this: every child would be held 20px off the border
     and the portrait would float in a white margin.

     overflow:hidden rather than rounding the picture's own two left corners --
     the card already states its radius once, and the picture inherits the shape
     by being clipped to it. One value to change instead of three that have to
     agree. */
  /* FLEX, NOT GRID, and the reason is the square. The picture's side is the
     card's height, and a grid `auto` track is sized before the row height is
     known -- so the column would take the image's intrinsic width and the
     square would come out wrong. A stretched flex item has its height resolved
     first, and aspect-ratio then gives the width from it. */
  /* NO HEIGHT HERE AT ALL. The text decides: name, meta and a quote clamped to
     three lines, plus the body's padding. The portrait is then a square of
     whatever that came to, so the card resizes with the type at every width --
     which it could not do while a fixed 150px cap was standing in for the
     measurement.

     The cap was load-bearing once, and the reason it no longer is worth
     recording: an <img> with width and height both auto takes its intrinsic
     size, so a 1768px upload set its own width and dragged the card to 1770px
     tall. What fixed that was moving the aspect-ratio onto a wrapper <div>,
     which has no intrinsic size -- not the cap. With the wrapper in place the
     picture cannot drive the height, so nothing needs to hold it down.

     overflow:hidden stays, for the radius: it is what clips the picture to the
     card's corners rather than restating them on two of its own.

     NO max-height EITHER, and the earlier one was papering over the bug rather
     than solving it: while the image was setting the height, a cap was the only
     thing keeping the card off 1770px. With the image out of flow the text is
     the only thing left that can make the card tall, and the quote's three-line
     clamp is what bounds that. A cap on top would only ever cut text off.

     Cards in a grid row stretch to the tallest of them, so a row is level and
     every portrait in it is the same square -- which is what makes a row read
     as a row rather than as four unrelated boxes. */
  .spotlight-card{
    background:#fff;border:1px solid var(--border);border-radius:var(--radius-lg);
    padding:0;display:flex;align-items:stretch;text-align:left;
    overflow:hidden;
  }
  /* THE PICTURE MUST NOT HAVE A SAY IN THE HEIGHT. It follows the text, and the
     text is the name, the meta line and the clamped quote.

     A <div> wrapper has no intrinsic size, but the <img> inside it does, and
     that is what a flex line measures. So the card came out exactly as tall as
     whatever file was in it: 200px once thumbnails existed, and 1770px before
     that, with a 1768px upload. Both times the picture was setting the height
     and the text was going along with it -- which is backwards.

     position:absolute takes the image out of the wrapper's content sizing
     altogether, so the wrapper contributes nothing to the line and the line's
     height is the body's. The picture follows the text, which is the point.

     THE WIDTH IS STATED, NOT DERIVED, AND THAT IS A BUG FIX. It was
     aspect-ratio:1 against the stretched height -- and that is circular: in
     flexbox an item's main size is resolved before the line's cross size is
     known. Chromium has a special case that makes it work. WebKit falls back to
     the content instead, and the content is nothing now the image is out of
     flow, so the wrapper computed to ZERO WIDTH and the portrait was simply
     absent on every iPhone. It rendered perfectly on the desktop I was
     measuring on, which is why nothing here caught it -- it was reported from a
     phone.

     5.75rem is measured on the LIVE RECORDS, not on the demo data. That
     distinction cost a wrong number once: locally most quotes are one line and
     the cards come to 70-76px, so 4.75rem looked right. Production's two
     outcomes both run to two lines, and its cards are 97px on a desktop, 94-95
     mid-range and 90-91 on a phone. 4.75rem would have been twenty pixels
     narrow on every card anybody has actually seen.

     In rem rather than px, so it still answers a reader who has asked for
     larger text.

     THIS NUMBER IS A COMPROMISE, AND NOW A SETTLED ONE. The quote is
     Outcome.description -- free text somebody types -- so the card's height
     depends on how long a sentence was written: 72px for one line, 97px for
     two, a 25px swing no fixed width can track. 5.75rem sits between the phone
     and the desktop, which is where the readers are; the 700-900px band, where
     the card is wide enough to fit one line, comes out landscape.

     THE FIX WAS GOING TO BE STRUCTURED FIELDS -- splitting the description into
     named parts rendered one per line, so the height became a property of the
     layout and a single width was genuinely square. That was dropped on
     2026-08-21 as not worth it any time soon, and this comment said "that is
     the next piece of work" until then. It is not, so the portrait is not
     square at every width and will not become square by waiting.

     What that leaves: flush and present beats square and absent. If the shape
     ever grates enough to revisit, the honest options are structured fields
     (the real fix), a fixed card height with the quote clamped to one line, or
     a portrait that is deliberately not square -- a 4:5 band would at least be
     consistently not-square instead of varying with the prose.

     inset:0 rather than width/height:100%, so the image fills the wrapper
     whatever the wrapper turns out to be. */
  .spotlight-figure-wrap{
    flex:0 0 auto;align-self:stretch;
    width:5.75rem;
    position:relative;
  }
  /* cover crops rather than distorts -- a face squashed to fit would be worse
     than one trimmed at the edges. */
  .spotlight-figure{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;display:block;}
  /* min-width:0 so a long word in the quote cannot push the card wider than its
     column and take the portrait with it.

     A flex column that centres its own children, because the card's height is
     not its own: grid rows stretch every card to the tallest in the row, so a
     two-line quote sits in a box built for a three-line one. Left at the top
     the short cards read as though something had been cut from underneath
     them. Centred, a row of uneven cards still looks deliberate.

     justify-content, not align-items -- the column's main axis is the vertical
     one, and align-items here would stretch the lines across the width instead. */
  .spotlight-body{
    flex:1 1 auto;min-width:0;overflow:hidden;
    padding:10px 12.5px;
    display:flex;flex-direction:column;justify-content:center;
  }
  .spotlight-avatar{width:42px;height:42px;border-radius:50%;background:var(--amber-100);color:#7A5410;display:flex;align-items:center;justify-content:center;font-weight:700;font-size:var(--text-sm);flex-shrink:0;}
  /* Two rows with a thin gap between them: who this is, then what they did.
     The gap is the whole separator -- a rule or a border would have made the
     card look like a table row, and at these sizes 8px of nothing reads as a
     division perfectly well. */
  .spotlight-head{display:flex;flex-direction:column;margin-bottom:8px;}
  .spotlight-name{font-weight:700;font-size:var(--text-md);}
  /* Smaller and lighter than the name it sits under, so the two lines in this
     row read as one block with a hierarchy rather than as two labels. 500 keeps
     it legible at 10.4-11.5px, where 400 starts to thin out on a light ground. */
  .spotlight-meta{font-size:var(--text-2xs);font-weight:500;color:var(--text-muted);}
  /* THE CLAMP IS NOW WHAT BOUNDS THE CARD, since nothing else does. The quote
     is staff-written free text of no fixed length, and the portrait is a square
     of the card's height -- so an unclamped quote would not merely make a tall
     card, it would make a square as wide as that card is tall and leave the
     text a sliver beside it. Three lines is the whole height budget.

     It also stops overflow:hidden slicing a last line through the middle of the
     letters, which reads as broken rather than as abbreviated. */
  .spotlight-quote{
    font-size:var(--text-sm);line-height:1.6;color:var(--text-secondary);
    display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;
    overflow:hidden;
  }

  /* A rendered block document: the blog's article and the privacy policy.
     ---------------------------------------------------------------------------
     .post-body has been in blog_post.html since the blog shipped and has never
     had a rule, so a published story renders at browser defaults -- Times
     margins inside a Raleway page. The policy page uses the same wrapper, and
     found it.

     Element selectors rather than classes, because this markup is built by
     foundation.richtext from stored blocks. Nobody writes a class into it and
     nobody can: the renderer emits a known, short list of tags and escapes
     everything else. */
  .post-body{font-size:var(--text-lg);line-height:1.75;color:var(--text-secondary);}
  .post-body p{margin:0 0 1.1em;}
  .post-body h2{font-size:var(--text-xl);line-height:1.3;color:var(--text-primary);margin:1.8em 0 .6em;font-weight:600;}
  .post-body h3{font-size:var(--text-lg);line-height:1.35;color:var(--text-primary);margin:1.5em 0 .5em;font-weight:700;}
  .post-body ul,
  .post-body ol{margin:0 0 1.1em;padding-left:1.4em;}
  .post-body li{margin-bottom:.4em;}
  /* A quote is somebody's words, so it is set apart by a rule in the brand's
     warm rather than by italics, which at this size reads as emphasis instead. */
  .post-body blockquote{margin:1.4em 0;padding:.2em 0 .2em 1.1em;border-left:3px solid var(--flame-500);color:var(--text-primary);}
  .post-body img{max-width:100%;height:auto;border-radius:var(--radius);margin:1.2em 0;}
  .post-body code{font-size:.95em;}

  /* THE POLICY'S BODY SITS ONE LEVEL DOWN from a blog post's.
     ---------------------------------------------------------------------
     A blog post's body owns its h2s: the page is h1 the title, and the body's
     divisions are h2. The policy's body does not -- the page is h1 the
     document and h2 the chapter, both outside this wrapper, so the chapter's
     sections are h3 and its sub-sections h4.

     Without this the section headings inherit the shared h3 rule, which is
     body-text size with weight on it -- correct for a minor heading in a story
     and much too quiet for "9.1 What counts as valid consent" in a legal
     document. These move the sizes down a level so the page looks exactly as
     it did while the heading outline is the right shape.

     Scoped to .policy-body rather than changing the shared rules, so a blog
     post renders unchanged. */
  .policy-body h3{
    font-size:var(--text-xl);line-height:1.3;color:var(--text-primary);
    margin:1.8em 0 .6em;font-weight:600;
  }
  .policy-body h3:first-child{margin-top:0;}
  .policy-body h4{
    font-size:var(--text-lg);line-height:1.35;color:var(--text-primary);
    margin:1.5em 0 .5em;font-weight:700;
  }

  /* TRUSTED BY -- the partner strip under the hero.
     ---------------------------------------------------------------------
     A quiet band, on purpose. It sits between the headline and the first real
     section, and its job is to be believed rather than looked at: the reader
     should register three names and move on.

     The heading is a real <h2> for the outline and typeset as a label, which
     is the opposite of the .doc-rail-title decision on the policy page. That
     one labelled a <nav> which already carried its own accessible name; this
     one names a section of content that has no other name.

     There is no .trust-band rule and the markup carries no such class: the
     band takes its padding from .section-public.tight and its ground from the
     alternating rule, so a name on it would have nothing behind it.

     Centred and wrapping, with no fixed columns. However many partners there
     are, they centre -- a grid would leave three logos hugging the left of a
     four-track row, and the count here changes whenever somebody ticks a box.

     THE LOGOS ARE SIZED BY HEIGHT, NOT WIDTH, which is the whole trick to a
     logo row: a wordmark is wide and a roundel is square, and constraining the
     width makes the roundel enormous next to a wordmark set in 8pt. A common
     height and `width:auto` makes them look like a set. max-width is a backstop
     for an extremely wide mark on a narrow screen. */
  /* A real band, so it takes .section-public.tight for its padding and its
     ground from the alternating rule like every other one. Nothing to declare
     here but the fact that it is shallower than a section of content -- .tight
     already says that -- so this rule carries no background of its own. Setting
     one would put it outside the alternation and reintroduce exactly the
     "two identical grounds with no seam" problem that rule exists to avoid. */
  /* The label is .eyebrow.center and takes everything from there -- size,
     colour, tint, border, radius. This carries only what an <h2> brings that a
     <div> does not: a browser default top margin, which would push the badge
     off the band's own padding. margin-bottom is left to .eyebrow so the gap
     under it matches every other band's. */
  .trust-label{margin-top:0;}
  .trust-names{
    list-style:none;margin:0;padding:0;
    display:flex;flex-wrap:wrap;justify-content:center;align-items:center;
    gap:20px 44px;
  }
  .trust-names li{display:flex;align-items:center;}
  .trust-logo{
    /* 55px, and images.LOGO_BOX stores 200 so this has 3.636x behind it. The two
       move together: raising this without raising that serves an upscaled
       image, which is soft on every retina screen.

       max-width is a backstop for a freakishly wide mark, not a size. At 190px
       it was doing the sizing instead -- a wordmark capped on width came out
       shorter than the text fallback beside it and the row looked mismatched.
       340 is the widest a 6:1 wordmark can be at this height without three of
       them overflowing the container at 1440px; past that the row wraps, which
       is correct rather than broken. */
    height:55px;width:auto;max-width:340px;
    object-fit:contain;display:block;
    /* Logos arrive in whatever colours their owners use, and a row of them
       against a quiet band is visually louder than anything around it. A
       little desaturation at rest settles the row without editing anybody's
       mark; hover returns it in full. Not grayscale(1) -- that reads as
       "disabled" and flattens two different logos into the same grey. */
    filter:saturate(.75);
    transition:filter .18s ease;
  }
  .trust-names li:hover .trust-logo{filter:none;}
  /* A partner who has consented but not yet sent artwork. Set as a wordmark so
     the row still reads as a row of marks rather than as a gap. */
  .trust-wordmark{
    font-size:var(--text-md);font-weight:700;letter-spacing:.01em;
    color:var(--text-secondary);
  }

  /* THE ABOUT PAGE'S TWO COLUMNS: the argument, and the latest story.
     ---------------------------------------------------------------------
     ONE COLUMN IS THE BASE and two is the exception, which is the opposite way
     round from the rest of this file. The reason is the breakpoint: the split
     is wanted from 1440px UP, and this file's ladder is written with
     max-width, where a `max-width:1440px` block applies AT 1440 and would take
     the columns away at exactly the width they are supposed to appear. A
     min-width query says 1440-and-above once, exactly, with no fractional
     pixel to explain.

     Below that the card is a card under the prose, and the story reads as
     something that follows the argument rather than something beside it. */
  .about-split{
    display:grid;
    grid-template-columns:minmax(0,1fr);
    gap:36px;
  }

  @media (min-width:1440px){
    /* THE RATIO IS THE MEASURE. 1.6fr : 1fr with a 48px gap puts the left
       column at 640px inside the container at 1440 -- measured, not estimated
       -- which is about 75 characters at --text-lg and the width this site's
       prose was capped at until .p-public was changed to max-width:100%.

       That cap is why the ratio is load-bearing now rather than cosmetic: with
       the paragraphs filling whatever they are given, THE COLUMN is the only
       thing deciding how long a line gets. Widen this track and the prose gets
       less readable with it, silently.

       The right column lands at ~400px: wide enough for a title on two lines
       and a summary on several, narrow enough to read as secondary.

       minmax(0, …) on both tracks. A grid track's default minimum is its
       content, so one long unbroken word in a title would push the card wider
       than its share and drag the prose column narrower -- the failure looks
       like the ratio being ignored, which sends you hunting in the wrong
       place. */
    .about-split{
      grid-template-columns:minmax(0,1.6fr) minmax(0,1fr);
      gap:48px;
    }
  }

  /* Nothing published: the prose has the column to itself, at every width. */
  .about-split.is-single{grid-template-columns:minmax(0,1fr);}

  /* The heading loses its top margin: it is the first thing in its column and
     the eyebrow above already spaced them. */
  .about-argument > .h-public:first-child{margin-top:0;}

  /* THE CARD IS AS TALL AS THE PROSE BESIDE IT, and the body text is what
     absorbs the difference.
     ---------------------------------------------------------------------
     align-self:stretch against the grid row, then a flex column inside: the
     kicker, title, meta, picture and link are all fixed, and the summary is
     the one flexible track. It takes the room the others leave and clips what
     will not fit.

     WHY NOT A WORD COUNT. How much text fills a column depends on the column's
     width, the viewport and the reader's own font size -- three things a
     server cannot know. A fixed excerpt leaves a gap under it at most widths
     and overflows at the rest. The projection sends 120 words, four times the
     tallest card measured, and this decides how much of it is seen.

     min-height:0 is not optional. A flex item's default minimum is its content,
     so without it the summary refuses to shrink below its full text and pushes
     the card taller than the row instead of clipping inside it.

     THE CARD IS TAKEN OUT OF THE ROW'S SIZING above 1440, which is the part
     that took two goes. `align-self:stretch` alone does not do it: a grid row
     is as tall as its TALLEST item, so once the card's own text needed more
     room than the prose -- which it did as soon as the column narrowed and the
     same words took more lines -- the card sized the row and the prose
     stretched to IT. Measured before the fix: 616 against 545 at 1200, 843
     against 651 at 901.

     Absolutely positioning the card inside a stretched wrapper leaves the
     wrapper with no content of its own, so the row is sized by the prose alone
     and the card fills whatever that turns out to be. The same trick the
     spotlight portrait uses, for the same reason: an element that must fit a
     box must not be allowed to size it.

     Both of those live in the min-width block above. HERE the card simply
     flows: below 1440 there is one column and no neighbour to match, so the
     card is its own height and the summary's ceiling keeps it a preview. */
  .feature-card{
    background:var(--card);border:1px solid var(--border);
    border-radius:var(--radius-lg);padding:24px;
    display:flex;flex-direction:column;
    box-shadow:0 2px 6px rgba(0,18,74,.06), 0 18px 40px rgba(0,18,74,.14);
  }
  .feature-kicker{
    font-size:var(--text-2xs);font-weight:700;text-transform:uppercase;
    letter-spacing:.08em;color:var(--text-muted);margin:0 0 10px;flex:none;
  }
  /* text-lg: one step above the body's --text-md, so it reads as the card's
     heading without competing with the h1 in the column beside it. */
  .feature-title{
    font-size:var(--text-lg);line-height:1.3;font-weight:700;
    margin:0 0 6px;flex:none;
  }
  .feature-title a{color:var(--text-primary);text-decoration:none;}
  .feature-title a:hover{color:var(--blue-700);text-decoration:underline;}
  .feature-meta{
    font-size:var(--text-xs);color:var(--text-muted);margin:0 0 14px;flex:none;
  }
  /* A band rather than the picture's own shape: the card is a fixed width and
     a fixed height, and a portrait would eat the text it is meant to
     introduce. `cover` crops to the band, which is right for a photograph and
     is why this is not used for logos. */
  .feature-image{
    width:100%;height:auto;object-fit:cover;display:block;
    border-radius:var(--radius);margin:0 0 14px;flex:none;
  }
  /* The one flexible track. The mask fades the last line rather than cutting
     it mid-stroke, which is what says "this continues" -- and the link beneath
     says where. */
  .feature-summary{
    font-size:var(--text-md);line-height:1.65;color:var(--text-secondary);
    margin:0 0 16px;
    flex:1 1 auto;min-height:0;overflow:hidden;
    /* The ceiling is the single-column default. Above 1440 it is lifted and
       the card's own height does the clipping instead. Without it here the 120
       words the projection sends -- sized to fill a desktop column -- would run
       down most of a phone screen. */
    max-height:10.5em;
    /* A FIXED FADE AT THE BOTTOM, not a percentage of the box. As
       `linear-gradient(#000 72%, transparent)` the fade scaled with the
       element: on the tall card the two-column layout produces, the last 28%
       of a 450px box was invisible, so several lines of perfectly good text
       were masked out and the card showed a large blank gap above the link.
       Two rem fades the final line and nothing else, whatever the height. */
    -webkit-mask-image:linear-gradient(#000 calc(100% - 2rem),transparent 100%);
    mask-image:linear-gradient(#000 calc(100% - 2rem),transparent 100%);
  }
  .feature-more{
    display:inline-block;font-size:var(--text-sm);font-weight:700;
    color:var(--blue-700);text-decoration:none;flex:none;margin-top:auto;
  }
  .feature-more:hover{text-decoration:underline;}

  /* THE TWO-COLUMN OVERRIDES, and they sit HERE rather than beside the grid
     rule they belong with -- deliberately, and it is the whole reason this
     block is duplicated at two points in the file.
     ---------------------------------------------------------------------
     These override the card's base rules, which are written just above. A
     media query adds no specificity, so an override that appears BEFORE the
     rule it is overriding loses on source order and does nothing at all.

     Written that way first: `max-height:none` sat next to the grid rule fifty
     lines up, the base `max-height:10.5em` below it won, and the summary was
     capped at six lines inside a card four times that tall. It measured as
     "text fills 100% of its box" while the box was plainly wrong -- the sort
     of failure that looks like flexbox misbehaving. .action-group in app.css
     was bitten by the same ordering a few hours earlier. */
  @media (min-width:1440px){
    /* Out of the row's sizing: an absolutely positioned card leaves its
       wrapper with no content, so the row is sized by the prose alone and the
       card fills whatever that turns out to be. */
    .about-feature{position:relative;align-self:stretch;}
    .feature-card{position:absolute;inset:0;height:auto;}

    /* No ceiling: the card's height is the prose's, and the summary clips to
       whatever the fixed parts leave it. */
    .feature-summary{max-height:none;}
  }

  /* THE FOUNDER'S QUOTE — a dark card inside an ordinary band.
     ---------------------------------------------------------------------
     THE CARD CARRIES THE GROUND, not the band. The section around it takes its
     cream or white from the alternation like every other one; only this panel
     is dark. That is what the mockup's .founder-quote is -- it has a
     border-radius, which a full-width band would have no use for. Built as a
     band first, from reading the colour and not the radius.

     --blue-900 rather than the mockup's --green-900: the mockup is green and
     gold, the site is keyed to the logo, and this is the ground the footer and
     the hero stats already use.

     THE GREYS ARE THE FOOTER'S, already measured against #00124A: #C8D2E4 is
     11.6:1 and #A7B4CC is 8.5:1. White for the quote itself, which is the one
     thing here anybody is meant to read, at 17.7:1.

     FULL WIDTH OF THE CONTAINER. It was capped at 760px first, on the argument
     that a quotation set the whole width reads as a paragraph with a
     background rather than as something somebody said. Nothing else shares
     this band, so the cap only left a gap to its right; the panel takes the
     width and the smaller type does the work of making it read as a quote. */
  .founder-quote{
    margin:0;max-width:100%;
    background:var(--blue-900);
    border-radius:var(--radius-lg);
    padding:32px 34px;
  }
  .founder-quote blockquote{margin:0;}
  .founder-quote p{
    margin:0;
    font-size:var(--text-lg);line-height:1.7;font-weight:400;
    color:#fff;
    /* Hanging punctuation would be tidier, but support is thin enough that
       half the readers would see the quote indented and half would not. */
  }
  .founder-quote figcaption{
    margin-top:20px;
    display:flex;flex-direction:column;gap:2px;
  }
  .fq-name{font-size:var(--text-sm);font-weight:700;color:#C8D2E4;}
  .fq-role{font-size:var(--text-xs);color:#A7B4CC;}

  /* CORE VALUES — a card pasted across the seam between two bands.
     ---------------------------------------------------------------------
     .values-anchor is a zero-height layer pinned to the FIRST section's
     bottom edge, which is the seam. The card inside is pulled up by half its
     own height, so it straddles the join exactly however tall either section
     turns out to be -- and they are not equal, 301px against 350px at 1440.
     Anchoring to the boundary rather than to some midpoint is what lets either
     section's copy grow without the card sliding off the join.

     The anchor holds a .container so the card lands on the same horizontal
     grid as the prose beside it, rather than that grid being reimplemented in
     arithmetic here.

     z-index 1 is all it needs and all it should have: the sticky nav is 900
     and the policy header 800, so this passes under both.

     THE ANCHOR IS A POSITIONING DEVICE, NOT A SURFACE, and pointer-events is
     what says so. The anchor is a zero-height line, but the .container inside
     it is in normal flow and is as tall as the card -- 946px at 1440 -- so its
     box hangs that far BELOW the seam. translateY on the card moves what is
     painted and not what is laid out, so the container stays down there,
     invisible, on z-index 1, over whatever the page put beneath it. On this
     page that is the footer: nine of its ten links were unclickable at 1440
     and every one of them looked perfectly normal.

     Nothing is drawn in that box, so nothing should be hit-tested in it.
     `none` on the anchor covers the container it holds; `auto` on the card
     gives back the one part that is really there, which also keeps its text
     selectable. */
  .values-band{position:relative;}
  .values-band > .section-public{position:relative;}
  /* height:0 IS LOAD-BEARING. With `bottom:0` and an automatic height, an
     absolutely positioned box grows UPWARDS from the edge it is pinned to, so
     the anchor became as tall as the card and the card's translateY(-50%) then
     lifted it clear of the seam entirely -- measured 361px above the join,
     sitting wholly inside the upper section. Collapsed to a line, the anchor
     is the seam, the card flows down from it, and the transform pulls it back
     half its own height so it straddles. overflow stays visible, which is the
     default and the whole point. */
  .values-anchor{
    position:absolute;left:0;right:0;bottom:0;height:0;z-index:1;
    pointer-events:none;
  }
  .values-text{max-width:56%;}

  .values-card{
    width:38%;margin-left:auto;
    transform:translateY(-50%);
    pointer-events:auto;
    background:var(--card);
    border:1px solid var(--border);
    border-radius:var(--radius-lg);
    padding:28px 30px;
    /* Heavier than --shadow, which is tuned for a card sitting on its own
       ground. This one is lifted off two of them and has to read as being in
       front of both. */
    box-shadow:0 2px 6px rgba(0,18,74,.06), 0 18px 40px rgba(0,18,74,.14);
  }
  .values-kicker{
    font-size:var(--text-2xs);font-weight:700;text-transform:uppercase;
    letter-spacing:.08em;color:var(--flame-600);
    margin:0 0 18px;
  }
  .values-list{list-style:none;margin:0;padding:0;}
  .values-list li + li{
    margin-top:16px;padding-top:16px;border-top:1px solid var(--border);
  }
  .values-title{
    font-size:var(--text-lg);line-height:1.35;font-weight:700;
    color:var(--text-primary);margin:0 0 6px;
  }
  .values-desc{
    font-size:var(--text-md);line-height:1.6;color:var(--text-secondary);
    margin:0;
  }

  /* Below 1200 the overlay stops being an effect and starts being a problem.
     The card is 38% of the width; the prose it sits beside is 56%. Those two
     only coexist because there is room for both, and there stops being room
     well before the layout gets narrow enough to look broken -- the prose
     column reaches ~470px at 1200, which is already tight for the line
     lengths in it.

     So the overlay is switched off rather than shrunk. position:static drops
     the anchor back into normal flow at the point where it is written, which
     is ahead of "Financial Integrity in Action" -- the source order was
     chosen for exactly this moment. Nothing moves; the positioning simply
     stops applying and the card lands where it already was in the document.

     The transform goes too. A half-height lift is meaningless once there is
     no seam to straddle, and left in place it would pull the card up over the
     section's top padding. The shadow drops back to --shadow because the card
     is sitting on one ground again, not lifted off two. */
  @media (max-width:1200px){
    /* height:auto is as load-bearing here as height:0 is above, and for the
       mirror-image reason. Collapsed to a line, an absolutely positioned box
       still paints its overflow; a static one collapses AND leaves nothing
       below it, so the card printed directly on top of the prose. Static flow
       needs the box to be the height of what is in it. */
    /* pointer-events back to normal with it. In flow the box is exactly where
       it is drawn, so there is no phantom half hanging over anything and no
       reason for the element to be exempt from being clicked. */
    .values-anchor{position:static;height:auto;pointer-events:auto;}
    .values-card{
      width:auto;margin-left:0;margin-bottom:40px;
      transform:none;
      box-shadow:var(--shadow);
    }
    .values-text{max-width:none;}
  }

  /* Anchored headings land behind the sticky chrome when jumped to from the
     contents rail, so the reader arrives mid-sentence with the heading hidden
     above them. scroll-margin-top is the fix that needs no script and no layout
     change -- it only affects where a scroll stops.

     The blog's own posts have no sticky title, so this clears the nav alone
     there; the policy page adds its header on top and overrides it below. */
  .post-body h2[id],
  .post-body h3[id],
  .post-body h4[id]{scroll-margin-top:88px;}
  .policy-body h2[id],
  .policy-body h3[id],
  .policy-body h4[id]{
    scroll-margin-top:calc(var(--nav-h) + var(--doc-head-h) + 24px);
  }

  /* ---------------------------------------------------------------------
     THE POLICY: A DOCUMENT LAYOUT, NOT A MARKETING PAGE

     Three columns -- the chapters, the chapter, the chapter's sections. Its own
     container because .container is 70% of the viewport, which is right for one
     column of prose and cannot fit three: at 1440px that is 1008px against the
     ~1300 this needs. A documentation page earns more of the screen than a
     landing page, so this one takes it.

     THE MEASURE IS WHAT SIZES EVERYTHING. Body text is --text-lg, 16.8px at the
     top of its clamp, so ~68 characters is about 680px and that is the middle
     column's ceiling. The rails are fixed and the middle takes what is left up
     to that -- the reverse (fluid rails, fixed middle) hands extra width to
     navigation, which does not benefit from it.

     THE RIGHT RAIL DROPS FIRST, at 1440. The left one navigates BETWEEN
     chapters and is the only way to reach them; the right one navigates WITHIN
     a chapter whose headings are already on the screen. Losing the second costs
     a reader a scroll; losing the first costs them the document. It reappears
     above the body as a <details> at every width where the column is gone, so
     it is never simply lost.

     The ladder is 1440 / 1200 / 900 -- the site's existing breakpoints, not new
     ones. This file has twice been bitten by two ladders disagreeing, so the
     columns are sized to fit these values rather than the values chosen to suit
     the columns. */
  .container-doc{max-width:min(94%,1440px);margin:0 auto;padding:0 32px;}

  /* THE TWO STICKY HEIGHTS EVERYTHING ELSE ON THIS PAGE IS MEASURED FROM.
     ---------------------------------------------------------------------
     The site nav pins itself to the top, the document title pins itself below
     the nav, and both rails pin themselves below that. Four numbers that have
     to agree, so they are one pair of variables rather than four literals
     scattered through the file -- the version with literals was already wrong
     once, when the rails' `top: 90px` was written for a header that did not
     stick yet.

     --doc-head-h is the TALLEST the header gets anywhere in its band, not its
     height at one width. The heading is a clamp(), so it moves continuously
     with the viewport while a breakpoint only changes in steps; erring high
     leaves a little gap under the header, erring low slides the rails behind
     it, and a gap is the harmless one.

     Measured at both ends of each band, header including its padding and rule:
       >1440       159 at 1920+, 144 at 1441
       1201-1440   144 at 1440,  137 at 1201
       901-1200    137 at 1200,  127 at 901
     A first pass guessed these from the title alone and came up ~30px short,
     which put the top of the chapter list behind the header. */
  .container-doc{--nav-h:63px;--doc-head-h:159px;}

  /* Centred, and pinned under the nav so the document scrolls beneath it.
     Twenty-four chapters share this title, and a reader three screens into
     §9 should still be able to see what they are reading.

     The background is not decoration: without it the body's text would show
     through as it passes underneath. --cream is the page's own ground, so the
     header reads as part of the page rather than as a panel over it, and the
     rule beneath it is what makes the boundary legible while text slides past.

     z-index below the nav's 900, above everything else: this passes under the
     nav and the content passes under this. */
  .doc-head{
    position:sticky;top:var(--nav-h);z-index:800;
    text-align:center;
    background:var(--cream);
    padding:24px 0 20px;
    border-bottom:1px solid var(--border);
    margin-bottom:32px;
  }
  /* The eyebrow's own 25px is spacing for a section heading with prose under
     it. Inside a bar that is permanently on screen it is 25px of nothing. */
  .doc-head .eyebrow{margin-bottom:12px;}
  .doc-head .h-public{margin:0;max-width: 100%;}

  .doc-layout{
    display:grid;
    grid-template-columns:280px minmax(0,680px) 260px;
    gap:48px;
    align-items:start;
  }

  /* Both rails stick, independently of each other and of the body. `top` clears
     the nav AND the document title above it -- computed from the two variables
     rather than restated, so moving either one moves these with it. max-height
     plus overflow means a twenty-four chapter contents scrolls inside its own
     column rather than running off the bottom of a short viewport with no way
     to reach the end. */
  .doc-chapters,
  .doc-sections{
    position:sticky;
    top:calc(var(--nav-h) + var(--doc-head-h) + 24px);
    max-height:calc(100vh - var(--nav-h) - var(--doc-head-h) - 48px);
    overflow-y:auto;
    /* The scrollbar sits off the text rather than over it. */
    padding-right:8px;
  }

  .doc-rail-title{
    font-size:var(--text-2xs);font-weight:700;text-transform:uppercase;
    letter-spacing:.06em;color:var(--text-muted);
    margin:0 0 12px;
  }

  /* list-style:none on an <ol> and the numbers come from the titles themselves,
     which already carry them ("9. Consent Framework"). A list marker as well
     would render "9. 9. Consent Framework". The <ol> stays an <ol> because the
     order is meaningful -- a policy is read in sequence. */
  .doc-chapter-list,
  .doc-section-list{list-style:none;margin:0;padding:0;}

  .doc-chapter{
    display:block;
    padding:7px 10px;
    border-left:2px solid transparent;
    border-radius:0 6px 6px 0;
    font-size:var(--text-sm);
    line-height:1.4;
    color:var(--text-secondary);
    text-decoration:none;
  }
  .doc-chapter:hover{background:var(--cream);color:var(--text-primary);}
  /* The chapter being read. Border and weight as well as colour, because colour
     alone is not a distinction for every reader. */
  .doc-chapter.is-current{
    border-left-color:var(--flame-500);
    background:var(--flame-100);
    color:var(--flame-600);
    font-weight:600;
  }

  /* The middle column. min-width:0 because a grid track's default minimum is
     its content, so one long unbroken string -- a URL, a CAC number -- would
     push the track wider than its declared maximum and take the rails with it. */
  .doc-body{min-width:0;}

  .doc-title{
    font-size:var(--text-xl);line-height:1.25;color:var(--text-primary);
    font-weight:600;margin:0 0 24px;
  }

  .doc-section-list li{margin-bottom:2px;}
  .doc-section-list a{
    display:block;padding:5px 10px;border-radius:6px;
    font-size:var(--text-xs);line-height:1.4;
    color:var(--text-secondary);text-decoration:none;
  }
  .doc-section-list a:hover{background:var(--cream);color:var(--text-primary);}
  /* H3 IS THE BASE LEVEL IN THIS RAIL, not h2, and that is not arbitrary: the
     page is h1 the document, h2 the chapter, so the chapter's own sections
     start at h3. They are the top level of what this rail lists and sit flush.

     h4 indents under them. Nothing in the policy uses one today -- every
     sub-part of every chapter is one level deep -- but the rule is here so a
     chapter that grows one reads correctly without a stylesheet change.

     h2 is kept flush as well. It should not appear inside a chapter body, but
     the editor offers H2 and a staff member may pick it; flush is the least
     wrong thing for a heading that outranks everything around it. */
  .doc-section-l2 a,
  .doc-section-l3 a{padding-left:10px;}
  .doc-section-l4 a{padding-left:24px;}

  /* The date this chapter's published text last changed. Bottom right, apart
     from the prose by a rule: it is provenance, not part of the document. */
  .doc-updated{
    margin:36px 0 0;padding-top:14px;border-top:1px solid var(--border);
    font-size:var(--text-xs);color:var(--text-muted);text-align:right;
  }

  /* Shown only where the right rail is not. Exactly one of the two is in the
     accessibility tree at any width, because display:none removes the other. */
  .doc-sections-inline{display:none;}
  .doc-sections-inline summary{
    cursor:pointer;padding:10px 12px;border:1px solid var(--border);
    border-radius:var(--radius);background:var(--cream);
    font-size:var(--text-sm);font-weight:600;color:var(--text-primary);
  }
  .doc-sections-inline[open] summary{margin-bottom:8px;}
  .doc-sections-inline{margin:0 0 28px;}

  /* The footer's greys were the mockup's, and the mockup's ground is green.
     Carried onto --blue-900 they read as muddy rather than as neutral, so these
     are retuned to the same lightnesses on a blue axis. Measured against
     #00124A: #C8D2E4 is 11.6:1, #A7B4CC is 8.5:1, #93A3C0 is 7.0:1 -- all well
     past AA, which matters here because the smallest of them is 11.5px. */
  .site-footer{background:var(--blue-900);color:#C8D2E4;padding:48px 0 24px;}
  /* Five columns. The first keeps its 1.4fr because it carries the mark and a
     sentence of prose, and the paragraph sets over four lines the moment that
     track narrows; the four link columns share what is left equally, so adding
     the fifth took width from them rather than from the brand. */
  .footer-grid{display:grid;grid-template-columns:1.4fr repeat(4,1fr);gap:30px;margin-bottom:30px;}
  .footer-brand .brand-logo{width:24px;height:24px;}
  .footer-col h4{font-size:var(--text-xs);text-transform:uppercase;letter-spacing:.05em;color:#93A3C0;margin:0 0 14px;font-weight:700;}
  .footer-col a{display:block;font-size:var(--text-sm);color:#C8D2E4;margin-bottom:9px;cursor:pointer;}
  .footer-col a:hover{color:#fff;}
  /* The stats column is a list where the others are links, and it has to sit at
     the same size and rhythm as them or the fourth column reads as a different
     kind of thing. */
  .footer-stats{list-style:none;margin:0;padding:0;}
  .footer-stats li{font-size:var(--text-sm);color:#C8D2E4;margin-bottom:9px;}
  .footer-stats strong{color:#fff;font-weight:700;}
  .footer-brand{display:flex;align-items:center;gap:10px;margin-bottom:14px;}
  .footer-brand .sl-name{color:#fff;font-size:var(--text-md);font-weight:700;}
  .footer-desc{font-size:var(--text-xs);line-height:1.6;color:#A7B4CC;max-width:260px;}
  /* The rule, then the copyright with the policy line pushed to the far right.
     space-between does that with two children and needs no third. */
  .footer-bottom{border-top:1px solid rgba(255,255,255,0.12);padding-top:18px;font-size:var(--text-xs);color:#93A3C0;display:flex;justify-content:space-between;flex-wrap:wrap;gap:8px;}

  /* The measure and the card ladder, in one place.
     ---------------------------------------------------------------------------
     Four columns only above 1920px. A spotlight card is a horizontal thing --
     a square portrait on the left, the name, place and quote to the right --
     and the portrait's side is the card's height, so a narrow card does not
     merely crowd the text, it hands the picture more of the width as it goes.
     At 1501px, four columns gave each card 233px with 40px left for the quote.

     The measure widens as the screen narrows, which is the opposite of how it
     reads: 70% of a large monitor is a comfortable column, and 70% of a laptop
     is a waste of a third of it. So the container is let out in steps, and the
     nav bar follows it exactly -- the logo has to sit on the same left edge as
     the copy beneath it.

     Ordered widest-first, and that ordering is load-bearing: these are all
     max-width queries of equal specificity, so the later one wins where they
     overlap.

     THE STEPS ONCE SHARED IT and no longer do, and neither do the spotlights. The three
     rows of cards on this site have three different shapes and have ended up
     with three different ladders, which is the honest answer rather than a
     failure to consolidate: a spotlight is horizontal and a narrow card hands
     its portrait more of the width as it goes, so three is its ceiling; a step
     card is a numeral, a title and a sentence, and there are exactly four of
     them, so anything other than two columns or one orphans the fourth.

     THREE IS THE MOST COLUMNS THE SPOTLIGHTS HAVE. Four used to appear above
     1920px and it was the same squeeze this ladder exists to avoid, at the
     other end: 307px per card with 107px left for the words. Two columns hold
     to 1680px, where three still leaves the text comfortable. */
  @media (max-width:1680px){
    .spotlight-grid,
    .track-grid{grid-template-columns:repeat(2,1fr);}
  }

  /* These two carry the measure only. The column counts they used to restate
     are gone: two columns already hold from 1680 down, so repeating them here
     said nothing and gave two more places for the ladder to disagree with
     itself. */
  @media (max-width:1440px){
    .container,
    .site-nav-inner{max-width:80%;}
    .site-nav-inner{gap:15px;}

    /* THE POLICY LOSES ITS RIGHT RAIL HERE. Three columns need ~1300px and
       94% of 1440 is 1354, so this is the last width at which they fit -- one
       step further down and the middle column would be squeezed below its
       measure to keep a list of links that the body already shows as headings.
       The list moves above the body as a <details>; it is not lost. */
    /* The title shrinks with the viewport, so what the rails have to clear
       shrinks with it. Measured 112px at 1441. */
    .container-doc{--doc-head-h:144px;}

    .doc-layout{grid-template-columns:260px minmax(0,720px);gap:40px;}
    .doc-sections{display:none;}
    .doc-sections-inline{display:block;}
  }

  @media (max-width:1200px){
    .container,
    .site-nav-inner{max-width:90%;}
    .site-nav-inner{gap:15px;}

    /* Measured 104px at 1200. */
    .container-doc{--doc-head-h:137px;}

    /* A ceiling, not 1fr. With the right rail gone the middle would otherwise
       take every pixel the rail used to hold -- 792px at this width, which is
       past 80 characters and further past the measure than the three-column
       layout ever goes. Left-over width is left over. */
    .doc-layout{grid-template-columns:240px minmax(0,760px);gap:32px;}

    /* The pipeline wraps to two rows of two. At four across below this width
       the labels start breaking mid-word -- "Prospective" over two lines in a
       cell 110px wide. A wrapped cell has no neighbour to share a border with,
       so each one takes its radius and its left border back. !important
       because the shared-border rules above are more specific than these
       (`.pipeline-step + .pipeline-step` is (0,2,0)); the alternative is
       restating that selector here, which puts the arrangement in two places
       that have to agree. */
    .pipeline-row{flex-wrap:wrap;}
    .pipeline-step{
      flex:1 1 45%;
      border-radius:var(--radius-lg) !important;
      border-left:1px solid var(--border) !important;
    }

    /* Two by two, for the same reason as the pipeline above it: four money
       figures across a 90% container is about 240px each, and --text-xl runs
       to 30px, so "₦3.7m" and its uppercase label stop sharing a line. */
    .impact-kpi-row{grid-template-columns:repeat(2,1fr);}

    /* Four short cards hold to 1200 -- 230px each at 1440, which is less
       than the programme page gives its four and enough for a title and two
       lines. Below this they halve rather than stepping to three, which
       would leave the fourth alone on a row of its own. */
    .involve-grid{grid-template-columns:repeat(2,1fr);}
  }

  /* One column, edge to edge. Below this the card has the whole content width,
     which is what a horizontal card needs once the portrait is 200px of it. */
  @media (max-width:900px){
    .steps-row,
    .track-grid,
    .involve-grid,
    .fund-public-grid,
    .blog-grid,
    .spotlight-grid{grid-template-columns:1fr;}

    /* The About split needs nothing here: one column is its base rule and the
       two-column layout is confined to a min-width block further up. */

    /* A step between the desktop 80 and the phone 44, so the marks shrink with
       the room rather than dropping straight to phone size on a tablet. */
    .trust-logo{height:60px;max-width:250px;}
    .trust-names{gap:18px 32px;}

    /* THE POLICY GOES SINGLE-COLUMN, and the chapter list becomes a bounded,
       scrolling box above the chapter. Twenty-four chapters at full height
       would be most of a phone screen before the first sentence; capped at 46vh
       they are still there to be scanned and scrolled without a tap, which a
       collapsed <details> would have cost.

       It stops being sticky here. A rail pinned to the top of a phone viewport
       is spending scarce height on navigation while somebody is trying to read.

       The container matches .container at this width rather than keeping its
       own: "94% capped at 1440" and "100% with 4% gutters" differ by about two
       percent here, and one gutter rule on a phone is easier to reason about
       than two. */
    /* THE TITLE STOPS STICKING HERE, for the reason the rails already stop:
       a phone has ~150px of chrome before this page contributes anything, and
       spending another 80 on a heading that never changes is spending the
       screen somebody is trying to read on. The nav alone stays pinned.

       --doc-head-h drops to zero rather than the rule being deleted, because
       the anchor offsets are computed from it -- a jumped-to heading here has
       only the nav to clear. --nav-h rises because the wrapping mobile nav
       measures 71px against the desktop 63. */
    .container-doc{
      max-width:100%;padding:0 4%;
      --nav-h:71px;--doc-head-h:0px;
    }
    .doc-head{
      position:static;
      padding:0 0 16px;
      margin-bottom:24px;
    }

    .doc-layout{grid-template-columns:1fr;gap:0;}
    .doc-chapters{
      position:static;max-height:none;overflow:visible;
      padding:0;margin-bottom:24px;
    }
    .doc-chapter-list{
      border:1px solid var(--border);border-radius:var(--radius);
      padding:6px;background:var(--cream);
      max-height:min(46vh,320px);overflow-y:auto;
    }
  }

  @media (max-width:1100px){
    /* Back to a wrapping row. Three tracks cannot hold the logo, six links and
       the button side by side on a narrow screen, and a centred middle track
       there would squeeze the links to one word a line. The logo reclaims its
       auto margin so the button stays at the right edge as things wrap. */
    .site-nav-inner{display:flex;flex-wrap:wrap;}
    .site-logo{margin-right:auto;}
    .site-navlinks{justify-content:flex-start;}

    /* NEITHER CARD GRID IS HERE, and that is deliberate. Both keep two columns
       down to 900px and only then go single -- see the ladder above. Either one
       left in this block would have won on source order, being later, and
       quietly held its cards to one column for the whole 900-1100 range. The
       steps did exactly that until they were moved onto the shared ladder. */

    /* The footer goes two across rather than one, with the brand across the
       top. Five equal stacked blocks wasted most of a tablet's width, and the
       first block is the odd one out anyway -- a mark and a sentence of prose
       where the other four are short lists.

       :first-child rather than a class, because the brand block is the only
       child of this grid without .footer-col and its position is what makes it
       the brand block. Add another bare <div> ahead of it and this moves. */
    .footer-grid{grid-template-columns:repeat(2,1fr);}
    .footer-grid > div:first-child{grid-column:1 / -1;}
  }

  /* The content column stops being a percentage of the window.
     ---------------------------------------------------------------------------
     70% is a desktop measure and nothing else. Being a percentage it takes the
     same 30% off a phone as off a monitor, and it does so before any element is
     laid out -- so every "the cards are cramped on mobile" symptom on this site
     starts here rather than at the cards. Measured, at .container's own width
     as a share of the viewport:

         1920  67%      900  63%      430  55%
         1440  66%      768  62%      390  54%
         1100  64%      600  59%      360  52%

     A phone was losing nearly half its screen. Released at 900px, the width the
     app uses for the same purpose (app.css: "Tablet and below: the content
     gutter tightens"), so the two products narrow alike.

     Percentage padding rather than px, because the requirement is a proportion:
     4% a side IS 92% of content, at every width, with nothing to recompute if
     the number changes.

     ONE STEP, NOT TWO. There was a second at 700px taking the gutter down to
     2.5% for 95% of content, and it read well in a desktop browser narrowed to
     a phone's width -- but not on the phones themselves, where 2.5% left too
     little between the text and the edge of the glass. 4% holds from 900px
     down, so there is one number here rather than two that have to agree.

     max-width:100% and not `none`: .container.narrow keeps its 760px cap, so a
     prose page still stops widening on a tablet rather than running to the full
     edge-to-edge measure. */
  @media (max-width:900px){
    .container{max-width:100%;padding:0 4%;}
    /* The nav bar's contents track .container exactly -- its own rule says to
       change the two together, and this is why: the bar spans the window but
       the logo has to sit on the same left edge as the copy beneath it. Both
       percentages resolve against the viewport, so they land on the same pixel
       without either needing to know the other's value.

       Vertical padding is untouched. It is the bar's height, which has nothing
       to do with how wide the screen is. */
    .site-nav-inner{max-width:100%;padding:14px 4%;}

    /* The hamburger, and the control that replaces it.
       -------------------------------------------------------------------------
       Identical boxes and identical icons, because to a reader they are one
       control that opens and closes -- tapping the same square twice. Only the
       href differs: one points at the panel, the other clears the fragment.

       Three bars drawn from one element and its two pseudo-elements, so the
       icon is a colour and a couple of lengths rather than a file to fetch. */
    .nav-toggle,
    .nav-close{
      display:inline-flex;align-items:center;justify-content:center;
      width:42px;height:42px;flex:0 0 auto;
      border:1px solid var(--border-strong);border-radius:8px;
      background:#fff;color:var(--text-primary);
    }
    /* Closed is the resting state, so the closer is not drawn until there is
       something to close. */
    .nav-close{display:none;}

    /* The swap, and it needs no :has(). Both controls are later siblings of the
       panel, so an ordinary ~ reaches them once the panel is targeted. That is
       the whole reason they sit after it in the markup. */
    .site-menu:target ~ .nav-toggle,
    .site-menu.open ~ .nav-toggle{display:none;}
    .site-menu:target ~ .nav-close,
    .site-menu.open ~ .nav-close{display:inline-flex;}
    .nav-toggle-bars{position:relative;}
    .nav-toggle-bars,
    .nav-toggle-bars::before,
    .nav-toggle-bars::after{
      display:block;width:18px;height:2px;border-radius:2px;background:currentColor;
    }
    .nav-toggle-bars::before,
    .nav-toggle-bars::after{content:"";position:absolute;left:0;}
    .nav-toggle-bars::before{top:-6px;}
    .nav-toggle-bars::after{top:6px;}

    /* Closed is the default, so a browser that has never heard of :target shows
       the bar and nothing else -- rather than a menu stuck open across the
       page. */
    .site-menu{display:none;}

    /* THE READER'S PLACE, and the whole reason :target is survivable here.
       -------------------------------------------------------------------------
       Following a fragment does not merely apply :target -- the browser also
       scrolls the targeted element to the top of the viewport, and it does that
       whether or not the element is already in view. The panel hangs 70px down
       from the top of a stuck bar, so opening the menu scrolled the page down
       by exactly the height of that bar and left the reader 70px from where
       they were. Closing it did not scroll back.

       scroll-margin-top cancels it. The browser scrolls to (element top -
       scroll-margin-top), so a margin equal to the panel's own offset from the
       top of the viewport resolves to the scroll position already in effect,
       and nothing moves. The bar is sticky, so that offset is the same at every
       scroll position and one value is correct everywhere.

       70px AND NOT 71px, which is the bar's full height: the panel is
       positioned with top:100%, and an absolutely positioned box resolves
       percentages against its containing block's PADDING box -- so it hangs
       from below the 1px border-bottom, not below the border. Keeping the two
       in step is what the number is doing here.

       This value must track the bar. If the bar's height changes and this does
       not, the reader is displaced by the difference --
       tests_browser.MobileMenuBrowserTests fails when it drifts, which is how
       it was found. */
    .site-menu{scroll-margin-top:70px;}

    /* A panel hung under the bar, positioned against .site-nav.
       -------------------------------------------------------------------------
       NOT position:fixed, and this cost an hour. .site-nav carries
       backdrop-filter, and a backdrop-filter makes an element the containing
       block for fixed descendants -- so `inset:0` resolved against the 70px
       header instead of the viewport. Every link computed as visible and every
       one was scrolled out of a 70px box by overflow-y. Only the close control
       fell inside it, which is precisely what showed on the page.

       Absolute against the bar is what was wanted anyway: the menu belongs to
       the bar, and hanging from top:100% keeps it there whether the header is
       stuck to the top of the window or sitting at the top of the document.

       The reader keeps their place either way -- the bar is sticky, so the
       targeted element is already on screen and there is nothing for :target to
       scroll to. That is asserted in tests_browser.py rather than assumed.

       `.open` sits beside `:target` and does nothing yet. It is the hook the
       staff app's drawers already use (app.css: `.drawer.open, .drawer:target`)
       so a script can later add Esc, focus and click-outside -- without the
       base ever depending on one having loaded. */
    .site-menu:target,
    .site-menu.open{
      display:flex;flex-direction:column;align-items:stretch;
      position:absolute;top:100%;left:0;right:0;z-index:1000;
      background:var(--cream);border-bottom:1px solid var(--border);
      box-shadow:0 12px 24px rgba(0,18,74,0.10);
      padding:8px 4% 20px;
      max-height:calc(100vh - 100%);overflow-y:auto;
    }

    /* Order, not DOM position. The close link reads last in the markup, after
       the things it closes, which is the sensible reading order; visually it
       belongs at the top where the hamburger just was. */
    /* Stacked, and each link a full-width target. 44px is the smallest thing a
       thumb should be asked to hit. */
    .site-menu:target .site-navlinks,
    .site-menu.open .site-navlinks{flex-direction:column;gap:0;width:100%;}
    .site-menu:target .site-navlinks a,
    .site-menu.open .site-navlinks a{
      font-size:17px;padding:14px 0;min-height:44px;display:flex;align-items:center;
      border-bottom:1px solid var(--border);
    }
    /* The underline that marks the current page is a 2px bottom border on the
       desktop bar, which here would be indistinguishable from the row dividers
       above. A bar down the left edge instead, in the same amber. */
    .site-menu:target .site-navlinks a.active,
    .site-menu.open .site-navlinks a.active{
      border-bottom-color:var(--border);
      box-shadow:inset 3px 0 0 var(--amber-600);padding-left:12px;
    }

    .site-menu:target .cta-donate,
    .site-menu.open .cta-donate{
      margin-top:20px;text-align:center;padding:14px 18px;font-size:15px;
    }
  }

  /* Phones: the footer stops being a row of columns and becomes a column.
     ---------------------------------------------------------------------------
     Five stacked blocks, all left-aligned against a 4% gutter, read as a list
     that lost its table. Centred they read as a footer.

     600px is the app's own smallest breakpoint (app.css uses 599/600 for the
     point where its tables stop being tables), so the two products change shape
     at the same widths.

     AFTER the 900 block, and that ordering is load-bearing: both are max-width
     queries of equal specificity, so the later one wins where they overlap. */
  @media (max-width:600px){
    /* The badge gives its label room back on a phone. Turning the eyebrow into
       a pill cost it 26px of width -- 12px of padding a side and a 1px border --
       and it used to be a plain block whose text had the whole container. The
       longest labels were the ones that noticed. Tighter padding and half the
       letter-spacing return about 20px of that, which is most of what the
       chrome took. */
    .eyebrow{padding:5px 8px;letter-spacing:.04em;}

    /* The trust strip tightens rather than becoming a tall column of marks.
       Three logos at 80px, stacked, is most of a phone screen before the
       reader has met the first section, and this band is meant to be
       registered on the way past rather than read. */
    .trust-names{gap:14px 24px;}
    .trust-logo{height:44px;max-width:190px;}
    .trust-wordmark{font-size:var(--text-sm);}

    /* Back to one column. Two columns of links on a phone leaves each about
       150px, and "Volunteer or mentor" wraps in the middle of the phrase.

       CENTRED BY text-align, NOT BY justify-items, and the difference is the
       whole bug. justify-items:center sizes each grid item to max-content --
       so a column came out exactly as wide as its longest link and not a pixel
       more. "Volunteer or mentor" had a 110px column for 110px of text, with
       331px of screen going spare beside it.

       That fits here and wraps on a real phone, because zero slack means any
       difference in metrics breaks the line: Raleway not yet loaded and a
       fallback standing in, a reader who has asked for larger text, a different
       rounding. Stretching the items to the full width and centring the text
       inside them gives every link three times the room it needs, and the
       layout stops depending on the font arriving. */
    .footer-grid{grid-template-columns:1fr;text-align:center;}
    .footer-col{width:100%;}
    /* The mark and the name are a flex row of their own, so text-align cannot
       reach them. */
    .footer-brand{justify-content:center;}
    /* It carries a 260px measure, which without auto margins would sit hard
       against the left edge of a centred column. */
    .footer-desc{margin-left:auto;margin-right:auto;}
    /* Nothing to space apart once the two lines are stacked; centred and with a
       gap they read as one closing block rather than as a broken row. */
    .footer-bottom{justify-content:center;text-align:center;gap:10px;}
  }
