/* brand.css — the Trust's own additions layered after tokens.css and
   theme.css (loaded last in <head>, via layout.html's "head" block, so
   it wins ties on source order). theme.css carries colour/shape as
   design tokens; this file carries the two things tokens.css has no
   slot for — the heading/wordmark typefaces, and the handful of
   one-off marketing-page sections (hero, timeline, causes) that aren't
   part of rastrillo's own vocabulary. Self-hosted fonts, served from
   this app's own static/fonts/ — nothing here fetches off-origin. */

/* Each @font-face src carries its own file's content hash in the
   filename, the identical 16-hex-char scheme rastrillo.Assets uses for
   every other static file (assets.go) — but applied by hand here,
   since @font-face lives in a plain CSS file, never Go-templated, so
   the usual {{asset "..."}} helper has no seam to reach it from. A
   bare, unhashed filename (what this used to be) is served
   Cache-Control: no-cache by that same code — deliberately, so a
   direct deep link to a static file keeps working after its content
   changes — which for a font file loaded via @font-face font-display:
   swap means every full-page navigation revalidates it over the
   network before the real font can paint, and the fallback-to-real
   swap (full weight range 500-800 on Bricolage Grotesque, well past
   what any system fallback renders) becomes visibly, repeatedly
   bold-flashing rather than a one-off flash on first visit — reported
   by Tony as flicker switching between the Cashbook's Current/Savings
   tabs, most visible in Edge, 2026-11-03. The hashed name gets the
   same immutable, year-long Cache-Control every other asset gets,
   because Assets.Handler() matches the hash against the file's own
   current content regardless of how the URL was built — nothing
   server-side had to change for this fix to work.

   If a font file here is ever replaced: recompute its hash
   (sha256sum file.woff2 | cut -c1-16) and update the matching src
   below. Forgetting to isn't a break — Assets.Handler() falls back to
   serving the new content as a no-cache mismatch, the same bare-name
   behaviour this file had before this comment — just loses the
   caching fix silently until the hash here is corrected. */
@font-face {
  font-family: "Newsreader";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/newsreader.6e4f2958c3a7c4a8.woff2") format("woff2");
}
@font-face {
  font-family: "Newsreader";
  font-style: italic;
  font-weight: 400 600;
  font-display: swap;
  src: url("fonts/newsreader-italic.5dfcd10d24af8c82.woff2") format("woff2");
}
@font-face {
  font-family: "Bricolage Grotesque";
  font-style: normal;
  font-weight: 500 800;
  font-display: swap;
  src: url("fonts/bricolage-grotesque.a79fdb52d4a5c765.woff2") format("woff2");
}
@font-face {
  font-family: "Source Sans 3";
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
  src: url("fonts/source-sans-3.7a19a7027e125257.woff2") format("woff2");
}
@font-face {
  font-family: "Source Sans 3";
  font-style: italic;
  font-weight: 400 600;
  font-display: swap;
  src: url("fonts/source-sans-3-italic.9a15dafc2c2b2414.woff2") format("woff2");
}

:root {
  --wft-serif: "Newsreader", "Iowan Old Style", Georgia, serif;
  --wft-logo: "Bricolage Grotesque", "Source Sans 3", system-ui, sans-serif;
  /* The logo's gold, stated directly — distinct from --rst-accent
     (which is the green/gold PAIR that swaps with light/dark mode).
     The original's active-nav underline is always this exact gold,
     regardless of light or dark mode, matching the logo mark. */
  --wft-gold: #d6a21a;
  /* tokens.css's own `html { scrollbar-gutter: var(--rst-scrollbar-gutter) }`
     (default "stable") reserves a scrollbar-width strip at the
     trailing edge on any engine that honours it — which, per that
     token's own comment, now includes Safari 18.2+, i.e. a current
     iPhone. iOS has no classic scrollbar to make room for, so nothing
     ever occupies the strip; it just sits there as bare body
     background past the header/footer's own (different) colour,
     reading as "the header doesn't reach the edge" — reported by Tony,
     2026-10-29. tokens.css's own comment names this exact opt-out.
     Harmless to turn off everywhere: this app's own pages are full-
     bleed bars on a bordered column, not content whose width need
     never shift, so the layout-stability tokens.css is reserving the
     gutter for was never really in play here. */
  --rst-scrollbar-gutter: auto;
}

/* Default link colour/underline, matching the original everywhere
   (not just marketing pages) — rastrillo's base theme leaves <a>
   unstyled otherwise, which renders browser-default blue. */
