/* freedomshope.org --- the whole design, one stylesheet.
   Typefaces come from assets/fonts.css (self-hosted, see tools/fetch_fonts.py).

   Decisions, so nobody has to guess later:
   - The 2014 logo, the #256ea5 accent and the 2014 background texture stay.
     Everything else is rebuilt. The texture (fhmbg1.jpg, a vignetted paper
     wash: teal at the top, cream through the middle, dusty rose at the foot)
     is what made the old site feel warm, and it is what gives the frosted
     panels something to sit against -- glass over flat white is just white.
   - Prose sits in a single column at a reading measure. The archive sidebar
     appears only in News, not on all 462 pages.
   - Galleries break out wider than the text. 276 of 462 pages are a report
     followed by a photo grid; that shape gets the attention.
   - Mobile is the default; the desktop layout is the enhancement. */

/* ---------------------------------------------------------------- tokens */

:root {
  --accent:        #256ea5;   /* the 2015 brand blue, kept --- 5.5:1 on white */
  --accent-deep:   #1a527b;   /* hover, and text on tinted panels --- 8.3:1 */
  --accent-wash:   #eef4f9;

  /* Measured, not estimated. The second number is against --paper-soft, which
     is the footer and the table headers; every pair clears WCAG AA for body
     text (4.5:1). --ink-faint was #6b7885 and came to 4.24 on --paper-soft,
     which is where the footer's copyright line sits. */
  --ink:           #16202a;   /* body text      16.5:1 / 15.5:1 */
  --ink-soft:      #4d5a67;   /* meta, captions  7.1:1 /  6.6:1 */
  --ink-faint:     #63707c;   /* least loud      5.1:1 /  4.8:1 */

  --paper:         #ffffff;
  --paper-soft:    #f6f8fa;
  --rule:          #e2e8ee;
  --rule-strong:   #cbd5de;

  /* Glass. The alphas are solved, not chosen. The texture's darkest corner is
     rgb(79 51 82) --- a deep plum, far darker than the cream middle suggests
     --- and the panels have to stay AA over *that*, because on a phone the
     panel is full width and the corners are under it. Measured:
        scrim 0.18 + glass 0.93  -> rgb(244 243 245), worst pair 4.6:1
        scrim 0.18 + thin  0.86  -> rgb(235 232 235), --ink 13.6:1
     Thinner looks better and fails, which is why the numbers are written down.
     Anything nested inside a panel (panel-wide, callout, chips, table heads)
     sits on the composited panel rather than the texture, so it can be far
     thinner and still clear 4.8:1. */
  --ground:        #eff1bf;                    /* the old body colour, kept */
  --glass:         rgb(255 255 255 / 0.93);
  --glass-thin:    rgb(255 255 255 / 0.86);
  --glass-line:    rgb(255 255 255 / 0.75);
  --glass-tint:    rgb(37 110 165 / 0.07);
  --blur:          saturate(170%) blur(16px);

  --shadow:        0 1px 2px rgb(30 40 30 / 0.06), 0 10px 30px rgb(30 40 30 / 0.10);
  --shadow-sm:     0 1px 2px rgb(30 40 30 / 0.06), 0 4px 12px rgb(30 40 30 / 0.07);

  --sans: 'Source Sans 3', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --serif: 'Source Serif 4', Georgia, 'Times New Roman', serif;

  --measure: 68ch;
  --shell: 1180px;
  /* The texture either side of the panel, at every width. It used to be zero
     below 1180px --- `.shell` had a max-width but no margin, so the card ran
     to the viewport edge from 640px all the way up, and a rule that squared
     its corners "for a phone" was describing something already true of every
     laptop. Reversed 2026-09-22: the ground shows down both sides now, which
     is also what gives the masthead and footer an edge to line up with. */
  --gutter: clamp(0.75rem, 2.5vw, 1.5rem);

  --step-0: 1.0625rem;
  --step-1: 1.25rem;
  --step-2: 1.5rem;
  --step-3: 1.875rem;
  --step-4: 2.25rem;

  --space: 1.5rem;
  --radius: 6px;
  --focus: 3px solid var(--accent);

  /* Motion. Two durations, because more than two is a house style nobody can
     keep. --t-fast is for something under the pointer right now --- it has to
     feel like a response, so anything slower reads as lag. --t is for a thing
     that appears or moves, where the extra 60ms is what stops it snapping.
     The ease is flat out of the gate and settles late, which is what makes a
     short duration read as smooth rather than merely quick.

     The `prefers-reduced-motion` block at the foot of this file zeroes every
     duration, so nothing added here needs to ask again. */
  --t-fast: 120ms;
  --t: 180ms;
  --ease: cubic-bezier(0.2, 0, 0.2, 1);
}

@media (min-width: 40rem) {
  :root { --step-0: 1.125rem; --step-3: 2.125rem; --step-4: 2.875rem; }
}

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

html { -webkit-text-size-adjust: 100%; }

/* A column the full height of the viewport, so the footer is pushed to the
   bottom of a short page instead of stopping wherever the content did --- the
   unlock form, /search/ and 404 are all short enough to have left it adrift in
   the middle with bare texture underneath.

   `min-height`, never `height`: a long page must still grow past the viewport.
   The three out-of-flow children (the .skip link, the ::before texture, the
   scripts) generate no flex items, so <main> is the only thing that grows. */
body {
  margin: 0;
  display: flex;
  flex-direction: column;
  background: var(--ground);
  color: var(--ink);
  font: var(--step-0)/1.7 var(--serif);
  font-synthesis-weight: none;
  text-rendering: optimizeLegibility;
  /* dvh tracks the phone's collapsing URL bar, where vh is the tallest the
     viewport ever gets and would push the footer below the fold on exactly
     the short pages this exists for. vh first, as the fallback. */
  min-height: 100vh;
  min-height: 100dvh;
}

