/* One small hand-written stylesheet. No framework, no build step.
   The public site is server-rendered HTML; nothing here is hydrated.

   Proposal, 2026-08. Same class vocabulary as before — every selector that
   existed still exists — with typographic scale, a self-hosted text face, a
   .subscriptions row that survives a narrow screen, and semantic colours moved
   into custom properties so the dark theme can lift them to a legible contrast. */

/* Self-hosted, same-origin: `default-src 'self'` covers it with no CSP change.
   Two files, latin only, variable weight, swap — so a cold cache renders in the
   fallback serif immediately and never blocks paint. */
@font-face {
  font-family: "Porch Serif";
  src: url("/static/fonts/source-serif-4-latin.woff2") format("woff2-variations");
  font-weight: 300 700;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Porch Serif";
  src: url("/static/fonts/source-serif-4-latin-italic.woff2") format("woff2-variations");
  font-weight: 300 700;
  font-style: italic;
  font-display: swap;
}

:root {
  color-scheme: light dark;

  --ink: #1a1a1a;
  --paper: #fdfdfb;
  --muted: #5c5c5c;
  --rule: #d8d6d0;
  /* A second, lighter hairline: rows inside a list should whisper, the rule that
     ends a section should not. */
  --rule-soft: #e7e5df;
  /* The only fill in the stylesheet. Used by every inset block so there is one
     surface, not five. */
  --wash: #f4f2ec;
  --link: #1a4d7a;

  /* Semantic colours were literals in six places. As properties the dark theme
     can restate them; at #a33 on #16161a the old error red was 3.0:1. */
  --good: #2d7d4f;
  --warn: #a86400;
  --bad: #b3261e;
  --err: #a33333;
  --mark-bg: #ffe680;
  --mark-ink: #1a1a1a;
  --warn-bg: #7a3d00;
  --warn-ink: #fff6ec;

  /* One measure for pages that are mostly interface, a narrower one for prose
     somebody actually reads through. */
  --measure: 42rem;
  --measure-read: 35rem;

  --font-text: "Porch Serif", ui-serif, Charter, "Iowan Old Style", Georgia, serif;
  --font-ui: system-ui, -apple-system, "Segoe UI", sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root {
    --ink: #e8e6e1;
    --paper: #16161a;
    --muted: #a3a099;
    --rule: #34343a;
    --rule-soft: #26262c;
    --wash: #1e1e24;
    --link: #8ab4de;

    --good: #6fbf8f;
    --warn: #d6a251;
    --bad: #e58a82;
    --err: #e79b95;
    --mark-bg: #4a3c00;
    --mark-ink: #ffeaa8;
  }
}

/* ---------------------------------------------------------------- *
 * Community palettes
 *
 * Four grounds, chosen once by an administrator. `city` is the :root above and
 * needs nothing here — it is what every community rendered on before this
 * existed, so it emits no attribute at all.
 *
 * Seven properties move: --ink, --paper, --muted, --rule, --rule-soft, --wash
 * and --link. --good, --warn, --bad, --err, --warn-bg/--warn-ink and --mark-*
 * are DELIBERATELY IDENTICAL in all four. An error is red in every community and
 * the search highlight is the same yellow everywhere: if semantic colors moved
 * with the palette, a community that picked green would get green-tinted error
 * states and the meaning would blur — and the contrast work behind those tokens
 * would need doing four times over instead of once.
 *
 * Every --link was checked at 4.5:1 or better against its own --paper in both
 * schemes. That is the one pair here that can fall below the line, because --ink
 * on --paper is near-black on near-white in every palette. Re-check it if you
 * touch a hex.
 * ---------------------------------------------------------------- */

[data-palette="forest"] {
  --ink: #1b201b;
  --paper: #fbfcf8;
  --muted: #566053;
  --rule: #d2dccf;
  --rule-soft: #e5ebe1;
  --wash: #eff4ea;
  --link: #2f5d3a;
}
[data-palette="desert"] {
  --ink: #211e17;
  --paper: #fdfaf2;
  --muted: #655b49;
  --rule: #e2d8c1;
  --rule-soft: #ede6d5;
  --wash: #f7f1e1;
  --link: #855610;
}
[data-palette="water"] {
  --ink: #141c1e;
  --paper: #f7fbfb;
  --muted: #4f6367;
  --rule: #cfe0e1;
  --rule-soft: #e2eded;
  --wash: #e8f3f3;
  --link: #0f6870;
}