a {
  color: var(--rst-accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
}
a:hover { color: var(--rst-accent-strong); text-decoration-color: currentColor; }

/* The original has exactly one button size everywhere (48px min
   height, 1.25rem side padding, 1rem text) — no separate "lg" variant
   the way rst-btn's size modifiers imply. rastrillo's own "lg" is
   actually SMALLER than this (14px text, 1.15rem padding), so using it
   anywhere was a mismatch from the start; templates now just use
   rst-btn/rst-btn="primary" and get the original's one size from here.
   The nav's own button is deliberately wider still (a separate, more
   specific rule), by request. */
.wft-marketing [rst-btn] {
  box-sizing: border-box;
  appearance: none;
  min-height: 48px;
  padding: 0.7rem 1.25rem;
  font-size: 1rem;
  line-height: 1.2;
}
/* The original's outline/secondary button (e.g. "Read our story",
   "Start a grant enquiry email") is see-through with green text and
   border — tokens.css's own [rst-btn] base is an opaque card
   (background: var(--rst-surface), ink-coloured text), a filled admin
   look rather than this outline one. Fills solid green with white text
   on hover, same as the original, rather than just shifting its own
   border/text colour the way [rst-btn]'s base :hover rule does. */
.wft-marketing [rst-btn]:not([rst-btn~="primary"]) {
  background: transparent;
  color: var(--rst-accent);
  border-color: var(--rst-accent);
}
.wft-marketing [rst-btn]:not([rst-btn~="primary"]):hover {
  background: var(--rst-accent);
  border-color: var(--rst-accent);
  color: var(--rst-on-accent);
}

/* ---------- marketing typography ----------
   rastrillo's base scale (--rst-fs-base: 14px, no unscoped h1/h2/h3
   size at all — font-size/weight for a heading comes entirely from
   whatever compact admin component it sits in, e.g. .rst-page-header
   h1 at 1.375rem) is built for a dense CRUD dashboard, not body copy.
   The public pages (home, news, apply, grants-info, privacy) opt into
   the original site's actual type scale instead, via a body class, so
   the trustee tool's screens (Requests/Awarded/Reports/grant forms)
   keep rastrillo's compact scale untouched. */
/* Selectors below are deliberately .wft-marketing (one class + one
   element = specificity 0,1,1), NOT body.wft-marketing (0,1,2) — kept
   equal to a plain component selector like .wft-article h2 so that a
   later, more specific-looking override (e.g. a heading's own
   margin-bottom) wins on source order rather than losing outright to
   this base rule regardless of where it's written. */
.wft-marketing {
  font-size: 1.0625rem;
  line-height: 1.6;
}
.wft-marketing h1,
.wft-marketing h2,
.wft-marketing h3 {
  font-family: var(--wft-serif);
  font-weight: 600;
  line-height: 1.12;
  letter-spacing: -0.01em;
  text-wrap: balance;
  margin: 0;
}
.wft-marketing h1 { font-size: clamp(2.4rem, 5.2vw, 4rem); letter-spacing: -0.02em; }
.wft-marketing h2 { font-size: clamp(1.85rem, 3.4vw, 2.6rem); }
.wft-marketing h3 { font-size: 1.4rem; line-height: 1.3; }
.wft-marketing p { margin: 0 0 1rem; }
.wft-marketing p:last-child { margin-bottom: 0; }

.wft-lead {
  font-size: clamp(1.15rem, 1.6vw, 1.3rem);
  line-height: 1.55;
  color: var(--rst-text-muted);
  max-width: 52ch;
}

/* tokens.css's .rst-page/[rst-page] is 64rem/1024px — the admin tool's
   own dashboard measure. The original's actual column is 1160px, which
   the home page's bands already use (.wft-band-inner); this carries
   the same width to every OTHER marketing page's content (News, Apply,
   Grants info, Privacy), scoped to .wft-marketing so the trustee
   tool's screens keep rastrillo's own, narrower column. */
.wft-marketing [rst-page] {
  max-width: 1160px;
  /* Matches a .section's own bottom padding on the original — the
     page's default (admin-dashboard) bottom padding read as tight
     next to the footer on pages that end in ordinary boxed content
     rather than a full-bleed band (Apply, News, Privacy). */
  padding-bottom: clamp(3.5rem, 8vw, 6.5rem);
}
/* rst-page's own bottom padding shows as a sliver of plain page
   background between the last full-bleed band and the footer, on
   pages that end in one (home, grants-info) — zeroed only then, since
   pages that end in ordinary boxed content (apply, news, privacy)
   still need that padding as their gap before the footer. */
.wft-marketing [rst-page]:has(> .wft-band:last-child) { padding-bottom: 0; }

/* rst-box/rst-box-head/rst-callout's default margins (1.5rem above a
   heading, 1rem around a box/callout) are an admin-dashboard rhythm —
   tight enough that News/Apply/Grants info/Privacy's section-to-
   section and box-to-text spacing read as cramped next to the home
   page's generous band padding. Scoped the same way as everything
   else here, so the trustee tool's own screens keep the compact
   rhythm that actually suits a dense CRUD list. */
.wft-marketing [rst-box-head] { margin-top: 3rem; margin-bottom: 1rem; }
/* :first-of-type (not :first-child) — the page-hero before it is a
   <section>, so the first <div rst-box-head> is still the first
   sibling div; :first-child would miss that and double up with the
   page-hero's own bottom margin/border instead. */
.wft-marketing [rst-box-head]:first-of-type { margin-top: 0; }
.wft-marketing [rst-box] { padding: 1.75rem 2rem; margin: 0 0 1rem; }
.wft-marketing [rst-callout] { padding: 1.1rem 1.25rem; margin: 1.5rem 0; }

/* ---------- grants info page ---------- */

/* The divider line sits below the facts cards, not between the lead
   paragraph and the cards — so wft-page-hero's own border is
   suppressed on this one page (:has(+ .wft-facts)) and restated here
   instead, after the cards. */
.wft-page-hero:has(+ .wft-facts) { border-bottom: 0; }
.wft-facts {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1rem;
  margin: 2rem 0 0;
  padding: 0 0 clamp(2rem, 5vw, 3.5rem);
  border-bottom: 1px solid var(--rst-line);
}
@media (max-width: 760px) {
  .wft-facts { grid-template-columns: 1fr; }
}
.wft-facts div { background: var(--rst-surface); border: 1px solid var(--rst-line); border-radius: var(--rst-radius); padding: 1.25rem; }
.wft-facts dt { font-size: 0.9rem; font-weight: 600; color: var(--rst-text-muted); }
.wft-facts dd { margin: 0.35rem 0 0; font-family: var(--wft-serif); font-size: 1.35rem; line-height: 1.25; }

.wft-plain-list { margin: 0 0 1rem; padding-left: 1.2rem; }
.wft-plain-list li { margin: 0.45rem 0; padding-left: 0.25rem; }
.wft-plain-list li::marker { color: var(--rst-accent); }

.wft-numbered { margin: 1.25rem 0 1.5rem; padding-left: 0; list-style: none; counter-reset: item; }
.wft-numbered li { counter-increment: item; position: relative; padding: 0.8rem 0 0.8rem 2.75rem; border-bottom: 1px solid var(--rst-line); }
.wft-numbered li:first-child { border-top: 1px solid var(--rst-line); }
.wft-numbered li::before {
  content: counter(item);
  position: absolute;
  left: 0;
  top: 0.75rem;
  width: 1.8rem;
  height: 1.8rem;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font: 600 0.95rem/1 inherit;
  color: var(--rst-accent);
  background: var(--rst-accent-soft);
}
.wft-numbered strong { display: block; font-weight: 600; }
.wft-numbered span { color: var(--rst-text-muted); font-size: 0.96rem; }

/* ---------- apply page ----------
   The original's form card is capped at 40rem — roughly half the
   1160px column, not the full width — and its field labels are a
   full-strength, bold line, not tokens.css's quiet muted one (right
   for a dense settings panel, not a one-off public form). */
.wft-apply-box { max-width: 40rem; }
/* tokens.css centres a switch's track against its label text by
   default — right for a short one-line label, but this one wraps onto
   2-3 lines at mobile widths, where a vertically-centred track reads
   as floating mid-sentence rather than introducing it. */
.wft-switch-wrap { align-items: flex-start; }
.wft-switch-wrap [rst-switch-track] { margin-top: 0.15rem; }
.wft-apply-box [rst-form] h2 { margin-top: 1rem; }
.wft-apply-box [rst-form] h2:first-child { margin-top: 0; }

/* ---------- address lookup (static/address-lookup.js) ---------- */
.wft-address-lookup { margin-bottom: 0.75rem; }
.wft-address-results {
  margin: 0.35rem 0 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--rst-line);
  border-radius: var(--rst-radius);
  overflow: hidden;
}
.wft-address-results li + li { border-top: 1px solid var(--rst-line); }
.wft-address-results button {
  display: block;
  width: 100%;
  padding: 0.6rem 0.85rem;
  border: 0;
  background: var(--rst-surface);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.wft-address-results button:hover,
.wft-address-results button:focus-visible { background: var(--rst-accent-soft); }
.wft-address-message { padding: 0.6rem 0.85rem; color: var(--rst-text-muted); }

/* ---------- org lookup (static/org-lookup.js) ---------- */
.wft-org-lookup { margin-bottom: 1rem; }
.wft-org-status { margin: 0.5rem 0 0; color: var(--rst-text-muted); }
.wft-org-lookup [data-org-results] { margin-top: 0.5rem; }
.wft-org-lookup [data-org-group="external"] { display: grid; gap: 0.75rem; }
.wft-org-lookup [data-org-results] h3 { margin: 0 0 0.25rem; font-size: 0.9rem; }
.wft-org-results {
  margin: 0;
  padding: 0;
  list-style: none;
  border: 1px solid var(--rst-line);
  border-radius: var(--rst-radius);
  overflow: hidden;
}
.wft-org-results li + li { border-top: 1px solid var(--rst-line); }
.wft-org-results button {
  display: block;
  width: 100%;
  padding: 0.6rem 0.85rem;
  border: 0;
  background: var(--rst-surface);
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}
.wft-org-results button:hover,
.wft-org-results button:focus-visible { background: var(--rst-accent-soft); }
.wft-org-message { margin: 0.35rem 0 0; color: var(--rst-text-muted); }
.wft-marketing [rst-field-label] {
  font-weight: 600;
  font-size: 1rem;
  color: var(--rst-text);
}
/* tokens.css's hint/help/error text is --rst-fs-xs (11.5px) everywhere,
   public site and trustee tool alike. Unscoped (not just .wft-marketing,
   as an earlier pass had it): an error message is save-blocking text,
   not a secondary note, and the trustee tool's own "Enter the amount
   awarded..."/"Enter the date paid..." messages need to be at least as
   legible as the data they're guarding, not smaller. Matches body size
   rather than splitting the difference. */
small[rst-field-hint],
[rst-field-help],
[rst-field-error] {
  font-size: var(--rst-fs-base);
}

/* ---------- privacy page: prose ----------
   Matches the original: Privacy and Cookies side by side in a two-
   column grid, with Contact about privacy on its own below. */
.wft-prose-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(2rem, 6vw, 5rem);
  align-items: start;
  margin-bottom: clamp(2rem, 6vw, 5rem);
}
@media (max-width: 760px) {
  .wft-prose-grid { grid-template-columns: 1fr; }
}
.wft-prose { max-width: 68ch; }
.wft-prose h2 { font-size: clamp(1.6rem, 3vw, 2.1rem); margin-bottom: 1rem; }
.wft-prose h3 { font-size: 1.2rem; margin: 1.75rem 0 0.5rem; }
.wft-prose p { color: var(--rst-text-muted); }
/* Grants info reuses .wft-prose-grid for its two-column layout but
   without the .wft-prose class on each column (no 68ch measure wanted
   there) — still needs the same gap under each column's own h2. */
.wft-prose-grid h2 { margin-bottom: 1rem; }
.wft-prose-grid h3 { margin-top: 2rem; margin-bottom: 0.5rem; }

/* ---------- marketing pages: hero page header ----------
   The large serif title + lead paragraph used by News, Apply, Grants
   info and Privacy — matching the original's .page-hero, not
   rastrillo's compact page-header partial (kept for the trustee
   tool's own admin screens). */
.wft-page-hero {
  padding-block: clamp(2rem, 5vw, 3.5rem) clamp(1.5rem, 3vw, 2rem);
  border-bottom: 1px solid var(--rst-line);
  margin-bottom: 2rem;
}
.wft-page-hero h1 { margin-bottom: 1rem; }

/* Scoped dark mode: the same toggle theme.css gives :root, but on any
   element. A section carrying data-theme="dark" gets the dark half of
   every light-dark() token inside it, regardless of the page's own
   scheme — this is how the homepage's closing band gets the Trust's
   forest-green/gold "dark band" look as an ordinary rst-box, with no
   separate colours to maintain. */
/* color-scheme alone only changes how light-dark() resolves for a
   property declared fresh within this subtree — it does not re-resolve
   an already-computed `color` a child merely inherited from outside
   the scope. So this rule also restates color directly, which (being
   declared here, inside the new color-scheme) picks up the dark half
   and then inherits correctly to every descendant that doesn't set its
   own. */
[data-theme="dark"] { color-scheme: dark; color: var(--rst-text); }
[data-theme="light"] { color-scheme: light; color: var(--rst-text); }

/* ---------- brand mark (header + footer) ---------- */

.wft-brand {
  display: flex;
  align-items: center;
  gap: 0.6rem;
  text-decoration: none;
  color: var(--rst-text);
}
.wft-brand img { width: 52px; height: 39px; }
.wft-brand-name {
  font-family: var(--wft-logo);
  font-weight: 800;
  font-size: 1.3rem;
  line-height: 1;
  letter-spacing: -0.02em;
  display: flex;
  flex-direction: column;
}
.wft-brand-name small {
  margin-top: 0.3rem;
  font-family: var(--wft-logo);
  font-weight: 500;
  font-size: 0.75rem;
  line-height: 1;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--rst-accent);
}
/* Below ~430px the full lockup (mark + "The Wood Family" + "Charitable
   Trust") plus the Menu button no longer fit one row, so Menu wrapped
   to a row of its own under empty space — costing a second row of
   chrome, on every page, before any content. Dropping the subtitle and
   shrinking the mark/wordmark is enough to keep both on one row. */
@media (max-width: 430px) {
  .wft-brand-name small { display: none; }
  .wft-brand-name { font-size: 1.05rem; }
  .wft-brand img { width: 40px; height: 30px; }
}

