/* 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;
  }
}

* { 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;
}

/* 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; }

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); }

/* ---- 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;
}

.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; }

/* 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;
}

/* 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; }

.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. */
.message .body { max-width: var(--measure-read); }
.message .body p { margin: 0 0 0.9rem; }
.message .body p:last-child { margin-bottom: 0; }
.message > .hint:last-child { margin: 1.1rem 0 0; }

.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; }