@media (prefers-color-scheme: dark) {
  [data-palette="forest"] {
    --ink: #e6eae2;
    --paper: #141814;
    --muted: #9aa596;
    --rule: #2f382e;
    --rule-soft: #232a22;
    --wash: #1c221b;
    --link: #93c9a3;
  }
  [data-palette="desert"] {
    --ink: #ece7dc;
    --paper: #191712;
    --muted: #a89c86;
    --rule: #3a3428;
    --rule-soft: #2a251c;
    --wash: #221e16;
    --link: #dfb264;
  }
  [data-palette="water"] {
    --ink: #dfeaea;
    --paper: #101819;
    --muted: #91a5a7;
    --rule: #2a3839;
    --rule-soft: #1f2b2c;
    --wash: #182324;
    --link: #6fc7cf;
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 1.0625rem/1.6 var(--font-text);
  -webkit-text-size-adjust: 100%;
}

main {
  max-width: var(--measure);
  margin: 0 auto;
  padding: 2.5rem 1.25rem 5rem;
  /* A size container, so the thread page's gutter can collapse on the width of
     the COLUMN rather than the viewport — a viewport query would be right only
     by coincidence. First and so far only container in the stylesheet; every
     @media block here is viewport-scoped and unaffected. */
  container-type: inline-size;
}

/* The thread page widens for its author gutter: 10rem gutter + 2rem gap + the
   reading measure needs more than --measure has. Everything in it that is NOT
   the message band — the reply box, the hints — stays at today's effective
   width, so only the conversation itself uses the extra room.

   The header and footer widen WITH it, via the body class: the three bands of
   the page share one pair of edges, or the nav rule and the footer rule read as
   belonging to some other page. Same deal for the flier editor's `wide`. */
main.thread { max-width: 54rem; }
main.thread > :not(.messages) { max-width: 39.5rem; }
.page-thread header, .page-thread .page-foot { max-width: 54rem; }
.page-wide header, .page-wide .page-foot { max-width: 62rem; }

/* The navigation lives in <header>, OUTSIDE <main>, so the nav landmark is not
   nested inside the main landmark and "skip to content" lands below it rather than
   above. That puts it outside the element carrying the measure, so the header takes
   the same column — including the same horizontal padding, which is what keeps the
   nav's rule flush with the content edge instead of overhanging it by the gutter.

   `.env-banner` sits before the header, not inside it, so it stays full-bleed. */
header {
  max-width: var(--measure);
  margin: 0 auto;
  padding: 2.5rem 1.25rem 0;
}

@media (max-width: 34rem) {
  main { padding: 1.5rem 1.05rem 4rem; }
  header { padding: 1.5rem 1.05rem 0; }
}

/* The nav already ends in a rule and its own margin; main's top padding on top of
   that would double the gap. */
header + main { padding-top: 0; }

/* Serif for anything that is read, sans for anything that is operated. The
   split is what tells a byline apart from a message without a colour or a rule. */
h1 {
  font-size: clamp(1.7rem, 1.45rem + 1vw, 2.05rem);
  line-height: 1.15;
  letter-spacing: -0.014em;
  font-weight: 600;
  margin: 0 0 0.6rem;
  text-wrap: balance;
}
h2 {
  font-size: 1.28rem;
  line-height: 1.25;
  letter-spacing: -0.006em;
  font-weight: 600;
  margin: 2.4rem 0 0.6rem;
  text-wrap: balance;
}
p { margin: 0 0 1rem; text-wrap: pretty; }
a {
  color: var(--link);
  text-decoration-thickness: 0.055em;
  text-underline-offset: 0.16em;
}
a:hover { text-decoration-thickness: 0.11em; }
code { font-size: 0.9375em; }
hr { border: 0; border-top: 1px solid var(--rule); margin: 2.5rem 0; }

/* The default ring is the browser's, which in several is invisible on this
   paper. One ring, everywhere, in the accent. */
:focus-visible { outline: 2px solid var(--link); outline-offset: 2px; }

/* Driven by the deployed ENVIRONMENT var, so it cannot drift from what is
   actually running. Deliberately hard to miss. */
.env-banner {
  background: var(--warn-bg);
  color: var(--warn-ink);
  padding: 0.6rem 1rem;
  font: 600 0.875rem/1.4 var(--font-ui);
  text-align: center;
  letter-spacing: 0.01em;
}
.env-banner a { color: inherit; }

/* ---- forms ---- */
label {
  display: block;
  font: 600 0.9375rem/1.4 var(--font-ui);
  margin: 1.4rem 0 0.3rem;
}
.hint + label { margin-top: 1.7rem; }

input[type="text"], input[type="email"], input[type="search"], select, textarea {
  width: 100%;
  padding: 0.55rem 0.65rem;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--paper);
  color: var(--ink);
  font: 1rem/1.45 var(--font-ui);
}
input[type="text"], input[type="email"], input[type="search"], select { min-height: 2.75rem; }
input:focus, select:focus, textarea:focus { border-color: var(--link); }
textarea { padding: 0.6rem 0.7rem; }

/* A file input is the one control browsers refuse to style through the shared
   field rule — left alone it renders in Arial at 21px on a native light-gray
   chip, ignoring both the dark theme and every palette, at under half the 44px
   hit target this stylesheet enforces on every sibling control. The outer
   element is the field; ::file-selector-button is the button inside it. */
input[type="file"] {
  width: 100%;
  min-height: 2.75rem;
  padding: 0.4rem 0.5rem;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--paper);
  color: var(--ink);
  font: 0.9375rem/1.45 var(--font-ui);
}
input[type="file"]::file-selector-button {
  margin-right: 0.7rem;
  padding: 0.35rem 0.8rem;
  min-height: 2rem;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--wash);
  color: var(--ink);
  font: 600 0.875rem/1.2 var(--font-ui);
  cursor: pointer;
}
input[type="file"]::file-selector-button:hover { border-color: var(--muted); }

button {
  margin-top: 1.6rem;
  padding: 0.6rem 1.15rem;
  min-height: 2.75rem;
  border: 1px solid var(--ink);
  border-radius: 4px;
  background: var(--ink);
  color: var(--paper);
  font: 600 0.9375rem/1.2 var(--font-ui);
  letter-spacing: 0.01em;
  cursor: pointer;
}
button:hover { background: color-mix(in oklab, var(--ink) 86%, var(--paper)); }

button.link {
  margin: 0; padding: 0; border: 0; background: none; min-height: 0;
  color: var(--link);
  font: 0.9375rem/1.45 var(--font-ui);
  text-decoration: underline;
  text-underline-offset: 0.16em;
  cursor: pointer;
}
button.link:hover { background: none; text-decoration-thickness: 0.11em; }

label.checkbox {
  display: flex; gap: 0.6rem; align-items: baseline;
  font: 400 1rem/1.5 var(--font-ui);
  margin: 1.2rem 0 0.25rem;
}
label.checkbox input { width: auto; min-height: 0; accent-color: var(--link); }

/* A fieldset is a grouping, not a box. Both of ours — the import form's list
   picker and the palette picker — were carrying the browser's default inset
   border, which is the one piece of chrome an ink-on-paper page has no room for.
   The legend then matches the h2 the other settings sections use, so a group with
   a legend and a section with a heading read the same. */
fieldset { border: 0; padding: 0; margin: 0; min-width: 0; }
legend {
  padding: 0;
  font: 600 1.25rem/1.3 var(--font-ui);
  margin-top: 2.25rem;
}

/* ---- palette picker ---- */

/* Four unlabeled miniatures of the site's own front page. Nothing to name and
   nothing to configure: the choice is made by looking. Each tile shows the light
   ground with its dark ground beneath, because one pick covers both. */
.palettes {
  list-style: none;
  padding: 0;
  margin: 0.9rem 0 0.4rem;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: 0.9rem;
}
.palettes li { position: relative; }
/* The radio is the control and the tile is its label. Hidden but focusable, so
   arrow keys still move between palettes as in any radio group. Not
   `display: none`, which would take it out of the tab order entirely. */