/* ---------- header / nav (shell overrides) ----------
   rastrillo's stock topbar is a small, muted-text nav bar with no
   sticky/blur treatment and a bordered account menu. The original
   site's header is a sticky, blurred bar with full-weight ink-coloured
   nav links and a solid green "Apply for a grant" button — matched
   here rather than left at the framework default. */

/* [rst-shell-topbar] is layout.html's one full-page wrapper — header,
   main and footer all live inside it, as plain block children, so the
   footer's own position was wherever the page's content happened to
   end. On every short page (sign-in, delete-confirm, Security, a 404,
   a nearly-empty Awarded) that left a bare strip of page background
   below it rather than the footer actually closing the page. A flex
   column at least viewport-tall, with the footer pushed down by its
   own margin-top: auto, is the standard fix — and costs main nothing,
   since a block child already fills a flex column's cross axis and
   margin-top: auto consumes whatever space is left over before it. */
/* Restores the margin [rst-box] used to contribute before it was
   removed as a redundant outer box around [rst-stats] on Cashbook and
   Reports (rst-stats already draws its own complete box) — [rst-stats]
   itself carries no margin of its own. */
[rst-stats] { margin: var(--rst-sp-4) 0; }

[rst-shell-topbar] {
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}
[rst-shell-foot] {
  margin-top: auto;
}

[rst-shell-bar] {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--rst-bg);
  padding-inline: 0;
}

/* Aligns the header's content to the original site's own 1160px
   column (not tokens.css's .rst-page, which is 64rem/1024px — a
   purely admin-dashboard measure that wraps text noticeably sooner
   than the original ever did, e.g. the homepage timeline's entries).
   [rst-shell-bar] itself stays full-width for its background. */
.wft-bar-inner {
  max-width: 1160px;
  margin-inline: auto;
  width: 100%;
  box-sizing: border-box;
  padding-inline: var(--rst-sp-4);
  min-block-size: 72px;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--rst-sp-4);
}

/* The original's nav sits hard right, after the brand — tokens.css
   instead reserves that position for the account menu. Moving the
   auto margin to nav pushes nav (and everything after it in the row)
   right as a cluster, with account trailing further right still. */
[rst-shell-nav] { margin-inline-start: auto; }
[rst-shell-account] { margin-inline-start: 0; }

[rst-shell-nav] a:not([rst-btn]) {
  font-weight: 600;
  font-size: 0.98rem;
  line-height: 1;
  color: var(--rst-text);
  border-block-end: 0;
  border-radius: 6px;
  padding: 0.6rem 0.75rem;
}
[rst-shell-nav] a:not([rst-btn]):hover {
  background: color-mix(in oklab, currentColor 12%, transparent);
  color: var(--rst-text);
}
[rst-shell-nav] a[aria-current]:not([rst-btn]) {
  font-weight: 600;
  text-decoration: underline;
  text-decoration-color: var(--wft-gold);
  text-decoration-thickness: 3px;
  text-underline-offset: 0.45em;
}
/* nav and its children were stretching to the header bar's own
   73px height (nav's cross-axis align-items resolves to "stretch" by
   default, and its align-self against .wft-bar-inner wasn't pinned
   either), rather than sizing to content and centering — which is why
   the button's green fill spilled past the header's own bottom edge
   into the section below. align-self:center on both fixes the actual
   cause; the button's own height then comes from its padding alone. */
[rst-shell-nav] { align-self: center; }
[rst-shell-nav] a { align-self: center; }
/* tokens.css's own wide-viewport rule for nav children
   ([rst-shell-topbar] [rst-shell-tail] > [rst-shell-nav] a {
   padding-inline: 0 }) is MORE specific (0,3,1) than a plain
   [rst-shell-nav] a[rst-btn] (0,2,1) — it was winning outright and
   zeroing the button's horizontal padding regardless of what this
   rule declared, which is why widening it kept silently not working.
   Matched here at equal structure (0,4,1) to actually beat it, and
   min-height/align-self restated for the same reason .wft-marketing
   [rst-btn]'s own min-height:48px (needed generally, for buttons in
   running text) was overriding this one's — on a flex row the
   general rule's min-height doesn't match THIS row's natural height,
   which is why the stretch-past-the-header bug kept coming back. */
[rst-shell-topbar] [rst-shell-tail] > [rst-shell-nav] a[rst-btn] {
  padding-inline: 1.25rem;
}
/* Same collision, same fix, for the plain links (Our story, What we
   support, News, Contact) — tokens.css's padding-inline: 0 targets
   every <a> in the nav, not just the one with [rst-btn]. */
[rst-shell-topbar] [rst-shell-tail] > [rst-shell-nav] a:not([rst-btn]) {
  padding-inline: 0.75rem;
}
[rst-shell-nav] a[rst-btn] {
  align-self: center;
  margin-inline-start: 0.5rem;
  min-height: 0;
  padding-block: 0.7rem;
  font-size: 1rem;
  line-height: 1.2;
}
/* tokens.css's own [rst-shell-nav] a (0,1,1) is more specific than its
   own [rst-btn~="primary"] (0,1,0), so without this the button's white
   text always lost to the shell nav's muted grey — found by checking
   getComputedStyle directly, not by inspecting source alone. */
[rst-shell-nav] a[rst-btn~="primary"],
[rst-shell-nav] a[rst-btn~="primary"]:hover { color: var(--rst-on-accent); }
[rst-shell-nav] a[rst-btn][aria-current] { box-shadow: inset 0 -3px 0 var(--wft-gold); }

[rst-shell-account] summary {
  font-weight: 600;
  font-size: 0.95rem;
  color: var(--rst-text);
}

/* ---------- footer (shell override) ----------
   Matches the original's dark green footer exactly: brand + address on
   the left, a link list on the right, a copyright line underneath. The
   green is the brand's own green-deep, stated directly rather than via
   the generic dark-mode --rst-bg token (deliberately darker, for a
   whole page in dark mode) — the footer keeps this exact shade
   regardless of the visitor's light/dark choice elsewhere on the page. */

[rst-shell-foot] {
  background: #1f3a18;
  color: #cfd6c6;
  text-align: left;
  padding-block: 2.5rem 1.75rem;
  font-size: 0.95rem;
}
[rst-shell-foot] a { color: #f4efe3; text-decoration-color: rgba(244, 239, 227, 0.45); }
[rst-shell-foot] a:hover { text-decoration-color: currentColor; }
/* Signed in, the footer-grid (brand/address/links) is gone entirely —
   without this, its padding/margin stayed reserved anyway, showing as
   a band of empty green with a stray border-top crossing it and
   nothing above or below. */
[rst-shell-foot]:has(.wft-footer--compact) { padding-block: 1.25rem; }
.wft-footer--compact .wft-footer-bottom { margin-top: 0; padding-top: 0; border-top: 0; }

.wft-footer {
  max-width: 1160px;
  margin-inline: auto;
  width: 100%;
  box-sizing: border-box;
  padding-inline: var(--rst-sp-4);
}
.wft-footer-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 2rem;
}
.wft-footer-brand {
  display: flex;
  align-items: flex-start;
  gap: 1rem;
  max-width: 40rem;
}
.wft-footer-brand img { width: 64px; height: 48px; flex: none; }
.wft-footer-brand strong {
  display: block;
  font-family: var(--wft-logo);
  font-size: 1.2rem;
  font-weight: 800;
  letter-spacing: -0.01em;
  color: #fff;
  margin-bottom: 0.4rem;
}
.wft-footer-links { list-style: none; margin: 0; padding: 0; display: grid; gap: 0; }
.wft-footer-links a,
.wft-footer-links button {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0.2rem 0;
  font: inherit;
  color: #f4efe3;
  background: none;
  border: 0;
  text-align: left;
  text-decoration: underline;
  text-decoration-color: rgba(244, 239, 227, 0.45);
  text-decoration-thickness: 1px;
  text-underline-offset: 0.18em;
  cursor: pointer;
  box-sizing: border-box;
}
.wft-footer-links a:hover,
.wft-footer-links button:hover { text-decoration-color: currentColor; }
.wft-footer-bottom {
  margin-top: 1.5rem;
  padding-top: 1rem;
  border-top: 1px solid rgba(244, 239, 227, 0.18);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}
.wft-footer-bottom p { margin: 0; }
.wft-version { font-size: 0.75rem; color: rgba(244, 239, 227, 0.6); margin-top: 0.2rem; }

.wft-theme-switch {
  display: inline-flex;
  gap: 0.15rem;
  padding: 0.2rem;
  border-radius: var(--rst-radius-pill);
  background: rgba(255, 255, 255, 0.08);
}
/* Same fight the pagination [hidden] rule below already won once: an
   author `display` rule beats the browser's own `[hidden]{display:none}`,
   so chrome.html's `hidden` attribute on this element did nothing on
   its own — the three buttons rendered dead but visible with JS off. */
