/* ============================================================================
   EAA DESIGN SYSTEM
   Effective Altruism Australia — the whole look of the website, in one file.
   ----------------------------------------------------------------------------
   There is no build step. This file is written by hand, committed, and served
   as-is. Nothing compiles it, nothing scans your HTML, and no class appears or
   disappears depending on what a page happens to use. If a class is in this
   file it works; if it isn't, it doesn't.

   HOW THIS FILE IS STRUCTURED (read before editing):

     1  FONTS            Where the two brand faces come from.
     2  PRIMITIVE RAMPS  Raw colour steps (--teal-500, --grey-200 …).
                         The ONLY place a raw hex is allowed to appear.
     3  SEMANTIC TOKENS  Meaning, not colour (--color-primary, --color-text …).
                         These point at section 2.
     4  TYPE / SPACE / RADIUS / SHADOW / GRADIENT / MOTION / LAYOUT tokens.
     5  BASE             Reset, then plain HTML elements. A page with no
                         classes at all still looks like the site.
     6  COMPONENTS       .btn, .card, .callout, .prose … the pieces worth
                         naming.
     7  UTILITIES        Small single-purpose classes, locked to the tokens
                         above. Spacing only comes in scale steps; colours
                         only come from the palette.
     8  FOCUS            The one focus ring, last so it beats every
                         component's own box-shadow.

   Sections 5–7 reference section 3 and 4. They never contain a raw hex or a
   raw pixel value, and neither should anything you add.

   ----------------------------------------------------------------------------
   HOW TO REBRAND (a handful of edits, all inside :root):

     • New brand hue → replace the 11 hexes in the --teal-* ramp with a new
       11-step ramp. Every button, link, gradient, shadow tint and focus ring
       follows automatically.
     • Repoint what "primary" or "heading" means → edit the SEMANTIC TOKENS in
       section 3 only. Don't touch components.
     • Shadow, glow and focus tints read three channel triplets, --shadow-rgb,
       --glow-rgb and --focus-rgb. Update them to match a new hue so shadows,
       glows and focus rings stay on brand.

   ----------------------------------------------------------------------------
   HOW A PAGE ADDS ITS OWN LOOK

   Write a <style> block at the top of the page body. That is expected and
   supported — this file is a floor, not a fence. Two things make a page that
   does so still look like the rest of the site:

     • Reach for the tokens. `padding: var(--space-8)` instead of `padding:
       33px`, `color: var(--color-primary)` instead of `#0c869b`. That is the
       whole point of the token layer, and it means a rebrand carries your
       page along with everything else.
     • Only invent what isn't here. Check the style guide at /style-guide/
       first — most of what a page needs already exists and is already
       consistent with every other page.

   Nothing enforces either of those. They are what makes the site read as one
   site rather than as a pile of pages, which is a thing people notice and no
   validator can check.

   ----------------------------------------------------------------------------
   TWO THINGS THAT ARE DELIBERATELY MISSING

     • No responsive prefixes. There is no `md:grid-cols-3`. Instead the things
       that need to respond do it on their own: multi-column grids collapse on
       narrow screens, large type is fluid (clamp), and section padding scales
       with the viewport. This costs some control and buys a system where
       forgetting the mobile case is not possible.
     • No arbitrary values. There is no `mt-[37px]`. Spacing comes in the
       steps below. A layout that genuinely needs 37px says so in the page's
       own <style> block, which is a deliberate act rather than a typo.
   ========================================================================== */


/* ============================================================================
   1. FONTS
   There are no @font-face rules here. Merriweather and Merriweather Sans are
   self-hosted in static/site/fonts/, and declared once, in
   sitebuilder/templates/site/partials/fonts.html, beside their preload links.
   That partial says why the rules are not in this file. This file only names
   the families, in --font-heading and --font-body below.

   Careful with comments in this file: collectstatic rewrites every url() it
   finds, comments included, and fails the deploy on one that names no real
   file. So don't write an example one in a comment — describe it instead.
   ========================================================================== */