.palettes input {
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0;
  margin: 0;
}
.palettes label {
  display: block;
  margin: 0;
  padding: 0.3rem;
  border: 1px solid var(--rule);
  border-radius: 6px;
  cursor: pointer;
}
.palettes label:hover { border-color: var(--muted); }
.palettes input:checked + label {
  border-color: var(--ink);
  box-shadow: inset 0 0 0 1px var(--ink);
}
.palettes input:focus-visible + label {
  outline: 2px solid var(--link);
  outline-offset: 2px;
}
/* The miniature: two stacked panes reading the tile's own --s-* values, which are
   the palette's real tokens rather than an approximation of them. A picker that
   shows a color the site does not use is worse than no picker — so if you change
   a palette above, change its .p-* block below too. */
.swatch {
  display: block;
  border-radius: 4px;
  overflow: hidden;
}
.swatch-pane {
  display: block;
  padding: 0.5rem 0.55rem;
  background: var(--s-paper);
}
.swatch-pane + .swatch-pane {
  background: var(--s-paper-d);
  padding-top: 0.45rem;
  padding-bottom: 0.5rem;
}
.swatch-bar, .swatch-line, .swatch-link {
  display: block;
  border-radius: 999px;
}
.swatch-bar {
  height: 0.5rem;
  width: 62%;
  background: var(--s-ink);
  margin-bottom: 0.35rem;
}
.swatch-line {
  height: 0.25rem;
  background: var(--s-muted);
  margin-bottom: 0.22rem;
}
/* Counted by element type, so among the four sibling spans this is the SECOND
   .swatch-line — bar, line, line, link. The lower pane has only three spans and
   its third is the link, so nothing there matches, which is the intent. */
.swatch-line:nth-of-type(3) { width: 78%; }
.swatch-link {
  height: 0.32rem;
  width: 44%;
  background: var(--s-link);
  margin-top: 0.35rem;
}
.swatch-pane + .swatch-pane .swatch-bar { background: var(--s-ink-d); }
.swatch-pane + .swatch-pane .swatch-line { background: var(--s-muted-d); }
.swatch-pane + .swatch-pane .swatch-link { background: var(--s-link-d); }

/* --s-muted is a light TINT of the ground, not --muted itself: at four pixels
   tall a true muted gray reads as black. These stand in for text at 1/12 scale. */
.p-city {
  --s-paper: #fdfdfb; --s-ink: #1a1a1a; --s-muted: #c9c7c1; --s-link: #1a4d7a;
  --s-paper-d: #16161a; --s-ink-d: #e8e6e1; --s-muted-d: #4a4a52; --s-link-d: #8ab4de;
}
.p-forest {
  --s-paper: #fbfcf8; --s-ink: #1b201b; --s-muted: #c6d0c2; --s-link: #2f5d3a;
  --s-paper-d: #141814; --s-ink-d: #e6eae2; --s-muted-d: #43503f; --s-link-d: #93c9a3;
}
.p-desert {
  --s-paper: #fdfaf2; --s-ink: #211e17; --s-muted: #ddd2b8; --s-link: #855610;
  --s-paper-d: #191712; --s-ink-d: #ece7dc; --s-muted-d: #4e4636; --s-link-d: #dfb264;
}
.p-water {
  --s-paper: #f7fbfb; --s-ink: #141c1e; --s-muted: #c6dcdd; --s-link: #0f6870;
  --s-paper-d: #101819; --s-ink-d: #dfeaea; --s-muted-d: #35494b; --s-link-d: #6fc7cf;
}

/* ---- tenant site ---- */
.site-nav {
  display: flex; gap: 0.35rem 1.15rem; align-items: baseline; flex-wrap: wrap;
  font: 0.9375rem/1.4 var(--font-ui);
  padding-bottom: 0.85rem; margin-bottom: 2rem;
  border-bottom: 1px solid var(--rule);
}
.site-nav a { text-decoration: none; }
.site-nav a:hover { text-decoration: underline; }
.site-nav .brand {
  font: 600 1.0625rem/1.3 var(--font-text);
  letter-spacing: -0.008em;
  color: var(--ink);
  margin-right: 0.4rem;
}
.site-nav .spacer { flex: 1 1 1rem; }

/* Rendered only when the queue is non-empty, so an empty badge can never sit
   there reading as zero. Tabular figures so 1 and 11 are the same width and the
   nav does not shift as the queue changes. */
.site-nav .count {
  display: inline-block;
  margin-left: 0.3rem;
  padding: 0.05rem 0.4rem;
  border-radius: 999px;
  background: var(--link);
  color: var(--paper);
  font: 600 0.8125rem/1.5 var(--font-ui);
  font-variant-numeric: tabular-nums;
}

/* On a phone the community's name takes the line it deserves and the four
   links sit under it, rather than all five wrapping into a ragged block. */
@media (max-width: 30rem) {
  .site-nav .brand { flex: 1 1 100%; margin: 0 0 0.4rem; }
  .site-nav .spacer { display: none; }
}

.tagline {
  color: var(--muted);
  font-size: 1.09rem;
  margin: -0.15rem 0 1.25rem;
  max-width: var(--measure-read);
}
.hint { color: var(--muted); font: 0.875rem/1.5 var(--font-ui); }
p.hint { margin: 0.35rem 0 1rem; }
input + .hint, select + .hint, textarea + .hint { margin-top: 0.4rem; }
.byline {
  color: var(--muted);
  font: 0.875rem/1.45 var(--font-ui);
  margin: 0 0 1rem;
}

/* The date IS the permalink — the archive convention, and it removes a whole
   accent-coloured line from under every message. Underlined on hover only, so the
   byline reads as one quiet line until somebody reaches for it. */
.byline a {
  color: inherit;
  text-decoration: none;
}
.byline a:hover { text-decoration: underline; }

/* Administrator tools inside a conversation. They have to be present and they
   have to lose: muted, small, sans, and no accent colour until hovered — so a page
   of prose is not competing with a page of links. */
.message-tools, .thread-tools {
  font: 0.8125rem/1.5 var(--font-ui);
  margin: 1rem 0 0;
}
.message-tools a, .thread-tools a,
.message-tools button.link, .thread-tools button.link {
  color: var(--muted);
  font-size: inherit;
  text-decoration-color: var(--rule);
  text-underline-offset: 0.16em;
  /* The remove action is a BUTTON (it posts), and a button centres its text —
     invisible until the label wraps in the floating panel, where the one form
     control read as misaligned against its sibling links. */
  text-align: left;
}
.message-tools a:hover, .thread-tools a:hover,
.message-tools button.link:hover, .thread-tools button.link:hover {
  color: var(--link);
  text-decoration-color: currentColor;
}

