/* ---------- Fonts, served from this site ----------
   SELF-HOSTED, and that is the point rather than a preference: linking
   fonts.googleapis.com meant every visitor's browser announced their IP
   address to Google before a word of the page was drawn. Nothing was
   stored and no cookie was set, but the site tells people there is no
   tracking of any kind, and a request to a third party on every page
   load is the sort of thing that makes a reader right to be sceptical.
   This was the last one.

   ONE FILE PER FAMILY PER SUBSET, not per weight: all three are
   VARIABLE fonts, and Google was serving the identical file for every
   weight we asked for (verified by sha256 — 30 downloads, 9 distinct
   files). The `font-weight` ranges below are the real axis ranges read
   out of the files, so the browser interpolates rather than
   synthesising a bold.

   The unicode-range lines are Google's own, kept verbatim. They are
   what stops a visitor reading English from ever downloading the
   Bengali subset — 187KB that only somebody reading Bengali needs.

   VERSIONED IN THE FILENAME, which is what asset_version() does for the
   stylesheet and cannot do here: a url() inside a static .css is not
   Jinja and cannot carry a computed token. So the token is the first
   eight hex of the file's own sha256, baked into the name. nginx serves
   /static with `expires 30d`, and that is only safe while the URL
   changes when the bytes do — replace a font and the name changes with
   it, so no browser can hold a month-old face for a typeface that has
   been swapped. Regenerate with tools/hash-fonts.py after adding or
   replacing a file. */