:root {
  /* ==========================================================================
     2. PRIMITIVE RAMPS  (raw values — the only hex in the file)
     ========================================================================== */

  /* Teal is the core of the brand. Three steps are the fixed brand colours:
     600, 900 and 950. The other steps of the ramp are built to match them. The
     hue stays at about 188.

     Primary Teal is at step 600, not at step 700. Therefore 700 is a real
     darker teal, and --color-link needs one. Primary Teal measures 4.29:1 on
     white, which is below the 4.5:1 minimum for body text. --teal-700 measures
     5.66:1. In a ramp with the brand colour at 700, no step between 700 and 800
     met the minimum. */
  --teal-50:  #eff8f9;
  --teal-100: #ddf2f5;
  --teal-200: #b7e0e7;
  --teal-300: #8ccad5;
  --teal-400: #5eb0c0;
  --teal-500: #339baf;
  --teal-600: #0c869b;   /* The brand Primary Teal. */
  --teal-700: #0a7184;
  --teal-800: #1a6273;
  --teal-900: #145565;   /* brand Dark Teal */
  --teal-950: #073845;   /* brand Deepest Teal */

  /* Bronze is the warm ramp. Use it for warmth and for secondary accents. Use
     it in small amounts. */
  --bronze-50:  #f9f6f3;
  --bronze-100: #f4efe9;  /* brand Warm Off-White */
  --bronze-200: #e2d4c6;
  --bronze-300: #cfb8a2;
  --bronze-400: #bb967c;
  --bronze-500: #ad7f62;
  --bronze-600: #a06d56;
  --bronze-700: #855849;
  --bronze-800: #6d4a3f;
  --bronze-900: #593d35;
  --bronze-950: #2f1e1b;

  /* Grey neutral — borders, dividers, muted text. */
  --grey-50:  #f6f6f6;
  --grey-100: #e7e7e7;
  --grey-200: #d1d1d1;
  --grey-300: #b0b0b0;
  --grey-400: #888888;
  --grey-500: #6d6d6d;
  --grey-600: #5d5d5d;
  --grey-700: #4f4f4f;
  --grey-800: #454545;
  --grey-900: #3d3d3d;
  --grey-950: #151515;   /* brand Charcoal */

  /* Absolutes */
  --white: #ffffff;
  --black: #000000;

  /* Ink is a cool neutral, with a hue near the teal. Use grey for borders and
     quiet text on a white page. Use ink for the same jobs on a coloured
     background or a photograph, where a true grey looks dirty. */
  --ink-50:  #f4f6f7;
  --ink-100: #eaeef0;
  --ink-200: #d1d9dc;
  --ink-300: #b4c0c4;
  --ink-400: #96a5aa;
  --ink-500: #7f8f95;
  --ink-600: #6d7d83;
  --ink-700: #5b696e;
  --ink-800: #4c575b;
  --ink-900: #3e474a;
  --ink-950: #222729;

  /* These are the status hues. They are outside the brand ramps on purpose. A
     warning colour that moved when the brand hue changed would stop looking
     like a warning. Information is not one of them. It is brand teal, because
     it says nothing is wrong.

     Each status is a full ramp, not one hex value. A status is never one
     colour. It is a tinted surface, a border, a text colour that meets the
     contrast minimum, and a solid fill. If you choose those four by eye from
     one middle step, a callout ends up with text at 2:1. Section 3 names the
     four steps each status uses. */

  /* Eucalypt is the success colour. */
  --eucalypt-50:  #f0f8f3;
  --eucalypt-100: #e0f4e7;
  --eucalypt-200: #bde4cd;
  --eucalypt-300: #95d1b0;
  --eucalypt-400: #6aba92;
  --eucalypt-500: #41a67a;
  --eucalypt-600: #239368;
  --eucalypt-700: #177b57;
  --eucalypt-800: #156549;
  --eucalypt-900: #14513c;
  --eucalypt-950: #0b2d21;

  /* Horizon is a cool blue. It was the information colour. Information is
     brand teal now, the same as a plain .callout, so no status uses this
     ramp. A chart series and --gradient-horizon still do. */
  --horizon-50:  #f1f6fe;
  --horizon-100: #e4efff;
  --horizon-200: #c3dafe;
  --horizon-300: #9fc2f5;
  --horizon-400: #7aa7e7;
  --horizon-500: #5c90d8;
  --horizon-600: #477ec5;
  --horizon-700: #3969a6;
  --horizon-800: #2f5787;
  --horizon-900: #26476d;
  --horizon-950: #14273d;

  /* Sunlit is the warning colour. */
  --sunlit-50:  #f9f5ed;
  --sunlit-100: #f9f0db;
  --sunlit-200: #f2dfb7;
  --sunlit-300: #ebcf92;
  --sunlit-400: #ddb768;
  --sunlit-500: #c99d3b;
  --sunlit-600: #ae8632;
  --sunlit-700: #8e6b22;
  --sunlit-800: #71551b;
  --sunlit-900: #5a4216;
  --sunlit-950: #31240b;

  /* Clay is the danger colour. */
  --clay-50:  #fff3f1;
  --clay-100: #ffe8e4;
  --clay-200: #ffc9c1;
  --clay-300: #f6a89c;
  --clay-400: #e58577;
  --clay-500: #d56859;
  --clay-600: #c15546;
  --clay-700: #a34639;
  --clay-800: #863b2f;
  --clay-900: #6c3127;
  --clay-950: #3d1b15;

  /* Mulberry is the highlight colour. It is not a status. Use it to say "look
     at this", where a success green would be untrue and a warning amber would
     cause alarm. */
  --mulberry-50:  #fbf3f8;
  --mulberry-100: #fae7f4;
  --mulberry-200: #f0cbe3;
  --mulberry-300: #e1abce;
  --mulberry-400: #cc89b5;
  --mulberry-500: #bb6fa1;
  --mulberry-600: #a95c8e;
  --mulberry-700: #8f4d77;
  --mulberry-800: #764062;
  --mulberry-900: #60344f;
  --mulberry-950: #361d2c;

  /* ==========================================================================
     3. SEMANTIC COLOUR TOKENS  (the rebrand hooks — repoint these)
     ========================================================================== */

  /* Brand */
  --color-primary:        var(--teal-600);
  --color-primary-hover:  var(--teal-900);
  --color-primary-active: var(--teal-950);
  --color-primary-soft:   var(--teal-50);
  --color-dark:           var(--teal-900);
  --color-dark-deeper:    var(--teal-950);
  --color-accent:         var(--bronze-500);
  --color-accent-hover:   var(--bronze-700);
  --color-accent-soft:    var(--bronze-100);

  /* Surfaces */
  --color-bg:             var(--white);      /* default page background */
  --color-surface:        var(--white);      /* cards, content areas */
  --color-surface-alt:    var(--bronze-100); /* warm section bands */
  --color-surface-soft:   var(--bronze-50);  /* barely-there warm tint */
  --color-surface-invert: var(--teal-950);   /* dark sections and footers */
  /* This is the background of an inline <code> element. It is a token, not a
     surface, because it is the one background that must follow the text it sits
     in. On a dark band, a fixed light background would put white text on a
     nearly white colour. */
  --color-code-bg:        var(--bronze-100);

  /* Text */
  --color-text:        var(--grey-950);
  --color-text-muted:  var(--grey-600);
  --color-text-subtle: var(--grey-500);
  --color-text-invert: var(--white);
  --color-heading:     var(--teal-900);

  /* Lines */
  --color-border:        var(--grey-200);
  --color-border-strong: var(--grey-300);
  --color-divider:       var(--grey-100);

  /* The link colour. It is deliberately NOT --color-primary. A link is
     body-size text on a white page, and Primary Teal measures 4.29:1 there.
     --teal-700 is the same hue one step darker, and it measures 5.66:1. A
     button keeps the brand colour. White on Primary Teal is a large-text
     contrast case, and the filled background is what makes the button stand
     out. */
  --color-link:       var(--teal-700);
  --color-link-hover: var(--color-dark);

  /* The status colours and the highlight colour.

     There are five roles and four steps for each one. The four steps are always
     the same steps of the ramp. Step 100 tints a surface. Step 200 is the
     border of that surface. Step 800 is text on that surface. Step 700 is a
     solid fill with white text on it. Taking each role from a fixed step of the
     ramp, instead of choosing a colour by eye, is what keeps every callout,
     badge and banner on the site readable. Every text-on-surface pair below
     measures above 6:1, and every white-on-solid pair above 5:1.

     Warning is one exception, and the cause is the yellow hue. A sunlit-700
     fill is too light to carry white text. Therefore the solid for warning is
     800 and its hover is 900. Every other role hovers one step down, from 700
     to 800. Information is the other exception: its surface is step 50. The
     note above its tokens says why.

     --color-<role> with no suffix is the solid fill. That is the value a border
     colour and a coloured word need. */
  --color-success:              var(--eucalypt-700);
  --color-success-surface:      var(--eucalypt-100);
  --color-success-border:       var(--eucalypt-200);
  --color-success-text:         var(--eucalypt-800);
  --color-success-solid:        var(--eucalypt-700);
  --color-success-solid-hover:  var(--eucalypt-800);

  /* Information is brand teal, and a plain .callout reads these tokens. It was
     the horizon blue, while .callout--info was teal, so "information" had two
     colours, and the donation form drew a blue icon on a teal callout.

     Its surface is step 50, not 100. teal-800 text on teal-100 measures
     5.95:1, which is under the 6:1 that the other roles keep. On teal-50 it
     measures 6.41:1. White on the teal-700 solid measures 5.66:1. */
  --color-info:                 var(--teal-700);
  --color-info-surface:         var(--teal-50);
  --color-info-border:          var(--teal-200);
  --color-info-text:            var(--teal-800);
  --color-info-solid:           var(--teal-700);
  --color-info-solid-hover:     var(--teal-800);

  --color-warning:              var(--sunlit-800);
  --color-warning-surface:      var(--sunlit-100);
  --color-warning-border:       var(--sunlit-200);
  --color-warning-text:         var(--sunlit-800);
  --color-warning-solid:        var(--sunlit-800);
  --color-warning-solid-hover:  var(--sunlit-900);

  --color-danger:               var(--clay-700);
  --color-danger-surface:       var(--clay-100);
  --color-danger-border:        var(--clay-200);
  --color-danger-text:          var(--clay-800);
  --color-danger-solid:         var(--clay-700);
  --color-danger-solid-hover:   var(--clay-800);

  --color-highlight:            var(--mulberry-700);
  --color-highlight-surface:    var(--mulberry-100);
  --color-highlight-border:     var(--mulberry-200);
  --color-highlight-text:       var(--mulberry-800);
  --color-highlight-solid:      var(--mulberry-700);
  --color-highlight-solid-hover: var(--mulberry-800);

  /* The chart series colours, in order. Take them in order and stop when you
     have enough. The order is chosen so that any number of colours from the
     start of the list stays easy to tell apart. A palette chosen for each chart
     never achieves this. Brand teal is first. The colours after it change
     between warm and cool, so that neighbours separate. None of them is a
     status colour used for a second purpose.

     Eight is the maximum, on purpose. A ninth series is not a colour problem.
     It means the chart should be two charts, or a bar chart. */
  --color-series-1: var(--teal-600);       /* The brand teal. */
  --color-series-2: var(--sunlit-600);
  --color-series-3: var(--mulberry-500);
  --color-series-4: var(--eucalypt-500);
  --color-series-5: var(--clay-500);
  --color-series-6: var(--horizon-500);
  --color-series-7: var(--bronze-700);
  --color-series-8: var(--ink-500);

  /* The diverging scale. Use it for a value with a meaningful middle, where
     above the middle and below the middle mean different things. Do not use it
     for a plain quantity. There, use steps of one ramp, for example --teal-100
     through --teal-900. The middle colour is the warm off-white, not pure
     white. Thus the neutral band looks like part of the scale, and not like a
     gap in it. */
  --color-diverging-low:  var(--clay-700);
  --color-diverging-mid:  var(--bronze-100);
  --color-diverging-high: var(--teal-800);

  /* ==========================================================================
     4a. TYPOGRAPHY
     Use Merriweather for headings ONLY. Use Merriweather Sans for body text and
     the interface ONLY. This is a firm brand rule. The base styles below
     already do this, so the default behaviour is correct. To break the rule you
     must add .font-heading to a paragraph deliberately.
     ========================================================================== */

  --font-heading: 'Merriweather', Georgia, 'Times New Roman', serif;
  --font-body:    'Merriweather Sans', Arial, Helvetica, sans-serif;
  --font-mono:    ui-monospace, 'SFMono-Regular', 'Menlo', 'Consolas', monospace;

  --weight-light:    300;
  --weight-book:     350;  /* Body text uses this weight. Both fonts are
                              variable, so 350 is a real weight. */
  --weight-regular:  400;
  --weight-medium:   500;
  --weight-semibold: 600;
  --weight-bold:     700;
  --weight-black:    900;  /* Use this with the heading font only. */

  /* The type scale. --text-base and every step from --text-3xl up change with
     the viewport width. Therefore no heading needs a class such as
     `md:text-5xl`: one class is correct on a phone and on a desktop. The
     minimum is the phone size and the maximum is the desktop size.

     Body text is 16px on a phone because 18px looked too big on an iPhone. Do
     not go below 16px: an iPhone zooms in when somebody taps a field with
     smaller text. */
  --text-xs:   0.75rem;    /* 12px */
  --text-sm:   0.875rem;   /* 14px */
  --text-base: clamp(1rem, 0.89rem + 0.47vw, 1.125rem);  /* 16 → 18 */
  --text-lg:   1.25rem;    /* 20px */
  --text-xl:   1.375rem;   /* 22px */
  --text-2xl:  1.5rem;     /* 24px */
  --text-3xl:  clamp(1.5rem,  1.24rem + 1.1vw, 1.875rem);   /* 24 → 30 */
  --text-4xl:  clamp(1.75rem, 1.32rem + 1.8vw, 2.25rem);    /* 28 → 36 */
  --text-5xl:  clamp(2rem,    1.31rem + 2.9vw, 3rem);       /* 32 → 48 */
  --text-6xl:  clamp(2.25rem, 1.22rem + 4.3vw, 3.75rem);    /* 36 → 60 */
  --text-display: clamp(2.5rem, 1.14rem + 5.7vw, 4.5rem);   /* 40 → 72 */

  --leading-none:    1;
  --leading-tight:   1.15;   /* large headings */
  --leading-snug:    1.3;    /* small headings */
  --leading-normal:  1.6;    /* body */
  --leading-relaxed: 1.75;   /* long-form reading */

  --tracking-tighter: -0.03em;
  --tracking-tight:   -0.015em;
  --tracking-normal:  0;
  --tracking-wide:    0.02em;
  --tracking-wider:   0.08em;  /* eyebrows and uppercase labels */

  /* Comfortable reading width. Applied by .prose and .measure, deliberately
     NOT to every paragraph on the site — a paragraph inside a card or a hero
     should fill its box, and a global measure makes those look broken.

     The target is about 66 characters a line, the middle of the usual 45–75.
     A `ch` is the width of a "0", and in Merriweather Sans that is about 25%
     wider than an average letter, so 52ch holds about 65 real characters.
     68ch looked right on paper and gave 83. Measured in Chromium with an
     article paragraph; measure again if the body font changes. */
  --measure: 52ch;

  /* ==========================================================================
     4b. SPACING  (4px base scale — the only spacing the utilities offer)
     ========================================================================== */
  --space-0:   0;
  --space-px:  1px;
  --space-1:   0.25rem;   /* 4   */
  --space-2:   0.5rem;    /* 8   */
  --space-3:   0.75rem;   /* 12  */
  --space-4:   1rem;      /* 16  */
  --space-5:   1.25rem;   /* 20  */
  --space-6:   1.5rem;    /* 24  */
  --space-8:   2rem;      /* 32  */
  --space-10:  2.5rem;    /* 40  */
  --space-12:  3rem;      /* 48  */
  --space-16:  4rem;      /* 64  */
  --space-20:  5rem;      /* 80  */
  --space-24:  6rem;      /* 96  */
  --space-32:  8rem;      /* 128 */

  /* The gap between one band of a page and the next. Fluid, so a section is
     not 96px tall on a phone. .section uses it. */
  --space-section: clamp(3rem, 6vw, 6rem);

  /* ==========================================================================
     4c. CORNER RADIUS  (brand: card 16 / control 8 / chip 5)
     ========================================================================== */
  --radius-xs:   5px;
  --radius-sm:   8px;
  --radius-md:   12px;
  --radius-lg:   16px;   /* brand default card radius */
  --radius-xl:   24px;
  --radius-2xl:  32px;
  --radius-pill: 999px;
  --radius-full: 50%;

  /* ==========================================================================
     4d. SHADOWS  — tinted with the brand dark teal, never neutral grey.
     Change --shadow-rgb on a rebrand and every elevation follows.
     ========================================================================== */
  --shadow-rgb: 20 85 101;   /* channels of --teal-900 (#145565) */

  --shadow-xs: 0 1px 2px rgb(var(--shadow-rgb) / 0.06);
  --shadow-sm: 0 1px 3px rgb(var(--shadow-rgb) / 0.07),
               0 2px 8px rgb(var(--shadow-rgb) / 0.06);
  --shadow-md: 0 4px 16px rgb(var(--shadow-rgb) / 0.12),
               0 1px 4px  rgb(var(--shadow-rgb) / 0.07);
  --shadow-lg: 0 8px 32px rgb(var(--shadow-rgb) / 0.16);
  --shadow-xl: 0 20px 48px rgb(var(--shadow-rgb) / 0.20);
  --shadow-inner: inset 0 1px 2px rgb(var(--shadow-rgb) / 0.08);

  /* Focus ring. Driven by the primary hue, and used by every interactive
     thing on the site so keyboard users get one consistent signal — tabbing
     from a text field to the button beside it must not change what focus
     looks like.

     It is an `outline`, not a box-shadow, and that is what makes the signal
     identical everywhere. An outline follows the element's own corner radius
     on its own, sits outside the box without affecting layout, and — the part
     that matters — does not collide with the element's own box-shadow. A
     box-shadow ring replaced a button's elevation the moment you focused it,
     and needed a border-radius override to look right, which quietly changed
     the shape of whatever it was focusing.

     Two tokens rather than one value, so the width stays the same on every
     control while the colour can be repointed per band. The rule that uses
     them is the last one in the file — see section 8. */
  --focus-rgb:   12 134 155;   /* channels of --teal-600 (#0c869b) */
  --focus-color: rgb(var(--focus-rgb) / 0.55);
  --focus-width: 3px;

  /* Glow. A shadow is an object sitting above a surface; a glow is light
     coming off it. Both are here because they do different jobs: --shadow-*
     for depth, --glow-* for emphasis. Same brand channels, so a rebrand
     carries the light as well as the shade. */
  --glow-rgb: 51 155 175;    /* channels of --teal-500 (#339baf) */

  --glow-sm: 0 4px 16px  rgb(var(--glow-rgb) / 0.14);
  --glow-md: 0 8px 28px  rgb(var(--glow-rgb) / 0.20);
  --glow-lg: 0 12px 40px rgb(var(--glow-rgb) / 0.28);
  /* For something white on a dark band — a button, a card edge. */
  --glow-white: 0 8px 30px rgb(255 255 255 / 0.16);

  /* ==========================================================================
     4e. GRADIENTS  (all built from the ramps, so a rebrand carries them)
     ========================================================================== */
  --gradient-primary:      linear-gradient(135deg, var(--teal-600), var(--teal-900));
  --gradient-primary-deep: linear-gradient(135deg, var(--teal-900), var(--teal-950));
  --gradient-bright:       linear-gradient(135deg, var(--teal-500), var(--teal-600));
  --gradient-warm:         linear-gradient(135deg, var(--bronze-300), var(--bronze-500));
  --gradient-teal-bronze:  linear-gradient(120deg, var(--teal-800), var(--bronze-600));
  --gradient-surface:      linear-gradient(180deg, var(--white), var(--bronze-50));

  /* Five more dark bands. They let a long page change its background without
     repeating the same teal. Each one has a brand colour at one end, or it is a
     neutral. None of them is a status hue used for a mood. A green band that is
     not about success is how a success colour stops meaning success.
     --gradient-ink is the neutral of the set. Use it when the band carries a
     photograph or a chart, and a brand colour would compete with it. */
  --gradient-ink:          linear-gradient(135deg, var(--ink-800), var(--ink-950));
  --gradient-horizon:      linear-gradient(135deg, var(--teal-700), var(--horizon-800));
  --gradient-forest:       linear-gradient(135deg, var(--eucalypt-700), var(--teal-900));
  --gradient-dusk:         linear-gradient(135deg, var(--mulberry-800), var(--ink-950));
  /* This is the one light gradient. It is warm, for a band that carries dark
     text. */
  --gradient-sunrise:      linear-gradient(120deg, var(--sunlit-200), var(--clay-300));
  --gradient-hero:         radial-gradient(120% 120% at 15% 15%,
                             var(--teal-800) 0%, var(--teal-950) 70%);
  /* A dark layer for text over a photograph. At the bottom it is strong enough
     to keep white text readable, whatever the photograph shows. */
  --gradient-scrim:        linear-gradient(180deg,
                             rgb(7 56 69 / 0) 0%, rgb(7 56 69 / 0.75) 100%);
  /* The diagonal wash that the home hero puts over its photograph. */
  --gradient-photo-wash:   linear-gradient(120deg,
                             rgb(7 56 69 / 0.92) 0%,
                             rgb(20 85 101 / 0.82) 45%,
                             rgb(12 134 155 / 0.65) 100%);
  /* This is a sheen across a line of text. It is not a rainbow through the
     text. The two ends are deliberately close in lightness. A reader should
     notice the effect on the second look. Close ends also keep both ends
     readable, and a wide range of lightness does not. .text-gradient uses it.
     The light version is for a dark band. */
  --gradient-text:       linear-gradient(100deg, var(--teal-800), var(--teal-950));
  --gradient-text-light: linear-gradient(100deg, var(--white), var(--teal-100));

  /* ==========================================================================
     4f. GLASS
     Glass is a surface that floats over a picture: a panel over a photograph,
     a label over a hero. The picture shows through, blurred, and that is the
     whole effect. Over a plain colour there is nothing to show, and glass is
     only a box of a slightly different colour.

     There are two materials. Each is 80% opaque, over a 16px blur:

       - --glass-tint is the deepest brand teal. It carries WHITE text.
       - --glass-frost is the palest brand teal. It carries DARK text.

     80% is measured, not chosen. It makes the text readable over ANY picture,
     so nobody has to check the photograph:

       - Dark glass over a pure white picture: white text 6.89:1, and muted
         text 4.95:1. Subtle text is 3.98:1 there, and 4.82:1 over a
         photograph that carries --gradient-photo-wash. Therefore do not set
         subtle text on glass over a photograph that has no wash.
       - Frost over a pure black picture: body text 10.58:1, muted text
         4.74:1, and links 4.83:1. .glass--frost sets the muted colour and the
         link colour darker than the page's, and these figures use those.

     The blur is 16px, because the reader must see COLOUR through the panel
     and not shapes. A shape behind a line of text is noise. saturate() puts
     back the colour that the tint takes out.

     The edge is a hairline at 22% white. It is a highlight and not an
     outline, because the panel's own colour says where the panel is. A
     control is different. .btn--glass has the 45% edge, which is 3.81:1 on
     the darkest band and 3.16:1 over a washed photograph.

     What this replaced. Glass was a 5% white tint over a 2px blur, so it was
     nearly clear, and text on it could not be read over a busy picture. Three
     things existed only to repair that: an 85% white outline to show where
     the panel was, a text shadow on every paragraph (.text-halo), and a ruled
     grid (.grid-lines) to give the thin blur something to blur. A clear box
     with a bright outline on a grid is also the most copied look on the web.
     A thick tint needs none of the three. The outline, the grid and the
     highlight along the top edge (.glass--sheen) are gone.

     No @supports guard is needed. A browser without backdrop-filter still
     draws the tint, so every ratio above holds. It loses the blur and nothing
     else.

     Each backdrop-filter is its own compositing layer. Use glass on one or
     two things in a band, and not on every card on a page. */
  --glass-blur:        16px;
  --glass-filter:      blur(var(--glass-blur)) saturate(1.5);
  --glass-tint:        rgb(7 56 69 / 0.8);        /* This is --teal-950. */
  /* The frost is TINTED, not white. White over a dark teal band looks grey.
     It is written as colour channels plus an alpha value, so a band on a
     different hue can change the colour and keep the ratios above. */
  --glass-frost-color: 239 248 249;               /* This is --teal-50. */
  --glass-frost:       rgb(var(--glass-frost-color) / 0.8);
  --glass-edge:        rgb(255 255 255 / 0.22);   /* The hairline on a panel. */
  --glass-edge-bright: rgb(255 255 255 / 0.45);   /* A control, and a panel on hover. */

  /* Use this on text that sits directly on a photograph, with no panel under
     it. It is a dark halo in the brand's deepest teal, not in black. Thus it
     looks like a shadow, and not like a second outline. See .text-halo. */
  --text-halo: 0 1px 2px rgb(7 56 69 / 0.85),
               0 0 6px   rgb(7 56 69 / 0.85);

  /* Two areas of brand colour over a dark background. They are radial
     gradients, not linear ones, so the colour changes across the band in two
     directions. */
  --gradient-mesh: radial-gradient(60% 80% at 12% 18%, var(--teal-700) 0%, transparent 60%),
                   radial-gradient(70% 90% at 88% 82%, var(--mulberry-800) 0%, transparent 62%),
                   linear-gradient(135deg, var(--teal-950), var(--ink-950));

  /* ==========================================================================
     4f. MOTION
     ========================================================================== */
  --duration-instant: 80ms;
  --duration-fast:    120ms;
  --duration-base:    200ms;
  --duration-slow:    320ms;

  --ease:        cubic-bezier(0.25, 0.46, 0.45, 0.94);  /* brand standard */
  --ease-out:    cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in:     cubic-bezier(0.55, 0, 1, 0.45);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);
  --ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);     /* subtle overshoot */

  --transition-colors: color var(--duration-base) var(--ease),
                       background-color var(--duration-base) var(--ease),
                       border-color var(--duration-base) var(--ease),
                       fill var(--duration-base) var(--ease);
  --transition-transform: transform var(--duration-base) var(--ease);
  --transition-shadow: box-shadow var(--duration-base) var(--ease);

  /* ==========================================================================
     4g. LAYERING
     ========================================================================== */
  --z-base:     0;
  --z-raised:   10;
  --z-dropdown: 1000;
  --z-sticky:   1100;
  --z-fixed:    1200;
  --z-overlay:  1300;
  --z-modal:    1400;
  --z-popover:  1500;
  --z-toast:    1600;
  --z-tooltip:  1700;

  /* ==========================================================================
     4h. LAYOUT
     The breakpoints are 768px and 1024px. They appear as numbers in the media
     queries below, because CSS cannot read a variable inside @media — so
     there is no --bp-* token pretending otherwise.
     ========================================================================== */
  --container-sm:  640px;
  --container-md:  768px;
  --container-lg:  1024px;
  --container-xl:  1200px;
  --container-2xl: 1320px;
  --container:     var(--container-xl);        /* what .container uses */
  --gutter:        clamp(1rem, 4vw, 2.5rem);   /* page side padding */

  --header-height: 5rem;  /* .site-header is fixed. The body starts this far down. */
}

/* An 80px bar is on every page and is fixed to the top of the screen. On a
   phone that bar takes a fifth of the screen. This rule changes the token, not
   the header, so that everything else follows. The body's clearance, the top of
   the menu panel and the donation form's --sticky-top all read this token.
   Thus there is one number to change, and nothing else to keep in step. */
@media (max-width: 767px) {
  :root { --header-height: 4.25rem; }
}


/* Honour the OS "reduce motion" setting everywhere, including on animations a
   page writes itself. Vestibular disorders are the reason; it costs us
   nothing. */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}


/* ============================================================================
   5. BASE
   Light reset, then plain HTML elements. A page that uses no classes at all
   still looks like this site — which is what makes an article body, written as
   markdown with no classes anywhere, come out right.
   ========================================================================== */

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

* { margin: 0; }

/* This is the colour behind the page. When a person scrolls past either end of
   the document, the browser shows the canvas. The canvas takes its colour from
   body, unless html has a colour of its own. Therefore, with only the body rule
   below, an over-scroll showed white under the dark footer. This rule stops
   body's colour reaching the canvas. body still paints white over its own box,
   so the page looks the same, and the dark colour shows only in the over-scroll
   strips. The strip at the top must clear the fixed header, which is why
   .has-fixed-header uses a margin. */
html {
  background-color: var(--color-surface-invert);
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* Chrome on a phone flashes a translucent blue box over anything tapped.
     It is touch feedback, not a focus ring: a keyboard user never sees it, so
     removing it costs them nothing. The feedback it gave comes from each
     control's own :active state instead (.btn and .choice press down). */
  -webkit-tap-highlight-color: transparent;
  scroll-behavior: smooth;
  /* This rule makes everything that scrolls the page to an element stop below
     the fixed header. It is written one time, here. It covers an in-page
     `#anchor` link and any script that calls `scrollIntoView()`. The donation
     form's Continue button showed the need for it, because that button put the
     top of the form under the navigation bar. scroll-padding belongs to the
     SCROLL CONTAINER, and for the page that is the root element. Therefore a
     rule on `body` would do nothing. The extra --space-4 keeps the target off
     the lower edge of the header.

     A document with no fixed header must say so. `styles/host.css` does that in
     the standalone donation form. */
  scroll-padding-top: calc(var(--header-height) + var(--space-4));
  tab-size: 4;
}

body {
  font-family: var(--font-body);
  font-weight: var(--weight-book);
  font-size: var(--text-base);
  line-height: var(--leading-normal);
  color: var(--color-text);
  background-color: var(--color-bg);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-rendering: optimizeLegibility;
  min-height: 100vh;
}

/* Headings. Merriweather, brand dark teal, never the body font. */
h1, h2, h3, h4, h5, h6 {
  font-family: var(--font-heading);
  color: var(--color-heading);
  font-weight: var(--weight-bold);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-tight);
  text-wrap: balance;
}
h1 { font-size: var(--text-5xl); line-height: var(--leading-tight); }
h2 { font-size: var(--text-4xl); }
h3 { font-size: var(--text-3xl); }
h4 { font-size: var(--text-2xl); }
h5 { font-size: var(--text-xl); }
h6 { font-size: var(--text-lg); }

p { text-wrap: pretty; }

a {
  color: var(--color-link);
  text-decoration: none;
  transition: var(--transition-colors);
}
a:hover { color: var(--color-link-hover); text-decoration: underline; }