.wft-theme-switch[hidden] { display: none; }
.wft-theme-switch button {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font: inherit;
  font-size: 0.85rem;
  font-weight: 600;
  color: #cfd6c6;
  background: none;
  border: 0;
  border-radius: var(--rst-radius-pill);
  padding: 0.4rem 0.9rem;
  cursor: pointer;
  box-sizing: border-box;
}
.wft-theme-switch button[aria-pressed="true"] { background: #fff; color: #1f3a18; }

/* tokens.css reserves a scrollbar gutter on <html> precisely so a
   scrollbar appearing/disappearing never shifts the layout sideways —
   but the CSS `vw` unit is defined against the full viewport including
   any scrollbar regardless of that reservation, so .wft-band/
   .wft-hero-band's `width: 100vw` below can still run a scrollbar's
   width wider than the page's own content box on a classic (non-
   overlay) scrollbar — a permanent few-pixel horizontal scrollbar on
   most Windows/Linux desktop browsers. Clipping it here is a narrower
   fix than reworking the full-bleed technique itself, and can't affect
   this app's own internal horizontal scrollers (.wft-table-scroll,
   .wft-admin-tabs further down), which manage their own overflow
   independently of the document's. */
html { overflow-x: clip; }

/* ---------- homepage: full-bleed section bands ----------
   The original's home page is a stack of full-viewport-width bands,
   alternating paper/beige/dark-green, with the original's own 1160px
   column of content centred inside each (NOT tokens.css's .rst-page,
   which is 64rem/1024px — a narrower, admin-dashboard measure that
   wraps text sooner than the original did, e.g. an extra line in the
   homepage timeline) — not a stack of bordered "cards" (rst-box's own
   look, right for the admin tool's dashboard screens but not what the
   original marketing page does). .wft-band breaks out of .rst-page's
   own centred, padded box (the usual trick: 100vw + negative margins
   pulled from the viewport, not the parent) so the colour spans edge
   to edge while .wft-band-inner re-centres the actual content to the
   same column the header and footer already use.

   Hero gets its own wrapper (.wft-hero-band, no .wft-band class) with
   different padding and deliberately NOT matched by the `+` selector
   below — the original's hero is a plain .hero, not a .section, so the
   first true .section (Our story) that follows it has no top border,
   exactly as here. */

.wft-band-inner {
  max-width: 1160px;
  margin-inline: auto;
  padding-inline: var(--rst-sp-4);
  box-sizing: border-box;
}
.wft-hero-band {
  /* Needs the same full-bleed treatment as .wft-band below — without
     it, .rst-page's own padding/centring left the hero's text ~84px
     further right than every other section's, out of line with the
     header and footer column despite sharing the same background
     colour (which is why it wasn't visible without measuring).

     100dvw, not 100vw: on iOS Safari/Chrome (same WebKit engine),
     `vw` is defined against the layout viewport, which is wider than
     the real, visible one — unlike the desktop classic-scrollbar case
     html { overflow-x: clip } above exists for, there is no scrollbar
     to blame here, iOS simply does not keep the two in step the way
     `dvw` (the dynamic viewport unit, written for exactly this) does.
     With plain 100vw the band rendered genuinely wider than the
     screen; `overflow-x: clip` hid the resulting horizontal scroll,
     but with no `initial-scale` set the browser was instead silently
     zooming the WHOLE page out to make it fit, which is what the
     header/footer's own "doesn't reach the edge" bug actually was —
     fixed in the markup (layout.html's initial-scale=1) once this
     stopped being the thing forcing that zoom (Tony, iPhone 16 Pro
     Max, 2026-10-29). */
  width: 100dvw;
  margin-left: calc(-50dvw + 50%);
  margin-right: calc(-50dvw + 50%);
  padding-block: clamp(2.5rem, 6vw, 5rem) clamp(3rem, 7vw, 5.5rem);
}
.wft-band {
  /* 100dvw — see .wft-hero-band's own comment just above. */
  width: 100dvw;
  /* The correct full-bleed formula: "50%" in a margin resolves against
     the containing block's content width, which already nets out
     .rst-page's own side padding and centring — the position:relative
     + left/right:50% variant this replaced does NOT (it offsets
     against the band's own 100dvw width instead), and measured 84px
     out of alignment with the header/footer column as a result. */
  margin-left: calc(-50dvw + 50%);
  margin-right: calc(-50dvw + 50%);
  padding-block: clamp(3.5rem, 8vw, 6.5rem);
}
.wft-band + .wft-band { border-top: 1px solid var(--rst-line); }
.wft-band--alt { background: var(--rst-surface-2); }
.wft-band--dark {
  background: #1f3a18;
  color: #f4efe3;
}
.wft-band--dark .wft-lead,
.wft-band--dark p { color: #cfd6c6; }
.wft-band--dark h2 { color: #fff; }
.wft-band--dark a:not([rst-btn]) { color: #e8bd4a; }
/* Needed as its own rule: .wft-band--dark's fixed gold (0,2,0) is more
   specific than the generic a:hover (0,1,1), so without this the
   generic hover colour change never got a chance to apply here at
   all — hovering these links did visibly nothing.

   Lightens rather than darkens: this is a light-on-dark pairing
   (gold text on forest green), so darkening toward black on hover —
   the usual move for a dark-on-light link — instead dropped it to
   about 3.3:1 against the band, under the 4.5:1 text floor. #f2cd66
   is a lighter gold (~9.8:1 here) that stays legible on hover.
   Hardcoded hex, not a --rst-accent-strong var(): .wft-band--dark
   never sets data-theme="dark"/color-scheme locally (every colour on
   this band is a fixed hex for exactly that reason), so a light-dark()
   token here would resolve to its LIGHT branch — #1f3a18, the band's
   own near-invisible background colour — while the page itself is in
   light mode. */
.wft-band--dark a:not([rst-btn]):hover {
  color: #f2cd66;
  text-decoration: underline;
  text-decoration-color: currentColor;
}
.wft-band--dark h3 { color: #fff; }
/* tokens.css's own :focus-visible (scoped to .rst-page via :where())
   rings in var(--rst-accent) — this band sits inside .rst-page but,
   like its own link colour above, never sets data-theme="dark", so in
   light mode that ring resolves to the light accent (a dim green,
   ~1.55:1 here) against this band's permanently-dark background: a
   real sub-3:1 WCAG 2.4.11 failure, not just a taste mismatch. Same
   fixed gold as .wft-btn-on-dark, for the same reason var() can't be
   used here. Specificity beats the :where() default without needing
   !important. */
.wft-band--dark :focus-visible {
  outline: 2px solid #e8bd4a;
  outline-offset: 2px;
}

.wft-steps {
  list-style: none;
  margin: 2.5rem 0 0;
  padding: 0;
  counter-reset: step;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 1.5rem;
}
.wft-steps li {
  counter-increment: step;
  padding-top: 1.25rem;
  border-top: 1px solid rgba(244, 239, 227, 0.25);
}
.wft-steps li::before {
  content: counter(step);
  display: block;
  font-family: var(--wft-serif);
  font-size: 2.4rem;
  line-height: 1;
  color: #e8bd4a;
  margin-bottom: 0.75rem;
}
.wft-steps h3 { font-size: 1.25rem; margin-bottom: 0.4rem; }
.wft-steps p { font-size: 0.98rem; }
@media (max-width: 760px) {
  .wft-steps { grid-template-columns: 1fr; gap: 0; }
  .wft-steps li { padding: 1.25rem 0; }
}

.wft-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 1.25rem; margin-top: 2.5rem; }
/* The hero uses this on a <p> (original: 1.75rem there vs 2.5rem for
   the steps-list actions, a <div>) — needs the p. prefix to beat
   .wft-marketing p's margin-top:0, the same recurring specificity gap
   as the other p.wft-* fixes above. */
p.wft-actions { margin-top: 1.75rem; }
/* The original's button ON the dark band is gold, not green — a
   green button on a dark green background loses contrast. Stated
   directly (not via the light/dark-swapping --rst-accent token) so it
   stays gold regardless of the visitor's own light/dark choice,
   matching the original's fixed, non-toggle-aware design. */
.wft-btn-on-dark,
.wft-btn-on-dark:hover {
  background: #e8bd4a;
  border-color: #e8bd4a;
  color: #1f3a18;
}
.wft-btn-on-dark:hover { background: #f2cd66; border-color: #f2cd66; }

.wft-section-head { max-width: 62ch; margin-bottom: 2.5rem; }
.wft-section-head h2 { margin-bottom: 1rem; }

/* ---------- homepage: hero ---------- */

.wft-hero {
  display: grid;
  gap: 2rem;
  align-items: center;
  grid-template-columns: 1fr;
}
/* Both grid items, not just .wft-hero-photo below: the text side's
   own automatic minimum width is ordinarily small (a grid item's
   min-content floor for wrapping prose is just its longest unbroken
   word), but the custom display faces (Newsreader/Bricolage
   Grotesque/Source Sans 3, @font-face above) swap in after first
   paint (font-display: swap) and can need more width than the
   fallback system font did — a second, later source of exactly the
   same "grid item won't shrink past its content's own size" overflow
   .wft-hero-photo's own comment describes, this time arriving after
   load rather than being there from the first frame, which is why it
   read as the page "immediately scrolling right" rather than simply
   being wrong from the start (Tony, 2026-10-29). */
.wft-hero > * { min-inline-size: 0; }
@media (min-width: 860px) {
  .wft-hero { grid-template-columns: 1.1fr 1fr; }
}
.wft-hero h1 { margin-bottom: 1.25rem; }
.wft-hero h1 em { font-style: italic; font-weight: 500; color: var(--rst-accent); }
/* min-inline-size: 0 — .wft-hero-photo is a direct item of .wft-hero's
   own grid, and a grid item's automatic minimum width defaults to its
   CONTENT's own natural size, not 0: for a replaced element like the
   <img> inside, that is the image's full intrinsic size (the width/
   height attributes on the tag, or the file's own dimensions),
   regardless of the img's own `width: 100%` below. Browsers differ on
   how strictly they enforce that floor — Chromium let it shrink past
   it, real Safari/Chrome for iOS (same WebKit engine) did not, which
   is why no desktop/emulated test here ever caught the photo forcing
   the whole page wider than the screen on a real iPhone (Tony,
   2026-10-29). This is the standard fix: tell the grid item it is
   allowed to go narrower than its content's own natural size. */
.wft-hero-photo { margin: 0; min-inline-size: 0; }
.wft-hero-photo img {
  width: 100%;
  height: auto;
  display: block;
  border-radius: var(--rst-radius);
  box-shadow: var(--rst-shadow-lift);
}
.wft-hero-photo figcaption {
  margin-top: 0.75rem;
  font-size: 0.85rem;
  color: var(--rst-text-muted);
}
p.wft-hero-meta {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.2rem;
  margin-top: 1.25rem;
  font-size: 0.9rem;
  color: var(--rst-text-muted);
}

/* ---------- homepage: story + timeline ----------
   Matches the original site's two-column layout: the story text in a
   wider left column, the timeline in a narrower column alongside it
   (not stacked below) — collapsing to one column on narrow screens. */

.wft-story-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr);
  gap: clamp(2rem, 6vw, 5.5rem);
  align-items: start;
}
.wft-story-text h2 { margin-bottom: 1.25rem; }
.wft-story-text p { max-width: 64ch; }
p.wft-family {
  margin-top: 1.75rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--rst-line);
  font-size: 0.98rem;
  color: var(--rst-text-muted);
}
.wft-family strong { color: var(--rst-text); font-weight: 600; }

@media (max-width: 960px) {
  .wft-story-grid { grid-template-columns: 1fr; }
}

/* Matches the original site's timeline exactly: a vertical gold-to-
   green gradient line with a small ringed circle at each entry, the
   year in large serif green type to its left. */

.wft-timeline { list-style: none; margin: 0; padding: 0; position: relative; }
.wft-timeline::before {
  content: "";
  position: absolute;
  left: 4.6rem;
  top: 0.6rem;
  bottom: 0.6rem;
  width: 2px;
  background: linear-gradient(var(--rst-accent-soft), var(--rst-accent));
}
.wft-timeline li {
  display: grid;
  grid-template-columns: 4.6rem 1fr;
  gap: 0 1.6rem;
  padding: 0 0 1.9rem;
  position: relative;
}
.wft-timeline li:last-child { padding-bottom: 0; }
.wft-timeline li::after {
  content: "";
  position: absolute;
  left: calc(4.6rem - 5px);
  top: 0.55rem;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--rst-bg);
  border: 2px solid var(--rst-accent);
}
.wft-timeline .wft-year {
  font-family: var(--wft-serif);
  font-size: 1.5rem;
  font-weight: 600;
  line-height: 1.1;
  color: var(--rst-accent);
  font-variant-numeric: lining-nums;
}
.wft-timeline h3 { font-size: 1.15rem; margin: 0.1rem 0 0.3rem; }
.wft-timeline p { font-size: 0.98rem; margin: 0; color: var(--rst-text-muted); }
.wft-timeline p a { white-space: nowrap; }

@media (max-width: 640px) {
  .wft-timeline::before { left: 3.9rem; }
  .wft-timeline li { grid-template-columns: 3.9rem 1fr; gap: 0 1.3rem; }
  .wft-timeline li::after { left: calc(3.9rem - 5px); }
  .wft-timeline .wft-year { font-size: 1.25rem; }
}

/* ---------- news / article pages ----------
   Matches the original site's article + photo + date treatment. */

.wft-article + .wft-article {
  margin-top: clamp(3rem, 7vw, 5rem);
  padding-top: clamp(2.5rem, 6vw, 4rem);
  border-top: 1px solid var(--rst-line);
}
.wft-article { max-width: 68ch; }
.wft-article h2 { font-size: clamp(1.6rem, 3vw, 2.2rem); margin-bottom: 1.25rem; }
.wft-date {
  font-size: 0.9rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--rst-text-muted);
  margin-bottom: 0.5rem;
}
.wft-fineprint { color: var(--rst-text-muted); font-size: 0.92rem; }
.wft-actions + .wft-fineprint { margin-top: 1rem; }
.wft-photo { margin: 2rem 0; }
.wft-photo img {
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 2;
  object-fit: cover;
  border-radius: var(--rst-radius);
  box-shadow: var(--rst-shadow-lift);
}
.wft-photo.wft-natural img { aspect-ratio: auto; }
.wft-photo.wft-portrait { max-width: 420px; }
.wft-photo figcaption {
  margin-top: 0.75rem;
  font-size: 0.92rem;
  color: var(--rst-text-muted);
  max-width: 60ch;
}