/* Both sets of tools collapse. `<details>` needs no script, so this survives
   JavaScript being off — it is markup, not an interaction. The native disclosure
   triangle gives way to a quiet +/– so nine folded tool lines on a nine-message
   thread read as punctuation, not as nine widgets. Applied to BOTH sets, so
   message- and thread-level tools stay the same control. */
details.message-tools summary, details.thread-tools summary {
  color: var(--muted);
  cursor: pointer;
  width: fit-content;
  list-style: none;
}
details.message-tools summary::-webkit-details-marker,
details.thread-tools summary::-webkit-details-marker { display: none; }
details.message-tools summary::before,
details.thread-tools summary::before { content: "+  "; }
details.message-tools[open] summary::before,
details.thread-tools[open] summary::before { content: "\2013  "; }
details.message-tools summary:hover, details.thread-tools summary:hover { color: var(--link); }
details.message-tools ul, details.thread-tools ul {
  list-style: none;
  margin: 0.6rem 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.45rem;
}

/* The thread's own tools close the page, so they get the rule and the room. A
   message's sit inside the flow of the thread and get neither — a hairline under
   every message would chop the conversation into cards. */
details.thread-tools {
  margin: 1.5rem 0 2.5rem;
  padding-top: 1rem;
  border-top: 1px solid var(--rule-soft);
}

.notice {
  padding: 0.9rem 1.05rem;
  border: 1px solid var(--rule-soft);
  border-radius: 4px;
  background: var(--wash);
  margin: 0 0 1.6rem;
}
.notice h2 { font: 600 1.02rem/1.3 var(--font-ui); margin: 0 0 0.4rem; }
.notice p { margin: 0 0 0.5rem; }
.notice p:last-child { margin-bottom: 0; }
.notice--error { border-color: var(--err); background: transparent; }
.notice--error ul { margin: 0.5rem 0 0; padding-left: 1.25rem; }
.notice--error p { margin: 0; }
/* Not an error: the thing worked, and there is something about it worth knowing
   before acting. `--warn` already exists and already has a dark-mode value, so this
   inherits both rather than introducing a second opinion about what amber is. */
.notice--warning { border-color: var(--warn); background: transparent; }
.notice--warning p { margin: 0; }

/* ---- the invitation to sign in ----

   On the community's front page, after the content. Reuses the `.notice`
   surface rather than introducing another kind of panel.

   Before this it was `.hint`-sized muted text directly under the rule closing
   the Resources list — the smallest thing on the page, reading as a footnote to
   a list of PDFs, and the only invitation to participate the front page has. */
.signin {
  margin: 2.5rem 0 0;
  padding: 1.1rem 1.2rem;
  border: 1px solid var(--rule-soft);
  border-radius: 4px;
  background: var(--wash);
  max-width: var(--measure-read);
}
.signin h2 {
  font: 600 1.05rem/1.3 var(--font-ui);
  margin: 0 0 0.35rem;
}
.signin p { margin: 0 0 0.6rem; }
.signin p:last-of-type { margin-bottom: 0; }
.signin a {
  font-weight: 600;
  text-underline-offset: 0.2em;
}

/* Available to a screen reader, absent from the page. Used for the "Error:"
   prefix on field messages, so the state is spoken rather than only coloured,
   and for the row context on every otherwise-identical "Remove" button. */
.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Five nav links precede the content of every page. Hidden until focused, so
   the first Tab on any screen is a way past them. */
.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
}
.skip-link:focus {
  left: 0.5rem; top: 0.5rem;
  z-index: 10;
  padding: 0.6rem 0.9rem;
  background: var(--paper);
  color: var(--link);
  border: 2px solid var(--link);
  border-radius: 4px;
  font: 600 0.9375rem/1.2 var(--font-ui);
}

/* Not colour alone: a red border says nothing to a red-green colourblind reader,
   and nothing at all to a screen reader. The word "Error" is in the text. */
.field-error {
  color: var(--err);
  font: 0.875rem/1.5 var(--font-ui);
  margin: 0.3rem 0 0;
}
.field-error::before { content: "▲ "; }
input[aria-invalid="true"], select[aria-invalid="true"], textarea[aria-invalid="true"] {
  border-color: var(--err);
  border-width: 2px;
}
#error-summary h2 { margin: 0 0 0.5rem; }
#error-summary:focus { outline: 2px solid var(--err); outline-offset: 2px; }

/* Development mailbox. Collapsed by default so a long list stays scannable;
   the newest is opened, because that is the one you came for. */
.devmail {
  border: 1px solid var(--rule-soft); border-radius: 4px;
  background: var(--wash);
  padding: 0.55rem 0.8rem; margin: 0 0 0.5rem;
}
.devmail summary { cursor: pointer; font: 0.9375rem/1.5 var(--font-ui); }
.devmail summary .hint { display: block; }
.devmail pre { white-space: pre-wrap; word-break: break-word; }

/* A notice is the most important thing on the page while it is running, so it
   gets the one enclosed, tinted block in the stylesheet — the severity colours
   the edge and, at 7%, the fill, so which kind it is survives being skimmed. */
.alert {
  border: 1px solid var(--rule);
  border-left: 4px solid var(--rule);
  border-radius: 4px;
  background: var(--wash);
  padding: 0.85rem 1.05rem;
  margin: 0 0 1.75rem;
}
.alert h2 { font: 600 1.05rem/1.3 var(--font-text); margin: 0 0 0.25rem; }
.alert p { margin: 0 0 0.5rem; }
.alert p:last-child { margin-bottom: 0; }
.alert--warning {
  border-color: color-mix(in oklab, var(--warn) 28%, var(--rule));
  border-left-color: var(--warn);
  background: color-mix(in oklab, var(--warn) 7%, var(--paper));
}
.alert--urgent {
  border-color: color-mix(in oklab, var(--bad) 28%, var(--rule));
  border-left-color: var(--bad);
  background: color-mix(in oklab, var(--bad) 7%, var(--paper));
}

/* ---- lists of things ---- */
/* Pinned threads sit above the chronological run, and the boundary between the
   two has to be legible or the archive just looks out of order. A heading names
   the group; the rule under the last pinned row is where "now we are in date
   order" begins. */
.pinned-heading {
  font-size: 0.8125rem;
  font-family: var(--font-ui);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 0.25rem;
}
.threads--pinned { margin-bottom: 2rem; border-bottom: 2px solid var(--rule); }
.threads--pinned li:last-child { border-bottom: 0; }