strong, b { font-weight: var(--weight-bold); }
em, i { font-style: italic; }
small { font-size: var(--text-sm); }

ul, ol { padding-inline-start: var(--space-6); }

blockquote {
  padding-inline-start: var(--space-4);
  border-inline-start: 3px solid var(--color-primary);
  color: var(--color-text-muted);
  font-style: italic;
}

hr {
  border: 0;
  border-top: 1px solid var(--color-divider);
  margin-block: var(--space-8);
}

code, kbd, samp, pre { font-family: var(--font-mono); font-size: 0.9em; }
code {
  background: var(--color-code-bg);
  padding: 0.15em 0.4em;
  border-radius: var(--radius-xs);
}
pre {
  background: var(--color-surface-alt);
  padding: var(--space-4);
  border-radius: var(--radius-sm);
  overflow-x: auto;
}
pre code { background: none; padding: 0; }

/* Images are block-level and never overflow their column. This one rule
   removes most of the ways an author can break a layout with a picture. */
img, svg, video, canvas { display: block; max-width: 100%; height: auto; }

figure { margin: 0; }
figcaption {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  margin-block-start: var(--space-2);
}

table {
  border-collapse: collapse;
  width: 100%;
}
th, td {
  text-align: left;
  padding: var(--space-3);
  border-bottom: 1px solid var(--color-border);
}
th {
  font-weight: var(--weight-semibold);
  color: var(--color-heading);
}

input, button, textarea, select { font: inherit; color: inherit; }
button { background: none; border: 0; }

::selection { background: var(--color-primary); color: var(--white); }

/* The focus ring is not here. It is the last rule in the file, in section 8,
   because it has to beat every component's own box-shadow. */


/* ============================================================================
   6. COMPONENTS
   ========================================================================== */

/* --- Page scaffolding ----------------------------------------------------- */

/* The horizontal frame for page content: a max width, centred, with side
   padding that grows with the viewport. Nearly every band of a page has one. */
.container {
  width: 100%;
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
.container-sm  { max-width: var(--container-sm); }
.container-md  { max-width: var(--container-md); }
.container-lg  { max-width: var(--container-lg); }
.container-xl  { max-width: var(--container-xl); }
.container-2xl { max-width: var(--container-2xl); }

/* The vertical rhythm between bands. Fluid, so sections are tighter on a
   phone without anybody writing a media query. */
.section    { padding-block: var(--space-section); }
.section-sm { padding-block: calc(var(--space-section) * 0.5); }
.section-lg { padding-block: calc(var(--space-section) * 1.5); }

/* Even vertical spacing between children, so a column of mixed elements does
   not need a margin class on each one. */
.stack > * + *    { margin-block-start: var(--space-4); }
.stack-sm > * + * { margin-block-start: var(--space-2); }
.stack-lg > * + * { margin-block-start: var(--space-8); }

/* A row that wraps: buttons side by side, tags, a list of links. */
.cluster {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-4);
}

/* Two columns that become one on a narrow screen. The commonest page layout
   there is — text beside a picture. */
.split {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--space-12);
  align-items: center;
}

/* --- Header and footer ---------------------------------------------------- */

/* The radius of the phone menu's circular wipe. Registered as a length so
   that the browser can animate it; see "Opening and closing" below. */
@property --menu-radius {
  syntax: "<length>";
  inherits: false;
  initial-value: 0px;
}

.site-header {
  position: fixed;
  top: 0;
  inset-inline: 0;
  z-index: var(--z-sticky);
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
}
.site-header__inner {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
  height: var(--header-height);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-6);
}
/* The logo takes the free space to its right, which pushes the links, the
   Donate button and the menu button to the far end of the bar. */
.site-header__home {
  display: flex;
  margin-inline-end: auto;
  min-width: 0;
}
.site-header__logo { height: 3.5rem; width: auto; }
/* The logo is the tallest element in the bar, so it becomes smaller with the
   bar. On a phone it is also the one thing in the bar that can give up width:
   the Donate button and the menu button keep their size, and below about
   360px the logo shrinks to fit beside them. */
@media (max-width: 767px) {
  .site-header__inner { gap: var(--space-3); }
  .site-header__logo { height: auto; width: 11.5rem; max-width: 100%; }
}

/* The Donate button is in the bar at every width, and again at the foot of the
   phone menu. It is the most important button on the site, so .btn--donate
   gives it more than a plain .btn--primary:

   - It is a little larger than a default button, and a pill.
   - Its fill is a shallow gradient from Primary Teal down to --teal-700, so it
     looks lit from above. Both ends carry white text at 4.29:1 or better,
     the same as any primary button. A lighter top (--teal-500) would look
     brighter, but white text on it measures 3.3:1.
   - It has a small glow in the brand teal, which grows a little on hover.
   - The heart beats once on hover, and jumps when the button is pressed.

   The selectors start `.btn.` because .btn comes later in this file and would
   win otherwise.

   --- The edge, from the outside in ---

   1. The focus ring. It is the site's one ring, drawn by the :focus-visible
      rule at the end of this file. That rule is a box-shadow, and so is the
      glow here, so the glow rules (more specific) replaced it, and keyboard
      users saw no ring on this button. The :focus-visible rule below puts the
      ring first and keeps the glow under it.
   2. The glow and a tight contact shadow, outside the border.
   3. A real 1px border, a dark teal at 45%. Over the gradient it reads as a
      crisp edge that is darker at the bottom, as a lit object's is. It is
      a border and not only a shadow for a second reason: in Windows'
      high-contrast mode (forced colours) the browser removes every shadow
      and gradient, and the border is what still draws the button's shape.
   4. A 1px white highlight just inside the top edge, at 22%. That is the
      light catching the rim, and it is what makes the gradient read as a
      surface and not as a stain.

   The gradient starts at the outer edge of the border (background-origin),
   so the border tints the gradient instead of showing a second copy of it. */
.btn.btn--donate {
  --donate-edge: inset 0 1px 0 rgb(255 255 255 / 0.22);
  --donate-glow: 0 1px 2px rgb(var(--glow-rgb) / 0.25), 0 3px 10px rgb(var(--glow-rgb) / 0.22);
  --donate-glow-hover: 0 2px 4px rgb(var(--glow-rgb) / 0.28), 0 5px 16px rgb(var(--glow-rgb) / 0.32);
  gap: 0.4em;
  padding: 0.875rem 1.625rem 0.875rem 1.875rem;   /* the heart is lighter than a letter */
  font-size: var(--text-base);
  letter-spacing: 0.01em;
  color: var(--white);   /* .site-nav a would win otherwise, inside the menu */
  border: 1px solid color-mix(in srgb, var(--teal-900) 45%, transparent);
  background: linear-gradient(180deg, var(--teal-600), var(--teal-700)) border-box;
  box-shadow: var(--donate-edge), var(--donate-glow);
  transition: var(--transition-shadow),
              transform var(--duration-base) var(--ease-out);
}
.btn.btn--donate:hover {
  color: var(--white);
  background: linear-gradient(180deg, var(--teal-600), var(--teal-700)) border-box;
  box-shadow: var(--donate-edge), var(--donate-glow-hover);
  transform: translateY(-1px);
}
.btn.btn--donate:active {
  box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.18), var(--donate-glow);
  transform: translateY(0);
}
.btn.btn--donate:focus-visible {
  box-shadow: 0 0 0 var(--focus-width) var(--focus-color),
              var(--donate-edge), var(--donate-glow);
}

/* The heart is filled, so it has no stroke. `.icon.` because .icon and
   .icon--inline come later in this file. */
.icon.btn__heart {
  width: 1.1em;
  height: 1.1em;
  fill: currentColor;
  stroke: none;
  transition: transform 140ms var(--ease-spring);
}
/* On hover it beats once: up, down, and a smaller second beat, which is what
   makes it read as a heartbeat and not as a bounce. */
.btn.btn--donate:hover .btn__heart {
  animation: donate-heartbeat 700ms var(--ease) 1;
}
@keyframes donate-heartbeat {
  0%   { transform: scale(1); }
  20%  { transform: scale(1.25); }
  40%  { transform: scale(1); }
  60%  { transform: scale(1.12); }
  100% { transform: scale(1); }
}
/* On a press it jumps. The link leaves the page as soon as the button is
   released, so anything that played after the click would be cut off. The
   press itself is the only time there is to show something, so the jump is
   on :active, and the spring easing gets it to full size in 140ms. A tap on
   a phone sets :active as well. */
.btn.btn--donate:active .btn__heart {
  animation: none;
  transform: scale(1.4);
}

/* In the bar it must keep its size, so the logo is what gives up width. */
.site-header__donate { flex-shrink: 0; }
/* On a phone the bar's button has no heart, to save width. The padding is
   then even, because the extra left padding was there to balance the heart. */
@media (max-width: 767px) {
  .btn.site-header__donate {
    padding: 0.6875rem 1.125rem;
    font-size: var(--text-sm);
  }
  .site-header__donate .btn__heart { display: none; }
}

/* Pages sit under a fixed header, so the body has to start below it. */
/* This is how the page starts below the fixed header. It uses a margin, not
   padding. Padding is inside body's box, so the browser paints it in body's
   white. That white is invisible under the opaque header. But when a person
   scrolls past the top of the page, it showed as a white band above the dark
   canvas. A margin is outside the box, so that strip is canvas, like the rest
   of the over-scroll. The min-height is reduced by the same amount, so that a
   short page is exactly one screen tall.

   The flex column is what keeps the footer at the bottom of a short page. The
   min-height makes the page one screen tall, and main takes the space that the
   content does not use. Without it, that space was empty white below the
   footer. */
.has-fixed-header {
  margin-block-start: var(--header-height);
  min-height: calc(100vh - var(--header-height));
  display: flex;
  flex-direction: column;
}
.has-fixed-header > main { flex: 1 0 auto; }

/* The list carries the `popover` attribute, so that the button can open it on a
   phone — see below. On a wide screen it is an ordinary row, and getting there
   means undoing the browser's own popover styling, which is more than the
   `display: none` people remember: the UA sheet also makes it a fixed box in
   the middle of the viewport, with a border, padding and its own background,
   and those apply whether it is open or not. An author rule beats a UA rule
   whatever the specificity, so listing them here is the whole fix. */
.site-nav {
  display: flex;
  position: static;
  inset: auto;
  width: auto;
  height: auto;
  overflow: visible;
  border: 0;
  background: none;
  color: inherit;
  flex-wrap: wrap;
  gap: var(--space-6);
  list-style: none;
  padding: 0;
  margin: 0;
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
}
.site-nav a { color: var(--color-text); }
.site-nav a:hover { color: var(--color-primary); text-decoration: none; }

/* The large Donate button at the foot of the phone menu. On a wide screen the
   bar's own button is always in view, so this one is not shown. */
.site-nav > .site-nav__cta { display: none; }   /* `>` to beat `.site-nav > li` */

/* --- The header menu's dropdowns ------------------------------------------

   A top-level item with children opens a panel on hover and on keyboard
   focus. `:hover` covers the pointer and `:focus-within` covers the keyboard,
   one selector each, and both are on the <li> so the panel stays open while
   the pointer is inside it.

   `.is-tapped` covers a finger. A phone showing the desktop site has this row
   and no hover, so a tap on a parent opened its page, or flashed the panel
   and then opened its page. The script in navbar.html makes the first tap
   add `.is-tapped` and the second tap follow the link.

   The one thing that has to be right is that there is *no gap* between the
   link and the top of the panel — cross one and the menu shuts under the
   pointer. So the items stretch to the full height of the bar and the panel
   starts at `top: 100%`, which is then the bar's own bottom edge rather than
   the bottom of a line of text floating in the middle of it. That is the whole
   reason for the three stretch declarations here. */
.site-nav { align-self: stretch; align-items: stretch; }
.site-nav > li { position: relative; display: flex; align-items: stretch; }
.site-nav > li > a { display: inline-flex; align-items: center; gap: 0.125rem; }

.site-nav__chevron {
  width: 1em;
  height: 1em;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  /* It says "there is more under here" before the visitor hovers, and turns
     over while the panel is open, so it also says which panel is showing. */
  transition: transform 240ms var(--ease-out);
}
.site-nav__group:hover > a .site-nav__chevron,
.site-nav__group:focus-within > a .site-nav__chevron,
.site-nav__group.is-tapped > a .site-nav__chevron {
  transform: rotate(180deg);
}

/* --- How the panel opens and closes ---------------------------------------

   Opening, the panel fades in while it drops 6px into place and grows from
   98% of its size, out of the corner nearest its link. Then its links fade in
   one after the other, 25ms apart. Each step is small. Together they read as
   one smooth arrival and not as several effects.

   Closing is quicker and plainer, and it waits 80ms first. A pointer that
   slips off the panel for a moment, or cuts across a corner on the way to a
   link, does not lose the menu.

   Only opacity and transform move, so the compositor does the work.

   Hidden with visibility, not display: a `display: none` panel cannot
   transition, and visibility: hidden takes its links out of the tab order, so
   a closed menu is not a set of invisible tab stops. Visibility changes at
   once on the way in, and only after the fade on the way out, which is what
   its delay in the closed state is for.

   The panel starts at `top: 100%`, the bar's own bottom edge, with no gap,
   because a gap closes the menu under a pointer crossing it. The 6px drop
   starts the panel OVER the bar, so even mid-animation there is no gap. */
.site-nav__sub {
  position: absolute;
  top: 100%;
  /* Out by the panel's border and padding and the link's padding, so the
     text in the panel lines up with the link above it. */
  inset-inline-start: calc(-1px - var(--space-2) - var(--space-3));
  z-index: var(--z-dropdown);
  min-width: 13rem;
  margin: 0;
  padding: var(--space-2);
  list-style: none;
  background: var(--color-surface);
  border: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
  border-radius: var(--radius-md);
  /* Two shadows: a tight one that defines the edge, and a wide, soft one,
     pulled in at the sides, that lifts the panel off the page. */
  box-shadow:
    0 1px 2px rgb(var(--shadow-rgb) / 0.06),
    0 16px 36px -10px rgb(var(--shadow-rgb) / 0.22);
  font-size: var(--text-sm);
  font-weight: var(--weight-book);
  transform-origin: top left;
  opacity: 0;
  visibility: hidden;
  transform: translateY(-6px) scale(0.98);
  transition:
    opacity 140ms var(--ease-in) 80ms,
    transform 140ms var(--ease-in) 80ms,
    visibility 0s linear 220ms;
}
.site-nav__group:hover > .site-nav__sub,
.site-nav__group:focus-within > .site-nav__sub,
.site-nav__group.is-tapped > .site-nav__sub {
  opacity: 1;
  visibility: visible;
  transform: none;
  transition:
    opacity 180ms var(--ease-out),
    transform 260ms var(--ease-out),
    visibility 0s;
}
/* There used to be a rule here anchoring the last item's panel to its right,
   so that it did not hang off the edge of the window. The Donate button now
   always sits after the last link, which leaves more room than a panel
   needs, so every panel opens from its link's left edge. */

/* The parent link stays coloured while its panel is open, so the eye can see
   which link the panel belongs to after the pointer moves down into it. */
.site-nav__group:hover > a,
.site-nav__group:focus-within > a,
.site-nav__group.is-tapped > a {
  color: var(--color-primary);
}

.site-nav__sub li {
  display: block;
  opacity: 0;
  transform: translateY(-3px);
  transition: opacity 100ms var(--ease-in), transform 100ms var(--ease-in);
}
.site-nav__group:hover .site-nav__sub li,
.site-nav__group:focus-within .site-nav__sub li,
.site-nav__group.is-tapped .site-nav__sub li {
  opacity: 1;
  transform: none;
  transition:
    opacity 220ms var(--ease-out) calc(50ms + var(--i, 0) * 25ms),
    transform 280ms var(--ease-out) calc(50ms + var(--i, 0) * 25ms);
}

/* Each link is a rounded row inside the panel's padding, not a bar from edge
   to edge, so the highlight has the same corners as the panel around it. */
.site-nav__sub a {
  display: block;
  padding: var(--space-2) var(--space-3);
  border-radius: var(--radius-sm);
  white-space: nowrap;
  transition: background-color 150ms var(--ease), color 150ms var(--ease);
}
.site-nav__sub a:hover {
  background: var(--color-primary-soft);
  color: var(--color-link);
}

/* --- One box that moves between the dropdowns -----------------------------

   With the script in navbar.html, the dropdowns share one box. Move from one
   menu item to the next and the box slides across and changes size to fit the
   new list, instead of one box closing and another opening.

   The script adds `.is-morphing` and the box, `.site-nav__backdrop`, and it
   sets the box's size and place. Everything else is here. Each list keeps its
   own place and its own fade, and loses its own background, border and shadow,
   because the box draws them behind it. The box has the same border, so its
   size is the list's size.

   Without the script nothing here applies, and each item opens its own box as
   above. The box moves with `translate`, so `transform` is still free for the
   same small drop the single panel makes when it opens.

   `.is-placed` is what turns the slide on. The script removes it once every
   list has closed, so the next first hover puts the box straight under its
   item and does not slide it in from where it was last time. */
.site-nav__backdrop { display: none; }

