/* ============================================================================
   CHES — v3 foundation
   Palette: Marigold / White / Ink. Chosen against the sector, not in a vacuum:
   every major peer (Pratham, HelpAge, charity: water, CRY, MSF) runs a
   saturated warm brand colour on a white ground. The v2 forest-and-brass
   scheme read as boutique hotel rather than NGO.

   Tokens are named for their JOB, not their colour, so a future palette move
   is a value change in this block and nothing else.

   Every foreground-background pair used for text was computed, not eyeballed
   — ratios recorded beside each token so a later edit can't quietly break
   them. --brand is a FILL, never small text on white: it is only 2.26:1 there.
   ========================================================================= */

:root {
  /* --- ground + ink --- */
  --ink:         #241f18;  /* headings, dark bands        16.75:1 on white */
  --ink-deep:    #141210;  /* footer, gradient floor */
  --ground:      #ffffff;  /* page ground */
  --ground-warm: #faf6ef;  /* alternating sections */
  --raised:      #ffffff;  /* cards, raised surfaces */

  /* --- accent: three tints, each with exactly one job --- */
  --brand:       #f5a524;  /* fills + buttons. Ink label on it =  9.24:1 */
  --brand-ink:   #8a5300;  /* small text / links on white         6.10:1 */
  --brand-lit:   #ffc35c;  /* accent text on ink bands           10.55:1 */

  /* --- supporting --- */
  --neutral:     #a89e93;  /* rules, decorative only — never small text */
  --neutral-ink: #6b625a;  /* neutral-toned small text            5.77:1 on white */
  --muted:       #565049;  /* body copy                           7.48:1 on white */

  --border:      color-mix(in srgb, var(--ink) 12%, var(--ground));
  --border-firm: color-mix(in srgb, var(--ink) 22%, var(--ground));
  --overlay:     color-mix(in srgb, var(--ink) 78%, transparent);

  --text:        var(--ink);
  --text-muted:  var(--muted);

  /* --- type ---
     Libre Franklin at 900 replaces Newsreader. Newsreader was a mistake I made
     and the client caught it: it sits on the documented list of faces that
     model-generated pages default to, right beside the Fraunces it replaced.
     Swapping one default for another was not a fix.

     Libre Franklin is a Franklin Gothic revival — the newspaper-masthead
     grotesk. It carries institutional authority at weight rather than through
     ornament, and it is the register every large NGO actually uses (Smile
     Foundation, HelpAge, Save the Children all run a heavy sans). It also
     survives being set white over a photograph, which a text serif does not.

     To revert: change these two lines and the <link> in the page heads. */
  --font-display: "Libre Franklin", "Helvetica Neue", Arial, sans-serif;
  --font-body:    "Geist", system-ui, -apple-system, "Segoe UI", sans-serif;

  /* --- metrics --- */
  --max-width: 1240px;
  --measure: 62ch;
  --gutter: clamp(1.15rem, 4vw, 2.5rem);

  /* The field photographs are all 1500x1050 — upscaled from the original 912x638
     and then normalised to identical dimensions on conversion, so this one ratio
     is exact for every single one and no frame can crop by a rounding error.
     Every frame that displays a photograph uses it: a 4/3 or 16/9 frame would
     slice heads and painted banners out of shots never composed for it, and the
     banners are the site's strongest proof. */
  --photo-ratio: 1500 / 1050;
  --radius: 4px;          /* v1 used 10px; premium editorial reads squarer */
  --radius-lg: 8px;

  --shadow:    0 1px 2px color-mix(in srgb, var(--ink) 8%, transparent),
               0 4px 16px color-mix(in srgb, var(--ink) 6%, transparent);
  --shadow-lg: 0 12px 48px color-mix(in srgb, var(--ink) 14%, transparent);

  /* --ease drives small UI transitions. --ease-soft is the long-tailed
     entrance curve. --ease-mask is near-linear at the start and settles hard,
     which is what a mask wipe needs to look mechanical rather than bouncy. */
  --ease:      cubic-bezier(0.22, 0.61, 0.36, 1);
  --ease-soft: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-mask: cubic-bezier(0.65, 0.05, 0.36, 1);
}

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

/* scroll-padding-top on the scroll container is what actually governs where a
   fragment jump lands — scroll-margin-top on the target alone did not do it.
   Without this, every in-page link ("About", the programme tabs, the FAQ
   anchors) parks its target underneath the sticky header, so the visitor
   arrives at a heading they cannot see. This reserves the header's height for
   every anchor on the site at once. */