.list-index, .threads, .results, .subscriptions { list-style: none; padding: 0; margin: 0; }
.list-index li, .threads li, .results li {
  padding: 0.85rem 0;
  border-bottom: 1px solid var(--rule-soft);
}
.list-index > li:first-child, .threads > li:first-child, .results > li:first-child {
  border-top: 1px solid var(--rule-soft);
}
.list-index .hint, .threads .hint, .results .hint { display: block; margin-top: 0.2rem; }
.list-index > li > a, .threads > li > a, .results > li > a {
  font: 600 1.0625rem/1.35 var(--font-text);
  letter-spacing: -0.004em;
  text-decoration: none;
}
.list-index > li > a:hover, .threads > li > a:hover, .results > li > a:hover {
  text-decoration: underline;
}

/* ---- events ---- */
/* A date column and a title, nothing else. Small communities have a handful of
   events, so this is a list — not a calendar, not a card grid. The date is a
   grid column rather than a prefix so titles align down the page. */
ul.events { list-style: none; padding: 0; margin: 0.6rem 0 1.5rem; }
ul.events > li {
  display: grid;
  grid-template-columns: 4.75rem 1fr;
  column-gap: 1.1rem;
  padding: 0.85rem 0;
  border-bottom: 1px solid var(--rule-soft);
}
ul.events > li:first-child { border-top: 1px solid var(--rule-soft); }
.event-when {
  font: 0.875rem/1.35 var(--font-ui);
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  padding-top: 0.12rem;
}
/* The day of the month is the thing you scan for. */
.event-when strong {
  display: block;
  font: 600 1.0625rem/1.15 var(--font-ui);
  color: var(--ink);
}
.event-title { font-weight: 600; text-decoration: none; }
.event-title:hover { text-decoration: underline; }
.event-where { display: block; margin-top: 0.1rem; }
/* Same rule as the admin tools in the reading-first pass: present, and losing
   to the content. */
.event-add {
  font: 0.8125rem/1.5 var(--font-ui);
  color: var(--muted);
  text-decoration-color: var(--rule);
  text-underline-offset: 0.16em;
}
.event-add:hover { color: var(--link); text-decoration-color: currentColor; }
.event-past .event-when strong { color: var(--muted); font-weight: 400; }
@media (max-width: 34rem) {
  ul.events > li { grid-template-columns: 4.25rem 1fr; column-gap: 0.85rem; }
}

/* The row that eight screens are built from: subscriptions, lists, blocks,
   roster, domains, alerts, import results, tenants.

   The label takes the row and pushes the actions right while there is room for
   both; below that the basis resolves to 100%, the label claims its own line and
   the actions wrap underneath as a group — instead of one action per line with
   the last one squeezed to two characters wide. No markup change. */
.subscriptions li {
  display: flex; flex-wrap: wrap; align-items: baseline;
  column-gap: 1.25rem; row-gap: 0.3rem;
  padding: 0.8rem 0;
  border-bottom: 1px solid var(--rule-soft);
}
.subscriptions > li:first-child { border-top: 1px solid var(--rule-soft); }
.subscriptions li > span:first-child { flex: 1 1 min(100%, 18rem); min-width: 0; }
.subscriptions li > span:first-child > .hint { display: block; margin-top: 0.15rem; }
.subscriptions li > span:first-child a { font-weight: 600; text-decoration: none; }
.subscriptions li > span:first-child a:hover { text-decoration: underline; }
.subscriptions form { margin: 0; display: flex; gap: 0.4rem; align-items: center; }

/* A row action may carry a control — the alert list puts a duration beside "Show
   again". The global form rule stretches every select to 100%, which is right in a
   stacked form and wrong inside a flex row, so it is sized to its content here. */
.subscriptions select {
  width: auto;
  padding: 0.15rem 0.3rem;
  font: 0.875rem/1.5 var(--font-ui);
}
.subscriptions li > a, .subscriptions li > span:not(:first-child) {
  flex: 0 0 auto;
  font: 0.9375rem/1.45 var(--font-ui);
}
/* Actions are the whole hit area on a phone, so they get a real one. */
.subscriptions li > a, .subscriptions li button.link {
  display: inline-flex; align-items: center; min-height: 2.25rem;
}
@media (max-width: 34rem) {
  .subscriptions li > a, .subscriptions li button.link { min-height: 2.75rem; }
}

.search {
  display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center;
  margin: 1.25rem 0 2rem;
}
.search input[type="search"] { flex: 1 1 14rem; width: auto; min-width: 0; }
.search select { flex: 0 1 auto; width: auto; }
.search button { margin: 0; }
.search a { font: 0.9375rem/1.45 var(--font-ui); }
/* An error message inside the row is a block, not a fourth control. */
.search .field-error { flex: 1 1 100%; margin: 0; }

/* Three corpora, one query. A group is a heading, a capped list, and a link to
   the rest — the same shape as "All upcoming events", so it needs no new
   vocabulary. */
.result-group { margin: 0 0 2.25rem; }
.result-group > h2 { margin-top: 0; }
.result-group > .results { margin-bottom: 0.6rem; }

/* A past event in search results, and a removed message's flag in a thread's
   byline: the same quiet pill, because both say a state in a word rather than a
   colour alone. */
.removed-flag,
.result-past {
  font: 600 0.75rem/1.5 var(--font-ui);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  border: 1px solid var(--rule);
  border-radius: 999px;
  padding: 0.02rem 0.4rem;
  margin-right: 0.15rem;
  white-space: nowrap;
}

.snippet { margin: 0.3rem 0 0; max-width: var(--measure-read); }
mark { background: var(--mark-bg); color: var(--mark-ink); }

/* ---- the archive ---- */
.message { padding: 1.75rem 0; border-bottom: 1px solid var(--rule); }
.message:last-of-type { border-bottom: 0; }
.message h1 { margin: 0 0 0.4rem; }
.message h2 {
  font-size: 1.22rem;
  line-height: 1.3;
  letter-spacing: -0.004em;
  margin: 0 0 0.35rem;
}
/* Narrower than the page: the one thing here a stranger reads for pleasure. An
   event's details are read the same way, so they take the same measure. */
.message .body, .body { max-width: var(--measure-read); }
.message .body p, .body p { margin: 0 0 0.9rem; }
.message .body p:last-child, .body p:last-child { margin-bottom: 0; }
.message > .hint:last-child { margin: 1.1rem 0 0; }