@media (min-width: 768px) {
  .site-header__inner.is-morphing { position: relative; }

  .is-morphing .site-nav__backdrop {
    display: block;
    position: absolute;
    top: 0;
    left: 0;
    z-index: var(--z-dropdown);   /* the lists are later in the page, so on top */
    pointer-events: none;
    background: var(--color-surface);
    border: 1px solid color-mix(in srgb, var(--color-border) 70%, transparent);
    border-radius: var(--radius-md);
    box-shadow:
      0 1px 2px rgb(var(--shadow-rgb) / 0.06),
      0 16px 36px -10px rgb(var(--shadow-rgb) / 0.22);
    transform-origin: top left;
    opacity: 0;
    visibility: hidden;
    transform: translateY(-6px) scale(0.98);
    transition:
      opacity 140ms var(--ease-in) 80ms,
      transform 140ms var(--ease-in) 80ms,
      visibility 0s linear 220ms;
  }
  .is-morphing:has(.site-nav__group:hover, .site-nav__group:focus-within, .site-nav__group.is-tapped) .site-nav__backdrop {
    opacity: 1;
    visibility: visible;
    transform: none;
    transition:
      opacity 180ms var(--ease-out),
      transform 260ms var(--ease-out),
      visibility 0s;
  }
  .is-morphing:has(.site-nav__group:hover, .site-nav__group:focus-within, .site-nav__group.is-tapped) .site-nav__backdrop.is-placed {
    transition:
      opacity 180ms var(--ease-out),
      transform 260ms var(--ease-out),
      visibility 0s,
      translate 320ms var(--ease-out),
      width 320ms var(--ease-out),
      height 320ms var(--ease-out);
  }

  .is-morphing .site-nav__sub {
    background: none;
    border-color: transparent;
    box-shadow: none;
    transform: none;
  }

  /* The list being left fades at once, not after the usual 80ms wait. The box
     is already sliding away from it, and its links must not show outside the
     box. */
  .is-morphing .site-nav:has(.site-nav__group:hover) .site-nav__group:not(:hover) > .site-nav__sub,
  .is-morphing .site-nav:has(.site-nav__group:hover) .site-nav__group:not(:hover) .site-nav__sub li {
    transition:
      opacity 90ms var(--ease-in),
      visibility 0s linear 90ms;
  }
}

/* The menu button is the panel's only control, so it exists only where the
   panel does. */
.site-header__toggle {
  display: none;
  align-items: center;
  justify-content: center;
  width: 2.75rem;                 /* a comfortable thumb target */
  height: 2.75rem;
  margin-inline-end: calc(var(--space-2) * -1);   /* optically flush with the gutter */
  padding: 0;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--color-text);
  cursor: pointer;
}
.site-header__toggle:hover { color: var(--color-primary); }

/* On a phone the logo and three nav items only just fit across the bar, and
   the bar is a fixed height — so wrapping doesn't make it taller, it pushes
   the second line out of sight. A menu button and a panel below the bar is the
   version that still works when a fourth link is added.

   The @supports is the fallback: a browser that doesn't understand :popover-open
   ignores the attribute in the template too, so it keeps the row rather than a
   button that opens nothing. Same rule as .reveal — a feature that fails has to
   fail with the content visible. */
@supports selector(:popover-open) {
  @media (max-width: 767px) {
    .site-header__toggle { display: inline-flex; }

    /* The panel's layout is on .site-nav itself, not only on :popover-open.
       While the menu closes, the list stays on screen for the length of the
       closing transition, and by then it is no longer :popover-open. With the
       layout on :popover-open only, the list fell back to the desktop row for
       that time, and the links flashed up in the bar.

       Open, the panel is in the top layer, so it is positioned against the
       viewport and needs none of the header's stacking context. */
    .site-nav {
      display: none;
      flex-direction: column;
      /* The desktop row wraps. A column that wraps puts the links that do not
         fit into a second column, off the right edge of the screen. It must
         scroll instead. */
      flex-wrap: nowrap;
      gap: 0;
      /* It fills the screen below the bar. A panel that stopped part way
         down left the page showing under it, and covered the lower half of
         the Donate button's glow, so the glow looked cut off. */
      position: fixed;
      inset: var(--header-height) 0 0 0;
      width: auto;
      height: auto;
      max-height: none;
      overflow-y: auto;
      overscroll-behavior: contain;
      margin: 0;
      padding: var(--space-2) var(--gutter) var(--space-8);
      border: 0;
      background: var(--color-surface);
      /* This size is between --text-sm and --text-base, and the scale has no
         step there. The row's 14px is too small once each link is on its own
         line. The body's 18px makes a short menu fill the screen. The padding
         below gives the tap target its size, not the type. */
      font-size: 1rem;
    }
    .site-nav:popover-open { display: flex; }
    .site-nav a {
      display: block;
      padding-block: var(--space-3);
    }
    /* This applies to direct children only. Without the `>`, the sub-items get
       dividers as well. The menu then looks like one flat list of nine items,
       and not like three items with children. */
    .site-nav > li + li {
      border-block-start: 1px solid var(--color-border);
    }

    /* In the panel the second level is simply indented and always open. A
       disclosure inside a disclosure is a tap too many, and a panel that can
       scroll has the room. */
    .site-nav > li { display: block; }
    .site-nav .site-nav__sub {
      position: static;
      opacity: 1;
      visibility: visible;
      min-width: 0;
      padding: 0 0 var(--space-2) var(--space-4);
      border: 0;
      border-radius: 0;
      background: none;
      box-shadow: none;
      font-size: 1rem;
    }
    .site-nav .site-nav__sub a { padding: var(--space-2) var(--space-4); border-radius: 0; }
    /* The desktop panel's animation does not apply here. The second level is
       part of the panel, and arrives with its parent link. */
    .site-nav .site-nav__sub,
    .site-nav .site-nav__sub li {
      transform: none;
      transition: none;
    }
    .site-nav .site-nav__sub li { opacity: 1; }
    /* Nothing is hidden down here, so nothing needs pointing at. */
    .site-nav .site-nav__chevron { display: none; }

    /* The large Donate button sits at the foot of the screen, where a thumb
       is. `margin-top: auto` pushes it down in the panel's flex column. On a
       short screen with a long menu there is no free space, and it simply
       follows the last link. */
    .site-nav > .site-nav__cta {
      display: block;
      margin-block-start: auto;
      padding-block-start: var(--space-8);
      border: 0;
    }
    /* It arrives only once the opening wipe has finished, which takes 560ms.
       With the links' timing it came in part way through, while the circle
       was still crossing it. Change the wipe, change this.

       The links drop in from above, but this button rises from below, from
       the foot of the screen where it sits. On a close it sinks back. */
    .site-nav > .site-nav__cta { transform: translateY(0.75rem); }
    .site-nav:popover-open > .site-nav__cta { --delay: 560ms; }
    @starting-style {
      .site-nav:popover-open > .site-nav__cta { transform: translateY(0.75rem); }
    }
    .site-nav .site-nav__cta .btn {
      display: flex;
      width: 100%;
      padding: 1.125rem 2rem;
      font-size: var(--text-lg);
    }

    /* While the menu is open the bar's Donate button has no glow. The panel
       starts at the bar's lower edge, so it cut the glow off in a straight
       line. The two shadows are transparent rather than `none`, because the
       glow is one part of a box-shadow list and `none` there is invalid. */
    .site-header__inner:has(.site-nav:popover-open) .site-header__donate {
      --donate-glow: 0 0 0 transparent, 0 0 0 transparent;
      --donate-glow-hover: 0 0 0 transparent, 0 0 0 transparent;
    }

    /* The page behind a full-screen menu must not scroll. */
    html:has(.site-nav:popover-open) { overflow: hidden; }

    /* --- Opening and closing ----------------------------------------------

       Opening, the panel grows out of the menu button as a circle, centred
       on the button, until it covers the screen. Then the links drop in one
       after the other. Closing, the links all leave together, and the circle
       shrinks back into the button: a person who closes a menu has finished
       with it, so that half is faster.

       The circle is a mask, not a change of size. Size costs a layout on
       every frame. A mask only changes what is painted. It is a radial
       gradient, opaque in the middle and fading to nothing over its outer
       `--menu-feather`, so the growing edge is soft. It used to be a
       `clip-path: circle()`, which Chrome can animate on the compositor, but
       a clip's edge is always a hard line, and on a white panel over a dark
       page it looked harsh. The mask repaints the panel on each frame
       instead, which for one panel for half a second is a fair price.

       The browser can only animate the radius because `--menu-radius` is
       registered with @property, at the top of this file's header section,
       as a <length>. An unregistered custom property is a string, and it
       would jump from start to end. A browser without @property gets that
       jump: the menu appears and disappears at once.

       `--menu-origin` is the centre of the button, measured from the panel's
       top left corner. The panel starts below the bar, so the centre is
       above it: half the bar's height, negative. Across, it is the panel's
       right edge, minus the gutter, plus the 0.5rem the button reaches into
       the gutter, minus half the button's 2.75rem width.

       The open circle has to reach the far corner of the panel, plus the
       feather, so that the corner is fully opaque. The screen's diagonal,
       `hypot(100vw, 100dvh)`, always reaches, with little to spare. Any
       radius bigger than needed is time the animation spends changing
       nothing you can see: with 145vmax, the first quarter of a close looked
       like a pause. 145vmax is the fallback for a browser without `hypot()`,
       and it always reaches, because the far corner is never more than √2 ×
       the longer side of the screen (141vmax) away. The fallback has to be
       an @supports block and not two declarations in a row: a registered
       property checks its value only after the cascade, so a `hypot()` it
       cannot read would not fall back to the line above. It would become
       0px, and the menu would never show.

       Each link's delay comes from `--i`, its place in the list, which
       navbar.html writes. The delay is in the open state's transition only,
       because the browser uses the transition of the state it is moving TO.

       `display` and `overlay` are in the transition list with
       `allow-discrete`, which is what keeps the element on screen while it
       closes — without them the browser removes it at once and only the
       opening half of this would be visible. @starting-style is the state it
       animates *from*; a browser without it lands on the finished state, which
       is the menu appearing with no animation. The links need their own
       @starting-style, because a child of a `display: none` element has no
       earlier style to move from either. */
    .site-nav {
      --menu-origin:
        calc(100% - var(--gutter) + 0.5rem - 1.375rem)
        calc(var(--header-height) / -2);
      --menu-feather: 4.5rem;
      --menu-radius: 0px;
      mask-image: radial-gradient(
        circle at var(--menu-origin),
        black calc(var(--menu-radius) - var(--menu-feather)),
        transparent var(--menu-radius));
      transition:
        --menu-radius 480ms cubic-bezier(0.4, 0, 0.2, 1),
        display 480ms allow-discrete,
        overlay 480ms allow-discrete;
    }
    .site-nav:popover-open {
      --menu-radius: calc(145vmax + var(--menu-feather));
      transition:
        --menu-radius 560ms cubic-bezier(0.3, 0, 0.2, 1),
        display 560ms allow-discrete,
        overlay 560ms allow-discrete;
    }
    @supports (width: hypot(1px, 1px)) {
      .site-nav:popover-open {
        --menu-radius: calc(hypot(100vw, 100dvh) + var(--menu-feather));
      }
    }
    @starting-style {
      .site-nav:popover-open { --menu-radius: 0px; }
    }

    .site-nav > li {
      opacity: 0;
      transform: translateY(-0.5rem);
      transition:
        opacity 100ms var(--ease-in),
        transform 100ms var(--ease-in);
    }
    .site-nav:popover-open > li {
      --delay: calc(120ms + var(--i, 0) * 45ms);
      opacity: 1;
      transform: none;
      transition:
        opacity 260ms var(--ease-out) var(--delay),
        transform 420ms var(--ease-out) var(--delay);
    }
    @starting-style {
      .site-nav:popover-open > li {
        opacity: 0;
        transform: translateY(-0.5rem);
      }
    }

    /* The menu button's three lines turn into a cross while the menu is open,
       so the button says what pressing it again will do. The middle line
       shrinks to nothing and the outer two meet in the middle and turn. */
    .site-header__toggle path {
      transform-box: fill-box;
      transform-origin: center;
      transition:
        transform var(--duration-slow) var(--ease-out),
        opacity var(--duration-fast) var(--ease);
    }
    .site-header__inner:has(.site-nav:popover-open) .site-header__toggle path:nth-child(1) {
      transform: translateY(5px) rotate(45deg);
    }
    .site-header__inner:has(.site-nav:popover-open) .site-header__toggle path:nth-child(2) {
      transform: scaleX(0);
      opacity: 0;
    }
    .site-header__inner:has(.site-nav:popover-open) .site-header__toggle path:nth-child(3) {
      transform: translateY(-5px) rotate(-45deg);
    }
  }
}

/* The footer is a dark band on every page — it is in the .on-dark selector
   list above, so its headings, links and muted text are already right and it
   needs no class in the template. */
.site-footer {
  background: var(--color-surface-invert);
  padding-block-start: var(--space-16);
  font-size: var(--text-sm);
}

/* The logo on the dark footer. The committed logo is transparent but its
   wordmark is black, so on a dark band it disappears. The filter flattens it
   to pure white — brightness(0) takes every pixel to black at its existing
   alpha, invert(1) then takes it to white — which is exactly the white logo
   variant the brand guidelines call for on dark backgrounds. If a real white
   asset is ever committed, drop the filter and point at it. */
/* The footer's columns. This does not use .grid-cols-4. The fourth column is
   the "Key pages" menu, and it is absent when there are no footer menu rows.
   Four equal tracks then leave a quarter of the band empty on the right. These
   tracks are proportional instead. The wide track holds the logo and the ABN
   paragraph, and the subscribe form needs room for three fields and a button.
   The four-column case has a rule of its own, so neither shape has an empty
   area. */
.site-footer__grid {
  display: grid;
  gap: var(--space-12);
  grid-template-columns: 1.4fr 1.1fr 1.3fr;
}
.site-footer__grid:has(> :nth-child(4)) {
  grid-template-columns: 1.4fr 1.1fr 1.3fr 0.85fr;
}
@media (min-width: 768px) and (max-width: 1023px) {
  .site-footer__grid,
  .site-footer__grid:has(> :nth-child(4)) {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
@media (max-width: 767px) {
  .site-footer__grid,
  .site-footer__grid:has(> :nth-child(4)) {
    grid-template-columns: minmax(0, 1fr);
  }
}

.site-footer__logo {
  width: 15rem;
  max-width: 100%;
  height: auto;
  filter: brightness(0) invert(1);
}

/* A column heading, with a short brand rule under it. The rule is a
   pseudo-element rather than a border so it stays the same short length
   whatever the heading says — a border would run the width of the text and
   the four columns would each get a different one. */
.site-footer__heading {
  font-family: var(--font-heading);
  font-size: var(--text-lg);
  font-weight: var(--weight-bold);
  color: var(--white);
  margin-block-end: var(--space-6);
}
.site-footer__heading::after {
  content: "";
  display: block;
  width: 3.5rem;
  height: 2px;
  background: var(--teal-500);
  margin-block-start: var(--space-3);
}

/* The list of links in a footer column. Underlined, because in a block of
   quiet grey text on dark there is nothing else to say "this is a link". */
.site-footer__links {
  list-style: none;
  padding: 0;
  display: grid;
  gap: var(--space-3);
}
.site-footer__links a { text-decoration: underline; }

/* One line of contact detail: an icon, then the text, with the text staying
   in its own column when it wraps. */
.site-footer__detail {
  display: grid;
  grid-template-columns: 1.5rem 1fr;
  gap: var(--space-3);
  align-items: start;
  margin-block-end: var(--space-4);
}
.site-footer__detail .icon { color: var(--teal-400); }

/* A grid item stretches to fill its track. Without this rule the phone link and
   the email link are boxes as wide as the column. A click far past the end of
   the address then follows the link, and a drag from there moves the link
   instead of selecting the text. This rule makes each link only as wide as its
   text, so it behaves like the inline link it looks like. */
.site-footer__detail > * { justify-self: start; }

/* A click on the postal address copies it. It is a button, not a span, so that
   the keyboard can reach it and a screen reader says that it does something.
   Its style is the same as the phone and email links beside it. Thus the column
   reads as three lines of contact detail, and not as two links and a
   control. */
.site-footer__copy {
  font: inherit;
  text-align: start;
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  color: var(--color-link);
  transition: var(--transition-colors);
}
.site-footer__copy:hover { color: var(--color-link-hover); }
.site-footer__copy:hover .site-footer__copy-label { text-decoration: underline; }

/* The copy icon appears on hover, just after the end of the address.

   The icon hangs off an empty marker span. It is not part of the line itself. A
   browser can break a line on either side of an inline image, whatever the
   markup says, because there is no whitespace to remove. Therefore, in the
   flow, the icon lands alone on its own line whenever the address nearly fills
   the column. The marker is an inline box with no width, so it takes no part in
   line breaking and cannot land alone. The marker sits immediately after the
   last character, so the icon lands beside "3108" wherever the address wraps.
   The icon fades in and is never hidden with `display`, so nothing moves under
   the pointer. */
.site-footer__copy-icon-slot { position: relative; }
.site-footer__copy-icon {
  position: absolute;
  inset-inline-start: var(--space-2);
  /* The icon is centred on the marker's own line box, which is the text's line
     box. Thus the icon sits with the address, and not on its baseline. */
  top: 50%;
  transform: translateY(-50%);
  width: 1.05em;
  height: 1.05em;
  /* The base rule `svg { max-width: 100% }` measures against the marker, and
     the marker has no width. Without this rule the icon has no width either. */
  max-width: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease);
}
.site-footer__copy:hover .site-footer__copy-icon,
.site-footer__copy:focus-visible .site-footer__copy-icon { opacity: 1; }

/* A social account, drawn as a square tile. Tiles are better than plain logos
   here, because the logos have different visual weights. A shape around each
   one makes them read as one row. */
.social-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  border-radius: var(--radius-sm);
  background: var(--color-primary);
  color: var(--white);
  transition: var(--transition-colors), var(--transition-transform);
}
.social-link:hover {
  background: var(--teal-500);
  color: var(--white);
  transform: translateY(-2px);
}
/* Brand logos are solid shapes, not line drawings, so these fill rather than
   stroke — the opposite of .icon. */