/* ---------- homepage: causes grid ---------- */

.wft-causes {
  display: grid;
  gap: 0 clamp(2rem, 5vw, 4rem);
  grid-template-columns: 1fr;
  border-top: 1px solid var(--rst-line);
}
@media (min-width: 640px) {
  .wft-causes { grid-template-columns: repeat(2, 1fr); }
}
.wft-cause { padding: 1.9rem 0; border-bottom: 1px solid var(--rst-line); }
.wft-cause h3 { margin-bottom: 0.6rem; }
.wft-cause p { color: var(--rst-text-muted); margin: 0 0 0.6rem; max-width: 52ch; }
.wft-cause .wft-example { font-size: 0.92rem; color: var(--rst-text); }
.wft-cause .wft-example span { color: var(--rst-text-muted); }

/* p.wft-support-note (not just .wft-support-note): needs to tie
   .wft-marketing p's specificity (0,1,1), which otherwise wins outright
   — its margin:0 0 1rem shorthand sets margin-top:0 as a real longhand
   declaration, at specificity .wft-support-note's margin-top alone
   can't beat, zeroing the gap above this note regardless of source
   order. */
p.wft-support-note {
  margin-top: 2rem;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem 2rem;
  color: var(--rst-text-muted);
  font-size: 1rem;
}

/* ---------- homepage: how we give ---------- */

.wft-give-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(2rem, 6vw, 5rem);
}
.wft-give-grid h3 { margin-bottom: 1rem; }
@media (max-width: 960px) {
  .wft-give-grid { grid-template-columns: 1fr; }
}
.wft-checks { list-style: none; margin: 0 0 1.25rem; padding: 0; }
.wft-checks li { position: relative; padding: 0.55rem 0 0.55rem 2rem; border-bottom: 1px solid var(--rst-line); }
.wft-checks li::before {
  content: "";
  position: absolute;
  left: 0.15rem;
  top: 1.05rem;
  width: 0.8rem;
  height: 0.45rem;
  border-left: 2px solid var(--rst-accent);
  border-bottom: 2px solid var(--rst-accent);
  transform: rotate(-45deg);
}
p.wft-aside-note {
  margin-top: 1.5rem;
  padding: 1.1rem 1.25rem;
  background: var(--rst-surface);
  border: 1px solid var(--rst-line);
  border-radius: var(--rst-radius);
}
.wft-aside-note + .wft-aside-note { margin-top: 1rem; }

/* ---------- homepage: latest news teaser ---------- */

.wft-news-teaser {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr);
  gap: clamp(1.5rem, 5vw, 4rem);
  align-items: start;
}
@media (max-width: 960px) {
  .wft-news-teaser { grid-template-columns: 1fr; }
}
.wft-news-teaser h3 { font-size: clamp(1.4rem, 2.4vw, 1.8rem); margin-bottom: 0.8rem; }
.wft-muted { color: var(--rst-text-muted); }

/* ---------- homepage: contact ---------- */

.wft-contact-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr);
  gap: clamp(2rem, 6vw, 5rem);
  align-items: start;
}
@media (max-width: 960px) {
  .wft-contact-grid { grid-template-columns: 1fr; }
}
.wft-contact-list { margin: 1.75rem 0 0; padding: 0; }
.wft-contact-list dt { font-size: 0.9rem; font-weight: 600; color: var(--rst-text-muted); margin-top: 1.1rem; }
.wft-contact-list dt:first-child { margin-top: 0; }
.wft-contact-list dd { margin: 0.15rem 0 0; font-size: 1.1rem; }

.wft-contact-form h3 { margin-bottom: 0.35rem; }
.wft-contact-form > p:not(.wft-fineprint) { color: var(--rst-text-muted); }
.wft-field { margin-top: 1.1rem; }
.wft-field label { display: block; font-weight: 600; margin-bottom: 0.35rem; }
.wft-field input,
.wft-field textarea {
  width: 100%;
  box-sizing: border-box;
  font: inherit;
  color: inherit;
  background: var(--rst-surface);
  border: 1px solid var(--rst-line-strong);
  border-radius: var(--rst-radius-sm);
  padding: 0.7rem 0.85rem;
  min-height: 48px;
}
.wft-field textarea { min-height: 150px; resize: vertical; }
/* The one placeholder in the app (Requests' search field) was left at
   the browser's own UA default grey, which measured 3.25:1 in dark
   mode — under the 4.5:1 floor. --rst-text-muted is the same token
   every other piece of muted text already uses, measured well clear
   of that floor in both themes. */
[rst-input]::placeholder { color: var(--rst-text-muted); opacity: 1; }
/* Neither tokens.css nor this file styled [aria-invalid="true"] at
   all — the attribute was correctly set server-side on every rejected
   field (field-text.html's own partial does it whenever Error is
   passed), but nothing painted it, so "please fix the highlighted
   fields below" highlighted nothing. Matches the error text's own
   colour token, so the field and the message under it read as one
   signal. */