/* Photos that arrived attached to a post. Inside the body's measure and quiet
   under it: they came with the message, they are not the message.

   Cropped to 4:3 tiles, matching the page/event photo grids — a deliberate
   change of stance from the earlier "never cropped" rule here: each thumbnail
   links to the full, uncropped image, so the crop costs nothing but makes a row
   of mixed phone photos read as one tidy band instead of a ragged strip.

   `height: auto` releases the height ATTRIBUTE on the tag so `aspect-ratio` can
   engage — the attributes still reserve space before the image loads. */
.photos {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(7rem, 1fr));
  gap: 0.6rem;
  max-width: var(--measure-read);
  margin: 0.9rem 0 0;
}
.photos a { display: block; }
.photos img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 4px;
  background: var(--wash);
}

/* ---- the thread page's gutter layout ----

   The hierarchy, in order: the words, then who wrote them and when, then what an
   administrator can do about it. Author and time live in a left gutter, which
   does two things: the body column becomes one uninterrupted band of prose with
   nothing to step over, and the left edge becomes a scan path — who said what,
   top to bottom, without reading.

   SCOPED under `.messages`, never bare `.message`: the moderation queue and the
   component gallery render `.message` outside any thread and must keep the plain
   card layout above. `main` is a size container on every page, so the container
   block below is scoped for the same reason. */
.messages > .message {
  display: grid;
  grid-template-columns: 10rem minmax(0, 1fr);
  /* Two gutter rows: the byline, then the tools directly beneath it. The
     content column is one item spanning both, so a tall message never pushes
     the tools to the bottom of the gutter. */
  grid-template-rows: auto 1fr;
  /* 2.75rem, not 2: a long address fills the 10rem gutter to its edge, and at
     2rem the name reads as touching the prose. The page's width has slack past
     the reading measure, so the extra gap costs the body column nothing. */
  column-gap: 2.75rem;
  padding: 0;
  border: 0;
  margin: 0;
}
/* Column 2 is the default and the byline is the one exception, rather than each
   child naming its own column. Auto-placement would drop anything unlisted into
   the 10rem gutter — which is what a stray heading did, and what the next
   element somebody adds here would do too. */
.messages > .message > * { grid-column: 2; }
.messages > .message > .byline { grid-column: 1; grid-row: 1; }
.messages > .message > .message-content { grid-column: 2; grid-row: 1 / span 2; }
.messages > .message + .message { margin-top: 2rem; }

.messages .byline {
  font: 0.875rem/1.4 var(--font-ui);
  margin: 0;
  /* Optically aligned with the first line of body text beside it. */
  padding-top: 0.2rem;
}
/* The one full-ink token in the metadata. The name is what a reader needs; the
   timestamp is reference material sitting under it. */
.messages .byline .who {
  display: block;
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--ink);
  margin-bottom: 0.1rem;
}
.messages .byline time { display: block; }

/* The admin tools live in the gutter too, under the date — the right column is
   reserved for the words. DOM order is unchanged (tools stay last in the
   article, which is what the stacked layout below renders), so this is pure
   placement: an explicit row keeps the fold directly beneath the byline
   whatever the body beside it does. */
.messages > .message > .message-tools {
  grid-column: 1;
  grid-row: 2;
  align-self: start;
  position: relative;
  margin: 0.35rem 0 0;
}
/* And the open panel FLOATS instead of growing the column. `<details>` plus an
   absolutely positioned list is the scriptless dialog: it works with
   JavaScript off, which is the public site's whole contract — a real <dialog>
   needs script to open. */
.messages > .message > .message-tools[open] > ul {
  position: absolute;
  left: 0;
  top: calc(100% + 0.4rem);
  z-index: 10;
  min-width: 16rem;
  margin: 0;
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 6px;
  padding: 0.7rem 0.85rem;
  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.14);
}

/* A subject only when it is genuinely a different subject — somebody changed it
   mid-thread. Below the body's weight class: it is a note about the message,
   not a headline. */
.subject-changed {
  font: 600 0.9375rem/1.4 var(--font-ui);
  margin: 0 0 0.4rem;
}

.messages .message .body {
  font-size: 1.0625rem;
  line-height: 1.62;
  /* Mailing-list bodies carry tracking URLs longer than the measure. Break
     those anywhere; leave ordinary prose alone. */
  overflow-wrap: break-word;
}
.messages .message .body a { overflow-wrap: anywhere; }

/* Below 46rem of CONTAINER width there is no room for a gutter, so the byline
   becomes a stacked header line and one soft hairline marks each boundary.

   A container query, not a viewport one: a viewport query would be right only
   by coincidence, and wrong the moment a message renders in a narrower column.
   Requires `container-type: inline-size` on `main`, declared with the `main`
   rule above. */
@container (max-width: 46rem) {
  .messages > .message { display: block; }
  .messages > .message + .message {
    margin-top: 2.25rem;
    padding-top: 2.25rem;
    border-top: 1px solid var(--rule-soft);
  }
  .messages .byline {
    display: flex;
    flex-wrap: wrap;
    align-items: baseline;
    gap: 0 0.5rem;
    padding-top: 0;
    margin: 0 0 0.5rem;
  }
  .messages .byline .who { display: inline; margin: 0; }
  .messages .byline time { display: inline; }
}

/* A forwarded newsletter can be twenty times the length of the message that
   forwarded it, and one of those drowns the eight around it. So the body is
   split at a paragraph boundary past a character threshold: the first part
   renders normally, the remainder goes in a `<details>`.

   Split rather than height-clamped, deliberately. `<details>` cannot express
   "show a preview, reveal the rest" — closed means only the summary renders, so
   a clamped body inside one does not paint at all. Splitting server-side makes
   the preview real content instead of a cropped picture of it, needs no script,
   and keeps every word in the document for ctrl-F and screen readers. */
.body-rest {
  font: 0.875rem/1.5 var(--font-ui);
  margin: 0.9rem 0 0;
}
.body-rest > summary {
  color: var(--link);
  cursor: pointer;
  width: fit-content;
  list-style: none;
}
.body-rest > summary::-webkit-details-marker { display: none; }
.body-rest > summary::before { content: "\2193  "; }
.body-rest[open] > summary::before { content: "\2191  "; }
.body-rest[open] > summary { margin-bottom: 0.7rem; }
.body-rest .body { margin: 0; }

/* A removed message stays in place — a gap in a conversation is worse than a
   note explaining one. It just stops looking like live content. */
.message--removed .body { color: var(--muted); }