.social-link svg { width: 1.25rem; height: 1.25rem; fill: currentColor; }

/* The strip along the bottom: copyright on one side, the legal links on the
   other. */
.site-footer__bar {
  margin-block-start: var(--space-16);
  border-top: 1px solid rgb(255 255 255 / 0.12);
  padding-block: var(--space-6);
}
.site-footer__bar .container {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4) var(--space-6);
  align-items: center;
  justify-content: space-between;
}

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

/* A page's opening band. .hero on its own is a tall section; add .on-dark and
   a background (a gradient token, or a photo with .hero--photo) for the dark
   treatment the home page uses. */
.hero {
  position: relative;
  padding-block: clamp(4rem, 10vw, 9rem);
}
.hero__inner {
  position: relative;
  z-index: var(--z-raised);
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--gutter);
}
/* Put the photo in a style attribute on the section — it is content, not
   design. Set background-image to two layers: --gradient-photo-wash first,
   then the image, so the wash paints over it.

   The background-colour is the deepest brand teal, and it matters in two
   places. The wash is semi-transparent, so anywhere the photograph does not
   reach — while it loads, if it fails, or at a viewport shape `cover` cannot
   fill — the wash was painting over the page ground and the band came out
   pale. And it is the same colour --hero-fade-to lands on, so the band and
   its own fade agree instead of meeting at a seam. */
.hero--photo {
  background-color: var(--teal-950);
  background-size: cover;
  background-position: center;
}

/* Fades the foot of a photo band into the deepest brand teal, which is the
   band's own background colour. Thus the photograph resolves into brand
   colour, and the light page starts under it on a clean edge. Drop it in as
   the last child of a .hero.

   It used to fade to the page ground by default, and only the home page set
   the teal. The fade to white made the whole foot of the band look washed
   out, and no page used it. To fade somewhere else, set --hero-fade-to on the
   .hero. */
.hero__fade {
  position: absolute;
  inset-inline: 0;
  bottom: 0;
  height: var(--space-24);
  background: linear-gradient(180deg, transparent, var(--hero-fade-to, var(--teal-950)));
}

/* --- Band edges ----------------------------------------------------------- *
   Where one band meets the next, the join does not have to be a straight
   line. These four classes bend it: a shallow dome, or an angled drop.

   Read them one way and they always make sense: **the class goes on the band
   the shape is made of, and the shape bulges out of that band into its
   neighbour.** A white content band under a dark hero takes .curve-top, and
   the white curves up into the dark. The dark band could take .curve-bottom
   instead and bulge down into the white — same joint, opposite shape, and you
   pick whichever colour you want on the outside of the curve.

   Two custom properties, both set on the band, not on the shape:

     --edge-color   what the shape is painted in. Defaults to the page ground,
                    which is right whenever the band itself is white. Set it
                    to the band's own background otherwise:
                      <section class="section bg-invert curve-bottom"
                               style="--edge-color: var(--teal-950)">
     --edge-height  how far the shape reaches out of the band. Fluid by
                    default so it is not 80px tall on a phone.
     --edge-spread  curves only: how flat the dome is. See below.

   The shape reaches into the neighbouring band, so it eats --edge-height out
   of whatever that band has at the top or bottom — which is its padding, if
   there is enough of it, and its first line of text if there is not. The
   default height is deliberately a little under a plain .section's padding at
   every viewport width, so the common case needs no thought. Raise it and you
   have to look at what is on the other side of the join.

   Add .edge-reverse to mirror a slope left-to-right. It flips a curve too,
   which does nothing, because a dome is already symmetrical.

   Three things worth knowing before reaching for one:

   - The shape is drawn *outside* the band's box, so the band must not clip
     it. .aurora sets `overflow: hidden`, so a band with .aurora on it cannot
     also have a curve — put the curve on the band on the other side of the
     joint instead, which is the same picture.
   - Only one shape per edge. .curve-top uses ::before and .curve-bottom uses
     ::after, so a band can carry one of each, but .curve-top and .slope-top
     together would be two rules fighting over one pseudo-element.
   - The band's own background must be a flat colour where the shape meets it,
     or the join shows. A gradient band is fine as long as --edge-color
     matches the gradient at that edge.
   ------------------------------------------------------------------------- */

.curve-top, .curve-bottom, .slope-top, .slope-bottom { position: relative; }

.curve-top::before,
.curve-bottom::after,
.slope-top::before,
.slope-bottom::after {
  content: "";
  position: absolute;
  /* Above the neighbouring band's background. A positioned pseudo-element
     already paints over a later sibling's background, but not over one that
     is itself positioned — and .hero and .aurora both are. */
  z-index: 1;
  pointer-events: none;
  height: var(--edge-height, clamp(2rem, 5.5vw, 5rem));
  background: var(--edge-color, var(--color-bg));
}

/* The 1px overlap is what stops a hairline of the band underneath showing
   through at the join on a fractional device pixel ratio. */
.curve-top::before,
.slope-top::before { bottom: calc(100% - 1px); }
.curve-bottom::after,
.slope-bottom::after { top: calc(100% - 1px); }

.curve-top::before,
.curve-bottom::after,
.slope-top::before,
.slope-bottom::after { inset-inline: 0; }

/* A dome, cut out of a full-width box by an ellipse.

   --edge-spread is the ellipse's horizontal radius as a share of the band's
   width, and it is what stops the arc reading as a lens: at 50% the ellipse
   is exactly as wide as the band, so the curve runs down to nothing at both
   edges and comes to a point. Above 50% you see only the middle of a wider
   ellipse, which is the shape wanted — the larger the number the flatter it
   is. 60% is the default and what the home page uses.

   The alternative was a box wider than the band with a border-radius on it,
   which draws the same picture and makes the page scroll sideways: an
   overflowing decoration is still overflow, and `overflow-x: clip` on the
   root does not reliably contain it once it propagates to the viewport.
   Clipping the shape out of a box that fits is the version with no side
   effects anywhere else on the page. Reach for --edge-height, not width, to
   make a curve more pronounced. */
.curve-top::before   { clip-path: ellipse(var(--edge-spread, 60%) 100% at 50% 100%); }
.curve-bottom::after { clip-path: ellipse(var(--edge-spread, 60%) 100% at 50% 0); }

/* An angled drop: the join starts --edge-height higher on the left and comes
   down to meet the band on the right. */
.slope-top::before   { clip-path: polygon(0 0, 0 100%, 100% 100%); }
.slope-bottom::after { clip-path: polygon(0 0, 100% 0, 0 100%); }

.edge-reverse::before,
.edge-reverse::after { transform: scaleX(-1); }

/* --- Anything on a dark background ---------------------------------------- */

/* The one way to put content on a dark band. It repoints the text tokens
   rather than setting colours directly, so every child — headings, links,
   .text-muted, a .card inside it — follows without needing its own class. */
.on-dark,
.site-footer,
.bg-invert,
.bg-gradient-hero,
.bg-gradient-primary,
.bg-gradient-primary-deep,
.bg-gradient-teal-bronze,
.bg-gradient-ink,
.bg-gradient-horizon,
.bg-gradient-forest,
.bg-gradient-dusk,
.bg-gradient-mesh {
  --color-text: var(--color-text-invert);
  --color-heading: var(--white);
  --color-link: var(--white);
  --color-link-hover: var(--teal-200);
  --color-text-muted: rgb(255 255 255 / 0.78);
  --color-text-subtle: rgb(255 255 255 / 0.65);
  --color-border: rgb(255 255 255 / 0.25);
  --color-border-strong: rgb(255 255 255 / 0.4);
  /* The teal focus ring is invisible on a teal band — which is the whole
     point of the ring gone. On dark it becomes white, so a keyboard user
     gets the same signal here as anywhere else. Repointing the token means
     every input, button and link in the band follows without its own rule. */
  --focus-rgb: 255 255 255;
  --focus-color: rgb(255 255 255 / 0.55);
  /* This is a transparent white background, not a light solid one. It is just
     lighter than the band, so that a reader sees it as a chip. It also keeps
     the band's white text readable. */
  --color-code-bg: rgb(255 255 255 / 0.15);
  color: var(--color-text-invert);
}

/* The eyebrow's brand teal is unreadable on a dark teal band. Therefore on a
   dark band it uses the muted white that the band already defines. */
.on-dark .eyebrow { color: var(--color-text-muted); }

/* This returns to dark text inside a dark band. The case that matters is a
   white card. Without this rule the card takes the band's white text onto its
   own white background, and the content becomes invisible. The rule resets the
   colour tokens instead of setting `color`. Thus the card's headings, its links
   and its muted text all return together. */
.on-light,
.on-dark .card {
  --color-text: var(--grey-950);
  --color-heading: var(--color-dark);
  --color-link: var(--teal-700);
  --color-link-hover: var(--color-dark);
  --color-text-muted: var(--grey-600);
  --color-text-subtle: var(--grey-500);
  --color-border: var(--grey-200);
  --color-border-strong: var(--grey-300);
  --color-code-bg: var(--bronze-100);
  /* Back to the teal ring — a white one on a white card is no ring at all. */
  --focus-rgb: 12 134 155;
  --focus-color: rgb(var(--focus-rgb) / 0.55);
  color: var(--color-text);
}

/* --- Buttons -------------------------------------------------------------- */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  font-family: var(--font-body);
  font-weight: var(--weight-semibold);
  font-size: var(--text-base);
  line-height: 1;
  padding: 0.75rem 1.5rem;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  cursor: pointer;
  text-align: center;
  text-decoration: none;
  transition: var(--transition-colors), var(--transition-shadow),
              transform var(--duration-fast) var(--ease);
}
.btn:hover { text-decoration: none; }
.btn:active { transform: translateY(1px); }
.btn:disabled, .btn[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
  transform: none;
}

.btn--primary {
  background: var(--color-primary);
  color: var(--white);
  box-shadow: var(--shadow-sm);
}
.btn--primary:hover  { background: var(--color-primary-hover); color: var(--white); }
.btn--primary:active { background: var(--color-primary-active); }

.btn--secondary {
  background: var(--color-accent);
  color: var(--white);
  box-shadow: var(--shadow-sm);
}
.btn--secondary:hover { background: var(--color-accent-hover); color: var(--white); }

.btn--outline {
  background: transparent;
  color: var(--color-primary);
  border-color: var(--color-primary);
}
.btn--outline:hover { background: var(--color-primary); color: var(--white); }

.btn--ghost { background: transparent; color: var(--color-primary); }

/* This is the glass control: a button over a photograph. It is the same
   material as .glass, with the brighter edge, because a control must show
   where it is. On hover the edge becomes solid white. See section 4f for the
   numbers.

   THIS RULE IS SCOPED TO .on-dark, so it is closed by default and not only
   documented. Its label is white, and on a white page the fill would be the
   dark teal with nothing behind it to explain it. Outside a dark band the
   modifier does nothing, and the button keeps any other variant it carries.
   .bg-gradient-sunrise is the one light gradient, and it is deliberately not
   in the .on-dark list, so the same rule excludes it. */
.on-dark .btn--glass {
  background: var(--glass-tint);
  color: var(--white);
  border-color: var(--glass-edge-bright);
  backdrop-filter: var(--glass-filter);
  -webkit-backdrop-filter: var(--glass-filter);
}
.on-dark .btn--glass:hover {
  border-color: var(--white);
  color: var(--white);
}
.btn--ghost:hover { background: var(--color-primary-soft); }

/* For dark bands: a solid white button, and an outlined one beside it. */
.btn--white {
  background: var(--white);
  color: var(--color-dark);
  box-shadow: var(--shadow-lg);
}
/* On hover the button stays white and gains light. It does not become warm.
   .btn--white is used on dark bands and photographic bands, where the colour
   around it is teal. A bronze tint on hover moved the one white element on the
   band away from the colour of everything around it. The tint is now the
   coolest step of the brand ramp, which is hard to see on its own. The halo
   does most of the work. */
.btn--white:hover {
  background: var(--teal-50);
  color: var(--color-dark);
  box-shadow: var(--shadow-xl), var(--glow-white);
}

.btn--outline-white {
  background: transparent;
  color: var(--white);
  border-color: rgb(255 255 255 / 0.4);
}
.btn--outline-white:hover {
  background: rgb(255 255 255 / 0.1);
  border-color: rgb(255 255 255 / 0.7);
  color: var(--white);
}

.btn--sm    { padding: 0.5rem 1rem; font-size: var(--text-sm); }
.btn--lg    { padding: 1rem 2rem; font-size: var(--text-lg); }
.btn--pill  { border-radius: var(--radius-pill); }
.btn--block { display: flex; width: 100%; }

/* A label that wraps gets real line spacing and lines of even length, and
   the icon beside it never gets squashed. line-height: 1 is correct for one
   line and puts two lines on top of each other. */
.btn { line-height: 1.2; text-wrap: balance; }
.btn > .icon { flex: none; }

/* On a phone, buttons in a row take the full width, one under the other, so
   no label wraps because its neighbour took half the row. The large size
   steps down so its label stays on one line. CSS cannot fix a long label:
   keep it to about 20 characters (the style guide says so). */
@media (max-width: 767px) {
  .cluster > .btn { flex: 1 1 100%; }
  .btn--lg { font-size: var(--text-base); padding-inline: var(--space-6); }
}

/* --- Card ----------------------------------------------------------------- */