[rst-input][aria-invalid="true"],
[rst-textarea][aria-invalid="true"],
.wft-field input[aria-invalid="true"],
.wft-field textarea[aria-invalid="true"] {
  border-color: var(--rst-tone-negative-fg);
}
[rst-input][aria-invalid="true"]:focus-visible,
[rst-textarea][aria-invalid="true"]:focus-visible {
  outline-color: var(--rst-tone-negative-fg);
}
.wft-field input:focus-visible,
.wft-field textarea:focus-visible {
  outline: 3px solid var(--rst-accent);
  outline-offset: 1px;
  border-color: var(--rst-accent);
}

/* tokens.css's own :focus-visible rule is scoped to .rst-page and a
   handful of component roots (deliberately — :where() so an app rule
   can override it), which leaves the header bar and the footer — both
   outside .rst-page — on the browser's own default ring. Extending the
   same treatment here rather than editing the vendored tokens.css
   file directly. */
:where([rst-shell-topbar], [rst-shell-foot]) :focus-visible {
  outline: 2px solid var(--rst-accent);
  outline-offset: 2px;
}

/* tokens.css's mobile nav-disclosure summary is min-block-size: 24px
   (WCAG 2.2 AA's own floor) — bumped here, not in the vendored file,
   since this is the one nav control nearly every mobile visit taps. */
[rst-shell-topbar] [rst-shell-menu] > summary {
  min-block-size: 44px;
  box-sizing: border-box;
}

/* ---------- admin area: cross-nav tabs ----------
   Requests/Awarded/Reports used to live in the header nav alongside
   the marketing links; moved to a page-level tab row (the header
   showed only account info when signed in); tried a second row of the
   header (2026-10-24, one day); back in [rst-shell-nav] alongside the
   brand as of the same day — that second-row version kept the tabs
   permanently visible (sticky, like this one), but read as "still
   part of the logo block" rather than a change from it, which wasn't
   the point; this is the original position the very first entry in
   this comment describes, not a new one. [rst-shell-nav] a's own
   rules (above, in the header section) already cover both the
   marketing links and these — current-page underline, hover state —
   so nothing admin-specific is needed for the nav itself, only the
   badge below and Gift Aid's own Claims/Donors sub-nav, which still
   uses rastrillo's segmented-tabs component restyled flat (plain
   links, the active tab getting a tinted background rather than a
   filled pill) to match the original admin tool's own tab look, not
   rst-seg-tabs's default pill-on-tinted-track style. */
/* .wft-flat-tabs: the shared look for every segmented-tab row in the
   tool — Gift Aid's own Claims/Donors switcher AND Cashbook's own
   Current/Savings/Investment one, which used to keep rst-seg-tabs's
   own default (a white pill floating on a tinted track) while Gift
   Aid's got this flat treatment instead. Two tab rows with opposite
   active-state polarities on the same screen was the inconsistency;
   one ruleset, applied to both navs, is the fix. */
.wft-flat-tabs[rst-seg-tabs] {
  background: none;
  border-radius: 0;
  gap: 0.25rem;
  padding: 0;
  margin: 0;
  border-bottom: 0;
}
.wft-flat-tabs[rst-seg-tabs] a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0.6rem 1rem;
  border-radius: 6px;
  color: var(--rst-text-muted);
  font-weight: 600;
  box-sizing: border-box;
}
.wft-flat-tabs[rst-seg-tabs] a:hover { color: var(--rst-text); background: none; }
/* Was an inset box-shadow ring — the same shape, width and colour as
   :focus-visible's own outline inside .rst-page (tokens.css), so
   tabbing onto the current tab showed what looked like one ring with
   no way to tell "this is where you are" from "this is what you just
   focused." A filled background reads as "current location" without
   borrowing the focus ring's own visual vocabulary, so the two stay
   distinct when they land on the same tab at once. */
.wft-flat-tabs[rst-seg-tabs] a[aria-current] {
  background: var(--rst-accent-soft);
  color: var(--rst-accent);
  font-weight: 600;
}
/* ---------- page-header actions ----------
   chrome.html's own override of ui's page-header partial groups
   Show figures (now the page's own heading, not the admin-tabs bar
   that used to carry it — see the admin area comment above) with the
   primary action, when there is one, in this wrapper — so
   [rst-page-header]'s own flex row still has exactly two children
   (the titles block and this one) and lays out precisely as it did
   with a single action link. */
.wft-page-header-actions {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  flex-wrap: wrap;
}

/* ---------- figures toggle ----------
   See static/figures-init.js (sets the default, before first paint)
   and figures-toggle.js (owns the button). Blurred rather than
   emptied or replaced: the screen keeps its normal shape and scrolls
   the same way whether figures are shown or not, so showing someone
   the system with this on doesn't look broken, just private. Targets
   the two hooks every admin table/stat already carries — [rst-stat-num]
   for KPI tiles, .wft-num for every numeric table cell (amounts and
   counts alike) — so a page needs no per-figure markup of its own.

   td.wft-num specifically, not the bare class: .wft-num also marks the
   numeric <th> in every one of these tables (for right-alignment), and
   the unqualified selector was blurring "Requested"/"Total awarded"/
   "Amount awarded" right along with the figures underneath them — the
   one thing a trustee needs legible to know what's hidden. */
[data-figures="hidden"] [rst-stat-num],
[data-figures="hidden"] td.wft-num {
  filter: blur(0.3em);
  user-select: none;
}

/* ---------- Requests: search, status filter, sortable headers ----------
   AGENTS.md named this a later revisit, once a list screen got busy
   enough for search/filter to matter — this is that revisit for
   Requests specifically (Awarded and Reports are untouched). A plain
   GET form, so it works with JS disabled, and sortable headers are
   plain links carrying the current q/status forward — no client-side
   table logic, the server re-queries and re-renders. */
.wft-grants-filter {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 1rem;
}
.wft-grants-filter [rst-field] { margin: 0; flex: 1 1 14rem; }
.wft-table-scroll th a {
  color: inherit;
  text-decoration: none;
}
.wft-table-scroll th a:hover { text-decoration: underline; }
.wft-sort-hint { color: var(--rst-text-faint); font-weight: 400; }

/* ---------- admin tool: the plain <table> listings ----------
   AGENTS.md's settled decision: Requests/Awarded/Reports use plain
   <table> markup, not rst-list's grid idiom — named simple enough for
   three small listings that tokens.css itself never styles a bare
   <table>, so it rendered with the browser's own centred <th> and no
   column rhythm at all.

   .wft-table-scroll used to carry overflow-x: auto up here too, to
   contain a wide row inside the card rather than let it spill past
   the rounded border (a seven-figure award on a narrow screen, before
   this). That's gone: a <th> under position: sticky silently never
   sticks — it just sits at its static position and scrolls away with
   everything else — if any ancestor between it and the page is itself
   a scroll container, even one (like that div) that never actually
   has anything to scroll; confirmed with a stripped-down repro outside
   this app, nothing about this table or Chromium build specifically.
   Sticky and the horizontal-scroll fallback can't both live on this
   div. Measured instead: at 641–1024px wide, with a long organisation
   name and a seven-figure amount seeded in, none of these tables
   actually overflow their card any more — the stacked-card layout
   below 640px (further down this file) now carries the narrow-screen
   case on its own, which it didn't yet when the fallback was added.
   Revisit if a real listing ever does overflow again above 640px. */
.wft-table-scroll {
  overflow-x: visible;
}
.wft-table-scroll table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}
/* Sticky: the header row stays visible at the top of the viewport as
   the page scrolls past a long listing. top: var(--wft-header-height)
   clears [rst-shell-bar]'s own sticky header (z-index: 50) rather
   than sitting under it. Not a fixed px value any more: every table
   this rule reaches only ever shows on a signed-in screen, and since
   the admin section nav sits on the brand row itself (chrome.html's
   "nav"), [rst-shell-bar]'s rendered height now varies with viewport
   width — a single line at wide desktop widths, wrapping onto two or
   three as the nav and account menu run out of room before the
   framework's own 800px breakpoint folds them both into the hamburger
   disclosure (narrower still). static/header-height.js measures the
   real thing with a ResizeObserver and publishes it as this custom
   property; the var()'s own fallback, 89px — the plain single-row
   height, measured the same way a fixed value here always was — only
   ever matters for the first instant before that script's initial
   sync() runs, or with JavaScript off, where the sticky offset is
   wrong exactly as often as a hardcoded one would have been, never
   more. Get it wrong and this header sits a few pixels into the real
   one instead of under it, invisibly: both read as "stuck", correct
   scroll offset, right text, nothing about it looks broken except
   that the real header is drawn on top (z-index: 50 against this
   one's 2) and hides the top slice. The z-index here only needs to
   clear the table's own rows as they scroll past, not the page
   header, so it stays well below 50. Needs its own opaque background
   — .rst-box is var(--rst-surface), not the page's background — or
   row text would show through the header as it scrolls underneath.
   Turned off below 640px, where the stacked-card layout further down
   has no thead left to stick. */
.wft-table-scroll th {
  text-align: left;
  font-size: var(--rst-fs-sm);
  font-weight: 600;
  color: var(--rst-text-muted);
  white-space: nowrap;
  padding: 0 0.75rem 0.6rem 0;
  border-bottom: 1px solid var(--rst-line-strong);
  position: sticky;
  top: var(--wft-header-height, 89px);
  z-index: 2;
  background: var(--rst-surface);
}