/* The texture is painted by a fixed pseudo-element rather than
   `background-attachment: fixed`, which repaints the whole layer on every
   scroll frame in mobile Safari --- noticeable on a report with 136 photos. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: -1;
  background:
    linear-gradient(0deg, rgb(255 255 255 / 0.18), rgb(255 255 255 / 0.18)),
    url('/assets/background.jpg') center / cover no-repeat,
    var(--ground);
}

h1, h2, h3, h4, h5, h6, nav, .ui { font-family: var(--sans); }

h1, h2, h3, h4 {
  line-height: 1.2;
  font-weight: 700;
  letter-spacing: -0.012em;
  margin: 2.2em 0 0.6em;
  text-wrap: balance;
}
h1 { font-size: var(--step-4); margin-top: 0; }
h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-2); }
h4 { font-size: var(--step-1); }

p, ul, ol, blockquote, table, figure { margin: 0 0 1.25em; }
/* Prose only. Left unscoped this lands on every UI list too: the nav bar and
   the footer lay their items out in a flex row, so a top margin on items 2..n
   drops them all and leaves the first one looking misaligned. The grid of
   page links and the news card list picked up stray offsets from it as well.
   Every UI list below sets its own spacing. */
.prose li + li { margin-top: 0.35em; }

a { color: var(--accent); text-underline-offset: 0.16em; text-decoration-thickness: 1px; }
a:hover { color: var(--accent-deep); }

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

img { max-width: 100%; height: auto; }

hr { border: 0; border-top: 1px solid var(--rule); margin: 2.5em 0; }

blockquote {
  margin-inline: 0;
  padding: 0.2em 0 0.2em 1.25em;
  border-left: 3px solid var(--accent);
  color: var(--ink-soft);
  font-style: italic;
}
blockquote cite { display: block; margin-top: 0.6em; font: 600 0.9em var(--sans); font-style: normal; color: var(--ink-faint); }

table { border-collapse: collapse; width: 100%; font-size: 0.95em; }
th, td { border: 1px solid var(--rule); padding: 0.5em 0.7em; text-align: left; }
th { background: rgb(255 255 255 / 0.6); font-family: var(--sans); }

.table-scroll { overflow-x: auto; }

/* ----------------------------------------------------------- skip / shell */

.skip {
  position: absolute; left: -9999px;
  background: var(--accent); color: #fff;
  padding: 0.7em 1.1em; border-radius: 0 0 var(--radius) 0;
  font: 600 0.95rem var(--sans); z-index: 20;
}
.skip:focus { left: 0; top: 0; }

/* One column, centred, never touching the viewport.
 *
 *  `width` rather than `max-width`, so the gutter applies below --shell as
 *  well as above it --- max-width alone stops mattering the moment the
 *  viewport is narrower than it, which is exactly where the gutter was
 *  missing. Everything that uses .shell sets its own inner padding, so this
 *  rule is the box and nothing else.
 *
 *  `.slideshow` is here rather than styling itself, because it is the one
 *  other thing on the site that claims this column --- it sits outside the
 *  panel, as a direct child of <main>, so it cannot just take the class
 *  (`main > .shell` would frost it). It had `max-width: var(--shell)` with no
 *  margin and so had exactly the bug this rule exists to fix, found only
 *  after fixing it for .shell. Two users, one declaration, so a third cannot
 *  quietly drift. */
.shell,
.slideshow {
  width: min(var(--shell), 100% - 2 * var(--gutter));
  margin-inline: auto;
}

/* --------------------------------------------------------------- masthead */
/*
 *  One row wherever the menu fits beside the wordmark, a Menu button when it
 *  does not. The eight labels come to roughly 620px of nav at the compact
 *  size; with a 260px wordmark and the shell's gutters that needs about
 *  1050px, so 66rem is where the row gives out. Below it, flex-wrap drops the
 *  nav onto its own full-width line and the button controls it.
 */

.masthead {
  background: var(--glass);
  -webkit-backdrop-filter: var(--blur);
  backdrop-filter: var(--blur);
  border-bottom: 1px solid var(--glass-line);
  box-shadow: var(--shadow-sm);
  position: relative;
  z-index: 20;
}

.masthead__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 0.75rem var(--space);
  padding-block: 0.8rem;
}

/* The 2014 mark is a 1460x217 black wordmark carrying the full name, so the
   brand is the image alone --- repeating the name in text beside it would read
   as a mistake. The name lives in alt text for anyone not seeing the image. */
.brand { display: block; text-decoration: none; color: inherit; }
.brand img { display: block; width: min(250px, 56vw); height: auto; }
.brand__tagline {
  display: block; margin-top: 0.25rem;
  font: 400 0.78rem/1.3 var(--sans); color: var(--ink-faint);
  letter-spacing: 0.01em;
}

/* ------------------------------------------------------- masthead > tools */
/*
 *  Search, and the Menu button on a phone. One group so that the two stay
 *  together at the right-hand end of row one however narrow it gets; the
 *  button hides itself above 66rem and the search icon is then alone.
 */

.masthead__tools { display: flex; align-items: center; gap: 0.5rem; }

.nav-search {
  display: inline-flex; align-items: center; justify-content: center;
  flex: none; width: 2.6rem; height: 2.6rem;
  color: var(--ink); text-decoration: none;
  border: 1px solid transparent; border-radius: var(--radius);
  transition:
    color var(--t-fast) var(--ease),
    background-color var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease);
}
.nav-search svg { display: block; width: 20px; height: 20px; }
.nav-search:hover,
.nav-search:focus-visible {
  color: var(--accent-deep);
  background: var(--glass-tint);
  border-color: var(--rule-strong);
}
/* On /search/ itself. Same language as the menu: the accent is what says "you
   are here", and hover never borrows it. */