.card {
  background: var(--color-surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
  padding: var(--space-8);
}
.card--flat { box-shadow: none; border: 1px solid var(--color-border); }
.card--soft { background: var(--color-surface-alt); box-shadow: none; }

/* A whole card that is a link. Lifts on hover, and keeps heading colour —
   without this the card's text turns link-blue. */
.card--link {
  display: block;
  color: inherit;
  transition: var(--transition-shadow), var(--transition-transform),
              border-color var(--duration-base) var(--ease);
}
.card--link:hover {
  transform: translateY(-2px);
  box-shadow: var(--shadow-xl);
  text-decoration: none;
  color: inherit;
}

/* Two to four ways onward, such as the row under the home hero. Put
   .link-panel on a .card, and make each link a .link-panel__item:

     <div class="card link-panel">
       <a class="link-panel__item" href="/donate/">
         <h3 class="link-panel__title">
           Donate
           <svg class="icon icon--inline" viewBox="0 0 24 24" aria-hidden="true">
             <path d="M5 12h14M13 6l6 6-6 6"/>
           </svg>
         </h3>
         <p>One sentence that says where the link goes.</p>
       </a>
       ...
     </div>

   It is ONE surface with a hairline between the links, and each link is a
   heading, an arrow and a sentence. It replaced .card--action, which was a
   row of three matching cards, each with an icon in a gradient tile. That row
   is the layout every generated page has, and its icons said nothing that the
   headings did not. Three more reasons:

     - On a phone, three cards under each other were about 750px of scroll
       before the page began. Three rows of one panel are about 400px.
     - The heading is the link. The old card ended in a second label
       ("Explore") that said the heading again.
     - A link needs no tile, no gradient and no lift to show that it is a
       link. The arrow says so, and the hover is the heading's colour and a
       small move of the arrow. That is one response to one hover.

   The hairline is a pseudo-element, not a border, so it can stop short of the
   panel's edges. Each link takes the panel's radius, so the focus ring is
   rounded on every one. Nothing here uses overflow: hidden, because that
   would cut the focus ring off. */
.link-panel {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  padding: 0;
}

.link-panel__item {
  position: relative;
  display: block;
  padding: var(--space-8);
  border-radius: inherit;
  color: inherit;
}
.link-panel__item:hover { color: inherit; text-decoration: none; }

.link-panel__item + .link-panel__item::before {
  content: "";
  position: absolute;
  inset-block: var(--space-6);
  inset-inline-start: 0;
  width: 1px;
  background: var(--color-border);
}

/* The hairlines beside a focused link would show through its ring. */
.link-panel__item:focus-visible::before,
.link-panel__item:focus-visible + .link-panel__item::before { opacity: 0; }

.link-panel__title {
  font-size: var(--text-2xl);
  margin-block: 0 var(--space-2);
  transition: var(--transition-colors);
}
.link-panel__title .icon {
  margin-inline-start: var(--space-1);
  color: var(--color-primary);
  stroke-width: 2.25;
  transition: var(--transition-transform);
}
.link-panel__item:hover .link-panel__title { color: var(--color-link); }
.link-panel__item:hover .link-panel__title .icon { transform: translateX(0.25rem); }

.link-panel__item > p { margin: 0; color: var(--color-text-muted); }

/* Below 1024px three columns are narrow, so the padding steps down. On a
   phone the links are rows, and the hairline lies between them. */
@media (max-width: 1023px) {
  .link-panel__item { padding: var(--space-6); }
}
@media (max-width: 767px) {
  .link-panel { grid-auto-flow: row; }
  .link-panel__item + .link-panel__item::before {
    inset-block: 0 auto;
    inset-inline: var(--space-6);
    width: auto;
    height: 1px;
  }
}

/* A tinted tile for an icon. It holds an inline <svg>, and sets `color`, so an
   icon drawn with `stroke: currentColor` takes the brand teal without saying
   so itself — and inverts on its own inside a dark band.

   Use it for an icon that has a job, such as the one icon that tells a reader
   what kind of thing a card is. Do not put one at the top of each card in a
   row. See .link-panel above. */
.card__icon {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3rem;
  height: 3rem;
  border-radius: var(--radius-md);
  background: var(--color-primary-soft);
  color: var(--color-primary);
  font-size: var(--text-2xl);
  margin-block-end: var(--space-5);
}
.card__icon svg { width: 1.5rem; height: 1.5rem; }

/* Icon and title on one row, for a card whose heading is a word or two. The
   tile keeps its size and the title sits beside it, so the card opens on a
   line rather than on a stack of two. Put the icon and the heading inside it
   and neither needs its own margin — the row supplies the gap and the space
   below. */
.card__head {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin-block-end: var(--space-4);
}
.card__head .card__icon { flex: none; margin-block-end: 0; }
.card__head > * { margin-block: 0; }

/* House style for a line icon: drawn on a 24-unit grid, no fill, and a stroke
   that inherits its colour from whatever it sits in. Every icon on the site
   is written this way, which is what stops one of them arriving three times
   heavier than its neighbours. */
.icon {
  width: 1.5rem;
  height: 1.5rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.75;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* An icon set in a line of text. It takes its size from the type rather than a
   fixed one, so the same icon works beside a small label and inside a large
   button, and it sits on the baseline like a letter.

   The base reset sets every svg to display: block, which is right for an icon
   in a tile and wrong here — it would put the arrow on its own line — so this
   puts it back in the text flow. */
.icon--inline {
  display: inline-block;
  width: 1em;
  height: 1em;
  vertical-align: -0.125em;
}

/* A link or a button that ends in an arrow. The arrow moves a little on
   hover. Put .link-arrow on the link, and put an .icon.icon--inline arrow
   inside it:

     <a href="/donate/" class="link-arrow">
       Donate now
       <svg class="icon icon--inline" viewBox="0 0 24 24" aria-hidden="true">
         <path d="M5 12h14M13 6l6 6-6 6"/>
       </svg>
     </a>

   The arrow used to be a "→" character. The site now draws it, for the same
   reason it draws every other icon. A character comes from a different font on
   every device, at a weight the page does not control, and it cannot take the
   site's stroke weight.

   The stroke is much heavier than .icon's 1.75. An arrow at the end of a line
   is set at 1em, so it is the smallest thing on that line. At the house weight
   it looked thin beside the bold text of a button. This rule is the one place
   that sets the weight, so every arrow at the end of a link matches.

   The round cap and the round join come from .icon, and this rule does not
   repeat them. They are as much the reason for the extra weight as the weight
   itself. At 1.75 a round cap on a 1em arrow is too small to see. Only at this
   weight do the ends and the point of the head look drawn, and not cut off. */
.link-arrow .icon { stroke-width: 2.75; transition: var(--transition-transform); }
.link-arrow:hover .icon { transform: translateX(0.25rem); }

/* --- Quote ---------------------------------------------------------------- */

/* A testimonial or a pull quote. Put .quote on the <figure>, and open it with
   a .quote__mark.

   The mark sits above the text in normal flow. It was an oversized glyph hung
   over the card's top-left corner, which put it across the rounded corner and
   straight into the accent border that the base blockquote style draws — two
   quotation marks arguing about the same quotation. Now there is one, it is
   inside the card like everything else, and the blockquote drops the border
   and the italic it would carry in an article body. */
.quote__mark {
  display: block;
  font-family: var(--font-heading);
  font-weight: var(--weight-black);
  font-size: var(--text-4xl);
  line-height: 1;
  color: var(--color-primary);
  opacity: 0.35;
  margin-block-end: var(--space-3);
  user-select: none;
}
.quote blockquote {
  border: 0;
  padding: 0;
  font-style: normal;
}

/* --- Callout -------------------------------------------------------------- */

/* A tinted box for an aside, a warning, a key figure. The default is brand
   teal; the modifiers carry the status colours. */
.callout {
  border: 1px solid var(--color-info-border);
  background: var(--color-info-surface);
  border-radius: var(--radius-md);
  padding: var(--space-5) var(--space-6);
}
/* The tinted surface says which status this is. The border is step 200 of the
   same ramp, the -border token of each role, one or two steps from the
   surface. It is there only so the box keeps its edge on a tinted band.

   The border used to be the strong step of each ramp, 600 to 800. A dark line
   round a pale box made the line the loudest part of the callout, and a box
   of text looked like a form control. Before that the surfaces were all the
   same grey, and only a 4px stripe down one edge told them apart. */
/* .callout--info has no rule. Information is what a plain .callout already
   says, so the two are the same brand teal. The class stays valid in markup. */
.callout--success   { border-color: var(--color-success-border);   background: var(--color-success-surface); }
.callout--warning   { border-color: var(--color-warning-border);   background: var(--color-warning-surface); }
.callout--danger    { border-color: var(--color-danger-border);    background: var(--color-danger-surface); }
.callout--highlight { border-color: var(--color-highlight-border); background: var(--color-highlight-surface); }

/* This is for a dark band or a photograph. There, the solid surfaces above
   would be five bright blocks.

   It is the glass material from section 4f, with 12% of the status colour
   over it. The text becomes white. The BORDER carries the status, in step 200
   of the status ramp, and the wash only supports it. The wash is step 400,
   because at 12% a pale tint only makes the glass milky.

   Over a pure white picture, which is the worst case, white text measures
   5.85:1 or more on all six, and the border measures 4.23:1 or more. On a
   dark band the text is above 10:1 and the border above 7:1.

   The text used to carry a shadow by default, and the border step was limited
   by a ruled grid behind the panel. Both were repairs for a fill of 12% with
   nothing under it. The fill now has the 80% tint under it.

   THIS RULE IS SCOPED TO .on-dark. Anywhere else the modifier does nothing at
   all, and the solid callout above shows instead. That is the failure we
   want, because an unscoped version would put white text on a pale surface. */
.on-dark .callout--glass {
  /* The first declaration is for a browser without color-mix. It drops the
     second, and the text is still white on the dark tint. */
  background: var(--glass-tint);
  background:
    linear-gradient(
      color-mix(in srgb, var(--callout-wash, var(--teal-400)) 12%, transparent),
      color-mix(in srgb, var(--callout-wash, var(--teal-400)) 12%, transparent)),
    var(--glass-tint);
  border-color: var(--callout-border, var(--teal-200));
  backdrop-filter: var(--glass-filter);
  -webkit-backdrop-filter: var(--glass-filter);
  color: var(--white);
}
.on-dark .callout--success.callout--glass   { --callout-wash: var(--eucalypt-400);  --callout-border: var(--eucalypt-200); }
.on-dark .callout--warning.callout--glass   { --callout-wash: var(--sunlit-400);    --callout-border: var(--sunlit-200); }
.on-dark .callout--danger.callout--glass    { --callout-wash: var(--clay-400);      --callout-border: var(--clay-200); }
.on-dark .callout--highlight.callout--glass { --callout-wash: var(--mulberry-400);  --callout-border: var(--mulberry-200); }
.on-dark .callout--warm.callout--glass      { --callout-wash: var(--bronze-400);    --callout-border: var(--bronze-200); }
.callout--warm    { border-color: var(--bronze-200);  background: var(--color-accent-soft); }

/* --- Statistic ------------------------------------------------------------ */

.stat__value {
  font-family: var(--font-heading);
  font-size: var(--text-4xl);
  font-weight: var(--weight-black);
  color: var(--color-primary);
  line-height: var(--leading-none);
}
.stat__label {
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  margin-block-start: var(--space-2);
}

/* --- Small pieces --------------------------------------------------------- */

/* The small uppercase line above a heading. */
.eyebrow {
  display: inline-block;
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wider);
  text-transform: uppercase;
  color: var(--color-primary);
}

.badge {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  padding: 0.25rem 0.6rem;
  border-radius: var(--radius-pill);
  background: var(--color-primary-soft);
  /* This is step 200 of the badge's own ramp, the same as a callout's border. A
     tint this pale needs an edge, or it disappears into a white card. */
  border: 1px solid var(--teal-200);
  color: var(--color-dark);
}
.badge--warm    { background: var(--color-accent-soft); border-color: var(--bronze-200); color: var(--bronze-900); }
.badge--neutral { background: var(--grey-100); border-color: var(--grey-200); color: var(--grey-800); }

.divider {
  border: 0;
  border-top: 1px solid var(--color-divider);
  margin-block: var(--space-8);
}

/* --- Form controls -------------------------------------------------------- */

.field { display: block; margin-block-end: var(--space-4); }

.label {
  display: inline-block;
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--color-text);
  margin-block-end: var(--space-1);
}

.input, .textarea, .select {
  width: 100%;
  font-family: var(--font-body);
  font-size: var(--text-base);
  color: var(--color-text);
  background: var(--white);
  border: 1px solid var(--color-border-strong);
  border-radius: var(--radius-sm);
  padding: 0.65rem 0.85rem;
  transition: var(--transition-colors), var(--transition-shadow);
}
.textarea { min-height: 7rem; resize: vertical; }
/* A select is as tall as an .input beside it. Without this it is shorter: the
   browser sets a select's line-height to `normal`, whatever the rule above
   says, and Safari also ignores its padding, so a select there was 26px
   against the input's 52px. Only a height works in both. The sum is the
   input's own: one line of text at --leading-normal, the padding above and
   below, and the two 1px borders. Change it if the padding or the border
   above changes. */
.select { height: calc(var(--leading-normal) * 1em + 1.3rem + 2px); }
/* Safari's own select still came out shorter than the input beside it, height
   or not. `appearance: none` makes the select a plain box that obeys the rules
   above in every browser, so the arrow is drawn here instead: two gradient
   triangles in currentColor, which follow the text colour with no hex value. */
.select {
  appearance: none;
  padding-inline-end: 2.25rem;
  background-image:
    linear-gradient(45deg, transparent 50%, currentColor 50%),
    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position:
    calc(100% - 1.15rem) 50%,
    calc(100% - 0.85rem) 50%;
  background-size: 0.3rem 0.3rem;
  background-repeat: no-repeat;
}
/* A field takes the same ring as everything else — the rule above already
   gives it one. The only thing added here is the border colour, and it is on
   :focus rather than :focus-visible because a text field being active is
   worth showing whether you arrived by tab or by click. The ring itself stays
   on :focus-visible, so a mouse click does not draw one. */
.input:focus, .textarea:focus, .select:focus {
  border-color: var(--color-primary);
}
.input::placeholder, .textarea::placeholder { color: var(--color-text-subtle); }

/* A pill field needs more room at each end, or the text starts in the curve. */
.input.rounded-pill { padding-inline: var(--space-5); }

/* A field on a dark band. The default .input is white, which is right for a
   single field on a teal panel and wrong for three stacked in a footer —
   three white blocks become the loudest thing on the page, and the footer is
   not where the eye should end up.

   The fill is 9% white, so the field is a little lighter than the band. The
   numbers below are for the footer's band, --teal-950:

     - The edge is 42% white: 3.5:1 against the band and 3.1:1 against the
       fill. It was 22%, which measured 1.95:1, under the 3:1 that a field's
       boundary needs. On a lighter band 42% is under 3:1 too, so use the
       white .input there.
     - The placeholder is 65% white, 5.15:1. Typed text is 9.71:1.
     - On focus the edge goes to 50% and the ring is 28%, not 55%. The edge
       used to become pure white. A 1px pure white line round a pill shows its
       steps on a screen of standard density, and with the site's ring beside
       it there were two bright lines round one field. The edge and the ring
       now read as one soft band. Only --focus-color changes, which is the
       token the ring is designed to take.
     - color-scheme tells the browser that this control is dark. Chrome then
       draws a field it has filled in for the visitor in its dark colours,
       not as a pale blue block. That is Chrome's documented behaviour, and
       nobody has tested it on this field.

   For a short time the fill was 20% black, so that the field looked set into
   the band. Staff preferred the lighter fill. */
.input--dark {
  color-scheme: dark;
  background: rgb(255 255 255 / 0.09);
  border-color: rgb(255 255 255 / 0.42);
  color: var(--white);
  --focus-color: rgb(255 255 255 / 0.28);
}
.input--dark::placeholder { color: rgb(255 255 255 / 0.65); }
.input--dark:hover { border-color: rgb(255 255 255 / 0.5); }
.input--dark:focus {
  background: rgb(255 255 255 / 0.12);
  border-color: rgb(255 255 255 / 0.5);
}

.hint {
  display: block;
  font-size: var(--text-sm);
  color: var(--color-text-muted);
  margin-block-start: var(--space-1);
}

/* --- Long-form text ------------------------------------------------------- */

/* Everything an article body needs. markdown_render wraps its output in this,
   so a writer's markdown — no classes anywhere — comes out with the right
   rhythm, the right reading width and the right heading spacing.
   The measure lives here rather than on `p` globally, so a paragraph in a
   hero or a card still fills its box. */
.prose {
  max-width: var(--measure);
  font-size: var(--text-lg);
  line-height: var(--leading-relaxed);
}
.prose > * + * { margin-block-start: var(--space-5); }
.prose h1, .prose h2, .prose h3,
.prose h4, .prose h5, .prose h6 { margin-block-start: var(--space-10); }
.prose h1:first-child, .prose h2:first-child, .prose h3:first-child,
.prose h4:first-child, .prose h5:first-child, .prose h6:first-child {
  margin-block-start: 0;
}
.prose li + li { margin-block-start: var(--space-2); }
.prose li > ul, .prose li > ol { margin-block-start: var(--space-2); }
.prose a { text-decoration: underline; }
.prose img { border-radius: var(--radius-lg); }
.prose blockquote { margin-block: var(--space-8); }
.prose table { margin-block: var(--space-8); font-size: var(--text-sm); }

/* An image in an article is allowed to break out past the reading measure —
   pictures read better wider than text does. */
.prose > img, .prose > figure { max-width: none; }

/* Footnotes, as Python-Markdown emits them. */
.prose .footnote {
  margin-block-start: var(--space-12);
  padding-block-start: var(--space-6);
  border-top: 1px solid var(--color-divider);
  font-size: var(--text-sm);
  color: var(--color-text-muted);
}

/* Same reading width, for anything that isn't an article body. */
.measure { max-width: var(--measure); }

/* --- Article layout ------------------------------------------------------- */

/* An article page: the reading column, and a sidebar beside it.

   The column is `--measure` wide and no wider, whatever the container does —
   that is the whole point of the sidebar. An article in a full-width container
   with nothing beside it either has to be narrow, which leaves the page looking
   half-empty next to the home page, or has to stretch, which makes it hard to
   read. The sidebar fills the room instead, so the article page is as wide as
   every other page and the text is still the right length. */
.article-layout {
  display: grid;
  grid-template-columns: minmax(0, var(--measure)) minmax(0, 1fr);
  gap: var(--space-16);
  align-items: start;
}

/* Below 1024px there isn't room for both, so the sidebar goes under the
   article — the same collapse the grid utilities do, for the same reason. */
@media (max-width: 1023px) {
  .article-layout { grid-template-columns: minmax(0, 1fr); }
}

/* The sidebar follows the reader down a long article. `top` clears the fixed
   header. It only sticks where there is a second column to stick in; on a
   narrow screen the media query above has already put it in the flow. */
.article-aside {
  font-size: var(--text-sm);
}
@media (min-width: 1024px) {
  .article-aside {
    position: sticky;
    top: calc(var(--header-height) + var(--space-8));
  }
}

.article-aside__title {
  font-family: var(--font-heading);
  font-size: var(--text-sm);
  font-weight: var(--weight-bold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-text-muted);
  padding-block-end: var(--space-3);
  border-bottom: 1px solid var(--color-divider);
}

.article-aside__list {
  list-style: none;
  padding: 0;
  margin: 0;
}
.article-aside__list li {
  padding-block: var(--space-4);
  border-bottom: 1px solid var(--color-divider);
}
.article-aside__list a {
  font-family: var(--font-heading);
  font-weight: var(--weight-bold);
  color: var(--color-text);
  text-decoration: none;
}
.article-aside__list a:hover { color: var(--color-primary); }
.article-aside__date {
  display: block;
  margin-block-start: var(--space-1);
  font-size: var(--text-xs);
  color: var(--color-text-muted);
}

/* --- News index ----------------------------------------------------------- */

/* /latest-news-and-resources/. The newest article is the lead, on a warm band
   with the masthead. Every other article is in the archive under it, one block
   per year.

   It is laid out like the contents page of a journal, not like a grid of
   cards. A grid gives every article the same weight. Here the newest one is
   large and the rest are a list that a person can scan by date and title,
   which is how somebody looks for an article they half remember. */
.news-top {
  background: var(--color-surface-alt);
  padding-block: var(--space-section);
}

.news-masthead {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--space-4) var(--space-8);
  padding-block-end: var(--space-8);
  margin-block-end: clamp(2rem, 5vw, 4rem);
  border-bottom: 1px solid var(--bronze-200);
}
.news-masthead h1 {
  font-size: var(--text-6xl);
  letter-spacing: var(--tracking-tight);
}
.news-masthead__lede {
  margin-block-start: var(--space-3);
  color: var(--color-text-muted);
}
.news-masthead__feed {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  color: var(--color-text-muted);
  text-decoration: none;
}
.news-masthead__feed:hover { color: var(--color-link); }