/* A row a URL fragment jumps straight to (a broken-chain report
   linking to #e-N on /events or /security-log) otherwise lands with no
   visual marker at all — nothing on the page said "this one". The
   scroll-margin-top matches the sticky header's own `top` above (plus
   a little headroom), so the target row settles below it instead of
   partly hidden under it. Never colour alone: :target already carries
   its own meaning from the URL fragment itself (this IS the thing
   that was linked to), so a background tint alone is acceptable here,
   unlike a status that needs an independent text label. */
.wft-table-scroll tbody tr:target {
  scroll-margin-top: calc(var(--wft-header-height, 89px) + 11px);
  background: rgba(220, 38, 38, 0.08);
}
.wft-table-scroll td {
  padding: 0.65rem 0.75rem 0.65rem 0;
  border-bottom: 1px solid var(--rst-line);
  vertical-align: top;
}
/* The Category cell's own rst-dropdown summary carries tokens.css's
   shared 0.4rem top padding (sized for a comfortable click target,
   same as every other rst-dropdown in the app, e.g. the account
   menu) — stacked on top of this table's own 0.65rem cell padding,
   that pushed the pill noticeably lower than the plain text in every
   sibling cell on the same row. Negative margin-top cancels exactly
   that extra padding so the pill's own text lines up with its row,
   without shrinking the clickable area: the padding is still there,
   the whole padded box just sits 0.4rem higher. Safe because the cell
   itself has more top padding (0.65rem) than this pulls back (0.4rem)
   — the summary never escapes the cell's own padding box. Cashbook is
   the only table with a dropdown in a data cell; scoped narrowly
   rather than touching every rst-dropdown in the app. */
.wft-table-scroll td[data-th="Category"] [rst-dropdown] > summary {
  margin-top: -0.4rem;
}
/* Jumping to a row via #transaction-{id} (cashbook's category/review
   forms round-trip here, see cashbookReturnURL) landed the row flush
   with the viewport's own top edge — exactly where both sticky layers
   sit on top of it (the shell bar, var(--wft-header-height) per the
   th rule above, and this table's own th row stacked under it, ~29px
   measured in a real browser), so the row the trustee just edited was
   scrolled TO, then hidden behind both of them: the next row down was
   the first one actually visible, reading as "it jumped past the line
   I changed." scroll-margin-top reserves that space for the browser's
   own anchor/scrollIntoView scroll, same fix as a fixed-header site
   needs for any #anchor link — a few px of headroom on top of the th
   row's own ~29px. */
.wft-table-scroll tbody tr {
  scroll-margin-top: calc(var(--wft-header-height, 89px) + 37px);
}
/* A Total/subtotal row on Reports or the Accounts tabs — bold text is
   never the only signal (the Label cell, e.g. "Total" or "Net assets",
   already says so in words), this is purely the visual weight an
   accounts statement's own total lines carry. */
[rst-total-row] td {
  font-weight: 700;
  border-top: 2px solid var(--rst-line-strong);
}
.wft-table-scroll tr:last-child td { border-bottom: 0; }
.wft-table-scroll th:last-child,
.wft-table-scroll td:last-child { padding-inline-end: 0; }
.wft-table-scroll .wft-num {
  text-align: right;
}
.wft-table-scroll .wft-nowrap {
  white-space: nowrap;
}

/* Below 640px the horizontal scroll above stopped being a scroll and
   started being a clip: the viewport is narrower than even the first
   few columns' min-content width added together, so Status — the one
   column the app's own convention insists must "never [rely on]
   colour alone, always a text label" — sat mostly off-screen to the
   right of a 390px phone with no visible cue that there was more to
   scroll to. Stacked cards instead: each row becomes its own bordered
   block, each cell keeps its column header as an inline label via
   data-th (set once per <td> in the three templates that use this
   class), and the organisation-name cell — already the row's own link
   — doubles as the card's title rather than repeating "Organisation"
   above itself. This does trade away native <table> semantics for a
   screen reader at this width, the same trade AGENTS.md already named
   for the horizontal-scroll table itself ("revisit if the list
   screens get busy enough... to matter") — three small listings, not
   a data grid a screen-reader user needs row/column navigation
   through. */
@media (max-width: 640px) {
  .wft-table-scroll { overflow-x: visible; margin: 0; padding: 0; }
  .wft-table-scroll table,
  .wft-table-scroll tbody,
  .wft-table-scroll tr,
  .wft-table-scroll th,
  .wft-table-scroll td { display: block; width: auto; }
  .wft-table-scroll th { position: static; }
  .wft-table-scroll thead tr {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
    border: 0;
    padding: 0;
    margin: -1px;
  }
  /* Requests is the one table in the tool with real sort links in its
     header (Organisation/Requested/Status/Received) — visually hiding
     the whole thead along with the other, link-free tables took sort
     itself away from a sighted mobile user, not just its old table-row
     presentation. A compact wrapped strip above the cards keeps every
     link (and the column it belongs to) genuinely visible, not just
     present for a screen reader. */
  .wft-table-scroll--sortable thead tr {
    position: static;
    width: auto;
    height: auto;
    overflow: visible;
    clip: auto;
    white-space: normal;
    display: flex;
    flex-wrap: wrap;
    gap: 0.4rem 0.9rem;
    margin: 0 0 0.85rem;
    padding: 0;
  }
  .wft-table-scroll--sortable th {
    display: inline-flex;
    padding: 0;
    border: 0;
    font-size: var(--rst-fs-sm);
  }
  .wft-table-scroll tbody tr {
    border: 1px solid var(--rst-line);
    border-radius: var(--rst-radius-sm);
    padding: 0.85rem 1rem;
    margin-bottom: 0.75rem;
  }
  .wft-table-scroll tbody tr:last-child { margin-bottom: 0; }
  .wft-table-scroll td {
    border-bottom: 0;
    padding: 0.3rem 0;
    text-align: left !important;
  }
  .wft-table-scroll td[data-th]::before {
    content: attr(data-th);
    display: block;
    font-size: var(--rst-fs-sm);
    font-weight: 600;
    color: var(--rst-text-muted);
    margin-bottom: 0.15rem;
  }
  .wft-table-scroll td.wft-td-title {
    padding-top: 0;
    font-weight: 600;
    font-size: 1.05rem;
  }
  .wft-table-scroll td.wft-td-title::before { content: none; }
  /* The event log's Hash cell is one long unbroken string — the one
     cell in any of these three tables that isn't naturally short, so
     it's the one that needs an explicit wrap permission rather than
     silently overflowing its own card. */
  .wft-table-scroll .wft-hash code { overflow-wrap: anywhere; }
}

/* Cashbook's Category cell wraps the status-pill trigger in
   [rst-dropdown]'s own summary, whose padding (tokens.css) stacks on
   top of the pill's own inline-start padding — the dot ends up ~18px
   right of the column's left edge instead of lined up under the
   "Category" header the way every other column's content is.
   Zeroing just the summary's own inline-start padding leaves the
   pill's padding (shared by every status-pill in the app) as the only
   offset, same as the Actions column's pill below. */
.wft-table-scroll [data-th="Category"] [rst-dropdown] > summary {
  padding-inline-start: 0;
}

/* Cashbook's Actions cell: a status-pill plus a single toggle used to
   be a bare [rst-btn], which wrapped onto two lines the moment the
   column narrowed below "Mark unreviewed"'s own width and read as a
   stray box. A row-menu kebab (tokens.css's own per-row action idiom)
   keeps a fixed-width trigger regardless of label length; the pill
   stays inline beside it so the state is still readable without
   opening the menu. */
.wft-row-actions {
  display: flex;
  align-items: center;
  gap: 0.4rem;
}

/* ---------- Inbox: a synced email's own body ----------
   Graph hands this app plain text (see graph.go's Prefer header), so
   there is never HTML here to sanitise — just line breaks to keep. A
   <pre> already preserves them; the one rule needed on top is wrapping
   a long line instead of making the card scroll sideways, which plain
   <pre> does not do on its own. */
.wft-email-body {
  white-space: pre-wrap;
  word-wrap: break-word;
  font-family: inherit;
}

/* A grant's own Summary/Notes are a <p>, not a <pre> — plain text a
   trustee or applicant typed with real line breaks in a <textarea>,
   which html/template's default escaping keeps as literal "\n"
   characters a <p> otherwise collapses into one run-on paragraph.
   pre-line (not pre-wrap) is the right rule for prose text: it keeps
   the line breaks but still collapses any run of plain spaces, which
   pre-wrap would preserve literally. */
.wft-prewrap {
  white-space: pre-line;
}

/* ---------- Event log: a CRUD entry's own payload ----------
   Collapsed by default (<details>, no JS needed) so the common case —
   skimming the Summary column — stays uncluttered; a trustee who wants
   the actual field values opens it per row. */
.wft-event-payload {
  margin-top: 0.35rem;
}
.wft-event-payload summary {
  cursor: pointer;
  font-size: var(--rst-fs-xs);
  color: var(--rst-text-muted);
}
.wft-event-payload pre {
  background: var(--rst-surface-2);
  border: 1px solid var(--rst-line);
  border-radius: var(--rst-radius-sm);
  margin: 0.4rem 0 0;
  padding: 0.6rem 0.75rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--rst-fs-xs);
  white-space: pre-wrap;
  word-wrap: break-word;
}

/* ---------- Pagination: "go to page" ----------
   Styling for chrome.html's own override of the vendored "pagination"
   partial (adapted from the sibling ips app) — a small GET form beside
   the numbered strip, and the number field the current-page pill turns
   into on click (pagergoto.js).

   The current-page pill is a <button> (pagergoto.js turns a click on
   it into the number field beside it) — tokens.css's own shared pill
   rule only reaches `a` and `span`, the two elements the vendored
   partial used, so a bare button carried none of it: no border, no
   radius, no padding, nothing but the browser's own button chrome
   under the [aria-current="page"] fill tokens.css already paints on
   top. That's the flat, ill-shaped block a trustee saw instead of a
   pill. Restating the shared pill properties for `button` here, after
   tokens.css, is the fix — can't extend that file's own selector list
   without editing a file this app keeps purely vendored. */