html { scroll-behavior: smooth; scroll-padding-top: 92px; }

/* ---------- page shell ----------
   Everything used to run edge to edge, so on a very wide monitor — or any time
   the page is zoomed out — the dark bands stretched the full width of the
   screen while the text inside them stayed at 1240px, leaving enormous empty
   runs of colour at both ends. The page read as a few narrow columns of content
   floating in a huge field of nothing.

   Capping the whole document, bands included, keeps the site as one object with
   a defined edge. Past 1560px the surplus becomes plain white margin rather
   than stretched sections. Below that nothing changes at all. */
html { background: var(--ground); }

body {
  margin: 0;
  max-width: 1560px;
  margin-inline: auto;
  background: var(--ground);
  color: var(--text);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  line-height: 1.6;
  letter-spacing: -0.005em;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  overflow-x: hidden;      /* parallax translate can overhang by a few px */
}

/* ---------- type scale ----------
   Display sizes run much larger than v1. The single biggest difference
   between a template site and an expensive one is confidence at the top of
   the scale and restraint at the bottom. */

/* Heavier weight and tighter tracking as the size climbs — a grotesk set at
   display size needs negative tracking to stop looking gappy, and the floor
   for that is -0.04em. Bottom margin is deliberately smaller than the space
   above a heading, so a heading binds to the text it introduces rather than
   floating between two blocks. */
h1, h2, h3, h4 {
  font-family: var(--font-display);
  color: var(--ink);
  margin: 0 0 0.42em;
  line-height: 1.02;
  text-wrap: balance;          /* no orphaned last word in a headline */
}

h1 { font-size: clamp(2.7rem, 7vw, 5rem);   font-weight: 900; letter-spacing: -0.038em; }
h2 { font-size: clamp(1.9rem, 4.2vw, 3rem); font-weight: 900; letter-spacing: -0.032em; }
h3 { font-size: clamp(1.1rem, 1.6vw, 1.32rem); font-weight: 700; letter-spacing: -0.018em; line-height: 1.22; }
h4 { font-size: 0.98rem; font-weight: 700; letter-spacing: -0.008em; }

/* Space above a heading is generous, space below is tight — the rule the
   spacing floor asks for, applied once here instead of per-component. */
* + h2 { margin-top: 1.7em; }
* + h3 { margin-top: 2em; }

p { margin: 0 0 1.15rem; color: var(--text-muted); }
ul { color: var(--text-muted); padding-left: 1.1rem; }
li { margin-bottom: 0.45rem; }

a { color: var(--brand-ink); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 3px; }

strong, b { font-weight: 600; color: var(--ink); }

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

/* Available to assistive tech, invisible on screen. Twelve links all reading
   "Enquire about this" are indistinguishable to someone tabbing through with a
   screen reader; this appends the programme name to each without cluttering the
   card. clip-path over display:none because hidden elements are not announced. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---------- unified photo grade ----------
   The 13 field photographs were shot across many years on different cameras,
   so each one arrives with its own colour cast — some yellow-green, some cold,
   highlights blown to different degrees. Side by side that inconsistency is
   what reads as "old snapshots," far more than the resolution does: a viewer
   registers mismatched colour long before they count pixels.

   One grade across all of them pulls the set together, so they read as a body
   of work that was art-directed rather than as whatever was on the hard drive.
   Deliberately mild — enough to unify, not enough to look filtered, and it
   keeps the skin tones and the painted banners honest. Revisit once the
   upscaled files land, since those may not need it. */
.photo-card img,
.split__media img,
.feature__media img,
.hero__media img {
  filter: saturate(0.9) contrast(1.04) brightness(1.01);
}

/* ---------- browser surfaces ----------
   The parts nobody draws still carry the design, and shipping them at their
   browser defaults is the cheapest tell that a page was assembled rather than
   built: a blue selection highlight and a grey scrollbar belong to no design
   system on earth. Themed from the palette. */

::selection { background: var(--brand); color: var(--ink); }

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