/* Bricolage Grotesque — latin */
@font-face{
  font-family:'Bricolage Grotesque';
  font-style:normal;
  font-weight:200 800;
  font-display:swap;
  src:url('../fonts/bricolage-grotesque-latin.a79fdb52.woff2') format('woff2');
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Bricolage Grotesque — latin-ext */
@font-face{
  font-family:'Bricolage Grotesque';
  font-style:normal;
  font-weight:200 800;
  font-display:swap;
  src:url('../fonts/bricolage-grotesque-latin-ext.776f6dca.woff2') format('woff2');
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Bricolage Grotesque — vietnamese */
@font-face{
  font-family:'Bricolage Grotesque';
  font-style:normal;
  font-weight:200 800;
  font-display:swap;
  src:url('../fonts/bricolage-grotesque-vietnamese.b50a9d90.woff2') format('woff2');
  unicode-range:U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
/* Noto Serif Bengali — latin */
@font-face{
  font-family:'Noto Serif Bengali';
  font-style:normal;
  font-weight:100 900;
  font-display:swap;
  src:url('../fonts/noto-serif-bengali-latin.6b52f9bb.woff2') format('woff2');
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Noto Serif Bengali — latin-ext */
@font-face{
  font-family:'Noto Serif Bengali';
  font-style:normal;
  font-weight:100 900;
  font-display:swap;
  src:url('../fonts/noto-serif-bengali-latin-ext.b400a793.woff2') format('woff2');
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Noto Serif Bengali — bengali */
@font-face{
  font-family:'Noto Serif Bengali';
  font-style:normal;
  font-weight:100 900;
  font-display:swap;
  src:url('../fonts/noto-serif-bengali-bengali.8ae8a8ce.woff2') format('woff2');
  unicode-range:U+0951-0952, U+0964-0965, U+0980-09FE, U+1CD0, U+1CD2, U+1CD5-1CD6, U+1CD8, U+1CE1, U+1CEA, U+1CED, U+1CF2, U+1CF5-1CF7, U+200C-200D, U+20B9, U+25CC, U+A8F1;
}
/* Public Sans — latin */
@font-face{
  font-family:'Public Sans';
  font-style:normal;
  font-weight:100 900;
  font-display:swap;
  src:url('../fonts/public-sans-latin.5ed4d31c.woff2') format('woff2');
  unicode-range:U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
/* Public Sans — latin-ext */
@font-face{
  font-family:'Public Sans';
  font-style:normal;
  font-weight:100 900;
  font-display:swap;
  src:url('../fonts/public-sans-latin-ext.3a00a32f.woff2') format('woff2');
  unicode-range:U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
/* Public Sans — vietnamese */
@font-face{
  font-family:'Public Sans';
  font-style:normal;
  font-weight:100 900;
  font-display:swap;
  src:url('../fonts/public-sans-vietnamese.94593e69.woff2') format('woff2');
  unicode-range:U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}

:root{
  --green:#00563F;
  --green-deep:#02382A;
  --green-tint:#E9F1EC;
  --red:#E8333F;
  --paper:#F6F5EF;
  --ink:#17251F;
  --ink-soft:#4A5A52;
  /* The quiet secondary line under an admin figure. It used to be
     --ink-soft at opacity:.8, which computes to #6e7b75 — 4.41:1 on
     white, failing AA by 0.09. A real token rather than a token dimmed
     at the point of use: an opacity is invisible to anyone reading the
     palette, and the colour it produces is nobody's decision. 4.87:1. */
  --ink-muted:#68746E;
  /* WCAG's "large text" threshold: at 14pt BOLD (18.6667px) or 18pt
     regular, the AA contrast requirement drops from 4.5:1 to 3:1. 18.7
     and not 18.6667, because a rounding that lands below the line is a
     failure. ONE thing uses it: the dashboard's launch blocker, where
     being loud is the point of the sentence. It is not the general
     answer to the brand red — see --red-ink. */
  --large-text:18.7px;
  /* The brand red, darkened until white on it is 5.64:1 and it on white
     is 5.64:1 — comfortably past AA at any size. This is what carries
     every RED SURFACE and every red word on the site: both Donate
     buttons, Subscribe, the admin Delete links, the album photo count.
     Growing type to reach the 3:1 large-text bar was tried first and
     was the wrong lever — it changed the design to fix a colour, and
     every element it touched came out louder than it was drawn to be.
     --red itself is untouched and still the flag red: it is the eyebrow
     dot, the focus ring, the alert borders and the launch-blocker text,
     none of which is small white-on-red.
     On whether this reads as a second colour, measured rather than
     guessed: THE BRAND MARK ITSELF IS PAINTED #C82028. --red-ink is
     0.77 dE2000 from it, which is below the distance at which anyone
     can tell two samples apart; --red was 8.46 from it. So in the
     header, where the pill sits beside the mark, this change makes the
     two AGREE where they used to differ — the pill was the odd one out
     and now it is not. Against --red itself the difference is 8.1
     dE2000 and almost all of it lightness (L* 51.7 to 43.7), hue and
     chroma barely moving: the same red, darker, not another red. */
  --red-ink:#C9202B;
  --line:#DCE3DD;
  --white:#FFFFFF;
  --radius:14px;
  --font-display:"Bricolage Grotesque", sans-serif;
  --font-body:"Public Sans", -apple-system, sans-serif;
  --font-bn:"Noto Serif Bengali", serif;
  --max:1120px;
  --header-h:78px;
}
*{margin:0;padding:0;box-sizing:border-box}
html{scroll-behavior:smooth}
@media (prefers-reduced-motion: reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{animation:none!important;transition:none!important}
}
body{
  font-family:var(--font-body);background:var(--paper);color:var(--ink);
  line-height:1.65;font-size:16.5px;-webkit-font-smoothing:antialiased;
}
img{max-width:100%;display:block}
a{color:inherit}
.wrap{max-width:var(--max);margin:0 auto;padding:0 24px}
section{padding:80px 0}
section.tinted{background:var(--green-tint)}

/* <picture> wraps every content image (templates/_picture.html);
   display:contents makes it invisible to layout so every `.x img`
   rule and every grid cell behaves exactly as with a bare <img>. */
picture{display:contents}
/* ---------- Type ---------- */
h1,h2,h3{font-family:var(--font-display);line-height:1.12;letter-spacing:-.01em}
/* h1 and h2 share the page-title size deliberately. Every public page
   below the homepage titles itself with an h1 now, and it should look
   exactly as the h2 it replaced did — the change is to the OUTLINE, not
   to the design. The hero and the two card shells override below. */
h1,h2{font-size:clamp(28px,4vw,40px);font-weight:700;margin-bottom:16px}
.lede{color:var(--ink-soft);max-width:62ch}
.eyebrow{
  display:flex;align-items:center;gap:10px;
  font-size:13px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;
  color:var(--green);margin-bottom:14px;
}
.eyebrow::before{content:"";width:11px;height:11px;border-radius:50%;background:var(--red);flex:none}
.eyebrow .bn{font-family:var(--font-bn);font-weight:700;text-transform:none;letter-spacing:0;color:var(--ink-soft);font-size:14px}
/* Breadcrumbs: a list that wraps, separators drawn here so they are
   not read aloud, the current page as plain text. */
.breadcrumbs{margin:0 0 18px;font-size:14px}
.breadcrumbs ol{list-style:none;margin:0;padding:0;display:flex;flex-wrap:wrap;gap:4px 0;align-items:center}
.breadcrumbs li{display:flex;align-items:center;min-width:0}
.breadcrumbs li+li::before{content:"\203A";margin:0 8px;color:var(--ink-muted)}
.breadcrumbs a{color:var(--green);font-weight:700;text-decoration:none}
.breadcrumbs a:hover{text-decoration:underline}
.breadcrumbs [aria-current]{color:var(--ink-soft);overflow-wrap:anywhere}
/* "When do backup emails arrive?" on the backups panel. */
.backup-mail-help{margin-top:18px}
.backup-mail-list{margin:0 0 14px 18px;padding:0}
.backup-mail-list li{margin:4px 0}
.backup-mail-status p{margin:6px 0}
.backup-mail-warn{color:var(--red-ink)}
.backup-mail-test{margin-top:12px;display:flex;flex-wrap:wrap;gap:10px;align-items:center}
.backup-mail-test small{color:var(--ink-soft)}
/* The receipt line on an admin row, and the thank-you page printed:
   the page is the receipt when no email went, so the chrome goes. */
.receipt-state{display:block;color:var(--ink-muted)}
.receipt-missing{color:var(--red-ink)}
.inline-form{display:inline}
@media print{
  body:not(.admin-body)>header,body:not(.admin-body)>footer,.cookie-notice,
  .breadcrumbs,.thanks-cta,.no-print,.skip-link{display:none !important}
  .thanks{max-width:none;text-align:left}
}
/* "More from EBWA" at the foot of a detail page: a tinted band, the
   listing cards, then the two ways back. */
.more-from{padding-top:0}   /* the article's own 80px is the gap */
.more-from h2{margin:0}
.more-cards{margin-top:28px}
.more-links{display:flex;flex-wrap:wrap;gap:12px;margin:36px 0 0}

/* ---------- Buttons ---------- */
.btn{
  display:inline-flex;align-items:center;gap:10px;
  font-family:var(--font-display);font-weight:600;font-size:16px;
  padding:12px 26px;border-radius:999px;text-decoration:none;border:none;cursor:pointer;
  transition:transform .15s ease, box-shadow .15s ease;
}
.btn:hover{transform:translateY(-2px)}
.btn:focus-visible{outline:3px solid var(--red);outline-offset:3px}
.btn-green{background:var(--green);color:#fff;box-shadow:0 6px 18px rgba(0,86,63,.28)}
.btn-ghost{border:2px solid var(--green);color:var(--green);background:transparent}
/* Same size and weight as .btn-green — it is the same kind of button and
   should read as one. White on --red is 4.21:1 and fails at this size,
   so the SURFACE is --red-ink (5.64:1) rather than the type being grown
   to meet the bar. */
.btn-red{background:var(--red-ink);color:#fff;box-shadow:0 6px 18px rgba(201,32,43,.26)}
/* Serious but reversible — suspending a member. 7.0:1 on white text. */
.btn-amber{background:#7A5309;color:#fff;box-shadow:0 6px 18px rgba(122,83,9,.26)}
.btn-small{padding:7px 16px;font-size:14px}
.btn-ghost.danger{border-color:var(--red-ink);color:var(--red-ink)}

/* ---------- Skip to content ----------
   Off-screen until focused, rather than display:none — a hidden element
   cannot take focus, so display:none would make the link unreachable by
   the very people it is for. It comes back on :focus, which is the only
   time anybody needs to see it. */
.skip-link{
  position:absolute;left:-9999px;top:0;z-index:200;
  background:var(--green);color:#fff;padding:12px 20px;
  font-family:var(--font-display);font-weight:700;font-size:15px;
  text-decoration:none;border-radius:0 0 10px 0;
}
.skip-link:focus{left:0}
.skip-link:focus-visible{outline:3px solid var(--red);outline-offset:-3px}
/* The target must not draw a focus ring of its own: focus is moved there
   programmatically, and a box around the whole page would be alarming. */
#main:focus{outline:none}

/* ---------- Header ---------- */
header{
  position:sticky;top:0;z-index:50;
  background:rgba(246,245,239,.92);backdrop-filter:blur(10px);
  border-bottom:1px solid var(--line);
}
.nav{display:flex;align-items:center;gap:22px;height:var(--header-h)}
.brand{display:flex;align-items:center;gap:12px;text-decoration:none;flex:none}
.brand-mark{display:inline-flex;align-items:center;flex:none}
.brand-mark img{height:56px;width:auto;display:block}   /* 78px header: 11px above and below */
/* The login badge is the only branding on an otherwise bare page, so it
   carries it: roughly double the old 90px. Capped against the card's own
   width so it shrinks with the card on a phone rather than pressing
   against the padding. */
.login-card .brand-mark img{height:180px;max-width:100%;object-fit:contain}
/* White circular backing so the badge reads as a seal on the dark green */
.foot-logo{
  width:96px;height:96px;border-radius:50%;background:#fff;flex:none;
  display:flex;align-items:center;justify-content:center;
  margin-bottom:14px;box-shadow:0 2px 10px rgba(0,0,0,.18);
}
.foot-logo img{height:88px;width:88px;display:block}
.brand-name{font-family:var(--font-display);font-weight:800;font-size:25px;line-height:1.05}
/* The strapline is the widest thing in the brand block; it is decorative,
   so it steps down before the nav is ever squeezed. */
/* The subtitle: 13px, in --ink (not --ink-soft) — at this size on paper
   the soft ink was 6.6:1, fine for AA but a word that is meant to be
   READ off a header deserves more, and the dark ink is 14:1. */
.brand-name small{display:block;font-family:var(--font-body);font-weight:600;font-size:13px;color:var(--ink);letter-spacing:.04em;white-space:nowrap}
/* Nav sits hard right and never wraps — if it cannot fit, the menu
   button takes over at the breakpoint below instead of going two lines. */
.main-nav{margin-left:auto}
/* align-items:center matters now: the group triggers are full header
   height (that is the hover bridge down to their panel), so without it
   the Donate pill and the Admin link hang off the top of the row. */
.nav-links{display:flex;align-items:center;gap:20px;list-style:none;flex-wrap:nowrap}
.nav-links a{
  text-decoration:none;font-weight:600;font-size:14.5px;color:var(--ink-soft);
  padding:6px 2px;border-bottom:2px solid transparent;white-space:nowrap;
  transition:color .15s, border-color .15s;
}
.nav-links a:hover{color:var(--green);border-color:var(--red)}
/* ---------- Grouped nav ----------
   CSS only: the panel opens on hover and on :focus-within, so a keyboard
   walks trigger -> items -> next trigger without a line of script, and
   the whole thing behaves identically with JavaScript off. The items are
   `visibility:hidden` rather than `display:none` so they can transition;
   hidden items are not focusable, which is exactly what we want until
   the group has focus. */
.nav-group{position:relative}
/* Full-height trigger, so there is no gap for the pointer to fall
   through on the way down to the panel. */
.nav-group > a{
  display:flex;align-items:center;gap:6px;height:var(--header-h);
  padding:0 2px;border-bottom:none;
}
.nav-group > a::after{
  content:"";width:0;height:0;flex:none;
  border-left:4px solid transparent;border-right:4px solid transparent;
  border-top:5px solid currentColor;opacity:.65;
  transition:transform .15s ease;
}
.nav-group:hover > a,.nav-group:focus-within > a{color:var(--green)}
.nav-group:hover > a::after,.nav-group:focus-within > a::after{transform:rotate(180deg)}
.nav-drop{
  position:absolute;top:calc(var(--header-h) - 10px);left:-14px;z-index:60;
  min-width:216px;list-style:none;display:grid;gap:2px;padding:10px;
  background:var(--paper);border:1px solid var(--line);border-radius:14px;
  box-shadow:0 18px 40px rgba(2,56,42,.16);
  opacity:0;visibility:hidden;transform:translateY(-6px);
  /* Opacity and position fade; VISIBILITY DOES NOT. Transitioning it
     delays the switch to `visible` by the length of the transition, and
     an item that is still hidden cannot take focus — so a keyboard user
     tabbing at any speed sailed straight past the second and third
     panels. Only the look is animated; focusability changes at once. */
  transition:opacity .15s ease, transform .15s ease;
}
.nav-group:hover > .nav-drop,
.nav-group:focus-within > .nav-drop{opacity:1;visibility:visible;transform:none}
/* The rightmost group's panel opens LEFTWARDS, or it hangs off the edge
   of the window. The nav is right-aligned, so how far right that group
   sits depends on what else is in the row — and with donations switched
   off there is no Donate pill after it: measured 76px of sideways
   scroll at 1024 and 900 in that state. A panel is visibility:hidden
   until it is hovered, and a hidden panel still counts toward the
   page's scrollWidth, so this was a sideways scroll on every page with
   no panel open at all. */
.nav-group-last > .nav-drop{left:auto;right:-14px}
.nav-drop a{
  display:block;padding:9px 12px;border-radius:9px;border-bottom:none;
  white-space:nowrap;font-size:15px;
}
.nav-drop a:hover{background:var(--green-tint);color:var(--green);border-color:transparent}
.nav-drop a:focus-visible{outline:3px solid var(--green);outline-offset:-1px}

/* The one primary action in the header: a red pill carrying the same
   weight as .btn-green, without the button padding that would push the
   nav onto a second line. */
.nav-links a.nav-donate{
  background:var(--red-ink);color:#fff;border-radius:999px;padding:8px 18px;
  font-family:var(--font-display);font-weight:700;border-bottom:none;
  box-shadow:0 4px 14px rgba(201,32,43,.28);
}
.nav-links a.nav-donate:hover{color:#fff;border-color:transparent;filter:brightness(1.06)}

/* The phone number lives in the mobile menu, the footer and /contact —
   the header row is spoken for by the nav and the Donate action. */
.nav-phone-item,.nav-home-item{display:none}      /* menu-only; see the mobile breakpoint */
.menu-btn{display:none;background:none;border:none;cursor:pointer;padding:8px}
.menu-btn span{display:block;width:24px;height:2.5px;background:var(--ink);margin:5px 0;border-radius:2px}

/* ---------- Flash messages ---------- */
.flashes{padding-top:18px}
.flash{padding:12px 18px;border-radius:10px;font-weight:600;font-size:15px;margin-bottom:10px}
.flash-ok{background:#E3F2E8;color:#0B5C36;border:1px solid #BFE0CB}
.flash-error{background:#FCE9EA;color:#9B1B26;border:1px solid #F3C3C8}

/* ---------- Hero ---------- */
/* The red circle deliberately overhangs the photo. Below ~1080px that
   overhang reached past the viewport edge and gave the whole page a
   10px sideways scroll, so the section clips its own bleed. Nothing is
   lost at wide widths, where the circle sits well inside the viewport. */
.hero{padding:64px 0 76px;overflow:hidden}
.hero-grid{display:grid;grid-template-columns:1.1fr .9fr;gap:56px;align-items:center}
.hero h1{font-size:clamp(36px,5.2vw,56px);font-weight:800;margin:14px 0 20px}
.hero .lede{font-size:18px;margin-bottom:30px}
.hero-cta{display:flex;gap:14px;flex-wrap:wrap}
.hero-photo{position:relative}
.hero-photo img{
  border-radius:var(--radius);aspect-ratio:4/3.4;object-fit:cover;width:100%;
  box-shadow:0 24px 60px rgba(2,56,42,.22);
}
.hero-photo::before{
  content:"";position:absolute;inset:auto -34px -34px auto;width:150px;height:150px;
  border-radius:50%;background:var(--red);opacity:.92;z-index:-1;
}
.img-placeholder{
  border:2px dashed var(--line);border-radius:var(--radius);aspect-ratio:4/3.4;
  display:grid;place-items:center;color:var(--ink-soft);font-size:14px;font-weight:600;
  background:var(--white);text-align:center;padding:20px;
}

/* ---------- Rich content presets ----------
   Three deliberately different shapes sharing one set of parts. Reading
   width is capped at 68ch everywhere; every figure gets an aspect-ratio
   so images reserve their space before they load. Any transition here is
   already disabled under prefers-reduced-motion by the rule at the top. */
.rc{margin-top:34px}
.rc-text{max-width:68ch}
.rc-text p{color:var(--ink-soft);font-size:17px;line-height:1.75;margin-bottom:18px}
.rc-text p:last-child{margin-bottom:0}
.rc-text p:first-child{font-size:19px;color:var(--ink)}

/* A content image whose file is not on the server. Admins only — a
   visitor never gets one, the list is filtered before the macro runs.
   Deliberately unmistakable: this is a fault notice, not a design. */
.rc-missing{
  display:grid;place-items:center;background:#FFF7F7;
  border:2px dashed var(--red-ink);border-radius:12px;padding:20px;
}
.rc-missing-note{max-width:44ch;text-align:center;font-size:14px;line-height:1.55}
.rc-missing-note b{display:block;color:var(--red-ink);margin-bottom:6px;font-size:15px}
.rc-missing-note span{display:block;color:var(--ink-soft)}
.rc-missing-note code{font-size:13px;word-break:break-all}
.rc-missing-you{margin-top:8px;font-style:italic;font-size:13px}
.rc-figure{margin:0}
.rc-figure img{
  width:100%;aspect-ratio:var(--rc-ratio, 4 / 3);object-fit:cover;
  border-radius:var(--radius);background:var(--green-tint);
  box-shadow:0 14px 36px rgba(2,56,42,.14);
}
.rc-figure figcaption{
  margin-top:10px;font-size:13.5px;color:var(--ink-soft);line-height:1.5;
  padding-left:14px;border-left:3px solid var(--red);
}

/* Classic — lead photo beside the text, the rest in a strip below */
.rc-classic-top{display:grid;grid-template-columns:minmax(0,1.15fr) minmax(0,.85fr);gap:56px;align-items:start}
/* No photo and no placeholder: the words take the full reading width */
.rc-classic-top.is-textonly{grid-template-columns:minmax(0,68ch)}
.rc-lead{position:relative}
.rc-lead::after{
  content:"";position:absolute;inset:auto -22px -22px auto;
  width:104px;height:104px;border-radius:50%;background:var(--red);
  opacity:.92;z-index:-1;
}
/* NATURAL SHAPE on a detail page: the whole picture, never cropped,
   capped at 70vh so a tall poster does not push the words off a laptop.
   The img box is the picture's own size, centred, so nothing shows
   through a letterbox; the red circle moves to the figure and sits
   BEHIND it (z-index:-1), peeking from the corner, never over it. */
.rc-figure.is-natural img{aspect-ratio:auto;object-fit:contain;width:auto;height:auto;max-width:100%;max-height:70vh;display:block;margin:0 auto}
.rc-lead.is-natural{display:flex;flex-direction:column;align-items:center}
.rc-lead.is-natural::after{display:none}
.rc-lead.is-natural .rc-figure{position:relative;display:inline-block;max-width:100%}
.rc-lead.is-natural .rc-figure a{display:block}
.rc-lead.is-natural .rc-figure::after{
  content:"";position:absolute;right:-22px;bottom:-22px;
  width:104px;height:104px;border-radius:50%;background:var(--red);
  opacity:.92;z-index:-1;
}
/* No words at all: the picture alone, centred, not beside an empty column. */
.rc-classic-top.is-mediaonly{grid-template-columns:minmax(0,760px);justify-content:center}
.rc-flyer{margin:18px 0 0;text-align:center}
.rc-lead-flyer{align-self:center}
.rc-strip{
  display:grid;grid-template-columns:repeat(4,1fr);gap:18px;
  margin-top:44px;padding-top:34px;border-top:1px solid var(--line);
}

/* Gallery — text, then a masonry grid built from CSS columns */
.rc-gallery .rc-masonry{
  columns:3;column-gap:20px;margin-top:40px;
}
.rc-masonry .rc-figure{break-inside:avoid;margin-bottom:20px}
/* Staggered ratios are what make it read as masonry rather than a grid */
.rc-masonry .rc-figure:nth-child(3n+1) img{aspect-ratio:3 / 4}
.rc-masonry .rc-figure:nth-child(3n+2) img{aspect-ratio:1 / 1}
.rc-masonry .rc-figure:nth-child(3n+3) img{aspect-ratio:4 / 5}
.rc-masonry .rc-figure:first-child img{aspect-ratio:4 / 3}

/* Alternating — text and photo side by side, swapping sides down the page */
.rc-alt{display:grid;gap:76px}
.rc-alt-row{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:56px;align-items:center}
.rc-alt-row.is-flipped .rc-text{order:2}
.rc-alt-row.is-flipped .rc-alt-media{order:1}
.rc-alt-row.is-textonly{grid-template-columns:minmax(0,68ch)}
/* A spare image with no words left to sit beside becomes a full-width band */
.rc-alt-row.is-mediaonly{grid-template-columns:1fr}
.rc-alt-row.is-mediaonly .rc-figure img{aspect-ratio:16 / 7}
.rc-alt-row.is-mediaonly .rc-alt-media::before{display:none}
/* ...and so does a row whose WORDS are too short to hold up a column.
   The line goes above the photograph rather than beside it: centred in
   a column next to a 406px image it had 373px of nothing split above
   and below it, which reads as marooned rather than as paired.
   The row is a normal band with one row of text on top, so it needs no
   geometry of its own — only the ordering, since .is-flipped would
   otherwise put the words underneath on every other row and the
   rhythm of a caption above its picture is worth more here than the
   left-right alternation, which the full-width band has already
   given up. */
.rc-alt-row.is-shorttext{gap:18px}
.rc-alt-row.is-shorttext .rc-text,
.rc-alt-row.is-flipped.is-shorttext .rc-text{order:0}
.rc-alt-row.is-shorttext .rc-alt-media,
.rc-alt-row.is-flipped.is-shorttext .rc-alt-media{order:1}
.rc-alt-row.is-shorttext .rc-text{max-width:68ch}
.rc-alt-media{position:relative}
/* A tinted panel behind every other image, so the rhythm is visible even
   before the photographs arrive */
.rc-alt-row:not(.is-flipped) .rc-alt-media::before{
  content:"";position:absolute;inset:-18px -18px auto -18px;height:66%;
  background:var(--green-tint);border-radius:var(--radius);z-index:-1;
}
.rc-alt-row.is-flipped .rc-alt-media::before{
  content:"";position:absolute;inset:auto -18px -18px -18px;height:66%;
  background:var(--green-tint);border-radius:var(--radius);z-index:-1;
}

@media (max-width: 920px){
  .rc-classic-top{grid-template-columns:1fr;gap:36px}
  .rc-lead{order:-1}
  .rc-strip{grid-template-columns:repeat(2,1fr);gap:14px}
  .rc-gallery .rc-masonry{columns:2}
  .rc-alt{gap:52px}
  .rc-alt-row,.rc-alt-row.is-textonly{grid-template-columns:1fr;gap:28px}
  /* BELOW HERE A SHORT-TEXT ROW IS NOTHING SPECIAL. The row is already
     one column at this width, so there was no empty half to fix and
     nothing should change — and .is-mediaonly's 16/7 band would change
     it, cropping the photograph to less than half the height it had
     (274px to 150px at 390). A row with genuinely NO words keeps the
     band, as it always has; this is only the short-text case. */
  .rc-alt-row.is-shorttext .rc-figure img{aspect-ratio:5 / 4}
  .rc-alt-row.is-shorttext{gap:28px}
  .rc-alt-row.is-flipped .rc-text{order:0}
  .rc-alt-row.is-flipped .rc-alt-media{order:0}
}
@media (max-width: 640px){
  .rc-gallery .rc-masonry{columns:1}
  .rc-strip{grid-template-columns:1fr 1fr}
  .rc-lead::after{display:none}
  .rc-lead.is-natural .rc-figure::after{display:none}
}

/* ---------- Our Journey ----------
   Unlike About, a news post or an event, this page carries EVERY
   milestone at once. Rendered at full page scale the three presets stop
   being layouts and become three unrelated page designs stacked, and the
   year headings vanish into a wall of photographs — so inside
   .journey-entry each preset is scaled down to an image treatment for
   one entry: shorter figures, tighter gaps, none of the page-scale
   flourishes. Same macro, same markup, same three shapes; entry sized.
   The sticky year keeps you oriented however long the timeline gets. */
.journey-year{margin-top:56px}
.journey-year-head{
  position:sticky;top:var(--header-h);z-index:2;
  font-size:clamp(24px,2.6vw,30px);color:var(--green);margin-bottom:0;
  padding:10px 0 10px;border-bottom:2px solid var(--green);
  background:var(--paper);
}
/* EACH ENTRY IS A BOUNDED BLOCK, and this is the important one. The
   separator used to be a 1px line between entries, with 26px of padding
   above a title and 4px below the last thing in an entry — so a trailing
   photograph sat 31px above the NEXT entry's funder line and 31px below
   its own text. At those distances the eye has no way to tell which
   milestone a photograph belongs to, and on an entry whose preset puts
   images last it consistently read as belonging to the wrong one.
   A card settles it: everything inside the box is this milestone. It
   also gives the photographs a ground to sit on, which the 1px rule
   never did. */
.journey-entry{
  background:var(--white);border:1px solid var(--line);
  border-radius:var(--radius);padding:24px 28px 28px;margin-top:20px;
}
.journey-entry-title{font-size:clamp(20px,2vw,24px);margin:4px 0 0}
.journey-entry .event-date{margin-bottom:2px}

/* --- the presets, at entry scale --- */
.journey-entry .rc{margin-top:16px}
.journey-entry .rc-figure img{box-shadow:0 8px 20px rgba(2,56,42,.10)}
/* The photo is a link to the full-size file (the shared lightbox
   upgrades it). It must not pick up the underline and colour of a text
   link, and it needs a focus ring of its own — a keyboard user tabbing
   the page lands on every photograph here. */
.journey-entry .rc-figure a{display:block;border-bottom:none;line-height:0}
.journey-entry .rc-figure a:focus-visible{
  outline:3px solid var(--green);outline-offset:3px;border-radius:12px;
}
.journey-entry .rc-figure figcaption{margin-top:8px;font-size:12.5px}

/* Classic — lead beside the words, the rest as thumbnails underneath */
/* 400px, up from 340. The constraint that matters is that an entry's
   photograph must not read as a page-width band — at 400px it is 38% of
   the ~1050px inside a card at 1440, so it is plainly part of an entry
   and not a section of its own. Below that it was reading as a stamp
   beside the words rather than as a photograph. */
.journey-entry .rc-classic-top{
  grid-template-columns:minmax(0,1fr) minmax(0,400px);gap:34px;
}
.journey-entry .rc-lead::after{display:none}       /* page-scale flourish */
.journey-entry .rc-lead .rc-figure img{aspect-ratio:4 / 3}
/* The strip was the worst of it: 124px at 1440 and 96px at 360, which
   is an icon rather than a photograph. 190px puts three or four across
   the entry instead of eight, at a size where you can see what is in
   them. */
.journey-entry .rc-strip{
  grid-template-columns:repeat(auto-fill,minmax(190px,1fr));
  gap:14px;margin-top:18px;padding-top:0;border-top:none;
}

/* Gallery — a tidy grid of the entry's photos, not a page of masonry */
.journey-entry .rc-gallery .rc-masonry{
  columns:auto;display:grid;gap:16px;margin-top:20px;
  grid-template-columns:repeat(auto-fill,minmax(240px,1fr));
}
.journey-entry .rc-masonry .rc-figure{margin-bottom:0}
/* Beats the staggered nth-child ratios above by order, not !important */
.journey-entry .rc-masonry .rc-figure img{aspect-ratio:4 / 3}

/* Alternating — compact rows inside the entry, rather than half-page
   bands. The media column is a fixed width, so the swap between sides is
   still obvious while the entry stays the size of an entry; the flipped
   rows need the mirrored track order to keep it. */
.journey-entry .rc-alt{gap:22px}
.journey-entry .rc-alt-row{
  grid-template-columns:minmax(0,1fr) minmax(0,380px);
  gap:30px;align-items:start;
}
.journey-entry .rc-alt-row.is-flipped{
  grid-template-columns:minmax(0,380px) minmax(0,1fr);
}
.journey-entry .rc-alt-row.is-textonly{grid-template-columns:minmax(0,68ch)}
.journey-entry .rc-alt-row.is-mediaonly{grid-template-columns:minmax(0,380px)}
.journey-entry .rc-alt-media::before{display:none}  /* tinted page panel */
.journey-entry .rc-alt-media .rc-figure img{aspect-ratio:4 / 3}
.journey-entry .rc-alt-row.is-mediaonly .rc-figure img{aspect-ratio:4 / 3}

@media (max-width: 920px){
  /* One column, and the lead photo leads — the same stacking the
     presets use everywhere else at this width. Photos keep a ceiling
     rather than going full-bleed: a page of full-width 4:3 photographs
     is exactly the wall of images this layout exists to avoid, and one
     tablet-width milestone was 2,400px tall before this. */
  .journey-entry .rc-lead,
  .journey-entry .rc-alt-media{max-width:440px}
  .journey-entry .rc-classic-top{grid-template-columns:1fr;gap:22px}
  .journey-entry .rc-alt-row,
  .journey-entry .rc-alt-row.is-flipped,
  .journey-entry .rc-alt-row.is-textonly,
  .journey-entry .rc-alt-row.is-mediaonly{grid-template-columns:1fr;gap:18px}
  .journey-entry .rc-alt{gap:26px}
}
@media (max-width: 640px){
  .journey-year-head{position:static}   /* a phone screen is short enough */
  /* The card keeps its edges and gives back its inset. At 28px a side it
     was taking 56px out of a 342px phone column, and the photographs came
     out SMALLER than they were before the card existed — which is the
     opposite of the point. 14px still reads as a card and costs the
     photographs almost nothing. */
  .journey-entry{padding:18px 14px 20px}
  .journey-entry .rc-strip{grid-template-columns:repeat(auto-fill,minmax(120px,1fr))}
  .journey-entry .rc-gallery .rc-masonry{grid-template-columns:1fr 1fr}
}

/* ---------- FAQ ----------
   Plain <details>/<summary>: the browser gives us the keyboard handling,
   the ARIA and the open/closed state for free, and the page still works
   with the whole script gone. */
.faq-group{font-size:clamp(20px,2.4vw,26px);color:var(--green);margin:48px 0 0}
.faq-list{max-width:74ch;margin-top:22px;display:grid;gap:12px}
.faq-item{
  background:var(--white);border:1px solid var(--line);
  border-radius:var(--radius);padding:0 22px;
}
.faq-item summary{
  cursor:pointer;list-style:none;padding:18px 34px 18px 0;position:relative;
  font-family:var(--font-display);font-weight:600;font-size:17.5px;
  color:var(--ink);
}
.faq-item summary::-webkit-details-marker{display:none}
.faq-item summary::after{
  content:"+";position:absolute;right:0;top:50%;transform:translateY(-50%);
  font-size:24px;color:var(--green);line-height:1;
}
.faq-item[open] summary::after{content:"–"}
.faq-item summary:hover{color:var(--green)}
.faq-item summary:focus-visible{outline:3px solid var(--green);outline-offset:3px;border-radius:6px}
.faq-answer{padding:0 0 20px}
.faq-answer p{color:var(--ink-soft);font-size:16.5px;line-height:1.7;margin-bottom:12px}
.faq-answer p:last-child{margin-bottom:0}
.faq-foot{margin-top:36px;color:var(--ink-soft)}
.faq-foot a{color:var(--green);font-weight:700}

/* ---------- Server health panel ----------
   Read-only cards. The colour is the whole point of the traffic lights,
   so the level is on the card rather than on a number somewhere inside
   it — you should be able to tell across the room. */
.health-grid{
  display:grid;grid-template-columns:repeat(auto-fill,minmax(240px,1fr));
  gap:16px;margin-bottom:14px;
}
.health-card{
  background:var(--white);border:1px solid var(--line);
  border-left:4px solid var(--line);border-radius:12px;padding:18px 20px;
}
.health-card h3{
  font-family:var(--font-body);font-size:12.5px;font-weight:700;
  text-transform:uppercase;letter-spacing:.09em;color:var(--ink-soft);
  margin-bottom:10px;
}
.health-card b{display:block;font-family:var(--font-display);font-size:30px;color:var(--green);line-height:1.1}
.health-card span{display:block;font-size:14px;color:var(--ink-soft);margin-top:4px}
.health-card em{display:block;font-style:normal;font-size:12.5px;color:var(--ink-muted);margin-top:8px}
.health-card.is-green{border-left-color:var(--green)}
.health-card.is-amber{border-left-color:#C98A16}
.health-card.is-amber b{color:#9A6A10}
.health-card.is-red{border-left-color:var(--red)}
.health-card.is-red b{color:#9B1B26}
.health-card.is-unknown{border-left-color:var(--line)}
.health-list{list-style:none;display:grid;gap:8px;font-size:14px;color:var(--ink-soft)}
.health-list li{display:flex;justify-content:space-between;align-items:baseline;gap:10px;flex-wrap:wrap}
.health-list b{display:inline;font-family:var(--font-body);font-size:14px;color:var(--ink);font-weight:700}
.health-list small{flex-basis:100%;font-size:12px}
.health-bad{color:var(--red)}
.health-auto{display:inline-flex;align-items:center;gap:8px;margin-left:6px;font-size:13.5px}
.health-auto input{width:auto}

/* ---------- The downloadable visitor report ----------
   Print-friendly HTML rather than a generated PDF: the browser's own
   Save as PDF gives selectable text and the site's real fonts (which
   are ours now, so nothing is fetched while printing), with no
   dependency and no deploy step. What prints is .report-doc and
   nothing else — see the @media print block. */
/* THE PUBLIC SHELL STYLES BARE <header>, <footer> AND <section>, and
   those rules reach any admin page that uses one of them semantically.
   The report's masthead came out sticky, blurred and sitting on
   translucent paper; its colophon came out as a band of dark green with
   grey text on it, 1.79:1 and unreadable; and every <section> carried
   the public page bands' 80px of padding top and bottom, which on the
   printed report was 160px of white between one short paragraph and the
   next. All three are the right elements for a document, so the fix
   belongs here rather than in the markup — and it covers the next admin
   page to reach for them.
   Kept at .admin-body specificity and placed BEFORE the rules below, so
   it outranks the bare element selectors and anything after it outranks
   this. */
.admin-body :where(header,footer,section){
  position:static;background:none;backdrop-filter:none;
  border:0;color:inherit;padding:0;margin:0;z-index:auto;
}

.report-tools{margin-bottom:26px}
.report-period{max-width:520px}
.report-doc{
  background:var(--white);border:1px solid var(--line);
  border-radius:var(--radius);padding:44px 48px;max-width:820px;
  color:var(--ink);
}
.report-head{border-bottom:2px solid var(--green);padding-bottom:18px;margin-bottom:26px}
.report-head h2{font-size:26px;color:var(--green);margin-bottom:2px}
.report-org{font-size:14px;color:var(--ink-soft);margin-bottom:14px}
.report-missing{color:var(--red-ink)}
.report-head h3{font-size:18px;margin:0 0 4px}
.report-period-line{font-size:15px;color:var(--ink-soft)}
.report-period-line span{color:var(--ink-muted)}
.report-figures{display:flex;gap:44px;margin-bottom:14px}
.report-figure b{display:block;font-family:var(--font-display);font-size:42px;color:var(--green);line-height:1.05}
.report-figure span{font-size:14px;color:var(--ink-soft)}
.report-caveat{
  font-size:13.5px;line-height:1.6;color:var(--ink-soft);
  border-left:3px solid var(--line);padding-left:14px;margin-bottom:30px;
  max-width:68ch;
}
.report-doc h4{font-size:15px;margin:0 0 10px;color:var(--green)}
.report-doc section{margin-bottom:28px}
.report-table{width:100%;border-collapse:collapse;font-size:14.5px}
.report-table th,.report-table td{
  text-align:left;padding:9px 10px;border-bottom:1px solid var(--line);
  vertical-align:top;font-weight:400;
}
.report-table thead th{font-weight:700;font-size:13px;text-transform:uppercase;letter-spacing:.06em;color:var(--ink-soft)}
.report-table th small{display:block;color:var(--ink-muted);font-size:12.5px}
.report-foot{border-top:1px solid var(--line);padding-top:14px;font-size:13px;color:var(--ink-soft)}

@media print{
  /* Everything that is the ADMIN goes; what is left is the document. */
  .admin-side,.report-tools,.flash,.skip-link,.report-missing{display:none !important}
  .admin-shell{display:block}
  .admin-main{padding:0}
  .admin-body{background:var(--white)}
  .report-doc{border:none;border-radius:0;padding:0;max-width:none}
  .report-figure b{color:#000}
  .report-head{border-bottom-color:#000}
  .report-head h2,.report-doc h4{color:#000}
  /* A table split across a page break is a table somebody has to read
     twice to be sure of. */
  .report-table,.report-figures,.report-caveat{break-inside:avoid}
  /* A scrolling box is the answer on a phone and nonsense on paper:
     printed, the right-hand end of a clipped table is simply gone. */
  .report-doc .table-scroll{overflow:visible;border:none}
  a[href]::after{content:""}      /* no printed URLs: this is a record */
}

/* ---------- Visitor statistics ---------- */
.stat-cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:14px;margin:18px 0 8px}
/* WHITE, like .health-card and .admin-stat, and for the same reason:
   --ink-muted is 4.87:1 on white and only 4.46:1 on --paper, which is
   under AA. The palette note in CLAUDE.md says exactly this and it
   still caught me — the card looked right and the axe run said
   otherwise. */
.stat-card{background:var(--white);border:1px solid var(--line);border-radius:12px;padding:16px 18px}
.stat-card b{display:block;font-family:var(--font-display);font-size:28px;color:var(--green);line-height:1.1}
.stat-card span{display:block;font-size:13px;color:var(--ink-soft);margin-top:2px}
.stat-card em{display:block;font-style:normal;font-size:12.5px;color:var(--ink-muted);margin-top:6px}
.stat-card.is-past b{color:var(--ink-soft)}
.stats-chart-head{font-size:15px;margin:26px 0 10px;color:var(--ink)}
/* The chart is inline SVG built server-side — no library, nothing to
   fetch, and it scales with the box rather than a pixel width. */
.stats-chart{background:var(--paper);border:1px solid var(--line);border-radius:12px;padding:16px 18px 8px}
.stats-chart svg{display:block;width:100%;height:auto}
.stats-bar{fill:var(--green);opacity:.75}
.stats-bar.is-today{fill:var(--red-ink);opacity:1}
.stats-tick{fill:var(--ink-soft);font-size:11px;font-family:var(--font-body)}

/* ---------- Collections listing ---------- */
.collection-raised{margin-top:10px;font-size:14px;color:var(--ink-soft)}
.collection-raised b{color:var(--green);font-size:16px}
/* A completed collection is a card like any other, so that it reads as
   something that HAPPENED rather than as a leftover. It is quieter than
   an open one — the eye should land on what is still open first — which
   is a lighter ground and a muted date chip, not a smaller card. */
.event-card.is-complete{background:var(--paper)}
.event-card.is-complete .event-date{color:var(--ink-muted)}
.event-card.is-complete .collection-raised b{color:var(--ink)}

/* The notice that stands where the payment form was. */
.collection-finished{
  margin-top:36px;padding:28px 30px;border-radius:var(--radius);
  background:var(--green-tint);border:1px solid var(--line);max-width:68ch;
}
.collection-finished h2{font-size:22px;margin-bottom:12px;color:var(--green)}
.collection-finished p{color:var(--ink-soft);line-height:1.65}
.collection-finished-cta{margin-top:12px}
.collection-finished a{color:var(--green);font-weight:600}

/* ---------- Stats ---------- */
.stats{background:var(--green);color:#fff;padding:50px 0}
.stats-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:28px;text-align:center}
.stat b{display:block;font-family:var(--font-display);font-size:clamp(32px,4vw,44px);font-weight:800}
.stat span{font-size:14px;font-weight:600;opacity:.85}

/* ---------- Cards ---------- */
.cards{display:grid;grid-template-columns:repeat(3,1fr);gap:22px;margin-top:40px}
.card{
  background:var(--white);border:1px solid var(--line);border-radius:var(--radius);
  padding:30px 28px;transition:transform .18s ease, box-shadow .18s ease;
}
.card:hover{transform:translateY(-5px);box-shadow:0 18px 40px rgba(2,56,42,.12)}
.card-icon{
  width:52px;height:52px;border-radius:50%;background:var(--green-tint);
  display:grid;place-items:center;margin-bottom:18px;font-size:24px;
}
.card h3{font-size:20px;font-weight:700;margin-bottom:8px}
.card p{font-size:15px;color:var(--ink-soft)}

/* ---------- Event cards ---------- */
/* ONE grid for every listing card on the site: the events, news and
   collections pages and the three homepage strips. Three across, two
   below 920, one below 640 — the media queries at the foot of the file.
   Above 920 the columns are auto-fit rather than a fixed three, and
   that is what lets a lone card grow: a single event in a three-column
   row sat at 343px with two thirds of the row empty, which is what a
   small charity's events page looks like most weeks. The 276px minimum
   is the width that still fits THREE at 921px (873px of content,
   3×276 + 44), so with three or more items nothing moves at any width;
   with two, each takes half; with one, the track is the full row and
   the :only-child cap below holds it to half — the width a two-column
   row would give it, so a lone card looks like a card and not a band.
   The cap lives in the same media query on purpose: below 920 the
   forced 1fr 1fr / 1fr tracks already give a lone card its half or
   its whole, and a 50% cap inside a half-width track would quarter it. */
.event-cards{display:grid;grid-template-columns:repeat(3,1fr);gap:22px;margin-top:36px}
@media (min-width: 921px){
  .event-cards{grid-template-columns:repeat(auto-fit,minmax(276px,1fr))}
  /* Half the row, and never past 460px: half of 1072 is 525, and at
     4:3 that is a 392px photograph — a single event made the homepage
     strip TALLER than three did (885 against 772). 460 gives a 345px
     photo and a strip within a few pixels of the three-item row.
     Declared twice: the plain calc for an engine without min(). */
  .event-cards>.event-card:only-child{max-width:calc(50% - 11px);max-width:min(460px,calc(50% - 11px))}
}
.event-card{
  background:var(--white);border:1px solid var(--line);border-radius:var(--radius);
  overflow:hidden;text-decoration:none;display:flex;flex-direction:column;
  transition:transform .18s ease, box-shadow .18s ease;
}
.event-card:hover{transform:translateY(-5px);box-shadow:0 18px 40px rgba(2,56,42,.12)}
/* 4:3, the shape of a photograph off a phone and of the gallery's
   album covers. 16:9 cropped a quarter off every landscape upload and
   showed a 192px letterbox at the widest screen, which read as small
   beside every other photograph on the site. Six treatments share this
   one line, so they cannot come apart. */
/* Cropped from the TOP, not the middle: most of these are posters, and
   the headline is at the top while the dates and phone numbers at the
   bottom are what a centred crop lost. The shape stays fixed so the grid
   lines up; the detail page shows the whole picture. */
.event-card img{aspect-ratio:4/3;object-fit:cover;object-position:top;width:100%}
.event-card-body{padding:22px 24px}
/* Either level: a card title is an h3 on the homepage, where it sits
   under a section h2, and an h2 on the listing pages, where the page
   title above it is the h1. The card looks the same in both. */
.event-card h2,.event-card h3{font-size:19px;font-weight:700;margin:6px 0 8px}
.event-card p{font-size:14.5px;color:var(--ink-soft)}
.event-date{
  font-size:13px;font-weight:700;color:var(--green);letter-spacing:.05em;text-transform:uppercase;
}
.event-card.is-past{opacity:.75}

/* ---------- Event detail ---------- */
/* 780 is a reading width for PROSE, from before rich content existed.
   The collection page still lives inside it: that page is hand-built
   around one photograph and a payment form, and a payment form at
   1072px would be a new problem. */
.event-detail{max-width:780px}
/* The event and news pages do NOT. Each is one rich-content owner,
   like About, so the macro gets About's full 1072px and its presets
   come out at the same size on all three pages — measured identical at
   every shared viewport. The prose inside is already held to 68ch by
   .rc-text, so lifting the cap moves only the photographs; the title
   and the date line keep the old measure on their own. */
.rich-detail .event-title,.rich-detail .event-meta{max-width:780px}
.event-title{font-size:clamp(30px,4.4vw,46px);font-weight:800;margin-bottom:16px}
.event-meta{display:flex;flex-wrap:wrap;gap:18px;font-weight:600;font-size:15px;color:var(--ink-soft);margin-bottom:28px}
.badge-past{background:var(--line);border-radius:999px;padding:2px 12px;font-size:13px}
/* Cancelled: the same red as the error flash and the admin's red pill,
   which already pass AA, and never `--red` on its own. On a card the
   badge sits on its own line above the title; the photograph goes
   grey so a cancelled event is read as such from across the page,
   not only once the badge is found. */
.badge-cancelled{display:inline-block;background:#FCE9EA;color:#9B1B26;border-radius:999px;padding:2px 12px;font-size:13px;font-weight:700;letter-spacing:.02em;text-transform:uppercase}
.event-card .badge-cancelled{margin:6px 0 2px}
.event-card.is-cancelled img{filter:grayscale(1);opacity:.8}
.event-cancelled{background:#FCE9EA;border-left:4px solid var(--red);border-radius:6px;padding:16px 20px;margin:0 0 24px;max-width:68ch}
.event-cancelled p{margin:0;color:#7A1520;line-height:1.55}
.event-cancelled p+p{margin-top:8px}
.event-cancelled a{color:#7A1520;font-weight:600}
.event-hero{
  border-radius:var(--radius);margin-bottom:32px;width:100%;
  aspect-ratio:16/7;max-height:440px;object-fit:cover;
}
.event-body p{margin-bottom:16px;max-width:68ch}

/* ---------- Gallery ---------- */
/* ---------- Gallery: albums, masonry, lightbox ----------
   The masonry is the same CSS-columns approach as the rich-content
   gallery preset, with one difference that matters here: the ratios are
   the photographs' own, not a staggered pattern, so a portrait phone
   photo stays portrait instead of being cropped square. Each ratio is
   set inline from the file, so the column heights are right before a
   single image has loaded. */
.album-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:22px;margin-top:36px}
.album-card{
  background:var(--white);border:1px solid var(--line);border-radius:var(--radius);
  overflow:hidden;text-decoration:none;display:flex;flex-direction:column;
  transition:transform .18s ease, box-shadow .18s ease;
}
a.album-card:hover{transform:translateY(-3px);box-shadow:0 14px 32px rgba(2,56,42,.12)}
.album-card img{width:100%;aspect-ratio:4/3;object-fit:cover}
.album-card-empty{
  width:100%;aspect-ratio:4/3;background:var(--green-tint);
  display:grid;place-items:center;font-size:34px;
}
.album-card.is-all{background:var(--green);color:#fff;justify-content:center;min-height:180px}
.album-card.is-all .album-card-body b,
.album-card.is-all .album-card-body span,
.album-card.is-all .album-card-body em{color:#fff}
.album-card.is-all .album-card-body span{opacity:.85}
.album-card-body{padding:18px 20px;display:grid;gap:6px}
.album-card-body b{font-family:var(--font-display);font-size:19px;color:var(--green)}
.album-card-body span{font-size:14px;color:var(--ink-soft);line-height:1.5}
.album-card-body em{font-style:normal;font-size:13px;font-weight:700;color:var(--red-ink)}

.photo-masonry{columns:3;column-gap:18px;margin-top:36px}
.photo-item{break-inside:avoid;margin:0 0 18px}
.photo-item a{display:block;border-radius:12px;overflow:hidden}
.photo-item img{
  width:100%;border-radius:12px;background:var(--green-tint);
  transition:transform .2s ease;
}
.photo-item a:hover img{transform:scale(1.02)}
.photo-item a:focus-visible{outline:3px solid var(--green);outline-offset:3px}
.photo-item figcaption{font-size:13.5px;color:var(--ink-soft);margin-top:8px;font-weight:500}

/* Lightbox. Hidden with the `hidden` attribute so it is inert — and
   invisible to a screen reader — until script opens it. */
.lightbox{
  position:fixed;inset:0;z-index:100;background:rgba(2,56,42,.94);
  display:grid;grid-template-columns:auto 1fr auto;align-items:center;
  gap:10px;padding:20px;
}
.lightbox[hidden]{display:none}
body.lightbox-open{overflow:hidden}
/* Nothing from the page behind should bleed through the backdrop while
   someone is looking at a photograph. */
body.lightbox-open .cookie-notice{display:none}
.lightbox-figure{
  margin:0;display:flex;flex-direction:column;align-items:center;
  justify-content:center;gap:14px;min-width:0;max-height:92vh;
}
.lightbox-figure img{
  max-width:100%;max-height:80vh;width:auto;border-radius:10px;
  box-shadow:0 24px 60px rgba(0,0,0,.45);
}
.lightbox-figure figcaption{
  color:#fff;font-size:15px;text-align:center;max-width:60ch;line-height:1.5;
}
.lightbox-figure figcaption[hidden]{display:none}
.lightbox-close,.lightbox-nav{
  background:rgba(255,255,255,.12);color:#fff;border:none;cursor:pointer;
  border-radius:999px;font-family:var(--font-display);line-height:1;
}
.lightbox-close{
  position:absolute;top:16px;right:18px;width:44px;height:44px;font-size:26px;
}
.lightbox-nav{width:52px;height:52px;font-size:34px;flex:none}
.lightbox-close:hover,.lightbox-nav:hover{background:rgba(255,255,255,.24)}
.lightbox-close:focus-visible,.lightbox-nav:focus-visible{outline:3px solid #fff;outline-offset:2px}
.lightbox-count{
  position:absolute;bottom:16px;left:0;right:0;text-align:center;
  color:rgba(255,255,255,.75);font-size:13.5px;font-weight:600;
}

/* ---------- Forms shared by the public pages ----------
   The honeypot: off-screen rather than display:none, because some bots
   skip anything a browser would not render. Never a real visitor's
   problem — it is aria-hidden and out of the tab order. */
.hp-field{position:absolute;left:-9999px}

/* ---------- Contact ---------- */
.contact-grid{display:grid;grid-template-columns:1fr 1fr;gap:44px;margin-top:36px}
.contact-card{background:var(--white);border:1px solid var(--line);border-radius:var(--radius);padding:34px}
.contact-card h2{font-size:20px;margin-bottom:18px}
.contact-row{display:flex;gap:14px;padding:12px 0;border-bottom:1px solid var(--line);font-size:15.5px}
.contact-row:last-of-type{border-bottom:none}
.contact-row .ico{flex:none;width:38px;height:38px;border-radius:50%;background:var(--green-tint);display:grid;place-items:center;font-size:17px}
.contact-row b{display:block;font-size:13px;text-transform:uppercase;letter-spacing:.08em;color:var(--ink-soft);font-weight:700}
.contact-row a{color:var(--green);font-weight:600;text-decoration:none}
.contact-form{
  grid-column:1 / -1;background:var(--white);border:1px solid var(--line);
  border-radius:var(--radius);padding:34px;
}
.contact-form h2{font-size:20px;margin-bottom:20px}
.contact-form .field{margin-bottom:18px}
.contact-form label{display:block;font-weight:700;font-size:14px;margin-bottom:6px}
.form-privacy{
  margin-top:16px;font-size:13.5px;color:var(--ink-soft);line-height:1.6;
  max-width:70ch;
}
.form-privacy a{color:var(--green);font-weight:600}
.map{border-radius:var(--radius);overflow:hidden;border:1px solid var(--line);min-height:340px}
.map iframe{width:100%;height:100%;min-height:340px;border:0}

/* ---------- Footer ---------- */
footer{background:var(--green-deep);color:rgba(255,255,255,.82);padding:56px 0 32px}
/* minmax(0,·) on every track, for the reason the admin shell spells out
   below: an `fr` track's automatic minimum is MIN-CONTENT, so one item
   that cannot be broken sizes the track wider than the footer and every
   cell overflows with it. Measured at the narrow end, where the column
   is one and the whole footer carries it: 3px of sideways scroll on the
   page at 360 (a Galaxy S10) and 43px at 320. The item that did it was
   the newsletter input and its button SIDE BY SIDE, which they no
   longer are — the floor stays at zero all the same, because it is the
   guard and not the fix, and the next unbreakable thing dropped into a
   footer column would do exactly the same. */
.foot-grid{
  display:grid;gap:36px;margin-bottom:40px;
  grid-template-columns:minmax(0,1.3fr) minmax(0,1.2fr)
                        minmax(0,.8fr) minmax(0,1fr);
}
.foot-grid h2{font-family:var(--font-display);color:#fff;font-size:16px;margin-bottom:14px}
.foot-grid ul{list-style:none;display:grid;gap:9px}
/* The .85 is for the quiet run of links. It must not reach the Donate
   button: opacity dims an element AND its text together, so the button
   came out at 3.98:1 — the only thing on the public site under 4:1, and
   worse than the red it is painted in. The :not() keeps it off. */
.foot-grid a:not(.foot-donate){text-decoration:none;font-size:14.5px;opacity:.85}
.foot-grid a:not(.foot-donate):hover{opacity:1}
.foot-about{font-size:14.5px;max-width:38ch}
/* Social icons: a row of 44px targets drawn in currentColor, so they
   are white on the footer's green and green on the contact card
   without a second set of pictures. .foot-grid ul is a grid, hence
   the more specific selector to lay this one out as a row. */
.social-links,.foot-grid .social-links{list-style:none;display:flex;flex-wrap:wrap;gap:8px;margin:16px 0 0;padding:0}
.social-links li{margin:0}
.social-links a,.foot-grid .social-links a{
  display:inline-flex;align-items:center;justify-content:center;
  width:44px;height:44px;border-radius:10px;color:inherit;opacity:1;
}
.social-links svg{width:22px;height:22px;fill:currentColor;display:block}
footer .social-links a{color:#fff;background:rgba(255,255,255,.1)}
footer .social-links a:hover{background:rgba(255,255,255,.2)}
footer .social-links a:focus-visible{outline:3px solid #fff;outline-offset:2px}
.contact-card .follow-us{font-size:15px;margin:22px 0 0}
.contact-card .social-links{margin-top:10px}
.contact-card .social-links a{color:var(--green);background:var(--green-tint)}
.contact-card .social-links a:hover{background:#d9e8df}
/* The footer's Donate button. On a phone the header pill is inside the
   collapsed menu, so this is the call to action most visitors actually
   see — it earns its own line and a top margin rather than sitting in
   the run of links. Colours come straight from .btn-red; only the
   spacing and the focus ring are its own, the global ring being a red
   that disappears against a red button on dark green. */
.foot-donate{margin-top:16px;text-decoration:none}
.foot-donate:focus-visible{outline:3px solid #fff;outline-offset:3px}
.foot-bar{border-top:1px solid rgba(255,255,255,.16);padding-top:22px;display:flex;justify-content:space-between;gap:16px;flex-wrap:wrap;font-size:13.5px;opacity:.7}
.foot-admin{text-decoration:none}
.foot-legal{display:flex;gap:18px;flex-wrap:wrap}

/* ================= ADMIN ================= */
.admin-body{background:#EFF2EE}
/* minmax(0,1fr), not 1fr: a grid track sized `auto` at the bottom takes
   its MIN-CONTENT width, so one long unbreakable value in the main
   column could drag the whole shell — sidebar and all — wider than the
   screen. This keeps the main column shrinkable and the page honest. */
.admin-shell{display:grid;grid-template-columns:240px minmax(0,1fr);min-height:100vh}
.admin-side{
  background:var(--green-deep);color:#fff;padding:26px 20px;
  display:flex;flex-direction:column;gap:30px;
}
.admin-side nav{display:grid;gap:4px}
.admin-side nav a{
  color:rgba(255,255,255,.78);text-decoration:none;font-weight:600;font-size:15px;
  padding:10px 14px;border-radius:10px;
}
.admin-side nav a:hover{background:rgba(255,255,255,.08);color:#fff}
.admin-side nav a.active{background:rgba(255,255,255,.14);color:#fff}
.admin-side-foot{margin-top:auto;display:grid;gap:8px}
.admin-side-foot a{color:rgba(255,255,255,.7);font-size:14px;text-decoration:none}
.admin-side-foot a:hover{color:#fff}
.admin-main{padding:40px 44px;max-width:900px}
.admin-h1{font-size:30px;font-weight:700;margin-bottom:6px}
.admin-sub{color:var(--ink-soft);margin-bottom:28px}
/* The title keeps at least 320px; a wide action row drops beneath it
   rather than squeezing the heading into a column. */
.admin-head-row{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:flex-start;gap:20px}
.admin-head-row > div:first-child{flex:1 1 320px;min-width:0}
/* The page's actions — Print, Export CSV, Back — as ONE row that wraps. */
.admin-actions{display:flex;gap:10px;flex-wrap:wrap;align-items:center;justify-content:flex-end;flex:none}
/* A <button> and an <a> in the same row: the button's line-height is the
   browser's 'normal', the link inherits the page's 1.6, and the two came
   out 8px different in height. One line-height, one height. */
.admin-actions .btn{line-height:1.25}
/* On a phone the row drops under the title and its buttons may wrap;
   above that it keeps its width and the title gives way. */
@media (max-width:640px){.admin-head-row{flex-wrap:wrap}.admin-actions{flex:1 1 100%;justify-content:flex-start}}
.admin-stats-head{
  font-family:var(--font-body);font-size:13px;font-weight:700;
  letter-spacing:.12em;text-transform:uppercase;color:var(--ink-soft);
  margin:0 0 12px;
}
/* A section heading at the top level of the page sits 30px below
   whatever came before it — the same gap .admin-stats and .admin-more
   already leave, so where one of those is above, the two margins
   collapse to a single 30px and nothing moves. Scoped to a direct child
   of .admin-main on purpose: inside a panel or a form (.admin-attention,
   .admin-form) the heading is the first thing in the box and has to sit
   against the box's own padding, not 30px below it. */
.admin-main > .admin-stats-head{margin-top:30px}
.admin-main > .admin-stats-head:first-child{margin-top:0}
/* auto-fit rather than a fixed 3: the row of cards changes with the
   feature flags, and 220px keeps three to a row both on the admin's
   900px column and on a tablet, folding to two on smaller screens. */
.admin-stats{
  display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));
  gap:18px;margin-bottom:30px;
}
.admin-stat{
  background:var(--white);border:1px solid var(--line);
  border-radius:var(--radius);padding:24px;text-align:center;
  display:flex;flex-direction:column;justify-content:center;
}
a.admin-stat{
  text-decoration:none;color:inherit;
  transition:border-color .15s ease,box-shadow .15s ease;
}
a.admin-stat:hover{border-color:var(--green);box-shadow:0 6px 18px rgba(0,86,63,.10)}
a.admin-stat:focus-visible{outline:3px solid var(--green);outline-offset:2px}
.admin-stat b{display:block;font-family:var(--font-display);font-size:34px;color:var(--green);line-height:1.15}
.admin-stat span{font-size:14px;color:var(--ink-soft);font-weight:600}
.admin-stat em{font-style:normal;font-size:12.5px;color:var(--ink-muted);margin-top:6px}
/* Something is actually waiting on the admin — the only card that shouts. */
.admin-stat-alert{border-color:var(--red);background:#FFF7F7}
.admin-stat-alert b{color:var(--red)}
a.admin-stat-alert:hover{border-color:var(--red);box-shadow:0 6px 18px rgba(232,51,63,.12)}
.admin-quick{display:flex;gap:14px;flex-wrap:wrap}
/* The decline reason sits in the row it belongs to. Narrow on purpose:
   it is a note, not an essay, and the column has three other actions
   beside it. */
.decline-form{display:flex;gap:8px;align-items:center}
.decline-form input{
  padding:6px 10px;border:1.5px solid var(--line);border-radius:8px;
  font:inherit;font-size:14px;background:var(--paper);width:7.5rem;
  min-width:0;
}

/* ---------- Dashboard: needs attention + recent activity ---------- */
/* Unpublished work is the one thing on a card worth colouring in. */
.admin-stat em.stat-draft{color:var(--red);font-weight:700}
.admin-attention{
  background:var(--white);border:1px solid var(--line);border-left:4px solid var(--red);
  border-radius:var(--radius);padding:22px 24px;margin-bottom:30px;
}
/* The box is also used for a sentence or two rather than a list — the
   testimonials page uses it that way — so its prose needs the same size
   as an .attention-item. */
.admin-attention p{font-size:15px}
.attention-list{list-style:none;display:grid;gap:12px}
.attention-item{
  display:flex;justify-content:space-between;align-items:baseline;gap:14px;
  flex-wrap:wrap;font-size:15px;padding-bottom:12px;border-bottom:1px solid var(--line);
}
.attention-item:last-child{padding-bottom:0;border-bottom:none}
.attention-item a{color:var(--green);font-weight:700;text-decoration:none;white-space:nowrap}
.attention-item.is-blocker span{color:var(--red);font-weight:700;font-size:var(--large-text)}
/* Long summaries and email addresses wrap; the table scrolls inside its
   own box rather than taking the page sideways on a phone. */
/* position:relative so this box is the CONTAINING BLOCK for anything
   absolutely positioned inside it. Without it, an absolutely positioned
   descendant is laid out against the initial containing block instead
   and is NOT clipped by the overflow here — it escapes the box and
   pushes the whole document sideways. The actions column's
   .visually-hidden "Actions" span is exactly that: 1px wide, invisible,
   and sitting at the far right of a 635px table, so it dragged the page
   111px past a 390px phone while the table itself sat clipped and
   innocent. Every admin list had it. */
.table-scroll{overflow-x:auto;position:relative}
/* Three of these are tab stops (see the templates): a stop with no
   visible focus ring is a WCAG 2.4.7 failure, and an unexplained pause
   in the tab order for everyone else. */
.table-scroll:focus-visible{outline:3px solid var(--green);outline-offset:2px;border-radius:8px}
.activity-table td{overflow-wrap:anywhere}
/* Recent activity: the three column widths are settled here rather than
   left to the table algorithm. The email column has to hold a REAL
   address — a personal Gmail, not just info@netbus.co.uk — and the
   summary gives up the room, being the one column that wraps without
   harm. Fixed layout is also what makes the ellipsis below possible:
   under the auto algorithm a nowrap cell widens the table instead of
   being clipped, and the table would simply scroll sideways. */
.activity-table{table-layout:fixed}
.activity-table th:first-child,
.activity-table td:first-child{width:172px}   /* "22 Aug 2026, 21:58" */
/* 240px, measured rather than guessed, in the 14px this table is set in
   below and including the cell's own 32px of side padding:
   info@netbus.co.uk wants 151px, anis2921956@gmail.com 195px, and
   anis.miah1987@googlemail.com exactly 240 — the long personal address
   still sits on one line, with nothing to spare, so treat 240 as a
   floor rather than a round number. Beyond that
   (firstname.lastname@googlemail.com, 273px) it is ellipsised instead,
   which the title attribute makes harmless, and the 70px reclaimed goes
   to the summary — the column doing the work, and the one that wraps
   without harm.
   The admin column is capped at 900px, so the table is 812px wide at
   every desktop width and this leaves the summary 400px, up from 330. */
.activity-table .activity-who{width:240px}
/* One line per row, always. An address too long for the column is cut
   with an ellipsis and kept whole in the title attribute — wrapping it
   would make every row in the table two lines high, which is the thing
   being fixed. */
.activity-table td.activity-who{
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}
/* A log is scanned, not read, so it is set a step down from the 15px the
   content tables use — 14px, the size already used for admin labels,
   tabs and table links rather than a new one — with the row padding
   brought in to match. Nothing here is a tap target: the cells hold
   text, and the phone view below is a stacked card and keeps its own
   sizes.
   Both classes are named because .admin-table's own padding and font
   size are set FURTHER DOWN the file: at equal specificity the later
   rule would win, and these would do nothing. */
.admin-table.activity-table th,
.admin-table.activity-table td{padding:10px 16px}
.admin-table.activity-table td{font-size:14px;line-height:1.45}
.admin-more{margin:14px 0 30px;font-size:14.5px}
.admin-more a{color:var(--green);font-weight:700;text-decoration:none}

.admin-form{background:var(--white);border:1px solid var(--line);border-radius:var(--radius);padding:30px;max-width:680px}
.field{margin-bottom:20px}
/* ...but not inside a table cell, where the row supplies the spacing and
   a bottom margin only makes the row taller than its neighbours. The
   users page has done this locally for its password fields since it was
   written; this is the same rule, said once. */
.admin-table .field{margin-bottom:0}
.field label{display:block;font-weight:700;font-size:14px;margin-bottom:6px}
/* ---------- Form controls: ONE definition for the whole site ----------
   Public forms and admin forms both use .field inside .admin-form, and
   the contact form and login card are the two that do not — so all four
   contexts are listed here and nowhere else. Selecting by CONTAINER
   rather than by input type is deliberate: type=number was missing from
   the old list, which is why the donation amount box sat there small and
   square while everything around it was rounded, and any type added
   later would have gone the same way. Checkboxes, radios and file
   pickers keep their native look. */
.field input:not([type=checkbox]):not([type=radio]):not([type=file]),
.field textarea,
.field select,
.contact-form input:not([type=checkbox]):not([type=radio]):not([type=file]),
.contact-form textarea,
.contact-form select,
.login-card input{
  width:100%;padding:11px 14px;
  border:1.5px solid var(--line);border-radius:10px;
  font:inherit;font-size:16px;line-height:1.4;background:var(--paper);
  color:var(--ink);
}
.field input:focus,.field textarea:focus,.field select:focus,
.contact-form input:focus,.contact-form textarea:focus,
.contact-form select:focus,.login-card input:focus{
  outline:none;border-color:var(--green);
}
.field input::placeholder,.contact-form input::placeholder,
.field textarea::placeholder,.contact-form textarea::placeholder{
  color:var(--ink-soft);opacity:.7;
}
/* A select sizes itself to its longest option unless told otherwise —
   that is what pushed the settings page sideways on a phone. */
.field select,.contact-form select{max-width:100%}
/* A number never needs the width of a name, and a stretched one invites
   a phone number to be typed into it. Everything else is shared.
   (max-width only bites here because the rule above no longer sets one
   on inputs: the shared selector's :not() chain outranks this one.) */
.field input[type=number]{max-width:220px}
.field textarea,.contact-form textarea{min-height:120px;resize:vertical}
.field small{display:block;color:var(--ink-soft);font-size:13px;margin-top:5px}
.field-row{display:grid;grid-template-columns:1fr 1fr;gap:18px}
.field-thumb{
  width:180px;aspect-ratio:4/3;object-fit:cover;border-radius:10px;
  margin-bottom:10px;border:1px solid var(--line);
}
.field-check label{display:flex;align-items:center;gap:10px;font-weight:600}
.form-actions{display:flex;gap:12px;margin-top:8px}
/* An advanced corner of a settings form: shut by default, and visibly a
   box rather than more of the form, so nobody wanders into it by
   accident. Plain <details>, so the browser brings the keyboard
   handling and it still opens with no script. */
.admin-advanced{
  border:1px solid var(--line);border-radius:10px;
  padding:14px 16px;margin-bottom:20px;background:var(--paper);
}
.admin-advanced > summary{
  cursor:pointer;font-weight:700;font-size:14px;color:var(--ink);
  list-style:none;
}
.admin-advanced > summary::-webkit-details-marker{display:none}
/* "+" and an en dash, the same pair the FAQ accordions use, rather
   than a triangle from the geometric-shapes block: the display font
   has no glyph for one and it rendered as an empty box. */
.admin-advanced > summary::before{
  content:"+";color:var(--green);font-size:19px;line-height:1;
  margin-right:9px;
}
.admin-advanced[open] > summary::before{content:"–"}
.admin-advanced > summary:focus-visible{
  outline:3px solid var(--green);outline-offset:3px;border-radius:4px;
}
.admin-advanced[open] > summary{margin-bottom:12px}
.admin-advanced .field:last-of-type{margin-bottom:0}

.tab-row{display:flex;gap:8px;margin-bottom:24px;flex-wrap:wrap}
.tab{
  padding:8px 18px;border-radius:999px;text-decoration:none;font-weight:600;font-size:14px;
  border:1.5px solid var(--line);color:var(--ink-soft);background:var(--white);
}
.tab.active{background:var(--green);color:#fff;border-color:var(--green)}

.admin-table{width:100%;border-collapse:collapse;background:var(--white);border:1px solid var(--line);border-radius:var(--radius);overflow:hidden}
.admin-table th{
  text-align:left;font-size:12.5px;text-transform:uppercase;letter-spacing:.07em;
  color:var(--ink-soft);padding:14px 16px;border-bottom:1px solid var(--line);background:var(--paper);
}
.admin-table td{padding:14px 16px;border-bottom:1px solid var(--line);font-size:15px}
.admin-table tr:last-child td{border-bottom:none}
.admin-table td a{color:var(--green);font-weight:600;text-decoration:none}
.nowrap{white-space:nowrap}
.table-link{
  background:none;border:none;color:var(--green);font-weight:600;font-size:14px;
  cursor:pointer;text-decoration:none;margin-right:10px;font-family:inherit;padding:0;
}
/* This was the first place the darker red was used, and the reason it
   exists: at 18.7px bold Delete became the loudest thing in every admin
   row, so the DESTRUCTIVE action read as the primary one. Contrast is
   the point, not size. The public buttons followed it here later. */
.table-link.danger{color:var(--red-ink)}
/* nowrap: "No payment recorded" is the longest label any pill carries
   and it wrapped to two lines on seventeen rows at once, which made
   every row in the member list taller than it needed to be. */
.pill{border-radius:999px;padding:3px 12px;font-size:13px;font-weight:700;
  white-space:nowrap}
.pill-green{background:var(--green-tint);color:var(--green)}
.pill-grey{background:#ECEEEC;color:var(--ink-soft)}
/* A third pill state, for a collection that has finished but is still on
   the site: not "on" and not "off". 6.10:1, in the same register as the
   green (7.60) and the grey (6.26) beside it. */
.pill-amber{background:#FBF1DC;color:#7A5309}
.pill-red{background:#FCE9EA;color:#9B1B26}

/* Unread enquiries: a count beside the menu item, and a tinted row in
   the list, so what is waiting is obvious without opening anything. */
.nav-badge{
  display:inline-block;min-width:20px;padding:1px 7px;border-radius:999px;
  background:var(--red);color:#fff;font-size:12px;font-weight:700;
  text-align:center;margin-left:4px;
}
.msg-unread td{background:#FFF7F7}
.msg-unread td:first-child{box-shadow:inset 3px 0 0 var(--red)}
/* The enquiry list (templates/admin/messages.html). The message takes
   the spare width; the preview is clamped to two lines and the full
   text is in a <details>. Both keep line breaks and wrap ANYWHERE, so a
   300-character URL wraps instead of widening the page. */
.msg-subject{display:block;margin-bottom:4px}
.msg-nosubject{color:var(--ink-muted);font-weight:600;font-style:italic}
.msg-preview,.msg-full{color:var(--ink-soft);font-size:14px;line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere}
.msg-full{margin-top:4px}
.msg-more summary{display:block;cursor:pointer}
.msg-actions{display:grid;gap:8px;justify-items:start}
.messages-table td{vertical-align:top}
@media (min-width:1024px){
  .messages-table{table-layout:fixed}
  /* Sized for the 696px the table has at 1024: "11 Oct 2026" needs 84px
     after its padding, a "Replied" pill 66px, the Reply button 80px. */
  .messages-table th,.messages-table td{padding-left:10px;padding-right:10px}
  .messages-table .pill{font-size:12px;padding:2px 9px}
  .messages-table .col-date{width:15%}
  .messages-table .col-from{width:19%}
  .messages-table .col-message{width:38%}
  .messages-table .col-status{width:13%}
  .messages-table .col-actions{width:15%}
}
/* STACKED CARDS ON A PHONE, for the lists whose rows hold a person:
   enquiries and applications. Each row becomes a card with its cells
   stacked and labelled from data-label; the outer table frame goes, the
   red edge of a new enquiry moves to the card. */
@media (max-width:640px){
  .stack-cards{border:0;background:transparent;border-radius:0}
  .stack-cards thead{display:none}
  .stack-cards,.stack-cards tbody,.stack-cards tr,.stack-cards td{display:block}
  .stack-cards tr{background:var(--white);border:1px solid var(--line);border-radius:var(--radius);padding:12px 14px;margin-bottom:12px}
  .stack-cards td,.stack-cards tr:last-child td{padding:5px 0;border-bottom:none;background:transparent}
  .stack-cards td[data-label]::before{content:attr(data-label);display:block;font-size:11px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--ink-muted);margin-bottom:2px}
  .stack-cards.messages-table tr.msg-unread{background:#FFF7F7;border-left:4px solid var(--red)}
  .stack-cards.messages-table .msg-unread td:first-child{box-shadow:none}
  .stack-cards .app-actions,.stack-cards .msg-actions{margin-top:4px}
}

/* What the membership fee costs and what happens to it, above the
   application form. Tinted rather than bordered: it is the page telling
   somebody something, not a warning. */
.apply-fee{
  background:var(--green-tint);border-radius:12px;padding:18px 22px;
  margin:26px 0 0;
}
.apply-fee p{margin:0 0 10px;font-size:16px;line-height:1.65}
.apply-fee p:last-child{margin:0;color:var(--ink-soft);font-size:15px}

/* ---------- Busy state, and the gallery's upload progress ----------
   static/js/busy.js swaps a control's label for a message and adds the
   spinner below; nothing here is needed for the page to work, and with
   the script absent none of it is ever on screen. */

/* NO DIMMING. The obvious way to say "this button is inert" is
   opacity, and opacity dims an element's TEXT AND ITS BACKGROUND
   TOGETHER — which is how the footer Donate button reached 3.98:1, the
   worst contrast on the public site. White on --green at .7 goes the
   same way. The label has already changed to a sentence and there is a
   spinner beside it, which says far more than a faded button; so the
   colours stay exactly as they are and only the cursor and the hover
   lift change. */
.is-busy{cursor:progress}
.btn.is-busy:hover{transform:none}
.busy-spin{
  display:inline-block;flex:none;width:14px;height:14px;border-radius:50%;
  border:2px solid currentColor;border-top-color:transparent;
  animation:busy-spin .7s linear infinite;
}
/* Under prefers-reduced-motion the rule at the top of this file stops
   it turning, leaving a plain ring. That is fine and deliberate: the
   spinner was never the message, the words are. */
@keyframes busy-spin{to{transform:rotate(360deg)}}

/* The gallery's per-file upload. Hidden until the script has a run to
   report, so a page with no JavaScript never shows an empty bar. */
.upload-progress{
  /* Same width as the form it belongs to (.admin-form is 680px), and
     pulled up under it, so the two read as one panel rather than as a
     second thing that has appeared on the page. */
  background:var(--white);border:1px solid var(--line);border-radius:12px;
  padding:16px 18px;margin:-16px 0 26px;display:grid;gap:10px;
  max-width:680px;
}
.upload-bar{
  height:10px;border-radius:999px;background:var(--green-tint);
  overflow:hidden;
}
.upload-bar i{
  display:block;height:100%;width:0;background:var(--green);
  border-radius:999px;transition:width .2s ease;
}
.upload-status{font-size:14.5px;color:var(--ink);font-weight:600}
.upload-file{font-size:13.5px;color:var(--ink-muted);word-break:break-word}
.upload-fails{margin:0;padding-left:18px;display:grid;gap:4px;
  font-size:13.5px;color:var(--red-ink)}

.admin-gallery-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}
.admin-gallery-item{background:var(--white);border:1px solid var(--line);border-radius:12px;padding:10px;display:grid;gap:8px}
.admin-gallery-item img{border-radius:8px;aspect-ratio:1/1;object-fit:cover;width:100%}
.admin-gallery-item span{font-size:13px;color:var(--ink-soft)}
.admin-pick{display:block;position:relative;cursor:pointer}
.admin-pick input{position:absolute;top:8px;left:8px;width:20px;height:20px;z-index:1;cursor:pointer}
.admin-pick input:checked + img{outline:3px solid var(--green);outline-offset:-3px}
.admin-pick-foot{display:flex;justify-content:space-between;align-items:center;gap:8px;font-size:12.5px;color:var(--ink-soft)}
.admin-move-bar{
  display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:18px;
  background:var(--white);border:1px solid var(--line);border-radius:12px;padding:14px 18px;
}
/* The "how to change the password" box on Settings: the FAQ accordion,
   reused. Commands sit in their own scrolling box so a long one-liner
   cannot push the admin sideways on a phone. */
.admin-help{margin:0 0 26px;max-width:74ch}
.admin-help summary{font-size:16px}
.admin-help .faq-answer{padding-bottom:18px}
.admin-steps{padding-left:20px;display:grid;gap:14px;color:var(--ink-soft);font-size:15px;line-height:1.6}
/* min-width:0, or each step is sized to the MIN-CONTENT width of the
   command inside it — one long one-liner and the whole admin scrolls
   sideways on a phone. The <pre> scrolls instead, which is the point. */
.admin-steps li{padding-left:4px;min-width:0}
.admin-steps li > pre{min-width:0}
.admin-help pre{
  background:var(--green-deep);color:#EAF3EE;border-radius:10px;
  padding:12px 14px;margin:8px 0 0;overflow-x:auto;max-width:100%;
}
.admin-help pre code{font-family:ui-monospace,Menlo,Consolas,monospace;font-size:13px;white-space:pre}
.admin-help p{color:var(--ink-soft);font-size:15px;margin-top:16px}
.admin-move-bar label{font-weight:700;font-size:14px}
.admin-move-bar select{
  padding:11px 14px;border:1.5px solid var(--line);border-radius:10px;
  font:inherit;font-size:16px;background:var(--paper);
}
.is-hidden{display:none}
/* Read aloud, never painted. NOT display:none or visibility:hidden —
   both remove the element from the accessibility tree as well as from
   the page, which is the opposite of what this is for. Used for the
   actions column heading in every admin list, where a visible "Actions"
   would be a word of furniture above a column of two links. */
.visually-hidden{
  position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;
  border:0;
}

/* ---------- Login ---------- */
.login-body{display:grid;place-items:center;min-height:100vh;background:var(--green-deep)}
.login-card{
  background:var(--white);border-radius:16px;padding:40px;width:min(400px,92vw);
  display:grid;gap:16px;text-align:center;
}
.login-card .brand-mark{justify-content:center}
/* The card is a grid with its own gap, so the shared h1/h2 margin would
   be a second gap under this heading alone. */
.login-card h1{font-size:24px;margin-bottom:0}
.login-card label{text-align:left;font-weight:700;font-size:14px;display:grid;gap:6px}
.login-note{font-size:14px;color:var(--ink-soft);line-height:1.5;margin:0}
.login-back{font-size:14px;color:var(--ink-soft);text-decoration:none}

/* ---------- Legal pages ---------- */
.legal-page{max-width:70ch}
.legal-page h2{margin-bottom:20px}
.legal-page p{margin-bottom:16px;color:var(--ink-soft);line-height:1.65}

/* ---------- Cookie notice ----------
   A strip along the bottom, not a modal: it never covers the page or
   traps focus, and the OK button is an ordinary submit button so it
   works from the keyboard with no JavaScript at all. */
.cookie-notice{
  position:fixed;left:0;right:0;bottom:0;z-index:60;
  background:var(--green-deep);color:#fff;
  display:flex;align-items:center;justify-content:center;gap:20px;
  flex-wrap:wrap;padding:14px 24px;font-size:14.5px;line-height:1.5;
  box-shadow:0 -6px 24px rgba(0,0,0,.2);
}
.cookie-notice p{max-width:74ch}
.cookie-notice a{color:#fff}
.cookie-ok{background:#fff;color:var(--green-deep);padding:9px 26px;font-size:14px}
/* Keep the strip from sitting over the last of the footer. The strip
   wraps onto more lines as the viewport narrows, so the clearance grows
   with it — measured at each step in tests/check_header_layout.py. */
body.has-notice footer{padding-bottom:110px}
/* The strip is fixed to the bottom of the screen, so on a phone it sits
   OVER the open mobile menu — 190px of it, which on a short screen is
   the last few items including Donate. Covered is as unreachable as off
   screen: measured at 360x740 and 360x640, the taps landed on the
   notice. Same treatment as the lightbox above: not dismissed, just not
   painted while the menu is over it. */
body.menu-open .cookie-notice{display:none}
@media (max-width: 900px){
  body.has-notice footer{padding-bottom:190px}
}
@media (max-width: 640px){
  .cookie-notice{justify-content:flex-start;padding:16px 20px;font-size:14px}
  body.has-notice footer{padding-bottom:210px}
}

.admin-thumb{
  height:34px;width:64px;vertical-align:middle;
  margin-right:10px;object-fit:contain;
}

/* ---------- Audit log ---------- */
.audit-pager{display:flex;gap:12px;margin-top:22px}

/* ---------- User management ---------- */
.admin-table details summary{cursor:pointer;display:inline-block;list-style:none}
.admin-table details summary::-webkit-details-marker{display:none}
.user-actions{
  display:grid;gap:16px;background:var(--paper);border:1px solid var(--line);
  border-radius:10px;padding:16px;margin-top:10px;min-width:260px;
}
.user-actions form{display:grid;gap:8px;justify-items:start}
.user-actions .field{margin:0}

/* ---------- Two-factor authentication ---------- */
.tfa-qr{
  display:grid;place-items:center;background:var(--white);border:1px solid var(--line);
  border-radius:var(--radius);padding:18px;width:max-content;margin-bottom:8px;
}
.tfa-qr img{width:200px;height:200px}
.tfa-secret{
  font-family:ui-monospace,Menlo,Consolas,monospace;font-size:15px;letter-spacing:1px;
  background:var(--paper);border:1px solid var(--line);border-radius:8px;
  padding:10px 14px;display:inline-block;word-break:break-all;
}
.tfa-codes{
  list-style:none;display:grid;grid-template-columns:repeat(2,max-content);gap:10px 40px;
  font-family:ui-monospace,Menlo,Consolas,monospace;font-size:17px;letter-spacing:1px;
  background:var(--paper);border:1px solid var(--line);border-radius:var(--radius);
  padding:22px 28px;margin:18px 0;
}

/* ---------- Responsive ---------- */
/* ---------- Header: responsive steps ----------
   The nav must never wrap to a second line, so each step drops the least
   important thing first: the phone, then the strapline, then the whole
   nav in favour of the menu button.

   The strapline briefly went at 1100px to make room for a tenth flat nav
   item. Three grouped items and the Donate pill are far narrower than
   that row ever was, so it is back to 980px where it belongs. */
@media (max-width: 980px){
  .brand-name small{display:none}   /* the desktop row needs the room */
  .nav-links{gap:16px}
}
@media (max-width: 899px){
  .nav-links{display:none}
  .menu-btn{display:block}
  .main-nav{margin-left:auto}
  /* The panel SCROLLS ITSELF when it is taller than the screen. It is
     absolutely positioned inside a sticky header, so it travels with the
     header and is not part of the page's own scroll: anything past the
     bottom of the screen used to be simply unreachable, and scrolling
     the page did not move it a pixel. Measured on a Galaxy S10 (360
     CSS px): the open menu is 720px tall with all four groups expanded,
     so at 360x740 the phone number was off the bottom and at 360x640 —
     the same phone once the browser's own chrome is showing — Contact
     us, Donate and the phone number all were. The Donate pill was the
     visible half of that: gone from the menu, and the menu is where it
     lives on a phone.
     Both max-heights on purpose, in this order: dvh is the viewport
     that EXCLUDES the browser's chrome, which is the whole problem
     here, and vh is what an older Samsung Internet understands. The
     second line simply does not apply where dvh is unknown. */
  .nav-links.open{
    display:grid;position:absolute;top:var(--header-h);left:0;right:0;
    background:var(--paper);padding:20px 24px 26px;gap:16px;
    border-bottom:1px solid var(--line);box-shadow:0 20px 40px rgba(0,0,0,.08);
    max-height:calc(100vh - var(--header-h));
    max-height:calc(100dvh - var(--header-h));
    overflow-y:auto;-webkit-overflow-scrolling:touch;
    /* Scrolling the menu must not start scrolling the page behind it
       once it reaches its end. */
    overscroll-behavior:contain;
  }
  .nav-links.open .nav-phone-item,.nav-links.open .nav-home-item{display:block}
  .nav-links.open .nav-home-item a{font-weight:700}
  /* No nested dropdowns to tap through on a phone: each group is a
     heading with its pages listed underneath, all visible at once. */
  .nav-links.open .nav-group{display:block}
  .nav-links.open .nav-group > a{
    height:auto;padding:2px 0 6px;font-size:13px;font-weight:700;
    text-transform:uppercase;letter-spacing:.1em;color:var(--ink-soft);
    pointer-events:none;                 /* the group's page is listed below */
  }
  .nav-links.open .nav-group > a::after{display:none}
  .nav-links.open .nav-drop{
    position:static;opacity:1;visibility:visible;transform:none;
    background:none;border:none;border-left:2px solid var(--line);
    border-radius:0;box-shadow:none;padding:0 0 0 14px;gap:0;
    margin-bottom:14px;
  }
  .nav-links.open .nav-drop a{padding:8px 0;font-size:16px}
  .nav-links.open a{font-size:16px}
  .nav-phone-item a{color:var(--green);font-weight:700}
}

@media (max-width: 920px){
  .hero-grid,.about-grid,.contact-grid{grid-template-columns:1fr;gap:40px}
  .cards,.event-cards{grid-template-columns:1fr 1fr}
  .album-grid{grid-template-columns:1fr 1fr}
  .photo-masonry{columns:2}
  .stats-grid{grid-template-columns:1fr 1fr;gap:34px}
  .foot-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
  .admin-shell{grid-template-columns:minmax(0,1fr)}
  .admin-side{flex-direction:row;align-items:center;gap:18px;flex-wrap:wrap}
  .admin-side nav{display:flex;gap:4px;flex-wrap:wrap}
  .admin-side-foot{margin:0;display:flex;gap:16px}
  .admin-main{padding:28px 22px}
  .admin-gallery-grid{grid-template-columns:repeat(3,1fr)}
}
@media (max-width: 640px){
  section{padding:60px 0}
  /* Two cards to a row rather than one: there is a card per module now,
     and a single column made the dashboard a very long scroll. */
  .admin-stats{grid-template-columns:1fr 1fr;gap:12px}
  .admin-stat{padding:18px 12px}
  .admin-stat b{font-size:28px}
  .admin-stat em{font-size:12px}
  .cards,.event-cards{grid-template-columns:1fr}
  .album-grid{grid-template-columns:1fr}
  /* Two columns of photographs still reads as a gallery on a phone;
     one column makes thirty photos an endless scroll. */
  .photo-masonry{columns:2;column-gap:12px}
  .photo-item{margin-bottom:12px}
  .lightbox{grid-template-columns:1fr;padding:12px}
  .lightbox-nav{
    position:absolute;bottom:52px;width:46px;height:46px;
  }
  .lightbox-nav.is-prev{left:16px}
  .lightbox-nav.is-next{right:16px}
  /* The row is brand, the collapsed nav (no width) and the button, with
     a gap either side of the nav: 22px twice was 44px of nothing, and
     with the subtitle back on a 390 it put the button 1px off the
     screen. 12px keeps the button on the right edge. */
  .nav{gap:12px}
  .brand-mark img{height:48px}      /* a tenth up; still off the menu button */
  .brand-name{font-size:21px}
  /* With the menu folded into its button there is room for the subtitle
     again on a phone, smaller — measured 307px of brand against a menu
     button starting at 326 on a 390 — and NOT on the smallest screens,
     where it would run into the button (307 against 296 on a 360):
     hidden under 385px. */
  .brand-name small{display:block;font-size:11px;letter-spacing:0}
  .field-row{grid-template-columns:1fr}
  /* Three columns is too narrow to read on a phone, so each entry becomes
     a small card: who and when on one line, what it was underneath. */
  .activity-table{display:block;border:none;background:none;border-radius:0}
  .activity-table tr{
    display:block;background:var(--white);border:1px solid var(--line);
    border-radius:12px;padding:12px 14px;margin-bottom:10px;
  }
  .activity-table tr:first-child{display:none}      /* the header row */
  /* Back to the body's own line height as well: the desktop table is
     tightened to fit rows on a screen, and a card is not a table row. */
  .admin-table.activity-table td{
    display:block;border:none;padding:0;line-height:1.65;
  }
  .activity-table td:first-child,.activity-table td:nth-child(2){
    display:inline;font-size:13px;color:var(--ink-soft);
  }
  .activity-table td:first-child::after{content:" · "}
  /* The summary keeps the 15px it always had: the desktop step down is
     about fitting three columns in 812px, and a card has one. */
  .activity-table td:last-child{margin-top:4px;font-size:15px}
  /* The stacked card is not a table: the column width and the one-line
     ellipsis above would only stop a long address wrapping inside a
     342px card and push it out of the side. Here it wraps, as it did
     before any of this. */
  .activity-table td.activity-who{
    width:auto;white-space:normal;overflow:visible;text-overflow:clip;
  }
  .admin-gallery-grid{grid-template-columns:1fr 1fr}
  .foot-grid{grid-template-columns:minmax(0,1fr)}
}
/* The smallest screens: the subtitle would run into the menu button
   (307px of brand against a button at 296 on a 360). ITS OWN BLOCK,
   AFTER the 899px one — opened inside it, it closed that block early
   and moved every rule below it (the stacked .field-row, the gallery
   grid, the activity cards) under 385px only. Found by
   check_admin_widths at 390. */
@media (max-width: 384px){
  .brand-name small{display:none}
}

/* ---------- Testimonials ---------- */
.quote-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:22px;margin-top:36px}
.quote-card{
  background:var(--white);border:1px solid var(--line);border-radius:var(--radius);
  padding:28px;display:flex;flex-direction:column;gap:16px;
}
.quote-card blockquote{font-size:15.5px;color:var(--ink);line-height:1.6}
.quote-card blockquote::before{content:"\201C";color:var(--red);font-family:var(--font-display);font-size:1.6em;line-height:0;vertical-align:-6px;margin-right:3px}
.quote-card figcaption b{display:block;font-size:15px}
.quote-card figcaption span{font-size:13.5px;color:var(--ink-soft)}

/* ---------- Partners ---------- */
.partner-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;margin-top:36px}
.partner-card{
  background:var(--white);border:1px solid var(--line);border-radius:12px;
  padding:20px 22px;text-decoration:none;display:block;
  transition:transform .15s ease, box-shadow .15s ease;
}
a.partner-card:hover{transform:translateY(-3px);box-shadow:0 12px 28px rgba(2,56,42,.10)}
/* One box for every logo, whatever shape the file is: contain, never
   crop or stretch. The white tile with a hairline edge is what makes a
   transparent PNG and a white-background JPEG look like the same kind of
   thing — without it one floats on the card and the other shows a
   rectangle. 200x100 displayed, so a wide wordmark and a square mark
   both sit centred in identical space. */
.partner-logo{
  display:block;width:200px;height:100px;max-width:100%;
  margin:0 auto 12px;padding:10px;box-sizing:border-box;
  object-fit:contain;object-position:center;
  background:var(--white);border:1px solid var(--line);border-radius:10px;
}
/* A logo-only card has nothing under the tile, so it should not carry the
   gap meant to separate the two. */
.partner-card .partner-logo:only-child{margin-bottom:0}

/* ---------- Partners: the scroller (more than four) ----------
   ONE offset moves this row: its own scrollLeft. The drift, the
   stepping, dragging, swiping and the arrows all push the same number,
   and the script wraps it back by exactly one set so the row never
   reaches an end. It used to be a CSS animation translating the track
   while the container scrolled underneath it — two offsets that added
   up, so the first drag or Tab into the row left the animation running
   off the end of the content and a gap appeared in the loop. */
.marquee-row{
  display:flex;align-items:center;gap:10px;margin-top:36px;
}
.marquee{
  /* Not only spacing: this gap plus the card width below is the STRIDE
     the stepping mode moves by, and the step wrap is sized against it.
     See the note on .marquee .partner-card. */
  --marquee-gap:18px;
  flex:1 1 auto;min-width:0;overflow-x:auto;
  scroll-snap-type:x proximity;-webkit-overflow-scrolling:touch;
  padding-bottom:10px;
  /* Without these the browser starts its own drag of the logo or the
     link as soon as the pointer moves, which cancels the pointer stream
     — no further move, no pointerup, and no click either. The script
     cancels dragstart as well, for browsers that ignore user-drag. */
  user-select:none;-webkit-user-select:none;
}
.marquee img,
.marquee a{-webkit-user-drag:none}
.marquee-track{display:flex;gap:var(--marquee-gap);width:max-content}
.marquee-set{display:flex;gap:var(--marquee-gap)}
/* LOAD-BEARING, not just a look: 260px plus the 18px gap is the stride
   that the arrows and the stepping mode move by, and the stepping mode's
   wrap is sized against it. Stepping wraps back by a set only once
   scrollLeft has gone PAST one, and it gets there one stride at a time,
   so the row needs a whole stride of room beyond the end of the first
   set — setWidth minus the visible width, against one stride. Five
   partners on the widest viewport is the tightest case and leaves 22px
   spare (300px of room against a 278px stride). Widen either value and
   that margin goes first: past it the browser clamps the last step
   before the wrap into a stub — the row still loops and still shows no
   gap, it just arrives untidily. tests/check_marquees.py
   measures the margin at every width and partner count and warns with
   the numbers rather than failing; check what it reports if you change
   either. */
/* The card width is a RANGE, not a number, because a row that has come
   to rest must not show a sliced card at its right edge — see
   fitWholeCards() in index.html. The script picks how many whole cards
   fit at the current width and grows them to fill it, between the base
   below and the max; the base is what a card is designed to be and what
   it stays with no JavaScript at all. */
.marquee-row{--marquee-card-base:260px;--marquee-card-max:360px}
.marquee-row[data-row="testimonials"]{
  --marquee-card-base:360px;--marquee-card-max:540px;
}
.marquee .partner-card,
.marquee .quote-card{
  flex:0 0 var(--marquee-card, var(--marquee-card-base));
  width:var(--marquee-card, var(--marquee-card-base));
  /* Snap to the START of a card, not its centre: the arrows move by
     exactly one card, and the resting position is a card against the
     left edge rather than one card centred with two half cards beside
     it. From a standing start, centre-snapping made the first press
     move a fraction of a card and every later one a whole one. */
  scroll-snap-align:start;
}
/* A quote is sentences, not a mark: 360px rather than 260. That width
   is load-bearing for the same reason the partner one is — it is half
   the stride the stepping mode moves by, and the step wrap needs a
   whole stride of room past the end of the first set. FOUR quotes on
   the widest viewport is the tightest case (four is where the row
   becomes a scroller at all), and 360 is what clears it: at 340 the
   check warns. Watch what tests/check_marquees.py prints if this
   changes. */

/* The copy set exists only to make the loop endless, so it is HIDDEN BY
   DEFAULT and revealed only while the row is genuinely animating. That
   direction is the whole point and must not be reversed: hiding it used
   to depend on the admin's `none` setting or on a
   prefers-reduced-motion match, which meant every way of NOT animating
   that neither of those knew about — the script blocked, erroring, or
   simply not run, or an engine too old to support the media feature —
   left the same five logos on the page twice, aria-hidden and
   untabbable but perfectly visible. Reported from a Galaxy S10 showing
   ten cards where a Note 10 showed five.
   So: the default state renders the real set alone, whatever the
   browser knows or the script manages, and .is-moving — added only
   once the drift or the stepping is actually running — is what brings
   the copies back. Failure now lands on a still row of real cards with
   arrows, never on visible duplicates. */
.marquee-set[aria-hidden="true"]{display:none}
.marquee-row.is-moving .marquee-set[aria-hidden="true"]{display:flex}
/* Grab-to-pull, wherever the row can move at all. */
.marquee-row .marquee{cursor:grab}
.marquee-row.is-dragging .marquee{
  cursor:grabbing;scroll-snap-type:none;
}
/* Snapping fights a drift that moves the row every frame. */
.marquee-row.is-moving .marquee{scroll-snap-type:none}

/* The arrows: the affordance for a row that is standing still, and now
   the DEFAULT state of the markup rather than something the script
   grants. A row that is not moving is a row somebody has to push, and
   the case where the script cannot grant anything is exactly the case
   that needs them. The script hides them again the moment it takes the
   row over — while it is drifting or stepping, or if there is nowhere
   to scroll to.
   With no JavaScript at all they cannot scroll the row, so the
   scrollbar-hiding rule below stays on .has-arrows — a class only the
   script adds. No script, no hidden scrollbar: the row is still
   scrollable by bar, wheel and swipe, and the buttons are the hint
   that it is. */
.marquee-arrow{
  flex:0 0 auto;width:42px;height:42px;border-radius:50%;
  border:1px solid var(--line);background:var(--white);color:var(--green);
  display:flex;align-items:center;justify-content:center;cursor:pointer;
  padding:0;transition:background .15s ease, border-color .15s ease,
  box-shadow .15s ease;
}
.marquee-arrow svg{width:22px;height:22px;display:block}
.marquee-arrow:hover:not(:disabled){
  background:var(--green);color:var(--white);border-color:var(--green);
}
.marquee-arrow:focus-visible{
  outline:3px solid var(--green);outline-offset:2px;
}
.marquee-arrow:disabled{opacity:.35;cursor:default}
.marquee-arrow[hidden]{display:none}
/* Once the buttons are there they say what the scrollbar was saying,
   and they say it better. */
.marquee-row.has-arrows .marquee,
.marquee-row.is-moving .marquee{
  scrollbar-width:none;-ms-overflow-style:none;
}
.marquee-row.has-arrows .marquee::-webkit-scrollbar,
.marquee-row.is-moving .marquee::-webkit-scrollbar{display:none}

/* Under prefers-reduced-motion the script never starts a drift or a
   step whatever the admin chose, so the row is static and gets the
   arrows — see the has-arrows rule above. Snapping comes back with it.
   The copies are already hidden by default; this overrides the .is-moving
   reveal as well, so that in the engines that do support the feature no
   sequence of classes can put a duplicate on screen. */
@media (prefers-reduced-motion: reduce){
  .marquee-row.is-moving .marquee-set[aria-hidden="true"]{display:none}
  .marquee{scroll-snap-type:x mandatory}
}
/* On a phone the arrows go UNDER the row rather than either side of it.
   Beside it they took 84px of a 342px window, leaving less than one
   card — so nothing was ever fully in view, and the row lost the hint
   that there is more to the right. */
@media (max-width: 640px){
  .marquee-row{flex-wrap:wrap;justify-content:center;gap:8px}
  .marquee{order:-1;flex:0 0 100%}
  .marquee-arrow{width:40px;height:40px}
  /* A card has to be narrower than the strip it scrolls in, or no card
     is ever fully in view. 360px is fine on a laptop and absurd on a
     360px phone. */
  .marquee-row[data-row="testimonials"]{
    --marquee-card-base:280px;--marquee-card-max:420px;
  }
}
.partner-card b{display:block;font-family:var(--font-display);font-size:16px;color:var(--green)}
.partner-card span{font-size:13.5px;color:var(--ink-soft)}

/* ---------- Community resources (reuses partner cards) ---------- */
.resource-links{display:flex;gap:16px;flex-wrap:wrap;margin-top:10px}
.resource-links a{font-size:14px;font-weight:600;color:var(--green);text-decoration:none}
.resource-links a:hover{text-decoration:underline}

/* ---------- Donate & collections ---------- */
.amount-presets{display:flex;gap:10px;flex-wrap:wrap;margin-bottom:10px}
/* The picked amount, so the row of buttons is visibly a CONTROL with a
   current value rather than four things that fill a box. Without it the
   only feedback was the number appearing below, which on a phone is
   under the thumb that just tapped. */
.amount-presets .is-picked{
  background:var(--green);color:#fff;border-color:var(--green);
}
.amount-presets .is-picked:hover{background:var(--green);color:#fff}

/* ---------- Money fields ----------
   A money input has to accept £12.50, so step="0.01", so its spinner
   arrows move a PENNY a press: a control that looks like it does
   something useful and does not, on the one field where a wrong number
   costs somebody money. The arrows go; everything else about the field
   stays exactly as it was — still type="number", so the range is still
   validated by the browser, the value is still a number, and a phone
   still opens the numeric keypad rather than the full alphabet.
   The quick-pick buttons above the field are what replaces them.
   Both vendor forms are needed: WebKit and Blink paint the spinner as
   two pseudo-elements, Firefox as part of the widget itself. */
input.money::-webkit-outer-spin-button,
input.money::-webkit-inner-spin-button{
  -webkit-appearance:none;appearance:none;margin:0;
}
input.money{-moz-appearance:textfield;appearance:textfield}
.gift-aid-note{display:block;margin-top:6px;font-weight:600;color:var(--green)}
.collection-progress{margin-top:28px}
.progress{height:10px;border-radius:999px;background:var(--green-tint);overflow:hidden;margin-top:10px}
.progress b{display:block;height:100%;background:var(--green);border-radius:999px}

/* ---------- Print (admin contributor lists) ---------- */
.print-only{display:none}
@media print{
  .admin-side,.flash,.no-print{display:none !important}
  .print-only{display:block}
  .admin-shell{display:block}
  .admin-main{padding:0}
  body.admin-body{background:#fff}
}

/* ---------- Video: click to load ----------
   The box holds either the poster button or, after a click, the iframe
   — both fill it, so the page does not jump when one becomes the
   other. 16:9 by default and set from the macro, so the aspect ratio is
   reserved before anything loads. */
.rc-video{
  position:relative;width:100%;aspect-ratio:var(--rc-ratio, 16 / 9);
  border-radius:var(--radius);overflow:hidden;background:var(--green-deep);
}
.rc-video iframe,
.rc-video .video-play{
  position:absolute;inset:0;width:100%;height:100%;border:0;
}
.video-play{
  padding:0;cursor:pointer;background:var(--green-deep);
  display:flex;align-items:center;justify-content:center;
}
.video-play img{
  position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  /* Dimmed, so the play button and the label read against any frame. */
  filter:brightness(.78);
}
.video-play:hover img{filter:brightness(.9)}
.video-play-icon{
  position:relative;width:74px;height:74px;border-radius:50%;
  background:var(--red);display:flex;align-items:center;
  justify-content:center;box-shadow:0 8px 26px rgba(0,0,0,.35);
  transition:transform .15s ease;
}
/* The triangle is drawn with borders rather than a glyph: no font to
   wait for, and it is the same shape in every browser. */
.video-play-icon::before{
  content:"";border-style:solid;border-width:15px 0 15px 25px;
  border-color:transparent transparent transparent #fff;
  margin-left:6px;
}
.video-play:hover .video-play-icon{transform:scale(1.06)}
.video-play:focus-visible{outline:3px solid #fff;outline-offset:-6px}
.video-play-note{
  position:absolute;left:0;right:0;bottom:14px;color:#fff;
  font-size:14px;font-weight:600;text-shadow:0 1px 6px rgba(0,0,0,.6);
}
.video-fallback{
  position:absolute;inset:0;display:flex;align-items:center;
  justify-content:center;color:#fff;font-weight:700;
}
/* Inside the alternating preset the media column already has its own
   frame, so the video simply fills it. */
.rc-alt-media .rc-video{height:100%}
.rc-lead-video{margin-bottom:26px}
/* A video sent down the page rather than into the lead slot. It is a
   band of its own at reading width — it is not paired with anything, so
   it should not pretend to be. */
.rc-video-block{margin:30px 0;max-width:760px}
.rc-video-end{margin-bottom:0;margin-top:34px}
/* The collection picture when it has been moved below the words: it is
   a band of its own down there rather than the hero at the top, so it
   takes the same spacing as a moved video. */
.event-hero.collection-image-below{margin:30px 0 0}

/* ---------- Footer newsletter ---------- */
/* STACKED, not side by side: the address goes on its own line and the
   button underneath it. Sharing one line meant the input gave up
   whatever the button needed, and in a 282px footer column that left
   the field at 133px at 1440 and 106px at 1024 — about eight characters
   of an email address, with the placeholder itself cut to "Your email
   ad". The button has since gone back to 14px, which would hand a few
   of those pixels back; not enough to make one line workable, and the
   field wanting the whole column was always the real reason. */
.foot-subscribe{display:flex;flex-direction:column;gap:10px}
/* Same height and font as every other input; the colours differ because
   it sits on the dark footer, and the pill shape to match the button
   below it. */
.foot-subscribe input{
  width:100%;min-width:0;padding:11px 16px;border-radius:999px;
  border:1.5px solid rgba(255,255,255,.25);
  background:rgba(255,255,255,.10);color:#fff;font:inherit;font-size:16px;
  line-height:1.4;
}
.foot-subscribe input::placeholder{color:rgba(255,255,255,.55)}
.foot-subscribe input:focus{outline:none;border-color:rgba(255,255,255,.6)}
/* Left-aligned to the input's edge and only as wide as its own word,
   NOT stretched. A full-width Subscribe is 282px of red in the desktop
   column and the whole width of the screen once the footer stacks —
   louder than Donate, which is the action that matters. This way it
   reads as the same kind of thing as the Donate button one column
   over, which is also a left-aligned pill under a paragraph. */
.foot-subscribe button{
  align-self:flex-start;
  border:none;border-radius:999px;padding:10px 18px;font-family:var(--font-display);
  font-weight:600;font-size:14px;background:var(--red-ink);color:#fff;
  cursor:pointer;
}
.foot-subscribe button:hover{filter:brightness(1.08)}

@media (max-width: 920px){
  .quote-grid,.partner-grid{grid-template-columns:1fr 1fr}
}
@media (max-width: 640px){
  .quote-grid,.partner-grid{grid-template-columns:1fr}
}

/* ---------- The admin guide at /admin/help ----------
   A document inside the admin, so it borrows the report's habits: a
   readable measure, screenshots that sit beside the step they
   illustrate, and a print stylesheet that drops the furniture. */
.guide{max-width:none}
.guide-toc{
  background:var(--white);border:1px solid var(--line);
  border-radius:var(--radius);padding:22px 26px;margin-bottom:34px;
}
.guide-toc h2{font-size:15px;text-transform:uppercase;letter-spacing:.06em;
  color:var(--ink-soft);margin-bottom:12px}
.guide-toc ol{margin:0;padding-left:20px;columns:2;column-gap:34px}
.guide-toc li{margin-bottom:7px;break-inside:avoid}
.guide-toc a{color:var(--green);text-decoration:none}
.guide-toc a:hover{text-decoration:underline}

.guide-section{
  background:var(--white);border:1px solid var(--line);
  border-radius:var(--radius);padding:28px 30px;margin-bottom:26px;
}
/* h2 and h3 here are inside a page that already has an h1, so they are
   sized down from the public scale rather than shouting. */
.guide-section h2{font-size:23px;margin:0 0 14px;color:var(--green)}
.guide-section h3{font-size:17px;margin:26px 0 8px}
.guide-section p,.guide-section li{max-width:68ch;line-height:1.65}
.guide-section p{margin-bottom:12px}
.guide-section ul{margin:0 0 14px 20px}
.guide-section li{margin-bottom:8px}
.guide-section code{
  background:var(--paper);border:1px solid var(--line);border-radius:5px;
  padding:1px 6px;font-size:.92em;
}

/* THE SCREENSHOTS. Capped so a wide one cannot dominate the page, and
   given a border because most of them are pale grey admin screens on a
   white card and would otherwise bleed into it. */
.guide-shot{margin:20px 0 24px;max-width:640px}
.guide-shot img{
  display:block;width:100%;height:auto;
  border:1px solid var(--line);border-radius:10px;background:var(--paper);
}
.guide-shot figcaption{
  font-size:13.5px;color:var(--ink-soft);line-height:1.55;
  margin-top:9px;padding-left:2px;max-width:60ch;
}

.guide-faq dt{font-weight:700;margin-top:18px}
.guide-faq dd{margin:5px 0 0;max-width:68ch;line-height:1.65;
  color:var(--ink-soft)}
.guide-end{margin-top:26px;padding-top:16px;border-top:1px solid var(--line)}

@media (max-width: 900px){
  .guide-toc ol{columns:1}
  .guide-section{padding:22px 20px}
}

@media print{
  /* What is left on the paper is the guide. */
  .guide-print-hint{display:none !important}
  .guide-section,.guide-toc{
    border:none;border-radius:0;padding:0;margin-bottom:26px;
    background:none;
  }
  .guide-section{border-top:1px solid #999;padding-top:16px}
  .guide-section h2{color:#000}
  /* A heading at the foot of a page with its text overleaf is the usual
     way a printed manual goes wrong. */
  .guide-section h2,.guide-section h3{break-after:avoid}
  /* AND THE PICTURES MUST NOT BLOW OUT THE PAGE. A 916px screenshot on
     A4 is wider than the printable area, so it is capped in real units
     rather than percentages, and kept with its caption. */
  .guide-shot{max-width:100%;break-inside:avoid}
  .guide-shot img{max-width:15.5cm;border:1px solid #999}
  .guide-shot figcaption{color:#333}
  .guide-faq dd{color:#000}
  .guide-toc{break-after:page}
}

/* ---- the admin's one confirmation dialog (templates/admin/base_admin.html)
   The <dialog> itself carries no padding or background: the box inside
   does, so a click whose target is the dialog can only be the backdrop
   — which is how confirm.js tells "outside" from "inside". */
.confirm-dialog{border:0;padding:0;background:transparent;width:min(92vw,460px);max-width:none;margin:auto}
.confirm-dialog::backdrop{background:rgba(23,37,31,.55)}
.confirm-box{background:var(--white);border:1px solid var(--line);border-radius:var(--radius);padding:28px;box-shadow:0 20px 60px rgba(0,0,0,.25)}
.confirm-box h2{font-size:22px;line-height:1.25;margin:0 0 10px}
.confirm-box p{margin:0 0 22px;line-height:1.6;color:var(--ink)}
.confirm-actions{display:flex;gap:12px;justify-content:flex-end;flex-wrap:wrap}

/* ---- thank-you pages: ONE layout for donate_success, collection_success,
   membership_applied and membership_paid. Everything centred, one text
   colour, ordinary paragraph spacing, and 32px of air above the button —
   the ledes used to be a second colour and the button sat on the last
   line. The eyebrow is a flex row, so it is centred with justify-content
   rather than text-align. */
.thanks{max-width:640px;text-align:center}
.thanks .eyebrow{justify-content:center}
.thanks h1{margin:0 0 16px}
.thanks p,.thanks .lede{color:var(--ink);max-width:none;margin:0 0 18px;line-height:1.65}
.thanks .lede{font-size:18px}
.thanks .thanks-cta{margin:32px 0 0}

/* ---- membership applications list (templates/admin/membership.html) */
.applications-table td{vertical-align:top}
.app-name{font-weight:700;font-size:16px;line-height:1.3}
.app-contact{font-size:14px;color:var(--ink-soft);margin-top:3px;line-height:1.45;overflow-wrap:anywhere}
.app-secondary{font-size:13.5px;color:var(--ink-muted);margin-top:6px;line-height:1.45}
.clamp2{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.app-reason summary{display:block;cursor:pointer}
.app-reason .app-more,.app-reason .app-less{display:inline-block;font-size:13px;font-weight:700;color:var(--green);margin-top:2px}
.app-reason .app-less,.app-reason .app-full{display:none}
.app-reason[open] .clamp2,.app-reason[open] .app-more{display:none}
.app-reason[open] .app-less{display:inline-block}
.app-reason[open] .app-full{display:block;margin-top:0}
.app-fee{display:grid;gap:6px;justify-items:start}
.app-fee .pill{white-space:normal;text-align:center;line-height:1.3}
.app-actions{display:grid;gap:10px}
.app-status{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.app-status select{flex:1 1 7rem;min-width:0}
.applications-table .decline-form{flex-wrap:wrap}
.applications-table .decline-form input{flex:1 1 7rem;width:auto}
.app-links{display:flex;gap:14px;flex-wrap:wrap;align-items:center;font-size:14px}
/* The abandoned application: quieter, not louder. */
.applications-table tr.is-quiet td{color:var(--ink-muted)}
.applications-table tr.is-quiet .app-name{font-weight:600;color:var(--ink-soft)}
@media (min-width:1024px){
  .applications-table{table-layout:fixed}
  .applications-table .col-date{width:13%}
  .applications-table .col-who{width:50%}
  .applications-table .col-actions{width:37%}
  .applications-table.has-fee .col-who{width:38%}
  .applications-table.has-fee .col-fee{width:15%}
  .applications-table.has-fee .col-actions{width:34%}
}

/* ---- contributor list (templates/admin/contributors.html) */
.contributors-table td{vertical-align:top}
.contributors-table th{white-space:nowrap}
/* A pill may wrap inside its cell: "Yes (test, not claimable)" is wider
   than a 15% column, and a pill that cannot wrap runs into Total. */
.contributors-table td .pill{white-space:normal;display:inline-block;line-height:1.3;text-align:left}
.contributors-table td small{display:inline-block;overflow-wrap:anywhere}
.who-name{font-weight:700;line-height:1.3}
.who-email{font-size:13.5px;color:var(--ink-muted);margin-top:3px;overflow-wrap:anywhere}
.status-stack{display:grid;gap:6px;justify-items:start}
.status-stack small{color:var(--ink-muted);font-size:13px}
/* The row's one action: a <details> whose summary reads as a link.
   With the script gone it opens inline; with it, the dialog opens. */
.row-action > summary{cursor:pointer;list-style:none;color:var(--green);font-weight:600;font-size:14px}
.row-action > summary::-webkit-details-marker{display:none}
.row-action .refund-form{display:grid;gap:10px;margin-top:8px;max-width:16rem}
.row-action .refund-form .field{margin-bottom:0}
.row-action .refund-form small{display:block;color:var(--ink-muted);font-size:12.5px;margin-top:4px}
@media (min-width:1024px){
  .contributors-table{table-layout:fixed}
  /* Seven columns in ~812px: tighter cell padding than the default 16px,
     and a date column wide enough for "11 Oct 2026" on one line. */
  .contributors-table th,.contributors-table td{padding-left:12px;padding-right:12px}
  .contributors-table .col-date{width:14%}
  .contributors-table .col-who{width:20%}
  .contributors-table .col-money{width:10%}
  .contributors-table .col-giftaid{width:17%}
  .contributors-table .col-status{width:19%}
}
/* Fields borrowed into the confirmation dialog, left-aligned under the
   sentence, with the spacing the table cell took away. */
.confirm-fields-host{text-align:left;margin:-4px 0 20px}
.confirm-fields-host .field{margin-bottom:14px}
.confirm-fields-host .field:last-child{margin-bottom:0}
.confirm-fields-host small{display:block;color:var(--ink-muted);font-size:12.5px;margin-top:4px}

/* ---- payments feed (admin/_payment_feed.html, dashboard and /admin/payments) */
.feed-table td{vertical-align:top}
.feed-when{white-space:normal}
.feed-table td .pill{white-space:normal;display:inline-block;line-height:1.3}
.feed-table tr.is-quiet td{color:var(--ink-muted)}
.feed-filters .field-row{align-items:end}
.feed-toggles{display:flex;gap:18px;align-items:center;flex-wrap:wrap;margin-top:4px}
.feed-toggles .field-check{display:flex;gap:8px;align-items:center;font-size:14px;margin:0}
.camp-strip{margin-bottom:22px}
.camp-strip .admin-stat b{font-size:28px}
@media (min-width:1024px){
  .feed-table{table-layout:fixed}
  .feed-table th,.feed-table td{padding-left:12px;padding-right:12px}
  .feed-table .col-when{width:17%}
  .feed-table .col-who{width:18%}
  .feed-table .col-what{width:33%}
  .feed-table .col-how{width:11%}
  .feed-table .col-outcome{width:21%}
}

/* ---- donations page and totals (admin/donations.html, donations_totals.html) */
.period-line{margin:18px 0 12px}
.totals-table td.num,.totals-table th.num{text-align:right;white-space:nowrap}
.totals-table tr.is-quiet td{color:var(--ink-muted)}
.totals-table tr.totals-grand th,.totals-table tr.totals-grand td{font-weight:700;border-top:2px solid var(--ink);background:var(--paper)}
.totals-table tr.totals-grand th{text-align:left}
@media (min-width:1024px){
  .donations-table .col-date{width:17%}
  .donations-table .col-who{width:30%}
  .donations-table .col-money{width:13%}
  .donations-table .col-giftaid{width:19%}
  .donations-table .col-status{width:21%}
}

/* ---- the dashboard's Recent payments card: the attention panel's box,
   the stat tiles, the activity table — modifiers only. */
.payments-card{border-left-color:var(--green)}
.payments-card-head{display:flex;justify-content:space-between;align-items:baseline;gap:12px;flex-wrap:wrap;margin-bottom:14px}
.payments-card-head .admin-stats-head{margin:0}
.payments-card-link{color:var(--green);font-weight:700;text-decoration:none;white-space:nowrap}
.payments-row-label{margin:0 0 8px;font-size:12px;color:var(--ink-muted)}
.payments-tiles{grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;margin-bottom:16px}
.payments-tiles .admin-stat{padding:14px 8px}
.payments-tiles .admin-stat b{font-size:24px}
.payments-tiles .admin-stat span{font-size:12.5px}
.payments-tiles .admin-stat em{font-size:12px}
/* Refunds above nought are a warning, not an alarm: amber, not the red
   that means somebody is waiting. Net is the figure that matters. */
.admin-stat-warn b{color:#7A5309}
.admin-stat-strong{border-color:var(--green);background:var(--green-tint)}
@media (max-width:640px){.payments-tiles{grid-template-columns:repeat(2,minmax(0,1fr))}}
.payments-rows{list-style:none;margin:4px 0 0;padding:0;border-top:1px solid var(--line)}
.payments-rows li{
  display:grid;grid-template-columns:15% 22% minmax(0,1fr) 9% auto;
  grid-template-areas:"when who what method amount";
  gap:6px 12px;align-items:center;padding:9px 0;border-bottom:1px solid var(--line);font-size:14px;
}
.payments-rows .when{grid-area:when;color:var(--ink-soft);white-space:nowrap}
.payments-rows .who{grid-area:who;overflow-wrap:anywhere}
.payments-rows .who a{color:var(--green);font-weight:700;text-decoration:none}
.payments-rows .what{grid-area:what;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;color:var(--ink-soft)}
.payments-rows .method{grid-area:method;color:var(--ink-soft);white-space:nowrap}
.payments-rows .amount{grid-area:amount;text-align:right;white-space:nowrap;font-weight:700}
/* A refund is money going out: a minus, in the red used for a loss. */
.payments-rows .amount.is-minus{color:var(--red-ink)}
.payments-rows .pill{white-space:nowrap;font-size:12px;padding:2px 9px}
/* The small Gift Aid marker on a donation that declared it. */
.ga-mark{display:inline-block;font-size:11px;font-weight:700;line-height:1.4;color:var(--green);border:1px solid var(--green);border-radius:999px;padding:0 7px;margin-left:4px;vertical-align:middle}
/* A phone: the name, the money and the badge on the first line, the time
   and the (truncated) description on the second. The name never
   truncates; the description does. */
@media (max-width:640px){
  /* The middle track is the flexible one, and the description spans
     it: an item spanning a flexible track is left out of the auto
     tracks' sizing, which is what lets it be clipped rather than
     widening the row (a span over two auto tracks grew them instead). */
  .payments-rows li{
    grid-template-columns:minmax(0,1fr) auto;
    grid-template-areas:"who amount" "what what" "when method";
    gap:3px 10px;font-size:13.5px;
  }
  .payments-rows .method{text-align:right}
  .payments-rows .when{font-size:12.5px}
  .payments-rows .what{font-size:12.5px}
}
.payments-empty{margin:4px 0 0;color:var(--ink-soft);font-size:15px}
.payments-empty a{color:var(--green);font-weight:700;text-decoration:none}
.payments-donations{margin:-4px 0 16px;font-size:14px}
.payments-donations a{color:var(--green);font-weight:700;text-decoration:none}

/* ---- bookings with several prices (collection_detail, admin bookings) */
.booking-heading{font-size:22px;margin:0 0 10px}
/* One compact block: an item per price, wrapping naturally; the
   concessions one line each with the note smaller on the same line. */
.price-block{display:flex;flex-wrap:wrap;gap:4px 22px;margin:0 0 8px;font-size:15px;line-height:1.5}
.price-item.is-out{color:var(--ink-muted)}
.concession-lines{list-style:none;margin:0 0 16px;padding:0;font-size:14.5px;line-height:1.5}
.concession-lines li{padding:2px 0}
.concession-lines small{color:var(--ink-soft);font-size:13px}
/* A line: price option, type and how many on one row, a light rule
   between lines, and the legend read aloud but not drawn. */
.booking-lines{border-top:1px solid var(--line)}
.booking-line{border:0;border-bottom:1px solid var(--line);border-radius:0;padding:12px 0 10px;margin:0;min-width:0}
.booking-line legend{font-weight:700;font-size:14px;padding:0}
.booking-line .field-qty{max-width:9rem}
.booking-grid{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1.4fr) 6.5rem;gap:10px;align-items:end}
.booking-grid .field{margin-bottom:0;min-width:0}
.booking-names{margin-top:10px}
.booking-names .one-name{margin-top:8px}
.booking-names .one-name label{font-size:13.5px}
.booking-remove{margin-top:8px}
.booking-add{margin:12px 0 4px}
.booking-total{font-weight:700;margin:10px 0 2px}
.booking-warning{color:var(--red-ink);font-weight:700;font-size:14.5px;margin:6px 0 0}
.booking-warning:empty{display:none}
.booking-note{margin:0 0 6px;color:var(--ink-soft)}
.form-section{font-size:18px;margin:26px 0 12px}
.pay-row{display:flex;align-items:center;gap:18px;flex-wrap:wrap}
.pay-total{margin:0;font-size:17px}
@media (max-width:640px){
  .booking-grid{grid-template-columns:minmax(0,1fr) 5.5rem}
  .booking-grid .field:first-child{grid-column:1 / -1}
}
.booked-list{list-style:none;margin:0 0 18px;padding:0}
.booked-list li{padding:6px 0;border-bottom:1px solid var(--line)}
.booked-small{display:block;white-space:normal;color:var(--ink-muted);font-size:12.5px;max-width:14rem}
/* the admin's setup rows */
.booking-setup{display:grid;gap:10px}
.setup-row{display:grid;grid-template-columns:2fr 1fr 1fr 1fr;gap:10px;border:1px solid var(--line);border-radius:10px;padding:10px 12px;margin:0}
.setup-row .field{margin-bottom:0;min-width:0}
.setup-row .field-wide{grid-column:1 / -1}
.setup-row-question{grid-template-columns:3fr 1fr}
.price-grid td,.price-grid th{text-align:right}
.price-grid th:first-child,.price-grid td:first-child{text-align:left}
.booking-stats td.num,.booking-stats th.num,.booking-list td.num,.booking-list th.num{text-align:right;white-space:nowrap}
.booking-group{margin:22px 0 10px;font-size:17px}
.booking-group small{font-weight:400;color:var(--ink-soft)}
@media (max-width:640px){
  .setup-row{grid-template-columns:1fr 1fr}
  .setup-row-question{grid-template-columns:1fr}
  .booking-line .field-qty{max-width:none}
}