.nav-search[aria-current='page'] {
  color: var(--accent-deep);
  background: var(--accent-wash);
  border-color: var(--accent);
}

/* ------------------------------------------------------------------ navbar */

.nav-btn {
  display: inline-flex; align-items: center; gap: 0.6rem;
  padding: 0.6rem 0.9rem;
  font: 600 0.95rem var(--sans); color: var(--ink);
  background: rgb(255 255 255 / 0.6);
  border: 1px solid var(--rule-strong); border-radius: var(--radius);
  cursor: pointer;
}
.nav-btn:hover { background: #fff; border-color: var(--accent); color: var(--accent-deep); }
.nav-btn__bars,
.nav-btn__bars::before,
.nav-btn__bars::after {
  display: block; width: 18px; height: 2px; border-radius: 2px;
  background: currentColor; content: '';
}
.nav-btn__bars { position: relative; }
.nav-btn__bars::before { position: absolute; top: -6px; }
.nav-btn__bars::after { position: absolute; top: 6px; }

/* Below 30rem the word "Menu" goes, and the button becomes a square to match
   the search icon beside it.
 *
 *  The masthead row is the wordmark (min(250px, 56vw)) plus the two tools, and
 *  with the label the tools come to about 150px. At 393px --- a current
 *  iPhone --- that leaves 125px of room for 150px of button, so the pair wrap
 *  onto a second row under the logo. Without the label they are 91px and fit
 *  down to about 320px.
 *
 *  Hidden, not removed: `.visually-hidden` keeps "Menu" as the button's
 *  accessible name, so a screen reader still announces it. `display: none` or
 *  `font-size: 0` would leave a button called nothing. */
@media (max-width: 30rem) {
  .nav-btn {
    width: 2.6rem; height: 2.6rem; padding: 0;
    justify-content: center; gap: 0;
  }
  .nav-btn__label {
    position: absolute; width: 1px; height: 1px; margin: -1px;
    padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
  }
}

.nav__list, .nav__sub { list-style: none; margin: 0; padding: 0; }
.navbar a {
  display: block; text-decoration: none; color: var(--ink);
  font: 600 0.95rem var(--sans);
  transition:
    color var(--t-fast) var(--ease),
    background-color var(--t-fast) var(--ease),
    border-color var(--t-fast) var(--ease);
}

/* Hover and "you are here" are different claims and now look different.
 *
 *  They used to be one declaration, which meant the menu stopped being able to
 *  say where you were the moment the pointer was anywhere in it --- every item
 *  the mouse crossed looked exactly as current as the current one.
 *
 *  The language, used at every level of the menu and by the search icon:
 *    hover    a neutral tint, --glass-tint. Transient: you could go here.
 *    current  the accent --- a solid bar and --accent-wash behind it.
 *             Persistent: you are here.
 *
 *  Current is declared after hover everywhere. The two selectors have equal
 *  specificity, so source order is what keeps the current item looking current
 *  while the pointer is on it. */
.navbar a:hover { color: var(--accent-deep); background: var(--glass-tint); }
.navbar a[aria-current='page'] { color: var(--accent-deep); }

/* No weight change on either state, at any width. The eight labels sit in a
   nowrap flex row that only just fits at 66rem, and a bolder current item
   would move every label after it --- on page load, not on hover, which makes
   it a layout that shifts between pages rather than an interaction. */

.nav-sub-btn {
  border: 0; background: none; cursor: pointer;
  color: var(--ink-soft); font: 600 1.15rem/1 var(--sans);
  padding: 0.4rem 0.7rem;
}

/* ---- narrow: the nav wraps to its own row and collapses ---- */
@media (max-width: 65.999rem) {
  /* Collapsed here rather than in the markup, so a visitor without JavaScript
     keeps the whole menu (see the <noscript> rule in layout.ts). */
  .navbar { display: none; flex-basis: 100%; }
  .navbar.is-open { display: block; }
  .navbar { padding-bottom: 0.5rem; }

  .nav__list > li { border-top: 1px solid var(--rule); }
  .navbar li { position: relative; }
  /* The left padding is 0.7rem rather than 0.2rem so the current item's bar
     has somewhere to sit. Applied to every row, not just the current one, so
     the labels stay in line with each other --- an indent that moved with the
     page you were on would read as a wobble. */
  .navbar a { padding: 0.8rem 0.2rem 0.8rem 0.7rem; }

  /* Collapsed, there are no bottom borders to carry the accent, so the bar
     goes down the left. An inset shadow rather than a border, because a border
     would shift the label by its own width. */
  .navbar a[aria-current='page'] {
    background: var(--accent-wash);
    box-shadow: inset 3px 0 0 var(--accent);
  }

  .nav-sub-btn { position: absolute; top: 0.25rem; right: 0; }
  .nav-sub-btn[aria-expanded='true'] span { font-size: 0; }
  .nav-sub-btn[aria-expanded='true'] span::before { content: '\2212'; font-size: 1.15rem; }

  /* Closed by the script; open by default when the script never ran. */
  .nav__sub { padding-left: 1rem; }
  .nav-sub-btn ~ .nav__sub { display: none; }
  .nav-sub-btn[aria-expanded='true'] ~ .nav__sub,
  .nav__sub.is-open { display: block; }
  .nav__sub a { font-weight: 400; font-size: 0.95rem; color: var(--ink-soft); }
}

/* ---- wide: one row, with flyouts ---- */
@media (min-width: 66rem) {
  .masthead { position: sticky; top: 0; }
  .nav-btn, .nav-sub-btn { display: none; }
  .masthead__inner { flex-wrap: nowrap; padding-block: 0.6rem; }

  /* The markup has the tools before the nav, because that is what makes the
     narrow layout wrap correctly (see masthead() in layout.ts). Here the menu
     wants to come first and the search icon to sit at the far right of it, so
     the two swap by `order` rather than by being written twice.

     `margin-left: auto` gathers the free space in front of the menu, which
     keeps the pair hard right exactly as the menu alone used to be --- the
     masthead's `justify-content: space-between` then has nothing left to
     spread, so adding a third child does not re-centre the nav. */
  .navbar { order: 1; margin-left: auto; }
  .masthead__tools { order: 2; }

  .nav__list { display: flex; align-items: stretch; }
  .nav__list > li { position: relative; display: flex; }
  .nav__list > li > a {
    display: flex; align-items: center; white-space: nowrap;
    padding: 0.7rem 0.6rem;
    border-bottom: 3px solid transparent;
  }
  .nav__list > li > a:hover {
    border-bottom-color: var(--rule-strong);
    background: var(--glass-tint);
  }
  .nav__list > li > a[aria-current='page'] {
    border-bottom-color: var(--accent);
    background: var(--accent-wash);
  }

  /* Anchored to the right edge of its tab, because the nav sits flush against
     the shell's right gutter and a panel that grew rightwards ran off screen.
     It did that even while closed: visibility:hidden still takes part in
     layout, so the third-level list was adding 137px to the document's scroll
     width and showing a band of bare texture down the right of every page
     from 66rem to about 1330px. Growing leftwards has room at every width. */
  .nav__sub {
    position: absolute; top: 100%; right: 0; z-index: 30;
    min-width: 13.5rem; padding: 0.35rem;
    background: rgb(255 255 255 / 0.96);
    -webkit-backdrop-filter: var(--blur);
    backdrop-filter: var(--blur);
    border: 1px solid var(--glass-line);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    visibility: hidden; opacity: 0;
    /* A panel that only fades still arrives all at once. Four pixels of travel
       is enough to read as "this opened" without becoming an animation anyone
       has to wait for. `visibility` cannot be interpolated, so it is stepped
       at the end of the fade out and immediately on the way in --- otherwise
       the panel would either vanish before fading or stay clickable after. */
    transform: translateY(-4px);
    transition:
      opacity var(--t) var(--ease),
      transform var(--t) var(--ease),
      visibility var(--t);
  }
  li:hover > .nav__sub,
  li:focus-within > .nav__sub {
    visibility: visible; opacity: 1; transform: translateY(0);
    transition-delay: 0s;
  }

  /* Third level (Projects > Africa > Ghana) flies out sideways, or it would
     land on top of the level above it. Its containing block is the panel
     above, not the tab, so `right: 100%` parks it against that panel's left
     edge. */
  .nav__sub .nav__sub { top: -0.35rem; right: 100%; }
  .nav__sub a {
    padding: 0.5rem 0.7rem 0.5rem 1.3rem;
    border-radius: 4px; font-weight: 500;
  }
  .nav__sub a:hover { background: var(--glass-tint); }
  .nav__sub a[aria-current='page'] {
    background: var(--accent-wash);
    color: var(--accent-deep);
    box-shadow: inset 2px 0 0 var(--accent);
  }
  /* The chevron points the way the panel actually opens. It sits in the left
     gutter every row already reserves, so one item having a submenu does not
     push its label out of line with its siblings. */
  .nav__sub .has-sub > a { position: relative; }
  .nav__sub .has-sub > a::before {
    content: '\2039'; position: absolute; left: 0.6rem; color: var(--ink-faint);
  }
}

/* Past 80rem there is room to breathe: a larger wordmark and looser tabs. */
@media (min-width: 80rem) {
  .brand img { width: 310px; }
  .nav__list > li > a { padding-inline: 0.85rem; font-size: 0.98rem; }
}

/* ------------------------------------------------------------------- page */

/* `1 0 auto`, not `1`: the basis stays at the content's own height, so this
   only ever adds the leftover space on a short page and never compresses a
   long one. */
main { display: block; flex: 1 0 auto; padding-block: clamp(1.5rem, 4vw, 3rem) 4rem; }

/* The content sits on a frosted panel over the texture --- the same boxed
   layout the old site had, but letting the ground show through instead of
   covering it with opaque white. `main >` keeps this off the masthead and
   footer shells. */
main > .shell {
  padding: clamp(1.4rem, 4vw, 3rem);
  background: var(--glass);
  -webkit-backdrop-filter: var(--blur);
  backdrop-filter: var(--blur);
  border: 1px solid var(--glass-line);
  border-radius: 16px;
  box-shadow: var(--shadow);
}

/* On a phone the panel keeps its corners and its border --- it has a gutter to
   sit in now --- but gives up most of its inner padding, because the reading
   measure needs those pixels more than the margin does. */
@media (max-width: 40rem) {
  main > .shell { padding-inline: 1.1rem; }
}

/* News puts the article and the archive on two panels, not one wide one.
   Inline padding of zero so those two sit on exactly the edges a single panel
   would --- it was `clamp(1rem, 4vw, 2rem)`, which inset them 32px further
   than every other page and left the masthead hanging outside them. */
main > .shell.with-sidebar {
  padding: 0; background: none; border: 0; box-shadow: none;
  -webkit-backdrop-filter: none; backdrop-filter: none;
}
.with-sidebar > div,
.with-sidebar > .sidebar {
  padding: clamp(1.4rem, 3vw, 2.25rem);
  background: var(--glass);
  -webkit-backdrop-filter: var(--blur);
  backdrop-filter: var(--blur);
  border: 1px solid var(--glass-line);
  border-radius: 16px;
  box-shadow: var(--shadow);
}
@media (max-width: 40rem) {
  .with-sidebar > div,
  .with-sidebar > .sidebar { padding-inline: 1.1rem; }
}

.page-head { margin-bottom: 2rem; }

/* The page title carries a marker stripe and a tab to its left.
 *
 *  The stripe spans the **whole heading block**, not the words. That is the
 *  decision, not an oversight: an h1 is a block and fills the panel, and a
 *  full-width band over a centred reading column is what balances the two.
 *  Shrink-wrapping it to the text --- `width: fit-content` --- was tried and
 *  rejected, so if a short title like "Search" ever looks like a bug, it is
 *  not one. design.test.ts holds this so the "fix" cannot be applied by
 *  accident.
 *
 *  The 20px tab is drawn outside the box, so it needs the panel's padding to
 *  sit in. That is `clamp(1.4rem, 4vw, 3rem)`, i.e. 22.4px at its tightest ---
 *  except below 40rem, where the panel goes edge to edge at 1.1rem and the tab
 *  bleeds ~2px off the left of the screen. A box-shadow adds nothing to
 *  scrollable overflow, so that is a clipped edge and never a sideways scroll.
 *
 *  Contrast holds: --ink on #ebe7bf is 13.1:1, and the stripe only ever sits
 *  under the descenders. design.test.ts asserts it rather than trusting it. */
.page-head h1 {
  box-shadow: inset 0 -9px 0 #ebe7bf, -20px 0px 0 #b79594;
}

/* On a phone the stripe stays and the tab goes.
 *
 *  40rem is exactly where it stops fitting, not a round number: the tab is
 *  drawn 20px outside the heading box and lives in the panel's inner padding,
 *  which is `clamp(1.4rem, 4vw, 3rem)` --- 25.6px at 641px, still room, but
 *  the phone rule below drops it to 1.1rem, and 17.6px is less than 20. So
 *  the tab would start outside the panel and sit on the texture, which reads
 *  as a leak rather than as part of the heading. */
@media (max-width: 40rem) {
  .page-head h1 { box-shadow: inset 0 -9px 0 #ebe7bf; }
}

.page-head .eyebrow {
  font: 600 0.8rem/1 var(--sans); letter-spacing: 0.08em; text-transform: uppercase;
  color: var(--accent); margin: 0 0 0.75rem;
}
.page-head time { font: 400 0.95rem var(--sans); color: var(--ink-faint); }

/* Prose is a grid so a gallery can break out wider than the text without
   being lifted out of the body HTML the converter produced. */
.prose {
  display: grid;
  grid-template-columns:
    [full-start] minmax(0, 1fr)
    [wide-start] minmax(0, 6rem)
    [text-start] min(var(--measure), 100%) [text-end]
    minmax(0, 6rem) [wide-end]
    minmax(0, 1fr) [full-end];
}
.prose > * { grid-column: text; min-width: 0; }
.prose > .gallery,
.prose > .table-scroll,
.prose > video { grid-column: wide; }

.prose > h2 { border-top: 1px solid var(--rule); padding-top: 1.6rem; }
.prose > h2:first-child { border-top: 0; padding-top: 0; }

/* ---------------------------------------------------------------- gallery */

.gallery {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 0.6rem;
  margin: 2rem 0 2.5rem;
}
@media (min-width: 40rem) {
  .gallery { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 0.85rem; }
}
.gallery figure { margin: 0; }
.gallery.cols-1 { grid-template-columns: minmax(0, 1fr); max-width: 40rem; }
.gallery.cols-1 img { aspect-ratio: auto; object-fit: contain; }
.gallery.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 40rem) { .gallery.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.gallery a {
  display: block; border-radius: 8px; overflow: hidden;
  background: var(--paper-soft);
  box-shadow: var(--shadow-sm);
  transition: transform 0.18s ease, box-shadow 0.18s ease;
}
.gallery a:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
.gallery img {
  display: block; width: 100%; aspect-ratio: 1; object-fit: cover;
  transition: transform 0.25s ease, opacity 0.25s ease;
}
.gallery a:hover img { transform: scale(1.03); opacity: 0.94; }
.gallery figcaption {
  font: 400 0.85rem/1.45 var(--sans); color: var(--ink-soft);
  margin-top: 0.4rem;
}

/* A single captioned image inside prose, from the old [caption] shortcode. */
figure.wp-caption { margin: 2rem 0; }
figure.wp-caption img { border-radius: 8px; box-shadow: var(--shadow-sm); }
figure.wp-caption figcaption {
  font: 400 0.9rem/1.5 var(--sans); color: var(--ink-soft); margin-top: 0.5rem;
}

video { width: 100%; height: auto; border-radius: var(--radius); background: #000; }

/* --------------------------------------------------------------- lightbox */

dialog.lightbox {
  border: 0; padding: 0; max-width: 100vw; max-height: 100vh;
  width: 100%; height: 100%; background: rgb(10 14 18 / 0.94);
}
dialog.lightbox::backdrop { background: rgb(10 14 18 / 0.94); }
.lightbox__frame {
  display: grid; grid-template-rows: 1fr auto; gap: 0.75rem;
  width: 100%; height: 100%; padding: clamp(0.75rem, 3vw, 2rem);
}
.lightbox__frame img {
  grid-row: 1; justify-self: center; align-self: center;
  max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 4px;
}
.lightbox__bar {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  color: #e8edf2; font: 400 0.95rem var(--sans);
}
.lightbox__bar button {
  font: 600 1rem var(--sans); color: #fff; background: rgb(255 255 255 / 0.12);
  border: 1px solid rgb(255 255 255 / 0.25); border-radius: var(--radius);
  padding: 0.5rem 0.9rem; cursor: pointer; min-width: 3rem;
}
.lightbox__bar button:hover { background: rgb(255 255 255 / 0.22); }

/* ------------------------------------------------------------ news layout */

.with-sidebar { display: grid; gap: clamp(2rem, 5vw, 3.5rem); }
@media (min-width: 62rem) {
  .with-sidebar { grid-template-columns: minmax(0, 1fr) 17rem; align-items: start; }
}
.sidebar { font-family: var(--sans); font-size: 0.95rem; }
.sidebar h2 { font-size: 1.05rem; margin: 0 0 0.7rem; }
.sidebar section + section { margin-top: 2rem; }
.sidebar ul { list-style: none; margin: 0; padding: 0; }
.sidebar li { margin: 0 0 0.45em; }
.sidebar a { color: var(--ink); text-decoration: none; transition: color var(--t-fast) var(--ease); }
.sidebar a:hover { color: var(--accent); text-decoration: underline; }
.sidebar .count { color: var(--ink-faint); font-size: 0.9em; }
/* The months are not indented under their year. They are the only thing in
   that list, the year is a heading rather than a parent to be nested beneath,
   and 0.9rem of indent bought nothing but a ragged left edge down the rail. */
.sidebar ul ul { margin: 0.3rem 0 0.8rem; }
.sidebar strong { font-weight: 600; }

.card-list { list-style: none; margin: 0; padding: 0; }
.card-list > li { padding-block: 1.6rem; border-top: 1px solid var(--rule); }
.card-list > li:first-child { border-top: 0; padding-top: 0; }
.card-list h2 { margin: 0 0 0.35rem; font-size: var(--step-2); }
.card-list h2 a { text-decoration: none; }
.card-list h2 a:hover { text-decoration: underline; }
.card-list .meta { font: 400 0.9rem var(--sans); color: var(--ink-faint); margin: 0 0 0.6rem; }
.card-list p { margin: 0; color: var(--ink-soft); }

/* The featured image on a card. aria-hidden, so it is decoration beside the
   heading link rather than a second tab stop to the same place. */
.card-list .card-img {
  display: block; margin-bottom: 0.9rem;
  border-radius: 10px; overflow: hidden;
  box-shadow: var(--shadow-sm);
}
.card-list .card-img img {
  display: block; width: 100%; aspect-ratio: 21 / 9;
  object-fit: cover; background: var(--accent-wash);
  transition: opacity 0.15s ease;
}
.card-list .card-img:hover img { opacity: 0.92; }

/* ------------------------------------------------------------- slideshow */
/*
 *  The 2014 Revolution Slider, rebuilt. A horizontal scroll-snap strip, so it
 *  swipes and scrolls with no JavaScript at all; /assets/slider.js adds the
 *  bullets and the autoplay on top. The old one was a 250px letterbox band,
 *  which cut these photos to a sliver --- this is taller and scales with the
 *  viewport instead.
 */
.slideshow {
  position: relative;
  /* Width and centring come from the shared column rule above. */
  margin-block: 0 2.5rem;
  border-radius: var(--radius);
  overflow: hidden;
  box-shadow: var(--shadow);
  background: var(--accent-wash);
}
.slideshow__track {
  display: flex; list-style: none; margin: 0; padding: 0;
  overflow-x: auto; scroll-snap-type: x mandatory;
  scrollbar-width: none;
}
.slideshow__track::-webkit-scrollbar { display: none; }
.slide {
  position: relative; flex: 0 0 100%; scroll-snap-align: start;
  height: clamp(320px, 38vw, 460px);
}
.slide img {
  display: block; width: 100%; height: 100%;
  object-fit: cover; background: var(--accent-wash);
}
/* The scrim is solved, not eyeballed: at its lightest the text sits on 0.68
   black, so even over a pure white photo the composite is rgb(82 82 82) and
   white text clears AA at 7.8:1. design.test.ts holds it there. */
.slide__text {
  position: absolute; inset: auto 0 0 0;
  padding: 1.4rem clamp(1.1rem, 4vw, 2.2rem) clamp(1.1rem, 3vw, 1.8rem);
  background: linear-gradient(to top, rgb(0 0 0 / 0.88), rgb(0 0 0 / 0.68));
  color: #fff;
}
/* The fade lives above the text box rather than inside it, so the scrim under
   the words is never lighter than its 0.68 stop no matter how tall the text
   runs. That is what makes the contrast provable instead of positional. */
.slide__text::before {
  content: ''; position: absolute; left: 0; right: 0; bottom: 100%;
  height: 3.5rem;
  background: linear-gradient(to top, rgb(0 0 0 / 0.68), rgb(0 0 0 / 0));
}
.slide__text h2 { margin: 0 0 0.4rem; font-size: var(--step-2); line-height: 1.15; }
.slide__text h2 a { color: #fff; text-decoration: none; }
.slide__text h2 a:hover { text-decoration: underline; }
/* One link, whole slide clickable: the overlay is on the heading's anchor, so
   there is exactly one tab stop and it carries the slide's name. */
.slide__text h2 a::after { content: ''; position: absolute; inset: 0; }
.slide__text p {
  margin: 0; max-width: 60ch; color: rgb(255 255 255 / 0.92);
  font-size: 0.95rem; line-height: 1.5;
}
@media (max-width: 40rem) {
  .slide__text p { display: none; }     /* the headline carries it on a phone */
}

/* The arrows, on anything with a mouse.
 *
 *  A phone swipes --- the strip is scroll-snap and always has been --- so
 *  arrows there would cover two thumbs' worth of photograph to do what a
 *  swipe already does. `hover: hover` and `pointer: fine` is the honest test
 *  for "there is a pointer to click them with"; the width floor keeps them out
 *  of a narrow window that happens to have a mouse.
 *
 *  `.has-js` because src/client/slider.ts builds them: if the script never
 *  arrives there are no arrows to show, and the strip still swipes and
 *  scrolls.
 *
 *  The scrim is solved, not eyeballed, the same way .slide__text's is. These
 *  sit at mid-height over a photograph that can be any colour, and the worst
 *  case is a white one: white on 0.55 black over white composites to
 *  rgb(115 115 115), which is 4.7:1 --- past the 3:1 that non-text needs, and
 *  past 4.5:1 as though the chevron were text. design.test.ts holds it. */
.slideshow__arrow {
  display: none;
  position: absolute; top: 50%; transform: translateY(-50%);
  z-index: 2;
  width: 2.75rem; height: 2.75rem;
  align-items: center; justify-content: center;
  color: #fff; background: rgb(0 0 0 / 0.55);
  border: 0; border-radius: 50%;
  cursor: pointer;
  transition: background-color var(--t-fast) var(--ease);
}
.slideshow__arrow svg { display: block; width: 22px; height: 22px; }
.slideshow__arrow:hover { background: rgb(0 0 0 / 0.75); }
.slideshow__arrow--prev { left: 0.75rem; }
.slideshow__arrow--next { right: 0.75rem; }

@media (min-width: 40rem) and (hover: hover) and (pointer: fine) {
  .slideshow.has-js .slideshow__arrow { display: flex; }
}

.slideshow__nav {
  position: absolute; left: 0; right: 0; bottom: 0.7rem;
  display: flex; justify-content: center; gap: 0.5rem; z-index: 2;
}
.slideshow__dot {
  width: 10px; height: 10px; padding: 0; border-radius: 50%;
  border: 1px solid rgb(255 255 255 / 0.85); background: rgb(0 0 0 / 0.35);
  cursor: pointer; transition: background 0.15s ease, transform 0.15s ease;
}
.slideshow__dot.is-current { background: #fff; transform: scale(1.25); }
.slideshow__dot:focus-visible { outline: 2px solid #fff; outline-offset: 3px; }

/* WordPress's featured image, above the title. The old theme cropped every
   cover to 881x229 (3.84:1); these originals run from 0.75 to 3.84 and from
   385px to 1936px wide, so something has to give. 21:9 keeps about twice as
   much of a 4:3 photo as the old crop did while still reading as a banner.
   The wash underneath means a short or slow image never shows the texture. */
.cover {
  margin: 0 0 1.6rem; border-radius: var(--radius); overflow: hidden;
  box-shadow: var(--shadow-sm);
}
.cover img {
  display: block; width: 100%; aspect-ratio: 21 / 9;
  object-fit: cover; background: var(--accent-wash);
}

/* Auto page listings, from [sibling-pages] / [child-pages]. */
.page-links { list-style: none; margin: 1.5rem 0 2rem; padding: 0; display: grid; gap: 0.6rem; }
@media (min-width: 40rem) { .page-links { grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); } }
.page-links a {
  display: block; padding: 0.85rem 1rem;
  border: 1px solid var(--glass-line); border-radius: 10px;
  font: 600 1rem var(--sans); text-decoration: none; color: var(--ink);
  background: rgb(255 255 255 / 0.6);
  box-shadow: var(--shadow-sm);
  transition: transform 0.15s ease, border-color 0.15s ease;
}
.page-links a:hover {
  border-color: var(--accent); color: var(--accent);
  background: #fff; transform: translateY(-1px);
}
.page-links .is-current > a {
  border-color: var(--accent); background: var(--accent-wash);
  color: var(--accent-deep);
}
.page-links .nav__sub,
.page-links ul { list-style: none; margin: 0.6rem 0 0 1rem; padding: 0; display: grid; gap: 0.5rem; }

/* --------------------------------------------------------------- unlock */

/* Unlock and 404 are a short card, not a full-width sheet. :has is the whole
   support story here --- an old browser just gets the wide panel. */
main > .shell:has(> .panel) { max-width: 46rem; }
.panel { max-width: 34rem; margin-inline: auto; }
.panel__note {
  margin-top: 1.75rem; padding-top: 1.25rem;
  border-top: 1px solid var(--rule);
  font: 400 0.92rem/1.6 var(--sans); color: var(--ink-faint);
}
.contact-form, .unlock-form { margin-top: 1.5rem; }
.cf-turnstile { margin-bottom: 1.25rem; }
.panel h1 { font-size: var(--step-3); }
.panel p { color: var(--ink-soft); }

.field { display: block; margin-bottom: 1.1rem; font-family: var(--sans); }
.field > span { display: block; font-weight: 600; margin-bottom: 0.35rem; font-size: 0.95rem; }
.field input, .field textarea {
  width: 100%; padding: 0.7rem 0.8rem;
  border: 1px solid var(--rule-strong); border-radius: var(--radius);
  font: var(--step-0) var(--sans); color: var(--ink);
  background: rgb(255 255 255 / 0.85);
}
.field input:focus, .field textarea:focus { outline: var(--focus); outline-offset: 1px; border-color: var(--accent); }
.field textarea { min-height: 9rem; resize: vertical; }

.btn {
  display: inline-block; border: 0; border-radius: var(--radius);
  background: var(--accent); color: #fff;
  font: 600 1rem var(--sans); padding: 0.75rem 1.4rem; cursor: pointer;
  text-decoration: none;
  transition: background-color var(--t-fast) var(--ease);
}
.btn:hover { background: var(--accent-deep); color: #fff; }

/* The Send button between the click and the answer (src/client/contact.ts).
   Dimmed rather than greyed, so it still reads as the button that was pressed,
   and a ring beside the label because "Sending…" alone is easy to miss. */
.btn.is-busy {
  display: inline-flex; align-items: center; gap: 0.6em;
  background: var(--accent); opacity: 0.75; cursor: progress;
}
.btn.is-busy::after {
  content: ""; width: 0.9em; height: 0.9em; border-radius: 50%;
  border: 2px solid rgb(255 255 255 / 0.4); border-top-color: #fff;
  animation: btn-spin 0.8s linear infinite;
}
@keyframes btn-spin { to { transform: rotate(1turn); } }

.notice { padding: 0.9rem 1.1rem; border-radius: var(--radius); font-family: var(--sans); font-size: 0.95rem; }
.notice--error { background: #fdf0ef; border: 1px solid #f0c8c4; color: #8a2b22; }
.notice--ok { background: var(--accent-wash); border: 1px solid #c3d9ea; color: var(--accent-deep); }

/* ------------------------------------------------- odds the templates use */

.pager {
  display: flex; align-items: center; justify-content: space-between;
  gap: 1rem; margin-top: 2.5rem; padding-top: 1.5rem;
  border-top: 1px solid var(--rule);
  font: 600 0.95rem var(--sans);
}
.pager span { color: var(--ink-faint); font-weight: 400; }
.pager a { text-decoration: none; }
.pager a:hover { text-decoration: underline; }

.page-tail { margin-top: 3rem; padding-top: 1.5rem; border-top: 1px solid var(--rule); }
.page-tail a { font: 600 0.95rem var(--sans); text-decoration: none; }
.page-tail a:hover { text-decoration: underline; }

/* Two blocks the homepage's old page-builder shortcodes become. */
.panel-wide {
  margin: 2.5rem 0; padding: clamp(1.25rem, 3vw, 2rem);
  background: rgb(255 255 255 / 0.55);
  border: 1px solid var(--glass-line);
  border-radius: 12px;
  box-shadow: var(--shadow-sm);
}
.panel-wide h2 { margin-top: 0; font-size: var(--step-2); }
.panel-wide p:last-child { margin-bottom: 0; }

.callout {
  margin: 3rem 0 1rem; padding: clamp(1.5rem, 4vw, 2.5rem);
  background: linear-gradient(140deg, rgb(37 110 165 / 0.10), rgb(255 255 255 / 0.55));
  border: 1px solid var(--glass-line);
  border-radius: 14px;
  box-shadow: var(--shadow-sm);
  text-align: center;
}
.callout h2 { margin-top: 0; color: var(--accent-deep); }
.callout p:last-child { margin-bottom: 0; }

.cta { margin: 1.5rem 0; }
.center { text-align: center; }
.icon { vertical-align: -0.2em; color: var(--accent); }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap;
}

.prose.home > .panel-wide,
.prose.home > .callout { grid-column: wide; }

/* --------------------------------------------------------------- search */

.search-form { display: flex; gap: 0.6rem; margin-bottom: 2rem; }
.search-form input { flex: 1; }
#search-results mark { background: #fdf3cf; color: inherit; padding: 0 0.1em; }

/* --------------------------------------------------------------- footer */

.sitefoot {
  border-top: 1px solid var(--glass-line);
  background: var(--glass);
  -webkit-backdrop-filter: var(--blur);
  backdrop-filter: var(--blur);
  padding-block: 2.5rem 3rem; margin-top: 4rem;
  font-family: var(--sans); font-size: 0.95rem; color: var(--ink-soft);
}
.sitefoot ul { list-style: none; margin: 0 0 1.5rem; padding: 0; display: flex; flex-wrap: wrap; gap: 0.4rem 1.5rem; }
.sitefoot a { color: var(--ink); text-decoration: none; transition: color var(--t-fast) var(--ease); }
.sitefoot a:hover { color: var(--accent); text-decoration: underline; }
.sitefoot small { color: var(--ink-faint); }

@media print {
  .masthead, .sitefoot, .navbar, .nav-btn, .skip, .pager { display: none; }
  /* The panel loses its padding below, so the title's 20px tab would hang off
     the edge of the paper. Most browsers drop background colours on print
     anyway; this makes that deliberate rather than lucky. */
  .page-head h1 { box-shadow: none; }
  /* Paper has its own pages. A flex column with a viewport min-height would
     stretch the first one to fill and give the engine a flex container to
     break across pages, which is where print layout goes wrong. */
  body { font-size: 11pt; background: #fff; display: block; min-height: 0; }
  main { flex: none; }
  body::before { display: none; }
  main > .shell,
  .with-sidebar > div,
  .with-sidebar > .sidebar {
    padding: 0; background: none; border: 0; box-shadow: none;
    -webkit-backdrop-filter: none; backdrop-filter: none;
  }
  .prose > * { grid-column: 1 / -1; }
}

/* Backdrop blur is a per-frame GPU cost. Anyone who has asked for less motion
   has usually asked for less of this too, and the layout does not depend on
   it --- the panels just become solid. */
@media (prefers-reduced-transparency: reduce) {
  .masthead__bar, .navbar, .sitefoot, .nav__sub,
  main > .shell, .with-sidebar > div, .with-sidebar > .sidebar {
    background: var(--paper);
    -webkit-backdrop-filter: none;
    backdrop-filter: none;
  }
}
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { transition-duration: 0.01ms !important; }
  .gallery a:hover, .page-links a:hover { transform: none; }
  /* A ring that cannot turn is not a spinner; the label says it alone. */
  .btn.is-busy::after { display: none; }
}