/* The date above a title. Tabular figures, so a column of dates in the
   archive lines up on the digits. */
.news-date {
  font-size: var(--text-sm);
  font-variant-numeric: tabular-nums;
  color: var(--bronze-800);
}

/* The lead. The picture takes the wider column and the text sits beside it,
   centred on it. */
.news-lead {
  display: grid;
  grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
}
.news-lead__image {
  display: block;
  overflow: hidden;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-md);
}
.news-lead__image img {
  width: 100%;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  transition: transform var(--duration-slow) var(--ease-out);
}
.news-lead:hover .news-lead__image img { transform: scale(1.02); }

.news-lead__title {
  margin-block: var(--space-3) var(--space-4);
  font-size: var(--text-4xl);
}
.news-lead__title a,
.news-item__title a {
  color: var(--color-heading);
  text-decoration: none;
}
.news-lead__title a:hover,
.news-item__title a:hover {
  color: var(--color-link);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}
.news-lead__excerpt {
  margin-block-end: var(--space-6);
  font-size: var(--text-lg);
  color: var(--color-text-muted);
}

/* A lead with no cover is type only, so it gets the width the picture would
   have had, and a larger title to fill it. */
.news-lead--text { grid-template-columns: minmax(0, 1fr); }
.news-lead--text .news-lead__body { max-width: var(--measure); }
.news-lead--text .news-lead__title { font-size: var(--text-5xl); }

/* One year of the archive: the year in the left margin, the list beside it.
   The year sticks while its list scrolls past, so a reader deep in a long
   year still knows which one they are in. */
.news-year {
  display: grid;
  grid-template-columns: 10rem minmax(0, 1fr);
  gap: var(--space-8);
  align-items: start;
}
.news-year + .news-year { margin-block-start: var(--space-16); }

.news-year__label {
  position: sticky;
  top: calc(var(--header-height) + var(--space-6));
  font-size: var(--text-5xl);
  font-weight: var(--weight-light);
  font-variant-numeric: tabular-nums;
  line-height: var(--leading-none);
  color: var(--color-primary);
}

.news-list {
  list-style: none;
  padding: 0;
  margin: 0;
  border-top: 2px solid var(--color-heading);
}

/* One row: date, then title and excerpt, then the cover if there is one. The
   title's link covers the whole row through its ::after, so the row is one
   target and still one link for a screen reader. */
.news-item {
  position: relative;
  display: grid;
  grid-template-columns: 5rem minmax(0, 1fr) 11rem;
  grid-template-areas: "date body thumb";
  gap: var(--space-6);
  padding-block: var(--space-6);
  border-bottom: 1px solid var(--color-divider);
}
.news-item__date  { grid-area: date; padding-block-start: 0.3em; }
.news-item__body  { grid-area: body; }
.news-item__thumb { grid-area: thumb; }

.news-item__title {
  font-size: var(--text-xl);
  line-height: var(--leading-snug);
}
/* The title's link covers the whole row, so a click on the date, the excerpt
   or the cover opens the article. `z-index` keeps it above the cover: the
   cover's hover zoom is a transform, and a transformed element paints over
   an earlier positioned one, so without it a click on the cover did nothing. */
.news-item__title a::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
}
.news-item__excerpt {
  margin-block-start: var(--space-2);
  max-width: var(--measure);
  color: var(--color-text-muted);
}

.news-item__thumb {
  overflow: hidden;
  border-radius: var(--radius-md);
}
.news-item__thumb img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  transition: transform var(--duration-slow) var(--ease-out);
}
.news-item:hover .news-item__thumb img { transform: scale(1.04); }
.news-item:hover .news-item__title a { color: var(--color-link); }

/* On a narrow screen the year sits above its list, and the archive has no
   pictures: each row is the date, the title and the excerpt. The lead keeps
   its cover. A cover beside the text left the text about 220px wide, so a
   title wrapped onto four lines. */
@media (max-width: 767px) {
  .news-lead { grid-template-columns: minmax(0, 1fr); }

  .news-year {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--space-4);
  }
  .news-year + .news-year { margin-block-start: var(--space-12); }
  .news-year__label {
    position: static;
    font-size: var(--text-4xl);
  }

  .news-item {
    grid-template-columns: minmax(0, 1fr);
    grid-template-areas:
      "date"
      "body";
    gap: var(--space-1);
    padding-block: var(--space-5);
  }
  .news-item__thumb { display: none; }
  .news-item__date { padding-block-start: 0; }
  .news-item__title { font-size: var(--text-lg); }
  /* Three lines are enough to decide whether to open it. The whole excerpt is
     one tap away. */
  .news-item__excerpt {
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 3;
    line-clamp: 3;
    overflow: hidden;
  }
}

/* --- Accessibility -------------------------------------------------------- */

/* Hidden on screen, still read aloud. For a label a sighted user gets from
   context and a screen reader user would not. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: absolute;
  top: var(--space-2);
  left: var(--space-2);
  z-index: var(--z-toast);
  padding: var(--space-2) var(--space-4);
  background: var(--color-surface);
  border-radius: var(--radius-sm);
  box-shadow: var(--shadow-md);
  transform: translateY(-200%);
}
.skip-link:focus { transform: translateY(0); }


/* --- Atmosphere ----------------------------------------------------------- *
   Light, depth and movement. Everything here is decoration. Remove any one of
   these classes and the page still reads correctly; it is only flatter. Each
   one is written to that rule, because decoration that carries meaning is
   decoration that somebody will eventually lose.

   Pseudo-elements draw all of it. Therefore a band gets its atmosphere from a
   class on the band, and never from an empty <div> in the markup.

   USE THESE. The default answer to "what does this band look like?" should
   include atmosphere: a gradient with .aurora and .grain over it, a shaped
   join into the band below, a glass panel over a photograph, and .reveal
   on a row that scrolls into view. A page of flat white sections is not the
   safe choice. It is unfinished. This layer is also the cheapest thing in the
   system: it needs no markup and no JavaScript, and a browser that can draw
   none of it still renders the page correctly.

   The word "decoration" above says what happens when you REMOVE one of these.
   It does not mean that adding one needs a reason. There are two limits, and
   neither one is a general request for restraint:

     • Use one or two treatments on a band, not five. Atmosphere is the
       contrast between bands. On a page where every join is shaped and every
       section glows, there is no shape at all. The home page shapes two of
       its six joints, and that is its shape.
     • Glass and blur cost something that the rest does not. Each
       backdrop-filter is its own compositing layer. Therefore use glass on a
       few elements on a band, and not on every card on the page. Glass also
       needs a dark, gradient or photographic background to sit on; see
       --glass-*. Everything else here is paint.
   ------------------------------------------------------------------------- */

/* Slow drifting colour behind a dark band. Two soft radial pools, one warm
   teal and one bright, at low opacity — enough to stop a flat gradient
   reading as a flat gradient. Put it on a section that already has a dark
   background; it paints over that background and under the content.

   The section needs `position: relative` and its content needs to sit above:
   .section and .hero are already relative, and .container is not positioned,
   so add .relative + .z-raised to the inner wrapper (or use .hero__inner,
   which already does both). */
.aurora { position: relative; overflow: hidden; }
.aurora::before {
  content: "";
  position: absolute;
  inset: -30%;
  z-index: 0;
  pointer-events: none;
  background:
    radial-gradient(38% 42% at 18% 28%, rgb(50 155 174 / 0.30) 0%, transparent 70%),
    radial-gradient(34% 38% at 82% 22%, rgb(30 229 242 / 0.12) 0%, transparent 70%),
    radial-gradient(46% 46% at 62% 88%, rgb(133 88 73 / 0.16) 0%, transparent 70%);
  filter: blur(40px);
  /* Slow enough that you cannot watch it move — you only notice the band
     looked different when you scrolled back. Anything faster reads as a
     screensaver. */
  animation: aurora-drift 48s var(--ease-in-out) infinite alternate;
}
@keyframes aurora-drift {
  from { transform: translate3d(-2%, -1%, 0) scale(1); }
  to   { transform: translate3d(2%, 2%, 0) scale(1.06); }
}

/* A faint film grain over a dark band, which stops a large area of flat
   colour banding on cheap screens and gives it some texture. The noise is an
   inline SVG turbulence — no image file, nothing to load. */
/* Use this on text that sits directly on a photograph, with no panel under
   it, where part of the picture is too light or too busy to read against.
   The halo is the background the reader's eye compares each character
   against. Inside its radius the local background becomes 85% --teal-950,
   whatever is underneath.

   There are two shadows, not one. The tight shadow gives each character its
   edge back. The wide shadow darkens the gaps between letters that the tight
   one cannot reach.

   State plainly what this is. WCAG measures text against its background colour
   and says nothing about a shadow. Therefore the correct claim is that the halo
   restores legibility, not that it restores the measured ratio. Where the
   measured ratio itself must hold, put the text on .glass, which is measured.
   Text on .glass needs no halo. */
.text-halo { text-shadow: var(--text-halo); }

/* This sets its own position. It does not rely on .aurora's position. Of the
   two effects, grain is the one that works well on its own.
   Without this rule its ::after moves to the nearest positioned ancestor and
   covers the page in specks. This rule sets position only, and no overflow. The
   film uses inset: 0, so nothing can extend past the box. Clipping here would
   also stop a band from carrying a .curve-* edge, and it would stop anything
   sticky inside the band, and neither failure would report itself. */
.grain { position: relative; }
.grain::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  opacity: 0.04;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

/* This puts a soft area of brand light behind an element, such as a photograph,
   a statistic or a panel. It sits behind the element, it is blurred, and it is
   a little smaller than its parent. Thus the colour looks like light coming out
   from under the element, and not like a border. */
/* `isolation` creates a stacking context, which keeps the z-index: -1 below
   inside this element. Without it the glow paints behind the whole section, and
   the section's background hides it. */
.halo { position: relative; isolation: isolate; }
.halo::before {
  content: "";
  position: absolute;
  inset: 8% 6%;
  z-index: -1;
  border-radius: var(--radius-2xl);
  background: var(--gradient-bright);
  filter: blur(44px);
  opacity: 0.22;
  pointer-events: none;
}

/* A panel over a photograph, for WHITE text. It is the deepest teal at 80%
   over a 16px blur, with a hairline edge. Section 4f has the numbers, and the
   reason for each one.

   It needs .on-dark above it, or on the panel itself, because the text colours
   come from there. A photograph band already has it. */
.glass {
  position: relative;
  background-color: var(--glass-tint);
  border: 1px solid var(--glass-edge);
  border-radius: var(--radius-lg);
  backdrop-filter: var(--glass-filter);
  -webkit-backdrop-filter: var(--glass-filter);
}

/* The other material: a pale panel for DARK text. It changes the colour
   tokens instead of setting `color`, which is the same method .on-light uses.
   Thus a heading, a link and muted text inside the panel all return to
   dark-on-light values together.

   The muted colour and the link colour are one step darker than the page's.
   The frost is a little darker than white, and over a black picture it is
   darker again. With these two, muted text measures 4.74:1 and a link 4.83:1
   in that worst case. With the page's own values they fail. The quiet colours
   always decide how thick a frost must be. */
.glass--frost {
  background-color: var(--glass-frost);
  border-color: rgb(255 255 255 / 0.6);
  --color-text: var(--grey-950);
  --color-heading: var(--color-dark);
  --color-link: var(--teal-900);
  --color-link-hover: var(--color-dark);
  --color-text-muted: var(--grey-700);
  --color-text-subtle: var(--grey-700);
  --color-border: var(--grey-200);
  --focus-rgb: 12 134 155;
  --focus-color: rgb(12 134 155 / 0.55);
  color: var(--grey-950);
}

/* A smaller corner, for a label or a bar. */
.glass--sm { border-radius: var(--radius-sm); }

/* A whole glass panel that is a link. It lifts, and its edge gets brighter.
   It used to get a lighter fill and a white glow as well, which was four
   answers to one hover. */
.glass--link {
  display: block;
  color: inherit;
  transition: var(--transition-transform),
              border-color var(--duration-base) var(--ease);
}
.glass--link:hover {
  transform: translateY(-2px);
  border-color: var(--glass-edge-bright);
  text-decoration: none;
  color: inherit;
}

/* Ink made of gradient. Only ever on a heading or a number — at body size the
   lighter end of the ramp fails contrast, and the text is a painted box as far
   as a browser's high-contrast mode is concerned. */
.text-gradient {
  background-image: var(--gradient-text);
  background-clip: text;
  -webkit-background-clip: text;
  color: transparent;
  /* A descender clipped by the text box is the usual way this goes wrong. */
  padding-block-end: 0.08em;
}
.text-gradient--light { background-image: var(--gradient-text-light); }

/* Glow on rest, and more of it on hover. .glow-hover is the one to reach for
   on a button: a call to action that lights up when you approach it. */
.glow    { box-shadow: var(--glow-md); }
.glow-sm { box-shadow: var(--glow-sm); }
.glow-lg { box-shadow: var(--glow-lg); }
.glow-hover { transition: var(--transition-shadow), var(--transition-transform); }
.glow-hover:hover { box-shadow: var(--glow-md); transform: translateY(-1px); }

/* A hairline that is a gradient rather than a colour — brighter at the top
   left, fading away. Drawn as a padding-box/border-box background pair,
   because a border cannot hold a gradient directly. */
.edge-gradient {
  border: 1px solid transparent;
  background-image:
    linear-gradient(var(--color-surface), var(--color-surface)),
    linear-gradient(135deg, rgb(var(--glow-rgb) / 0.55), rgb(var(--glow-rgb) / 0));
  background-origin: border-box;
  background-clip: padding-box, border-box;
}

/* Fade-and-rise as a band scrolls into view, with no JavaScript at all:
   animation-timeline drives the animation from the element's own position in
   the viewport. Browsers without it (Firefox today) simply see the finished
   state, which is why the rule is written inside @supports and the default is
   "visible" — a reveal that can fail closed hides content. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .reveal {
      animation: reveal-rise linear both;
      animation-timeline: view();
      /* Finished well before the band is at reading height. A reveal you are
         still waiting on while you read is an obstacle, not an entrance. */
      animation-range: entry 5% cover 18%;
    }
  }
}
@keyframes reveal-rise {
  /* A short lift from a soft start, not a slide in from nothing. */
  from { opacity: 0.35; transform: translateY(0.75rem); }
  to   { opacity: 1; transform: none; }
}


/* ============================================================================
   7. UTILITIES
   Single-purpose classes, every one of them locked to a token above. Use these
   for the last-mile adjustments; use the components above for structure.
   ========================================================================== */

/* --- Display -------------------------------------------------------------- */
.block        { display: block; }
.inline-block { display: inline-block; }
.inline       { display: inline; }
.flex         { display: flex; }
.inline-flex  { display: inline-flex; }
.grid         { display: grid; }
.hidden       { display: none; }

/* --- Flex ----------------------------------------------------------------- */
.flex-row     { flex-direction: row; }
.flex-col     { flex-direction: column; }
.flex-wrap    { flex-wrap: wrap; }
.flex-nowrap  { flex-wrap: nowrap; }
.flex-1       { flex: 1 1 0%; }
.flex-auto    { flex: 1 1 auto; }
.flex-none    { flex: none; }

.items-start    { align-items: flex-start; }
.items-center   { align-items: center; }
.items-end      { align-items: flex-end; }
.items-baseline { align-items: baseline; }
.items-stretch  { align-items: stretch; }

.justify-start   { justify-content: flex-start; }
.justify-center  { justify-content: center; }
.justify-end     { justify-content: flex-end; }
.justify-between { justify-content: space-between; }
.justify-around  { justify-content: space-around; }
.justify-evenly  { justify-content: space-evenly; }

/* --- Grid ----------------------------------------------------------------- */
/* These collapse on their own: one column below 768px, two between 768 and
   1024. That is why there is no `md:` prefix in this system. When a layout
   genuinely must keep its columns on a phone — a row of small logos, say —
   add .no-collapse. */
.grid-cols-1 { grid-template-columns: minmax(0, 1fr); }
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
.grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }

/* As many equal columns as fit, each at least 16rem wide. Good for a card row
   whose count changes with the content. */
.grid-auto { grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr)); }

.col-span-2    { grid-column: span 2; }
.col-span-3    { grid-column: span 3; }
.col-span-full { grid-column: 1 / -1; }

@media (max-width: 767px) {
  .grid-cols-2:not(.no-collapse),
  .grid-cols-3:not(.no-collapse),
  .grid-cols-4:not(.no-collapse),
  .grid-cols-5:not(.no-collapse),
  .grid-cols-6:not(.no-collapse) { grid-template-columns: minmax(0, 1fr); }

  .split:not(.no-collapse) { grid-template-columns: minmax(0, 1fr); }

  .col-span-2:not(.no-collapse),
  .col-span-3:not(.no-collapse) { grid-column: span 1; }
}