[rst-pagination] button {
  appearance: none;
  background: none;
  border: 1px solid var(--rst-line);
  border-radius: var(--rst-radius-sm);
  color: var(--rst-text-muted);
  cursor: pointer;
  display: inline-block;
  font: inherit;
  font-size: var(--rst-fs-sm);
  min-inline-size: 2rem;
  padding: 0.3rem 0.6rem;
  text-align: center;
}
/* [hidden] has to win over that shared `display: inline-block` two
   rules up, or toggling it from script — the pill swapping for the
   number field it becomes, the fallback goto box once the pill takes
   over — changes nothing on screen, since a plain [hidden] alone
   loses that fight on specificity. */
[rst-pagination] [hidden] {
  display: none;
}
[rst-pagination] > [data-pager-goto-input] {
  border-radius: var(--rst-radius-sm);
  font-size: var(--rst-fs-sm);
  font-variant-numeric: tabular-nums;
  padding: 0.3rem 0.4rem;
  text-align: center;
  width: 2.75rem;
}
[rst-pagination-goto] {
  align-items: center;
  display: flex;
  gap: 0.4rem;
  margin-inline-start: var(--rst-sp-2);
}
[rst-pagination-goto] label {
  color: var(--rst-text-muted);
  font-size: var(--rst-fs-sm);
  white-space: nowrap;
}
[rst-pagination-goto] [rst-input] {
  min-inline-size: 3.5rem;
  padding: 0.3rem 0.4rem;
  text-align: center;
}
/* "Showing 51–55 of 55" beside the pager, pushed apart by
   space-between the same way ips's own .ips-list-foot does it — the
   summary reads first (left, where a reader's eye already is after
   the table), the pager sits at the right where "turn the page" is
   expected. Wraps to its own line under the pager on a narrow screen
   rather than squeezing the numbered strip. nav's own block margin
   (tokens.css) is reset to 0 here: the row wrapper owns the spacing
   above/below the pair instead, or the two would double up. */
.wft-pager-row {
  align-items: center;
  display: flex;
  flex-wrap: wrap;
  gap: var(--rst-sp-3);
  justify-content: space-between;
  margin: var(--rst-sp-4) 0;
}
.wft-pager-row [rst-pagination] {
  margin: 0;
}
.wft-pager-summary {
  color: var(--rst-text-muted);
  font-size: var(--rst-fs-sm);
  margin: 0;
}

/* An unread row reads heavier, the same bold-row convention every
   mail client uses — weight, not colour alone, carries the signal
   (state-never-colour-alone applies to text weight too: a screen
   reader doesn't announce bold, but nothing here is ONLY told through
   it — Status is still a plain-text pill regardless). */
.wft-inbox-unread td { font-weight: 600; }

/* The nav badge on "Inbox" — see the "nav" block in chrome.html. A number,
   not a dot: "state is never colour alone" extends to "never a bare
   dot" here too — a trustee needs the count, not just "something's
   new". */
.wft-nav-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.25rem;
  height: 1.25rem;
  padding: 0 0.35rem;
  margin-inline-start: 0.35rem;
  border-radius: var(--rst-radius-pill);
  background: var(--rst-accent);
  color: var(--rst-on-accent);
  font-size: var(--rst-fs-sm);
  font-weight: 600;
  line-height: 1;
}

/* ---------- Inbox: folder filter strip ----------
   A three-pane Outlook-style layout (folders | list | reading pane)
   lived here briefly on 2026-10-03 and was reverted the same day —
   see AGENTS.md. This is what's left of it: a horizontal strip of
   status filters with counts, replacing the old status <select>+View
   form — a trustee wants to switch what they're looking at, not
   describe a query. */
.wft-inbox-folders {
  display: flex;
  flex-wrap: wrap;
  gap: var(--rst-sp-2);
  margin-bottom: var(--rst-sp-4);
}
.wft-inbox-folders a {
  display: flex;
  align-items: center;
  gap: var(--rst-sp-2);
  padding: var(--rst-sp-2) var(--rst-sp-3);
  border-radius: var(--rst-radius-sm);
  color: var(--rst-text);
  text-decoration: none;
}
.wft-inbox-folders a:hover { background: var(--rst-surface); }
.wft-inbox-folders a[aria-current] {
  background: var(--rst-accent-soft);
  font-weight: 600;
}
.wft-inbox-folder-count {
  font-size: var(--rst-fs-sm);
  color: var(--rst-text-muted);
}
.wft-inbox-folders a[aria-current] .wft-inbox-folder-count { color: inherit; }

/* Previous/Newer-Older navigation on a message's own page — see
   showInbox/prevNextInbox in inbox.go. */
.wft-inbox-prev-next {
  display: flex;
  justify-content: space-between;
  margin-top: var(--rst-sp-4);
}

/* The Trust-year select ("6 Apr 2024 – 5 Apr 2025") was rendered
   field-select's Short variant (tokens.css's [rst-input~="short"],
   8rem — sized for a postcode, not a date range), which clipped the
   label to "6 Apr 2024 – 5 /". Full-width by default (reports.html no
   longer passes Short), capped here so it doesn't stretch edge to
   edge of the box on a wide desktop viewport. */
/* [rst-input], not .rst-input: field-select's own partial renders
   rst-input as a bare attribute, never a class, so the class-selector
   version of this rule never matched anything — confirmed by an
   independent Fable-model audit, this had been a dead rule from the
   day it was written, and the select has been full-width in
   production the whole time. */
#year[rst-input] { max-width: 16rem; }

/* [rst-page] is a flex item of [rst-shell-topbar]'s own column flex
   box (tokens.css), and its `margin: 0 auto` gives it an auto
   cross-axis margin — which, per the flexbox spec, exempts an item
   from the stretch its container would otherwise apply, so it
   shrink-wraps to its own content's intrinsic width instead of filling
   the column. `width: 100%` restores the normal "fill the column, up
   to max-width, then centre" behaviour every other block-level use of
   [rst-page] already gets for free by not being a flex item with
   content narrower than its column.

   Originally scoped to Reports only (`:has(.wft-reports-page)`, added
   when the only observed symptom was Reports' narrower tabs — By
   category/By charity, a short table — rendering a different width
   from its wider ones — Income and expenditure, long line-item labels
   — on the same page). Widened to every [rst-page] 2026-10-29, the
   same day as the iPhone header/footer saga below: real desktop
   Chromium shrink-wraps a flex item like this to something close to
   its max-width regardless, which is why none of that saga's extensive
   Chromium-based testing ever caught this — but real WebKit's
   shrink-to-fit computation for a [rst-page] built almost entirely out
   of full-bleed `.wft-band`/`.wft-hero-band` children (each escaping
   via `width: 100dvw` + a negative `margin-inline`, so none of them
   contributes an ordinary intrinsic width the way flowed text or a
   percentage-sized box would) came out as 32px — exactly this app's
   two 16px side paddings with a ~zero content width between them —
   centred by its own now-huge auto margins. Confirmed directly against
   real WebKit (not a theory): `npx playwright install webkit` +
   `playwright install-deps webkit`, same one-time system-wide
   dependency install already recorded above for Chromium, gets a real
   WebKit build running headless on this box, close enough to iOS
   Safari's own engine to reproduce device-only CSS bugs no amount of
   Chromium emulation ever could — load-bearing for this fix and worth
   reaching for first next time a "only on my iPhone" report shows up,
   rather than another round of Chromium-blind guessing. The home page
   (an unbroken run of full-bleed bands, no Reports table in sight) hit
   the exact same 32px collapse live on woodfamilytrust.org.uk — this
   was never a Reports-only edge case, just the one screen with content
   varied enough, on a browser forgiving enough, that someone happened
   to notice the width changing under it. */
[rst-page] { width: 100%; }

/* A Reports/Accounts row that sits under a sub-heading row above it
   ("Grants paid" on the Income and expenditure tab). */
td.wft-indent { padding-inline-start: 1.5rem; }

/* Accounts statements (Reports' Income and expenditure / Balance sheet
   tabs): the statement's own heading sits in the table's header row,
   left, sharing a baseline with "Unrestricted" on the right, directly
   above the header rule — the same layout as the Accounts PDF. A real
   <h2> isn't allowed inside a <th>, so the heading is a
   role="heading" span, sized like the h2 it replaced. Below 640px the
   other tables hide their thead visually; a statement's stays visible
   (as a flex row) because it carries the heading. */
.wft-table-scroll--statement th { vertical-align: baseline; }
.wft-table-scroll--statement th.wft-statement-title {
  font-size: inherit;
  color: var(--rst-text);
  white-space: normal;
}
.wft-statement-title [role="heading"] { font-size: 1.5em; font-weight: 700; line-height: 1.2; }
@media (max-width: 640px) {
  .wft-table-scroll--statement thead tr {
    position: static;
    width: auto;
    height: auto;
    overflow: visible;
    clip: auto;
    white-space: normal;
    margin: 0 0 0.25rem;
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 0.75rem;
    border-bottom: 1px solid var(--rst-line-strong);
    padding-bottom: 0.6rem;
  }
  .wft-table-scroll--statement thead th { border: 0; padding: 0; }
}