/* Photos ATTACHED to a page or an event — a different thing from `.photos`
   above, which lays out photos that arrived by email and must never be cropped.
   These hang off the content rather than being laid into the prose: they sit
   after the body under their own heading, as a row of thumbnails linking to the
   full file. Nothing here is a lightbox — a link to an image is a thing every
   browser already handles.

   On a page or event, only the first three are shown; the rest live behind a
   link. So the inline grid is a fixed three across, and the standalone gallery
   — which is the whole point of that page — is free to fill the measure. */
.photo-grid {
  list-style: none;
  padding: 0;
  margin: 0.7rem 0 1rem;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 0.8rem;
}
.photo-grid--all {
  grid-template-columns: repeat(auto-fill, minmax(9rem, 1fr));
  margin-bottom: 1.5rem;
}
@media (max-width: 34rem) {
  .photo-grid { grid-template-columns: repeat(2, 1fr); }
}
.photo-grid img {
  display: block;
  width: 100%;
  /* `height: auto` releases the height ATTRIBUTE on the tag — without it the
     presentational hint wins, `aspect-ratio` never engages, and every tile
     renders at its natural height. The attributes still do their real job of
     reserving space before the image loads. The handoff omitted this; its mocks
     used drop targets rather than real dimensioned <img>s, so it never showed. */
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 4px;
  background: var(--wash);
}
.photo-grid a { display: block; text-decoration: none; }
.photo-grid a:focus-visible { outline-offset: 3px; }
.photo-grid .hint { display: block; margin-top: 0.3rem; }
@media (prefers-color-scheme: dark) {
  .photo-grid img { filter: brightness(0.92); }
}

/* Attached documents — uploaded PDFs and the like. No thumbnail is attempted:
   the honest rendering of a PDF is its name, its kind and its size, as a link. */
.documents { list-style: none; padding: 0; margin: 0.7rem 0 1rem; }
.documents li { margin: 0 0 0.45rem; }

/* Generated fliers on the event page. One line each, below the photos: printing
   is something a few people do, reading is what everyone does. (Distinct from
   `.documents` above, which lists files an admin UPLOADED.) */
ul.fliers { list-style: none; padding: 0; margin: 0.6rem 0 1.5rem; }
ul.fliers > li { padding: 0.55rem 0; border-bottom: 1px solid var(--rule-soft); }
ul.fliers > li:first-child { border-top: 1px solid var(--rule-soft); }
ul.fliers > li > a { font-weight: 600; text-decoration: none; }
ul.fliers > li > a:hover { text-decoration: underline; }
ul.fliers .hint { display: block; margin-top: 0.1rem; }

/* The flier editor is the one admin screen with a genuine second column, so it
   is the one page that widens. Without this the grid below never has room for
   its own breakpoint: main is capped at --measure (42rem), so a 62rem viewport
   query would fire while the container is still 42rem and the preview would end
   up wider than the form it previews. */
main.wide { max-width: 62rem; }

.flier-editor {
  display: grid;
  gap: 1.5rem 2rem;
  align-items: start;
}
@media (min-width: 62rem) {
  .flier-editor { grid-template-columns: minmax(0, 1fr) minmax(0, 21rem); }
  .flier-editor > .flier-preview { position: sticky; top: 1.5rem; }
}
.flier-preview > h2 { margin-top: 0; }
/* The sheet arrives from the server at its real size and is scaled down there
   (flier-fit.css), so the preview is the printable page rather than a
   lookalike. The pane is a fixed 21rem, which is what makes the scale factor a
   constant. */
.flier-preview iframe {
  display: block;
  width: 100%;
  aspect-ratio: 17 / 22;
  border: 1px solid var(--rule);
  border-radius: 2px;
  background: #fff;
}
.flier-preview--landscape iframe { aspect-ratio: 22 / 17; }

/* The same photos in the admin, where each one is a row of fields rather than a
   tile: alt text is the point of the screen, and it needs a real input. */
.photo-edit { list-style: none; padding: 0; margin: 0.7rem 0 1.25rem; }
.photo-edit > li {
  display: grid;
  grid-template-columns: 6rem 1fr;
  column-gap: 1rem;
  padding: 1rem 0;
  border-bottom: 1px solid var(--rule-soft);
}
.photo-edit > li:first-child { border-top: 1px solid var(--rule-soft); }
.photo-edit img {
  display: block;
  width: 100%;
  height: auto; /* same release of the height attribute as .photo-grid img */
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 4px;
  background: var(--wash);
}
/* A document's stand-in for the thumbnail: its kind, in a quiet tile the same
   shape a photo would take, so the rows line up. */
.photo-edit .doc-tile {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  aspect-ratio: 4 / 3;
  border: 1px solid var(--rule);
  border-radius: 4px;
  background: var(--wash);
  color: var(--muted);
  font: 600 0.8125rem/1 var(--font-ui);
  letter-spacing: 0.06em;
}
/* The first label in a row should not push the row open the way a form label
   does; the row's own padding is the spacing. */
.photo-edit label:first-of-type { margin-top: 0; }
.photo-edit .photo-row-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.4rem 1rem;
  margin: 0.5rem 0 0;
}
.photo-edit .photo-row-foot .spacer { flex: 1 1 1rem; }
@media (max-width: 34rem) {
  .photo-edit > li { grid-template-columns: 4.5rem 1fr; column-gap: 0.8rem; }
}

.summary {
  display: grid; grid-template-columns: auto 1fr;
  gap: 0.35rem 1.5rem; align-items: baseline;
  margin: 1.25rem 0 1.75rem;
}
.summary dt { font: 600 0.875rem/1.5 var(--font-ui); color: var(--muted); margin: 0; }
.summary dd { margin: 0; }

/* ---- the community's one photograph ---- */
/* One image per community, on the front page only. It is a masthead, not a
   gallery: fixed ratio, cropped by object-fit, so there is a single file to
   upload, no art direction for an admin to get wrong, and no caption to write. */
img.hero {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 5 / 2;
  object-fit: cover;
  /* Twice the 4px used elsewhere: on a 672px-wide photograph a 4px radius does
     not read at all, and 8px still belongs to the same vocabulary. */
  border-radius: 8px;
  background: var(--wash);
  margin: 0 0 1.75rem;
}
@media (prefers-color-scheme: dark) {
  /* A photograph at full brightness on this paper is the brightest thing on the
     screen by a wide margin. */
  img.hero { filter: brightness(0.92); }
}
/* At 360px a 5:2 crop is a 140px sliver. The same file is shown taller and
   edge to edge instead — object-fit does the work, so it is still one upload. */