html { scrollbar-color: var(--neutral) var(--ground-warm); }
::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track { background: var(--ground-warm); }
::-webkit-scrollbar-thumb {
  background: var(--neutral);
  border: 3px solid var(--ground-warm);   /* inset the thumb so it reads as a rail */
  border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover { background: var(--brand-ink); }

/* Every figure on this site is a number in a column that gets compared with
   another number. Proportional digits make those columns ragged. */
.stat__figure, .plate__figure, .fig, td, th { font-variant-numeric: tabular-nums; }

a:hover { text-underline-offset: 3px; text-decoration-thickness: 1px; }

/* ---------- layout primitives ---------- */

.container {
  width: 100%;
  max-width: var(--max-width);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.container--narrow { max-width: 880px; }

/* Tightened from the first v2 pass: 9rem top and bottom on a large screen put
   ~290px of dead ground between every section, which read as the page being
   unfinished rather than as breathing room. */
.section { padding-block: clamp(3.25rem, 6vw, 5.5rem); position: relative; }
.section--tight { padding-block: clamp(2.25rem, 4vw, 3.5rem); }
.section--warm { background: var(--ground-warm); }
.section--raised { background: var(--raised); }
.section--dark { background: var(--ink); }
.section--dark h1, .section--dark h2, .section--dark h3, .section--dark h4 { color: var(--ground); }
.section--dark p  { color: color-mix(in srgb, var(--ground) 76%, var(--ink)); }
.section--dark a  { color: var(--brand-lit); }

.lede {
  max-width: var(--measure);
  font-size: clamp(1.08rem, 1.5vw, 1.28rem);
  line-height: 1.55;
  color: var(--text-muted);
}

/* ---------- eyebrow: REMOVED ----------
   There was a letter-spaced brand-coloured caps label with a rule above every
   single heading on the site — 21 of them. Repeated at that frequency it stops
   being a device and becomes the page's signature, and it is the single most
   reliable marker of generated layout. A heading carries its own weight; the
   label was telling the reader what the heading was about to tell them.
   All 21 elements are deleted from the markup, not hidden here.            */

/* ---------- display accent ----------
   Colour only. This previously also switched the second half of every heading
   into italic — so "We take the clinic / to the street." changed face, weight
   axis and colour in one phrase, on every heading, on every page. Three
   decisions where one does the job, and the compounding of them is what read
   as machine-made. The colour shift alone is the gesture.                   */

.accent { color: var(--brand-ink); }
.section--dark .accent { color: var(--brand-lit); }

/* Underline that draws itself when the heading reveals. */
.rule-under {
  display: block;
  width: 64px;
  height: 2px;
  background: var(--brand);
  margin-top: 1.4rem;
  transform: scaleX(0);
  transform-origin: 0 50%;
  transition: transform 0.9s var(--ease-mask) 0.25s;
}
.is-visible .rule-under, .rule-under.is-visible { transform: scaleX(1); }

/* ---------- inline statistic ---------- */

.stat-inline {
  font-family: var(--font-display);
  font-weight: 700;
  color: var(--brand-ink);
  font-variant-numeric: tabular-nums;
}
.section--dark .stat-inline { color: var(--brand-lit); }

/* ============================================================================
   MOTION PRIMITIVES
   Everything animates transform / opacity / clip-path only. All three are
   compositor properties, so a page carrying fifty of these still composites
   without touching layout or paint on the main thread.
   ========================================================================= */

/* --- 1. plain reveals --- */
/* Deliberately restrained, and much shorter than v2's 900ms.
   The motion floor asks for ONE authored moment per page, not the same
   entrance on every section. Eleven sections each sliding 26px over 0.9s is
   not eleven moments — it is one mannerism repeated until the reader notices
   the mechanism instead of the content, and it makes the whole page feel like
   it is buffering. The hero's line-by-line headline is this page's authored
   moment. Everything below it now just arrives: a short, small settle that
   reads as responsiveness rather than as choreography. */
.r {
  opacity: 0;
  transform: translate3d(0, 12px, 0);
  transition: opacity 0.42s var(--ease-soft), transform 0.42s var(--ease-soft);
}
.r--left  { transform: translate3d(-14px, 0, 0); }
.r--right { transform: translate3d(14px, 0, 0); }
.r--scale { transform: scale(0.985); }
.r--fade  { transform: none; }
.r.is-visible { opacity: 1; transform: none; }

/* --- 2. mask reveal: content rises out of a clipped frame --- */
.r-mask { overflow: hidden; display: block; }
.r-mask > * {
  display: block;
  transform: translate3d(0, 105%, 0);
  transition: transform 1s var(--ease-mask);
}
.r-mask.is-visible > * { transform: none; }

/* --- 3. split-line reveal: JS wraps each line, CSS animates it ---
   The wrapper clips; the inner span carries the travel and the stagger. */
.split__line { display: block; overflow: hidden; }
/* Halved. At 1.05s with an 85ms line stagger a two-line heading took over a
   second to finish arriving — long enough that you scroll to a heading and then
   wait for it, which makes reading the page feel like it is loading. A reveal
   should be over before you consciously notice it started. */
.split__inner {
  display: block;
  transform: translate3d(0, 100%, 0);
  transition: transform 0.5s var(--ease-mask);
  transition-delay: calc(var(--line, 0) * 40ms);
}
.is-visible .split__inner { transform: none; }

/* --- 4. image wipe: the frame uncovers the photo, photo also de-scales --- */
.r-wipe { clip-path: inset(0 0 100% 0); transition: clip-path 1.15s var(--ease-mask); }
.r-wipe.is-visible { clip-path: inset(0 0 0 0); }
.r-wipe img { transform: scale(1.16); transition: transform 1.5s var(--ease-soft); }
.r-wipe.is-visible img { transform: scale(1); }

/* --- 5. staggered children --- */
/* 80ms × 6 children was 480ms before the last item even began moving. Halved
   the step and capped the travel: the group now reads as one gesture instead
   of a queue. */
.r-stagger > * {
  opacity: 0;
  transform: translate3d(0, 10px, 0);
  transition: opacity 0.4s var(--ease-soft), transform 0.4s var(--ease-soft);
  transition-delay: calc(var(--i, 0) * 38ms);
}

/* The impact band is the exception, and trimming it with everything else was a
   mistake — those four figures counting up ARE the page's proof, and they need
   long enough to be watched. Bigger travel, longer throw, wider spacing between
   the four so they land one after another rather than together. This is the one
   place on the page where the motion is the point. */
.stats.r-stagger > * {
  transform: translate3d(0, 26px, 0);
  transition: opacity 0.85s var(--ease-soft), transform 0.85s var(--ease-soft);
  transition-delay: calc(var(--i, 0) * 130ms);
}
.r-stagger.is-visible > * { opacity: 1; transform: none; }

/* Promote only while still waiting. main.js strips the class after the
   transition so we don't leave dozens of orphaned GPU layers alive. */
.r:not(.is-visible), .r-wipe:not(.is-visible), .r-mask:not(.is-visible) {
  will-change: opacity, transform, clip-path;
}

/* No-JS fallback. Every reveal primitive starts hidden or clipped, so without
   scripting the page would render mostly blank. This forces all of them to
   their end state — the site reads as a normal static page instead. */
.no-js .r, .no-js .r-stagger > * { opacity: 1; transform: none; }
.no-js .r-mask > *, .no-js .split__inner { transform: none; }
.no-js .r-wipe { clip-path: none; }
.no-js .r-wipe img { transform: none; }
.no-js .rule-under { transform: scaleX(1); }
.no-js .hero__media img { transform: scale(1.12); }
/* With no JS the carousel never advances, so show only the real photograph
   rather than stacking four images on top of each other. */
.no-js .hero__media[data-slides] img { opacity: 0; }
.no-js .hero__media[data-slides] img.is-active { opacity: 1; }
.no-js .timeline::after { height: calc(100% - 16px); }
.no-js .timeline__item::before { transform: scale(1); }

/* ---------- scroll progress ---------- */

.scroll-progress {
  position: fixed;
  inset: 0 0 auto 0;
  height: 2px;
  z-index: 300;
  pointer-events: none;
  background: transparent;
}
.scroll-progress__bar {
  height: 100%;
  background: var(--brand);
  transform: scaleX(0);
  transform-origin: 0 50%;
  will-change: transform;
}

/* ---------- skip link ---------- */

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  z-index: 400;
  background: var(--ink);
  color: var(--ground);
  padding: 0.8rem 1.2rem;
}
.skip-link:focus { left: 0; }

/* ---------- print ---------- */

@media print {
  .scroll-progress, .site-header, .utility-bar, .site-footer, .no-print { display: none !important; }
  .r, .r-wipe, .r-mask > *, .split__inner, .r-stagger > * {
    opacity: 1 !important; transform: none !important; clip-path: none !important;
  }
  body { background: #fff; }
}

/* ---------- reduced motion ----------
   Not a softening — a full stop. Every primitive resolves to its end state. */

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  .r, .r--left, .r--right, .r--scale { opacity: 1; transform: none; }
  .r-mask > *, .split__inner { transform: none; }
  .r-wipe { clip-path: none; }
  .r-wipe img { transform: none; }
  .r-stagger > * { opacity: 1; transform: none; }
  .rule-under { transform: scaleX(1); }
  .scroll-progress { display: none; }
}