@media (min-width: 768px) and (max-width: 1023px) {
  .grid-cols-3:not(.no-collapse),
  .grid-cols-4:not(.no-collapse),
  .grid-cols-5:not(.no-collapse),
  .grid-cols-6:not(.no-collapse) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* --- Gap ------------------------------------------------------------------ */
.gap-0  { gap: var(--space-0); }
.gap-1  { gap: var(--space-1); }
.gap-2  { gap: var(--space-2); }
.gap-3  { gap: var(--space-3); }
.gap-4  { gap: var(--space-4); }
.gap-5  { gap: var(--space-5); }
.gap-6  { gap: var(--space-6); }
.gap-8  { gap: var(--space-8); }
.gap-10 { gap: var(--space-10); }
.gap-12 { gap: var(--space-12); }
.gap-16 { gap: var(--space-16); }

.gap-x-2 { column-gap: var(--space-2); }
.gap-x-4 { column-gap: var(--space-4); }
.gap-x-6 { column-gap: var(--space-6); }
.gap-x-8 { column-gap: var(--space-8); }
.gap-x-12 { column-gap: var(--space-12); }

.gap-y-2 { row-gap: var(--space-2); }
.gap-y-4 { row-gap: var(--space-4); }
.gap-y-6 { row-gap: var(--space-6); }
.gap-y-8 { row-gap: var(--space-8); }
.gap-y-12 { row-gap: var(--space-12); }

/* --- Margin --------------------------------------------------------------- */
.m-0  { margin: var(--space-0); }
.m-1  { margin: var(--space-1); }
.m-2  { margin: var(--space-2); }
.m-3  { margin: var(--space-3); }
.m-4  { margin: var(--space-4); }
.m-6  { margin: var(--space-6); }
.m-8  { margin: var(--space-8); }

.mt-0  { margin-top: var(--space-0); }
.mt-1  { margin-top: var(--space-1); }
.mt-2  { margin-top: var(--space-2); }
.mt-3  { margin-top: var(--space-3); }
.mt-4  { margin-top: var(--space-4); }
.mt-5  { margin-top: var(--space-5); }
.mt-6  { margin-top: var(--space-6); }
.mt-8  { margin-top: var(--space-8); }
.mt-10 { margin-top: var(--space-10); }
.mt-12 { margin-top: var(--space-12); }
.mt-16 { margin-top: var(--space-16); }
.mt-20 { margin-top: var(--space-20); }
.mt-24 { margin-top: var(--space-24); }

.mb-0  { margin-bottom: var(--space-0); }
.mb-1  { margin-bottom: var(--space-1); }
.mb-2  { margin-bottom: var(--space-2); }
.mb-3  { margin-bottom: var(--space-3); }
.mb-4  { margin-bottom: var(--space-4); }
.mb-5  { margin-bottom: var(--space-5); }
.mb-6  { margin-bottom: var(--space-6); }
.mb-8  { margin-bottom: var(--space-8); }
.mb-10 { margin-bottom: var(--space-10); }
.mb-12 { margin-bottom: var(--space-12); }
.mb-16 { margin-bottom: var(--space-16); }
.mb-20 { margin-bottom: var(--space-20); }
.mb-24 { margin-bottom: var(--space-24); }

.my-0  { margin-block: var(--space-0); }
.my-2  { margin-block: var(--space-2); }
.my-4  { margin-block: var(--space-4); }
.my-6  { margin-block: var(--space-6); }
.my-8  { margin-block: var(--space-8); }
.my-12 { margin-block: var(--space-12); }
.my-16 { margin-block: var(--space-16); }

.mx-0    { margin-inline: var(--space-0); }
.mx-4    { margin-inline: var(--space-4); }
.mx-auto { margin-inline: auto; }
.ml-auto { margin-left: auto; }
.mr-auto { margin-right: auto; }

/* Negative top margins, for a band that overlaps the one above it. The only
   negative values in the system, and the only sizes we have needed. */
.-mt-4  { margin-top: calc(var(--space-4) * -1); }
.-mt-6  { margin-top: calc(var(--space-6) * -1); }
.-mt-8  { margin-top: calc(var(--space-8) * -1); }
.-mt-12 { margin-top: calc(var(--space-12) * -1); }
.-mt-16 { margin-top: calc(var(--space-16) * -1); }

/* --- Padding -------------------------------------------------------------- */
.p-0  { padding: var(--space-0); }
.p-1  { padding: var(--space-1); }
.p-2  { padding: var(--space-2); }
.p-3  { padding: var(--space-3); }
.p-4  { padding: var(--space-4); }
.p-5  { padding: var(--space-5); }
.p-6  { padding: var(--space-6); }
.p-8  { padding: var(--space-8); }
.p-10 { padding: var(--space-10); }
.p-12 { padding: var(--space-12); }

.pt-0  { padding-top: var(--space-0); }
.pt-2  { padding-top: var(--space-2); }
.pt-4  { padding-top: var(--space-4); }
.pt-6  { padding-top: var(--space-6); }
.pt-8  { padding-top: var(--space-8); }
.pt-12 { padding-top: var(--space-12); }
.pt-16 { padding-top: var(--space-16); }
.pt-20 { padding-top: var(--space-20); }
.pt-24 { padding-top: var(--space-24); }

.pb-0  { padding-bottom: var(--space-0); }
.pb-2  { padding-bottom: var(--space-2); }
.pb-4  { padding-bottom: var(--space-4); }
.pb-6  { padding-bottom: var(--space-6); }
.pb-8  { padding-bottom: var(--space-8); }
.pb-12 { padding-bottom: var(--space-12); }
.pb-16 { padding-bottom: var(--space-16); }
.pb-20 { padding-bottom: var(--space-20); }
.pb-24 { padding-bottom: var(--space-24); }

.py-0  { padding-block: var(--space-0); }
.py-2  { padding-block: var(--space-2); }
.py-4  { padding-block: var(--space-4); }
.py-6  { padding-block: var(--space-6); }
.py-8  { padding-block: var(--space-8); }
.py-10 { padding-block: var(--space-10); }
.py-12 { padding-block: var(--space-12); }
.py-16 { padding-block: var(--space-16); }
.py-20 { padding-block: var(--space-20); }
.py-24 { padding-block: var(--space-24); }

.px-0  { padding-inline: var(--space-0); }
.px-2  { padding-inline: var(--space-2); }
.px-4  { padding-inline: var(--space-4); }
.px-6  { padding-inline: var(--space-6); }
.px-8  { padding-inline: var(--space-8); }
.px-12 { padding-inline: var(--space-12); }

/* --- Typography ----------------------------------------------------------- */
.text-xs      { font-size: var(--text-xs); }
.text-sm      { font-size: var(--text-sm); }
.text-base    { font-size: var(--text-base); }
.text-lg      { font-size: var(--text-lg); }
.text-xl      { font-size: var(--text-xl); }
.text-2xl     { font-size: var(--text-2xl); }
.text-3xl     { font-size: var(--text-3xl); }
.text-4xl     { font-size: var(--text-4xl); }
.text-5xl     { font-size: var(--text-5xl); }
.text-6xl     { font-size: var(--text-6xl); }
.text-display { font-size: var(--text-display); line-height: var(--leading-tight); }

.font-heading { font-family: var(--font-heading); }
.font-body    { font-family: var(--font-body); }
.font-mono    { font-family: var(--font-mono); }

.font-light    { font-weight: var(--weight-light); }
.font-normal   { font-weight: var(--weight-regular); }
.font-medium   { font-weight: var(--weight-medium); }
.font-semibold { font-weight: var(--weight-semibold); }
.font-bold     { font-weight: var(--weight-bold); }
.font-black    { font-weight: var(--weight-black); }

.leading-none    { line-height: var(--leading-none); }
.leading-tight   { line-height: var(--leading-tight); }
.leading-snug    { line-height: var(--leading-snug); }
.leading-normal  { line-height: var(--leading-normal); }
.leading-relaxed { line-height: var(--leading-relaxed); }

.tracking-tighter { letter-spacing: var(--tracking-tighter); }
.tracking-tight   { letter-spacing: var(--tracking-tight); }
.tracking-normal  { letter-spacing: var(--tracking-normal); }
.tracking-wide    { letter-spacing: var(--tracking-wide); }
.tracking-wider   { letter-spacing: var(--tracking-wider); }

.text-left   { text-align: left; }
.text-center { text-align: center; }
.text-right  { text-align: right; }

.uppercase  { text-transform: uppercase; }
.lowercase  { text-transform: lowercase; }
.capitalize { text-transform: capitalize; }
.normal-case { text-transform: none; }

.italic     { font-style: italic; }
.not-italic { font-style: normal; }

.underline    { text-decoration: underline; }
.no-underline { text-decoration: none; }

.text-balance { text-wrap: balance; }
.text-pretty  { text-wrap: pretty; }

.truncate {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* --- Text colour ---------------------------------------------------------- */
.text-body    { color: var(--color-text); }
.text-muted   { color: var(--color-text-muted); }
.text-subtle  { color: var(--color-text-subtle); }
.text-heading { color: var(--color-heading); }
.text-primary { color: var(--color-primary); }
.text-dark    { color: var(--color-dark); }
.text-accent  { color: var(--color-accent); }
.text-invert  { color: var(--color-text-invert); }
.text-white   { color: var(--white); }
/* These use the -text step, not the solid step. A solid fill is chosen to carry
   white text, which is a different job from being read on a white page. */
.text-success   { color: var(--color-success-text); }
.text-warning   { color: var(--color-warning-text); }
.text-danger    { color: var(--color-danger-text); }
.text-info      { color: var(--color-info-text); }
.text-highlight { color: var(--color-highlight-text); }

/* --- Background ----------------------------------------------------------- */
.bg-page         { background-color: var(--color-bg); }
.bg-surface      { background-color: var(--color-surface); }
.bg-white        { background-color: var(--white); }
.bg-warm         { background-color: var(--color-surface-alt); }
.bg-warm-soft    { background-color: var(--color-surface-soft); }
.bg-invert       { background-color: var(--color-surface-invert); }
.bg-primary      { background-color: var(--color-primary); }
.bg-primary-soft { background-color: var(--color-primary-soft); }
.bg-accent       { background-color: var(--color-accent); }
.bg-accent-soft  { background-color: var(--color-accent-soft); }
.bg-transparent  { background-color: transparent; }
/* A thin white layer, for a panel on a dark band. */
.bg-white-10 { background-color: rgb(255 255 255 / 0.1); }
.bg-white-20 { background-color: rgb(255 255 255 / 0.2); }

.bg-gradient-primary      { background-image: var(--gradient-primary); }
.bg-gradient-primary-deep { background-image: var(--gradient-primary-deep); }
.bg-gradient-bright       { background-image: var(--gradient-bright); }
.bg-gradient-warm         { background-image: var(--gradient-warm); }
.bg-gradient-teal-bronze  { background-image: var(--gradient-teal-bronze); }
.bg-gradient-surface      { background-image: var(--gradient-surface); }
.bg-gradient-hero         { background-image: var(--gradient-hero); }
.bg-gradient-scrim        { background-image: var(--gradient-scrim); }
.bg-gradient-ink          { background-image: var(--gradient-ink); }
.bg-gradient-horizon      { background-image: var(--gradient-horizon); }
.bg-gradient-forest       { background-image: var(--gradient-forest); }
.bg-gradient-dusk         { background-image: var(--gradient-dusk); }
.bg-gradient-sunrise      { background-image: var(--gradient-sunrise); }
.bg-gradient-mesh         { background-image: var(--gradient-mesh); }

.bg-cover  { background-size: cover; }
.bg-center { background-position: center; }

/* --- Border --------------------------------------------------------------- */
.border    { border: 1px solid var(--color-border); }
.border-0  { border: 0; }
.border-t  { border-top: 1px solid var(--color-border); }
.border-b  { border-bottom: 1px solid var(--color-border); }
.border-l  { border-left: 1px solid var(--color-border); }
.border-r  { border-right: 1px solid var(--color-border); }

.border-strong  { border-color: var(--color-border-strong); }
.border-divider { border-color: var(--color-divider); }
.border-primary { border-color: var(--color-primary); }
.border-accent  { border-color: var(--color-accent); }
.border-white-40 { border-color: rgb(255 255 255 / 0.4); }

.rounded-none { border-radius: 0; }
.rounded-xs   { border-radius: var(--radius-xs); }
.rounded-sm   { border-radius: var(--radius-sm); }
.rounded-md   { border-radius: var(--radius-md); }
.rounded-lg   { border-radius: var(--radius-lg); }
.rounded-xl   { border-radius: var(--radius-xl); }
.rounded-2xl  { border-radius: var(--radius-2xl); }
.rounded-pill { border-radius: var(--radius-pill); }
.rounded-full { border-radius: var(--radius-full); }

/* --- Shadow --------------------------------------------------------------- */
.shadow-none  { box-shadow: none; }
.shadow-xs    { box-shadow: var(--shadow-xs); }
.shadow-sm    { box-shadow: var(--shadow-sm); }
.shadow-md    { box-shadow: var(--shadow-md); }
.shadow-lg    { box-shadow: var(--shadow-lg); }
.shadow-xl    { box-shadow: var(--shadow-xl); }
.shadow-inner { box-shadow: var(--shadow-inner); }

/* --- Size ----------------------------------------------------------------- */
.w-full  { width: 100%; }
.w-auto  { width: auto; }
.h-full  { height: 100%; }
.h-auto  { height: auto; }
.min-h-screen { min-height: 100vh; }

.max-w-sm  { max-width: var(--container-sm); }
.max-w-md  { max-width: var(--container-md); }
.max-w-lg  { max-width: var(--container-lg); }
.max-w-xl  { max-width: var(--container-xl); }
.max-w-none { max-width: none; }

.aspect-video  { aspect-ratio: 16 / 9; object-fit: cover; }
.aspect-square { aspect-ratio: 1 / 1; object-fit: cover; }

/* --- Position ------------------------------------------------------------- */
.relative { position: relative; }
.absolute { position: absolute; }
.fixed    { position: fixed; }
.sticky   { position: sticky; }
.static   { position: static; }

.inset-0   { inset: 0; }
.inset-x-0 { inset-inline: 0; }
.inset-y-0 { inset-block: 0; }
.top-0     { top: 0; }
.bottom-0  { bottom: 0; }
.left-0    { left: 0; }
.right-0   { right: 0; }

.z-base     { z-index: var(--z-base); }
.z-raised   { z-index: var(--z-raised); }
.z-sticky   { z-index: var(--z-sticky); }
.z-fixed    { z-index: var(--z-fixed); }
.z-overlay  { z-index: var(--z-overlay); }

/* --- Miscellaneous -------------------------------------------------------- */
.overflow-hidden { overflow: hidden; }
.overflow-x-auto { overflow-x: auto; }
.list-none  { list-style: none; padding-inline-start: 0; }
.list-disc  { list-style: disc; }
.list-decimal { list-style: decimal; }
.cursor-pointer { cursor: pointer; }
.select-none { user-select: none; }
.opacity-70 { opacity: 0.7; }
.transition { transition: var(--transition-colors), var(--transition-shadow),
                          var(--transition-transform); }

/* Hide on one size of screen, show on the other. The only responsive classes
   in the system, because sometimes a phone genuinely wants different words. */
@media (max-width: 767px) { .hide-on-phone { display: none; } }
@media (min-width: 768px) { .phone-only { display: none; } }

/* Print: drop the fixed header offset and the site chrome. */
@media print {
  .site-header, .site-footer, .skip-link { display: none; }
  .has-fixed-header { margin-block-start: 0; }
  /* This applies to html only. html holds the dark over-scroll colour, and a
     printer would print that as a full page of ink. body is already white, and
     paper has no over-scroll. */
  html { background: var(--white); }
}


/* ============================================================================
   8. FOCUS
   The last rule in the file, and that position is the whole design.

   There is one focus signal for the site — a link, a button, a card, a text
   field and a select all take exactly this, so tabbing from a field to the
   button beside it does not change what focus looks like. It is drawn as a
   box-shadow, and a box-shadow ring loses to any component that sets a
   box-shadow of its own. .btn--primary does. .card does. So this rule goes
   after all of them rather than in the base section where it started, where
   buttons and cards quietly had no ring at all.

   Three properties, three reasons:

     • box-shadow, not outline. An outline is drawn with visibly worse
       antialiasing on a rounded corner — on a pill-shaped field the steps are
       plain to see — and it cannot be animated. A box-shadow follows the
       element's own border-radius exactly, and needs no border-radius
       override, which the outline version did: that override changed the
       shape of whatever it was focusing.
     • Slightly transparent. The ring sits on the element's own edge, so at
       full opacity it hides that edge and the control looks like it changed
       size. At 0.55 you can see the border through it.
     • It animates in, over 200ms, growing from nothing. A ring that appears
       instantly reads as a rendering glitch when you are tabbing quickly; one
       that grows reads as an answer. The reduced-motion block near the top of
       this file already collapses it to nothing for anybody who asks.
   ========================================================================== */

:focus-visible {
  /* Transparent, so it draws nothing here. In Windows' high-contrast mode the
     browser removes the box-shadow below but gives a transparent outline a
     real colour, so this is the ring those users see. `outline: none` left
     them with no ring at all. */
  outline: 2px solid transparent;
  outline-offset: 2px;
  box-shadow: 0 0 0 var(--focus-width) var(--focus-color);
  animation: focus-ring-in var(--duration-base) var(--ease-out);
}

@keyframes focus-ring-in {
  from { box-shadow: 0 0 0 0 rgb(var(--focus-rgb) / 0); }
  to   { box-shadow: 0 0 0 var(--focus-width) var(--focus-color); }
}