@media (max-width: 34rem) {
  img.hero {
    aspect-ratio: 3 / 2;
    border-radius: 0;
    width: calc(100% + 2.1rem);
    margin: 0 -1.05rem 1.5rem;
  }
}

/* ---- allowance meters and requirement lists ---- */
.meter { margin: 0 0 1.1rem; max-width: 22rem; }
.meter__label { font: 0.875rem/1.5 var(--font-ui); color: var(--muted); }
.meter__track {
  height: 0.4rem; border-radius: 999px; overflow: hidden;
  background: var(--rule); margin: 0.3rem 0;
}
.meter__fill { height: 100%; background: var(--good); }
.meter--near .meter__fill { background: var(--warn); }
.meter--full .meter__fill { background: var(--bad); }
.meter__value { font: 0.875rem/1.5 var(--font-ui); }

ul.req { list-style: none; padding: 0; margin: 0.6rem 0 1.25rem; }
ul.req li {
  display: flex; gap: 0.6rem; align-items: baseline;
  padding: 0.3rem 0;
  font: 0.9375rem/1.5 var(--font-ui);
}
ul.req li > span:first-child { width: 1rem; flex: 0 0 1rem; color: var(--muted); }
.req--met > span:first-child { color: var(--good); }
.req--bad > span:first-child { color: var(--bad); }

pre.excerpt {
  white-space: pre-wrap;
  background: var(--wash);
  border: 1px solid var(--rule-soft);
  padding: 0.8rem 0.9rem;
  border-radius: 4px;
  font-size: 0.875rem;
  overflow-x: auto;
}
/* ---- the Markdown editor toolbar ---- */
/* EasyMDE ships its own colours, chosen for a light page and hardcoded. Its
   toolbar buttons carry WORDS here rather than icons — see admin-editor.js for
   why — so they also need room the icon-sized defaults do not give them. */
.editor-toolbar {
  border-color: var(--rule) !important;
  background: var(--wash);
  border-radius: 4px 4px 0 0;
}
.editor-toolbar button {
  font: 600 0.8125rem/1.6 var(--font-ui) !important;
  color: var(--muted) !important;
  height: auto;
  padding: 0.2rem 0.5rem;
  border-radius: 4px;
}
.editor-toolbar button:hover,
.editor-toolbar button.active {
  background: var(--paper) !important;
  border-color: var(--rule) !important;
  color: var(--ink) !important;
}
.editor-toolbar i.separator {
  border-left-color: var(--rule);
  border-right-color: transparent;
}
.CodeMirror {
  border-color: var(--rule) !important;
  border-radius: 0 0 4px 4px;
  background: var(--paper) !important;
  color: var(--ink) !important;
}
.CodeMirror-cursor { border-left-color: var(--ink); }
.editor-statusbar { color: var(--muted); }

.preview {
  border: 1px dashed var(--rule);
  padding: 0.9rem 1.05rem;
  border-radius: 4px;
}
.preview > :first-child { margin-top: 0; }
.preview > :last-child { margin-bottom: 0; }

/* Language switcher — the only CSS this change needs.
   One block for packages/worker/public/static/style.css.
   Uses existing tokens only, so both themes and all four palettes work. */

/* One control, in the footer of every page. Rarely touched, so it gets a line
   at the bottom rather than a place in the nav — but a real <form> with a real
   button, so it works with JavaScript off. */
/* THE SAME COLUMN AS `header` AND `main`, and split the same way `header` is split
   from `.site-nav`: the outer element carries the measure and the gutter, the inner one
   carries the rule. That is what keeps the footer's rule flush with the content edge
   rather than overhanging it by 1.25rem on each side — a border on the padded element
   is drawn at its outer edge, and next to the rules inside `main` the difference reads
   as a mistake.

   Full-bleed was worse still, which is how this arrived: the community name sat against
   the left edge of the window and the Change button walked off the right of a wide one. */
.page-foot {
  max-width: var(--measure);
  margin: 0 auto;
  /* No top padding or margin: `main` already ends in 5rem, and anything here would add
     to it rather than be absorbed — the doubling `header + main` exists to undo at the
     other end of the page. */
  padding: 0 1.25rem 2.5rem;
}
.foot-row {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.5rem 1.25rem;
  padding-top: 1rem;
  border-top: 1px solid var(--rule-soft);
  font: 0.875rem/1.5 var(--font-ui);
  color: var(--muted);
}
.foot-row .spacer { flex: 1 1 1rem; }
.locale {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.4rem;
  margin: 0;
}
/* Narrower than a form select and quiet by default: this is a footer control,
   not a field somebody came to fill in. */
.locale select {
  width: auto;
  min-height: 2.25rem;
  padding: 0.25rem 1.6rem 0.25rem 0.45rem;
  border-color: var(--rule-soft);
  font-size: 0.875rem;
  color: var(--muted);
  max-width: 14rem;
}
.locale select:hover { border-color: var(--rule); color: var(--ink); }
.locale button {
  margin: 0;
  padding: 0.25rem 0.6rem;
  min-height: 2.25rem;
  border-color: var(--rule);
  background: none;
  color: var(--muted);
  font-size: 0.8125rem;
}
.locale button:hover { background: var(--wash); color: var(--ink); }
@media (max-width: 34rem) {
  .locale select, .locale button { min-height: 2.75rem; }
  /* Matching the narrow-screen gutter `header` and `main` take. */
  .page-foot { padding: 0 1.05rem 2.5rem; }
}

/* The front door — the only CSS this change needs.
   One block for packages/worker/public/static/style.css.
   Existing tokens only, so both themes and all four palettes work. */

/* One statement of what this is, at a size between the heading and the body.
   Full ink rather than --muted: it is the argument, not a caption. */
.lead {
  font-size: clamp(1.2rem, 1.1rem + 0.55vw, 1.4rem);
  line-height: 1.45;
  letter-spacing: -0.004em;
  max-width: var(--measure-read);
  margin: 0 0 1.1rem;
  text-wrap: pretty;
}
/* One action on a page with one thing to do. A link, not a button: it navigates. */
.start {
  display: inline-flex;
  align-items: center;
  min-height: 2.75rem;
  font: 600 1.0625rem/1.2 var(--font-ui);
  text-underline-offset: 0.2em;
}
