/* Kestrel design system — "Fieldwork", Estuary teal accent.
   Decided in C2a (docs/DECISION-LOG.md, 2026-08-06); the proposal page
   is docs/design-proposals/2026-08-05-c2a-design-systems.html.

   Two rules this file exists to keep:
   1. Colour is a TOKEN, never a literal. The accent moved four times
      during C2a; it must stay a one-line change.
   2. Every text/background pair is MEASURED against WCAG AA (4.5:1
      body, 3:1 large/UI). Ratios are noted beside each token. Dan
      caught a 4.1:1 pair by eye in review — don't reintroduce one.
   No webfonts: a system stack means no external requests, no CSP
   surprises and no flash of unstyled text on a builder's 4G phone. */

:root {
  color-scheme: light dark;

  /* --- Fieldwork palette (light) ------------------------------------ */
  --ground: #F6F4F1;        /* warm stone */
  --surface: #FFFFFF;
  --surface-sunk: #EFECE7;
  --ink: #33393E;           /* 10.7:1 on ground */
  --muted: #5C5852;         /* 6.4:1 on ground, 7.1:1 on surface */
  --line: #E5E1DA;
  --line-strong: #D2CCC3;

  --accent: #14675F;        /* Estuary teal — white on it 6.7:1 */
  --accent-ink: #FFFFFF;
  --accent-hover: #0F534C;
  --accent-soft: #DCEDEA;
  --accent-soft-ink: #0E5049;   /* 8.9:1 on accent-soft */
  --structure: #5A6B7C;     /* kestrel slate: counts, badges, steps */

  --ok-bg: #E7F0E9;
  --ok-ink: #33683F;        /* 5.3:1 on ok-bg */
  --warn-bg: #FBEEDC;
  --warn-ink: #7A5216;      /* 5.6:1 on warn-bg */
  --danger-bg: #FBE7E3;
  --danger-ink: #96351F;    /* 5.4:1 on danger-bg */
  --danger: #96351F;

  /* --- shape + depth ------------------------------------------------- */
  --radius-card: 14px;
  --radius-pill: 999px;
  --radius-field: 10px;
  --shadow-card: 0 1px 4px rgba(51, 57, 62, .08);
  --shadow-raised: 0 2px 10px rgba(51, 57, 62, .10);
  --shadow-focus: 0 0 0 3px rgba(20, 103, 95, .28);

  /* --- spacing scale (4px base) -------------------------------------- */
  --s1: 4px;  --s2: 8px;  --s3: 12px; --s4: 16px;
  --s5: 24px; --s6: 32px; --s7: 48px; --s8: 64px;

  /* --- type ----------------------------------------------------------- */
  --font: "Segoe UI", system-ui, -apple-system, "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "Cascadia Mono", Consolas, monospace;
  --text-xs: 12px; --text-sm: 13.5px; --text-base: 15px;
  --text-lg: 18px; --text-xl: 22px; --text-2xl: 28px;
}

/* Dark theme: derived from the Nightshift rules in the C2a proposal —
   deep blue-slate ground, lifted surfaces, accent LIGHTENED so it keeps
   contrast on a dark ground (naive inversion would drop teal to ~2:1). */
@media (prefers-color-scheme: dark) {
  :root {
    --ground: #1C232C;
    --surface: #242D38;
    --surface-sunk: #1F2731;
    --ink: #E8ECEF;         /* 12.6:1 on ground */
    --muted: #A8B0B8;       /* 7.2:1 on ground */
    --line: #333E4A;
    --line-strong: #46525F;

    --accent: #5FC3B6;      /* on dark: 8.3:1 as text on ground */
    --accent-ink: #10231F;  /* dark text on the light accent = 9.1:1 */
    --accent-hover: #7BD3C7;
    --accent-soft: #1E3C39;
    --accent-soft-ink: #8FD8CD;
    --structure: #93A6B8;

    --ok-bg: #1F3A2C;   --ok-ink: #7FD79E;
    --warn-bg: #3C2F1A; --warn-ink: #F0C078;
    --danger-bg: #40241F; --danger-ink: #F2A493;
    --danger: #F2A493;

    --shadow-card: 0 1px 4px rgba(0, 0, 0, .35);
    --shadow-raised: 0 2px 10px rgba(0, 0, 0, .45);
    --shadow-focus: 0 0 0 3px rgba(95, 195, 182, .32);
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font);
  font-size: var(--text-base);
  line-height: 1.55;
  -webkit-text-size-adjust: 100%;
}

h1, h2, h3 { line-height: 1.2; text-wrap: balance; margin: 0 0 var(--s3); }
h1 { font-size: var(--text-2xl); font-weight: 700; }
h2 { font-size: var(--text-xl); font-weight: 700; margin-top: var(--s6); }
h3 { font-size: var(--text-lg); font-weight: 650; }
p { margin: 0 0 var(--s3); }
a { color: var(--accent); }
a:hover { color: var(--accent-hover); }
small, .text-sm { font-size: var(--text-sm); }
.muted { color: var(--muted); }
.tabular { font-variant-numeric: tabular-nums; }

:where(a, button, input, select, textarea, summary):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--s1);
}

/* --- app shell -------------------------------------------------------- */
.skip-link {
  position: absolute; left: -9999px; top: var(--s2);
  background: var(--surface); color: var(--ink);
  padding: var(--s2) var(--s4); border-radius: var(--radius-pill);
  box-shadow: var(--shadow-raised); z-index: 20;
}
.skip-link:focus { left: var(--s4); }

.app-header {
  background: var(--ground);
  border-bottom: 1px solid var(--line);
  position: sticky; top: 0; z-index: 10;
}
.app-header .bar {
  max-width: 1180px; margin: 0 auto;
  padding: var(--s3) var(--s4);
  display: flex; align-items: center; gap: var(--s4);
}
.brand {
  font-weight: 700; font-size: var(--text-lg); color: var(--accent);
  text-decoration: none; letter-spacing: .005em;
}
.brand:hover { color: var(--accent-hover); }

.nav { display: flex; gap: var(--s1); flex-wrap: wrap; }
.nav a {
  color: var(--muted); text-decoration: none; font-weight: 500;
  font-size: var(--text-sm); padding: var(--s2) var(--s3);
  border-radius: var(--radius-pill);
}
.nav a:hover { color: var(--ink); background: var(--surface-sunk); }
.nav a[aria-current="page"] {
  color: var(--ink); background: var(--surface); box-shadow: var(--shadow-card);
}

.header-end { margin-left: auto; display: flex; align-items: center; gap: var(--s3); }
.who { font-size: var(--text-sm); color: var(--muted); }
.nav-toggle { display: none; }

main { max-width: 1180px; margin: 0 auto; padding: var(--s5) var(--s4) var(--s8); }
.page-head { margin-bottom: var(--s5); }
.page-head p { color: var(--muted); margin: 0; }

.app-footer {
  max-width: 1180px; margin: 0 auto; padding: 0 var(--s4) var(--s6);
  color: var(--muted); font-size: var(--text-xs);
}

/* --- surfaces --------------------------------------------------------- */
.card {
  background: var(--surface); border-radius: var(--radius-card);
  box-shadow: var(--shadow-card); padding: var(--s4) var(--s5);
  margin-bottom: var(--s4);
}
.card > :last-child { margin-bottom: 0; }
.card-title { font-size: var(--text-sm); font-weight: 650; color: var(--muted);
  text-transform: uppercase; letter-spacing: .06em; margin-bottom: var(--s3); }

.stats { display: grid; gap: var(--s3); margin-bottom: var(--s5);
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
.stat {
  background: var(--surface); border-radius: var(--radius-card);
  box-shadow: var(--shadow-card); padding: var(--s3) var(--s4);
  text-decoration: none; color: inherit; display: block;
}
a.stat:hover { box-shadow: var(--shadow-raised); }
.stat b { display: block; font-size: var(--text-2xl); font-weight: 700;
  color: var(--structure); font-variant-numeric: tabular-nums; line-height: 1.15; }
.stat.hot b { color: var(--accent); }
.stat span { font-size: var(--text-xs); color: var(--muted); }

/* --- buttons ---------------------------------------------------------- */
.btn, button.btn, a.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--s2); border: 0; cursor: pointer; text-decoration: none;
  font-family: inherit; font-size: var(--text-sm); font-weight: 650;
  padding: var(--s3) var(--s5); border-radius: var(--radius-pill);
  min-height: 44px;  /* thumb-sized: the van test */
  background: var(--accent); color: var(--accent-ink);
}
.btn:hover { background: var(--accent-hover); color: var(--accent-ink); }
.btn.quiet {
  background: var(--surface); color: var(--ink);
  box-shadow: inset 0 0 0 1.5px var(--line-strong);
}
.btn.quiet:hover { background: var(--surface-sunk); color: var(--ink); }
/* Danger keeps the tinted fill (a solid red cannot carry white text in
   both themes — in dark mode --danger is a pale salmon) and gains a red
   outline, which is what actually reads as "red" at small sizes. */
.btn.danger { background: var(--danger-bg); color: var(--danger-ink);
  box-shadow: inset 0 0 0 1.5px var(--danger); }
.btn.danger:hover { background: var(--danger); color: var(--surface); }
.btn.small { min-height: 34px; padding: var(--s2) var(--s4); font-size: var(--text-xs); }
/* Smaller than .small and below the 44px thumb target on purpose: this is
   for destructive row actions, where "hard to hit" is the feature
   (869ejb2tv). Never use it for something you want people to press. */
.btn.tiny { min-height: 28px; padding: 0 var(--s3); font-size: var(--text-xs);
  font-weight: 600; width: auto; }
.btn[disabled] { opacity: .5; cursor: not-allowed; }
/* A table cell holding row controls. Right-aligned so the destructive one
   sits away from the row's own link, and immune to the phone rule that
   stretches every .btn across the card. Deliberately NOT display:flex on
   a desktop table — a flex <td> leaves table layout and takes the row's
   bottom border with it. `text-align` is enough: .btn is inline-flex. */
.row-actions { text-align: right; white-space: nowrap; }
.row-actions .btn { width: auto; flex: 0 0 auto; }
.actions { display: flex; gap: var(--s3); align-items: center;
  flex-wrap: wrap; margin-top: var(--s4); }

/* --- pills ------------------------------------------------------------ */
.pill {
  display: inline-block; border-radius: var(--radius-pill);
  padding: 2px var(--s3); font-size: var(--text-xs); font-weight: 650;
  background: var(--surface-sunk); color: var(--muted); white-space: nowrap;
}
.pill.ok { background: var(--ok-bg); color: var(--ok-ink); }
.pill.new { background: var(--accent-soft); color: var(--accent-soft-ink); }
.pill.warn { background: var(--warn-bg); color: var(--warn-ink); }
.pill.bad { background: var(--danger-bg); color: var(--danger-ink); }
/* CANNOT TELL — the third stop-loss verdict (869en5wb5), and the one that
   must never be mistaken for OK: it means the input to judge the rule does
   not exist, which is an absent measurement rather than a passing test.
   Dashed and unfilled on purpose. Every other pill on the site is a solid
   block of colour saying "this was measured and it came out like this";
   the outline says the opposite, and it says it without colour, so it
   still reads as a third thing in greyscale and to a colour-blind eye.
   Both tokens are existing ones — --ink on --surface-sunk is 9.4:1 light
   and 9.8:1 dark. */
.pill.unknown { background: var(--surface-sunk); color: var(--ink);
  border: 1px dashed var(--muted); }
/* Quiet qualifier pill: sits beside a status pill to narrow what it means
   (e.g. "No objection" + "Consultee response"). Deliberately unsaturated so
   it reads as a caveat, not a second status. Reuses existing tokens —
   measured 6.0:1 light, 6.9:1 dark, both above AA's 4.5:1. */
/* Smaller than a status pill on purpose. Dan, 2026-08-08: it read as a
   second status competing with the green one. Now it is a marker you look
   up in the key, so it only has to be legible, not loud. */
.pill.note { background: var(--surface-sunk); color: var(--muted);
  font-weight: 600; border: 1px solid var(--line);
  font-size: 11px; padding: 1px var(--s2); letter-spacing: .01em; }
/* Lead-type pill (C7 review): sits on the row below the status pill, so
   the Details cell reads "what happened" then "why you'd care". Outlined
   rather than filled — a row can carry three of these and filled pills at
   that density turn the table into confetti. */
.pill.lead { background: transparent; color: var(--muted);
  font-weight: 600; border: 1px solid var(--line); }
.lead-pills { margin-top: 4px; display: flex; flex-wrap: wrap; gap: 3px; }

/* --- info tip: a clickable "i", not a hover (869eed4ft) -----------------
   Dan: people did not know the pills could be interrogated, because a
   `title=` attribute is invisible until you happen to rest a pointer on
   it and never appears at all on a phone. This is <details>/<summary>, so
   it is a real control: it has a focus ring, it opens on Enter, and it
   needs no JavaScript. */
.info-tip { display: inline-block; position: relative; vertical-align: middle; }
.info-tip-mark {
  display: inline-flex; align-items: center; justify-content: center;
  width: 18px; height: 18px; border-radius: 50%;
  border: 1px solid currentColor; color: var(--muted);
  font-size: 11px; font-weight: 700; font-style: italic;
  line-height: 1; cursor: pointer; user-select: none;
  list-style: none;   /* Safari/Firefox marker */
}
.info-tip-mark::-webkit-details-marker { display: none; }
.info-tip-mark:hover, .info-tip[open] .info-tip-mark {
  color: var(--accent-soft-ink); background: var(--accent-soft);
  border-color: var(--accent-soft-ink);
}
.info-tip-mark:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.info-tip-body {
  position: absolute; z-index: 40; top: calc(100% + 6px); left: 0;
  width: min(30ch, 78vw);
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius);
  box-shadow: 0 6px 20px rgb(0 0 0 / 18%);
  padding: var(--s3); font-size: var(--text-sm);
  font-weight: 400; line-height: 1.5; white-space: normal; text-align: left;
}
/* The breakdown variant (869ekb512). The pills' tips say one sentence;
   the coverage map's stat tiles have to say a sentence AND itemise six
   numbers that add up to the tile, so the panel gets a reading width and
   the counts get a column of their own. Same component, same mark, same
   surface — only the body is richer. */
.info-tip-wide .info-tip-body { width: min(44ch, 88vw); }
/* A tile on the right of the row would push its panel off the card, so
   the modifier is applied by the markup rather than guessed at runtime —
   the same rule `.tip-right` follows. */
.info-tip-right .info-tip-body { left: auto; right: 0; }
.info-tip-body > p { margin: 0 0 var(--s2); }
.info-tip-body > p:last-child { margin-bottom: 0; }
.info-tip-headline { font-weight: 650; color: var(--ink); }
.info-tip-body .muted { color: var(--muted); font-size: var(--text-xs); }
.info-tip-body a { color: var(--accent); font-weight: 600; }
.info-tip-rows {
  list-style: none; margin: 0 0 var(--s2); padding: 0;
  display: grid; gap: var(--s2);
}
.info-tip-rows li { display: grid; grid-template-columns: 3.4em 1fr; gap: var(--s2); }
/* `font-size` and `display` are stated because the panel is rendered
   INSIDE a `.stat`, and `.stat b` is a 28px block heading. A count in the
   breakdown is a count, not a second headline. */
.info-tip-rows b {
  display: inline; font-size: var(--text-sm); line-height: 1.5;
  font-weight: 700; text-align: right; color: var(--structure);
}
.info-tip-rows span { font-size: var(--text-xs); }

/* The tile's label and its "i" sit on one line and wrap together. */
.stat-label { display: inline-flex; align-items: center; flex-wrap: wrap;
  gap: var(--s1); }

/* Narrow screens: a floating panel anchored inside a table cell has
   nowhere to go, so it becomes an ordinary block under the pill. The
   breakdown variant gives up its reading width here for the same reason —
   and the mark takes the whole line inside a stat tile, so the panel
   opens underneath at the tile's own width rather than being squeezed
   into a flex column a few characters wide. */
@media (max-width: 700px) {
  .info-tip { display: block; position: static; }
  .info-tip-body { position: static; width: auto; margin-top: var(--s2); }
  .info-tip-wide .info-tip-body { width: auto; }
  .stat-label > .info-tip { flex: 1 0 100%; }
  /* Two tiles fit across a phone, and a six-line breakdown does not fit
     in half of one. Opening it gives that tile the whole row; closing it
     puts the row back. `:has` degrades to a cramped-but-readable panel
     where it is unsupported, which is why nothing else depends on it. */
  .stats .stat:has(.info-tip[open]) { grid-column: 1 / -1; }
}

/* The medium picker sits above the editor, because it changes what
   everything below it means. */
.medium-row { display: flex; gap: var(--s4); align-items: end;
  flex-wrap: wrap; margin-bottom: var(--s3); }
.medium-row .postcard-only { display: flex; gap: var(--s4);
  align-items: end; flex-wrap: wrap; }
/* Any `display` rule beats the `hidden` attribute's own display:none, so
   an element given a display and hidden by script stays on screen. Caught
   by a browser test rather than by reading it. */
[hidden] { display: none !important; }
/* A postcard has no watermark, no photograph bands and no QR-in-the-body:
   those are letter furniture, and offering them here would be offering
   something we cannot print. The margin and header controls joined them
   on 11 Aug: a postcard's back is laid out by letters/postcard.py and has
   neither. One class now, so a new letter-only control cannot be
   forgotten here. */
body.is-postcard .letter-only { display: none; }

/* Drafts on the send picker: present, pickable, visibly a second class. */
.draft-block { margin-top: var(--s4); padding-top: var(--s4);
  border-top: 1px solid var(--line); }
.draft-block h2 { font-size: var(--text-sm); color: var(--muted);
  text-transform: uppercase; letter-spacing: .05em; margin: 0 0 var(--s1); }

/* --- the pill key (869efguqa) ----------------------------------------- */
/* A definition list, not a table: the pill IS the term. Two columns where
   there is room, stacked below it — the explanation is a sentence, and a
   sentence in a narrow column is worse than a sentence under its label. */
.pill-key { background: var(--surface); border-radius: var(--radius-card);
  box-shadow: var(--shadow-card); padding: var(--s4) var(--s5);
  margin-top: var(--s5); }
.pill-key h2 { font-size: var(--text-base); margin: 0 0 var(--s1); }
.pill-key h3 { font-size: var(--text-sm); color: var(--muted);
  text-transform: uppercase; letter-spacing: .05em;
  margin: var(--s5) 0 var(--s2); }
.pill-key-list { margin: 0; display: grid; gap: var(--s2) var(--s4);
  grid-template-columns: max-content 1fr; align-items: baseline; }
.pill-key-list dt { display: flex; gap: 3px; flex-wrap: wrap; }
.pill-key-list dd { margin: 0; font-size: var(--text-sm); color: var(--muted); }
@media (max-width: 640px) {
  .pill-key-list { grid-template-columns: 1fr; gap: var(--s1); }
  .pill-key-list dd { margin-bottom: var(--s3); }
}
.badge { display: inline-block; background: var(--structure); color: #fff;
  border-radius: var(--radius-pill); padding: 2px var(--s3);
  font-size: var(--text-xs); font-weight: 700; font-variant-numeric: tabular-nums; }

/* --- tables ----------------------------------------------------------- */
.table-wrap { overflow-x: auto; background: var(--surface);
  border-radius: var(--radius-card); box-shadow: var(--shadow-card); }
table.data { border-collapse: collapse; width: 100%; font-size: var(--text-sm); }
table.data th {
  text-align: left; font-size: var(--text-xs); font-weight: 650;
  color: var(--muted); text-transform: uppercase; letter-spacing: .05em;
  padding: var(--s3) var(--s4); border-bottom: 1px solid var(--line);
  white-space: nowrap;
}
table.data td { padding: var(--s3) var(--s4); border-bottom: 1px solid var(--line);
  vertical-align: top; }
table.data tbody tr:last-child td { border-bottom: 0; }
table.data tbody tr:hover { background: var(--surface-sunk); }
table.data td.tabular, table.data td.num { font-variant-numeric: tabular-nums; }

/* --- forms ------------------------------------------------------------ */
form p { margin: 0 0 var(--s3); }
label { font-size: var(--text-sm); font-weight: 600; color: var(--ink);
  display: inline-block; margin-bottom: var(--s1); }
input[type="text"], input[type="email"], input[type="password"], input[type="date"],
input[type="search"], input[type="url"], input[type="tel"], input[type="number"],
input:not([type]),   /* a bare <input> defaults to text but matches no
                        [type=] selector — caught in C2 review */
select, textarea {
  font-family: inherit; font-size: var(--text-base); color: var(--ink);
  background: var(--surface); border: 1.5px solid var(--line-strong);
  border-radius: var(--radius-field); padding: var(--s3);
  width: 100%; max-width: 460px; min-height: 44px;
}
textarea { min-height: 180px; line-height: 1.5; resize: vertical; }
input:focus, select:focus, textarea:focus {
  outline: none; border-color: var(--accent); box-shadow: var(--shadow-focus);
}
input[type="checkbox"], input[type="radio"] {
  width: 20px; height: 20px; min-height: 0; accent-color: var(--accent);
  vertical-align: middle;
}
.helptext, .helptext li { font-size: var(--text-xs); color: var(--muted); }
ul.errorlist { list-style: none; margin: 0 0 var(--s2); padding: 0;
  color: var(--danger-ink); font-size: var(--text-sm); font-weight: 600; }

.filters { display: flex; flex-wrap: wrap; gap: var(--s3); align-items: flex-end; }
.filters label { display: flex; flex-direction: column; gap: var(--s1);
  font-size: var(--text-xs); text-transform: uppercase; letter-spacing: .05em;
  color: var(--muted); }
.filters input:not([type="checkbox"]):not([type="radio"]),
.filters select { min-width: 150px; max-width: 220px; }
.filters .check { flex-direction: row; align-items: center; gap: var(--s2);
  text-transform: none; letter-spacing: 0; font-size: var(--text-sm);
  color: var(--ink); }

/* --- messages, notes, empty states ------------------------------------ */
.flash { border-radius: var(--radius-card); padding: var(--s3) var(--s4);
  margin-bottom: var(--s4); font-size: var(--text-sm); font-weight: 600;
  background: var(--ok-bg); color: var(--ok-ink);
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2) var(--s3); }
/* The way back from a delete that had no confirmation step (869ejb2tv).
   Looks like a link, is a POST — undoing is a state change, and a GET
   link would be followed by anything that prefetches. Generous target:
   unlike the Delete button, this one is meant to be easy to hit. */
.flash-undo { margin: 0; }
.flash-undo button {
  border: 0; background: none; cursor: pointer; font: inherit;
  color: inherit; text-decoration: underline; text-underline-offset: 3px;
  padding: var(--s1) var(--s2); min-height: 32px; border-radius: var(--radius-pill);
}
.flash-undo button:hover { background: rgb(0 0 0 / .08); }
.flash.error { background: var(--danger-bg); color: var(--danger-ink); }
.flash.warning { background: var(--warn-bg); color: var(--warn-ink); }
.note { background: var(--surface); border-left: 4px solid var(--accent);
  border-radius: var(--radius-field); padding: var(--s3) var(--s4);
  box-shadow: var(--shadow-card); margin-bottom: var(--s4);
  font-size: var(--text-sm); }
.note.warn { border-left-color: var(--warn-ink); }
.empty { text-align: center; color: var(--muted); padding: var(--s7) var(--s4); }

/* --- send stepper ----------------------------------------------------- */
.steps { display: flex; gap: var(--s2); flex-wrap: wrap; list-style: none;
  padding: 0; margin: 0 0 var(--s5); font-size: var(--text-xs); }
.steps li { background: var(--surface-sunk); color: var(--muted);
  border-radius: var(--radius-pill); padding: var(--s2) var(--s4); font-weight: 600; }
.steps li[aria-current="step"] { background: var(--structure); color: #fff; }
.steps li.done { background: var(--accent-soft); color: var(--accent-soft-ink); }

.summary { background: var(--surface); border-radius: var(--radius-card);
  box-shadow: var(--shadow-raised); padding: var(--s4) var(--s5);
  max-width: 520px; margin-bottom: var(--s4); }
.summary .line { display: flex; justify-content: space-between; gap: var(--s4);
  padding: var(--s2) 0; border-bottom: 1px solid var(--line);
  font-size: var(--text-sm); font-variant-numeric: tabular-nums; }
.summary .line:last-child { border-bottom: 0; }
.summary .line.total { font-size: var(--text-lg); font-weight: 700;
  color: var(--accent); padding-top: var(--s3); }

/* --- lists of saved things -------------------------------------------- */
.tiles { display: grid; gap: var(--s3); grid-template-columns: 1fr;
  max-width: 560px; }
/* A quick view is a button, and until 869ejyh15 it did not look like
   one: white card on a near-white page, distinguished only by a 1px
   shadow. Dan, 17 Aug 2026 — "existing white on white, it is hard to see
   that it IS a button". So: the accent tint the rest of the system uses
   for a pressable surface, a real outline, and a hover that moves.
   The outline is 1.5px `--line-strong`, the same weight `.btn.quiet`
   carries, so the two read as the same family of control. */
.tile { display: flex; justify-content: space-between; align-items: center;
  gap: var(--s3); background: var(--accent-soft); border-radius: var(--radius-card);
  box-shadow: inset 0 0 0 1.5px var(--line-strong), var(--shadow-card);
  padding: var(--s3) var(--s4); font-weight: 650;
  text-decoration: none; color: var(--accent-soft-ink); font-size: var(--text-sm); }
.tile:hover { box-shadow: inset 0 0 0 1.5px var(--accent), var(--shadow-raised);
  background: var(--surface); color: var(--accent-soft-ink); }
/* A saved filter is a container, not a link — its own <a> and its Delete
   button live inside it, so it keeps the plain card treatment. */
span.tile { background: var(--surface); color: inherit; font-weight: inherit;
  box-shadow: inset 0 0 0 1.5px var(--line), var(--shadow-card); }
/* The second line under a quick view's name: what it actually filters
   on, so the button does not have to be taken on trust. */
.tile-hint { display: block; font-weight: 400; font-size: var(--text-xs);
  color: var(--muted); margin-top: 2px; }
.tile:hover .tile-hint { color: var(--muted); }
/* the two built-in browse views sit side by side, half width each
   (869ehvfjw) — stacked full-width they read as separate headline
   actions rather than one pair of "what's arrived" views */
.tiles-pair { display: grid; gap: var(--s3); grid-template-columns: 1fr; }
@media (min-width: 561px) {
  .tiles-pair { grid-template-columns: 1fr 1fr; }
}

.pager { display: flex; gap: var(--s4); align-items: center;
  margin-top: var(--s4); font-size: var(--text-sm); color: var(--muted); }

/* the save-this-search row stays on one line where there's room */
@media (min-width: 821px) {
  form[data-testid="save-filter"] { flex-wrap: nowrap; }
}

/* --- responsive ------------------------------------------------------- */
@media (max-width: 820px) {
  .app-header .bar { flex-wrap: wrap; gap: var(--s2); }
  .nav-toggle {
    display: inline-flex; margin-left: auto; align-items: center;
    background: var(--surface); color: var(--ink); border: 0;
    box-shadow: inset 0 0 0 1.5px var(--line-strong);
    border-radius: var(--radius-pill); min-height: 40px; padding: 0 var(--s4);
    font: inherit; font-size: var(--text-sm); font-weight: 600; cursor: pointer;
  }
  /* Scoped to the header on purpose. Until 869ejb2v9 this rule was a bare
     `.nav`, so it also hid every *secondary* nav — which have no toggle
     button and no way back. On a phone that left 14 of the 15 back-office
     screens with no link anywhere in the product (the Menu only reaches
     /staff/, and the sub-nav on it was invisible), the /legal/ document
     switcher gone, and the Financials timeframe control gone. Secondary
     navs are styled as .subnav below instead of being hidden. */
  .app-header .nav { flex-basis: 100%; flex-direction: column; gap: var(--s1);
    display: none; padding-bottom: var(--s2); }
  .app-header .nav.open { display: flex; }
  .app-header .nav a { padding: var(--s3) var(--s4); font-size: var(--text-base); }
  .header-end { flex-basis: 100%; margin-left: 0; }

  /* Secondary navs stay on screen and wrap into a tappable chip block.
     No JS and no disclosure: an operator on site needs the destination
     one tap away, and a phone-shaped list of 15 links is cheaper than a
     screen you cannot leave. */
  .nav.subnav {
    background: var(--surface-sunk); border-radius: var(--radius-card);
    padding: var(--s2); gap: var(--s2) var(--s1);
  }
  .nav.subnav a {
    display: inline-flex; align-items: center; min-height: 36px;
    background: var(--surface); box-shadow: var(--shadow-card);
    padding: var(--s2) var(--s3);
  }
  .nav.subnav a[aria-current="page"] {
    background: var(--accent); color: var(--accent-ink);
  }
  main { padding: var(--s4) var(--s3) var(--s7); }
  h1 { font-size: var(--text-xl); }
  .filters { flex-direction: column; align-items: stretch; }
  .filters input:not([type="checkbox"]):not([type="radio"]),
  .filters select { max-width: none; }
  .btn { width: 100%; }
  .actions .btn { width: auto; flex: 1 1 auto; }

  /* Phone tables become cards — the browse screen is checked in a van. */
  table.data, table.data tbody, table.data tr, table.data td { display: block; width: 100%; }
  table.data thead { display: none; }
  table.data tr { background: var(--surface); border-radius: var(--radius-card);
    box-shadow: var(--shadow-card); margin-bottom: var(--s3); padding: var(--s2); }
  table.data td { border-bottom: 0; padding: var(--s1) var(--s3); }
  /* On a card the cell is a block, so flex it here to keep the control on
     the right; `table.data td` above is the more specific selector, so
     this has to be restated rather than inherited (869ejb2tv). */
  table.data td.row-actions { display: flex; justify-content: flex-end;
    align-items: center; gap: var(--s2); padding-top: var(--s2); }
  /* …but not as a row of its own. Dan, 2026-08-18 (869ejb2tv): "move the
     button up the card so it doesn't need its own row". A <td> is a block
     in card mode, so the only way to lift it out of the stack without
     reordering the DOM (which would move it on desktop too) is to take it
     out of flow and pin it to the card's top-right corner, level with the
     name. The first cell gets a right gutter so a long template name wraps
     before it runs under the button. */
  table.data tr:has(td.row-actions) { position: relative; }
  table.data tr:has(td.row-actions) td:first-child { padding-right: 6.5rem; }
  table.data td.row-actions { position: absolute; top: var(--s2);
    right: var(--s3); width: auto; padding: 0; }
  table.data td[data-label]::before {
    content: attr(data-label) " ";
    display: block; font-size: var(--text-xs); color: var(--muted);
    text-transform: uppercase; letter-spacing: .05em;
  }
  .table-wrap { background: none; box-shadow: none; }
}

@media (prefers-reduced-motion: reduce) {
  * { transition: none !important; animation: none !important; }
}

@media print {
  .app-header, .app-footer, .actions, .nav-toggle { display: none; }
  body { background: #fff; }
}

/* --- browse map (C7) --------------------------------------------------- */
#applications-map {
  height: 420px; border-radius: var(--radius); border: 1px solid var(--line);
  margin-bottom: var(--s3); z-index: 0;   /* keep tiles under the nav */
}
@media (max-width: 640px) { #applications-map { height: 300px; } }

/* The map's own controls. Leaflet's bar buttons are fixed-width squares
   built for single glyphs (26px, or 30px under `.leaflet-touch`, which is
   also on for a mouse), and the reset button carries a word. The doubled
   class is deliberate: leaflet.css loads after this file, so a plain
   `.leaflet-bar a.map-control-btn` ties with `.leaflet-touch .leaflet-bar a`
   and loses on order — the label then overflows a 30px button and is
   clipped at the map's edge. Measured, not guessed. */
.map-card .leaflet-bar a.map-control-btn {
  width: auto; min-width: 30px; padding: 0 var(--s2);
  font-size: var(--text-xs); font-weight: 600; white-space: nowrap;
}

/* "Widen" = full window width, not fullscreen (869efdru3). The card
   breaks out of the centred column by its own half-width; the body clips
   so the breakout can't add a horizontal scrollbar. The card's own side
   padding goes with it, or "full width" stops an inch short. */
.map-card.map-wide {
  /* --window-width is the *client* width, set by applications-map.js.
     `100vw` would include the vertical scrollbar, so the card would hang
     15px off the right and clip its own controls. */
  width: auto; max-width: none;
  margin-left: calc(50% - var(--window-width, 100vw) / 2);
  margin-right: calc(50% - var(--window-width, 100vw) / 2);
  border-radius: 0; padding-left: 0; padding-right: 0;
}
.map-card.map-wide > :not(#applications-map) {
  padding-left: var(--s4); padding-right: var(--s4);
}
.map-card.map-wide #applications-map {
  height: 70vh; border-radius: 0; border-left: 0; border-right: 0;
}
body.map-wide-active { overflow-x: hidden; }

/* --- impersonation banner (E4) ----------------------------------------
   Deliberately loud and full-bleed: an operator who forgets they are
   inside a customer's account can do real damage in that account's name. */
.impersonation-bar {
  background: var(--warn-bg); color: var(--warn-ink);
  border-bottom: 2px solid var(--warn-ink);
  padding: var(--s3) var(--s4);
  display: flex; align-items: center; gap: var(--s4); flex-wrap: wrap;
  font-size: var(--text-sm); font-weight: 600;
  position: sticky; top: 0; z-index: 15;
}
.impersonation-bar form { margin: 0; }

/* --- coverage map (A2) --------------------------------------------------
   A choropleth with no basemap: the districts are drawn on the sunk
   surface, so the shapes carry all the meaning and nothing external is
   fetched. Leaflet paints its own container background, hence the
   override on .leaflet-container. */
#coverage-map {
  height: 680px; border-radius: var(--radius); border: 1px solid var(--line);
  margin-bottom: var(--s3); z-index: 0; background: var(--surface-sunk);
  /* `position` is declared here on purpose. Leaflet's _initLayout reads the
     computed position and, finding `static`, writes `position: relative`
     as an INLINE style — which then beats any class rule, and the expand
     control below silently did nothing on production. Declaring it up
     front leaves Leaflet with nothing to fix. */
  position: relative;
}
#coverage-map.leaflet-container { background: var(--surface-sunk); }
@media (max-width: 640px) { #coverage-map { height: 420px; } }

/* Expand to the size of the WINDOW, not the screen: the browser chrome
   and the other tabs stay where they are. Escape or the button closes. */
#coverage-map.map-expanded {
  position: fixed; inset: 0; width: 100vw; height: 100vh;
  margin: 0; border-radius: 0; border: 0; z-index: 40;
}
body.map-expanded-open { overflow: hidden; }

/* --- coverage-map state palette (869enfu82) ---------------------------
 * The map's colours are TOKENS, like everything else in this file, and
 * `coverage-map.js` reads them back out with getComputedStyle — so the
 * palette lives here, in one place, and dark mode is a real second
 * palette rather than a filter over the first.
 *
 * Chosen with the data-viz skill's colour formula and MEASURED with its
 * validator, not eyeballed, against the surface the map actually renders
 * on (--surface-sunk: #EFECE7 light, #1F2731 dark) and with `--pairs
 * all`, because a choropleth can put any two states side by side.
 *
 *   light  #019B70 / #956300 / #990116
 *     lightness band PASS · chroma floor PASS
 *     CVD separation PASS  worst all-pairs #990116<->#956300 dE 10.7 (deutan)
 *     normal-vision  PASS  worst all-pairs dE 17.2
 *     contrast       PASS  all three >= 3:1 (3.01 / 4.39 / 7.52)
 *
 *   dark   #09AC87 / #BE8806 / #BC4A3C
 *     lightness band PASS · chroma floor PASS
 *     CVD separation PASS  worst all-pairs #BC4A3C<->#BE8806 dE 11.4 (deutan)
 *     normal-vision  PASS  worst all-pairs dE 16.1
 *     contrast       PASS  all three >= 3:1 (5.21 / 4.82 / 3.00)
 *
 * The `none` grey is deliberately outside that palette and fails three
 * of the validator's checks on purpose: it is out of the lightness band,
 * under the chroma floor and under 3:1, because it is a NEUTRAL and its
 * job is to recede. The check that matters is the one it passes — worst
 * all-pairs CVD dE 10.7 light / 11.0 dark against the three data
 * colours, so "we are collecting here" and "nothing is reaching us from
 * here" never collapse into each other. The first dark grey tried,
 * #525960, measured 7.2 from the failing red under protanopia; the slate
 * replaced it.
 *
 * What it replaces failed the same test: #4C9163 green against #BE5741
 * red measured **dE 4.7 under deuteranopia** all-pairs — the two colours
 * carrying the entire message were the exact pair ~8% of men collapse,
 * and they were close in luminance too, so greyscale did not rescue it
 * (869enfjm7, measured 21 Aug 2026).
 *
 * Colour is still not allowed to be the only channel. Every state also
 * carries a NAME and a GLYPH in the legend, the hover tooltip and the
 * popup (staffroom/districts.py: state_legend), the `mixed` state is
 * drawn with a diagonal fill PATTERN rather than a fourth hue, and the
 * table under the map says the same thing in words.
 */
:root {
  --map-ok:            #019B70;   /* 3.01:1 on --surface-sunk */
  --map-ok-hover:      #057957;
  --map-stale:         #956300;   /* 4.39:1 */
  --map-stale-hover:   #704A02;
  --map-failing:       #990116;   /* 7.52:1 */
  --map-failing-hover: #6E000C;
  --map-none:          #C6C1B8;   /* deliberately recessive: 1.52:1 */
  --map-none-hover:    #8F8A80;
  /* The stripes on a `mixed` district: the ok colour over the failing
     fill, so the swatch itself says "part of this is working". */
  --map-mixed-ink:     #019B70;
  --map-border:        #FFFFFF;
}

@media (prefers-color-scheme: dark) {
  :root {
    --map-ok:            #09AC87;   /* 5.21:1 on --surface-sunk */
    --map-ok-hover:      #04C49A;
    --map-stale:         #BE8806;   /* 4.82:1 */
    --map-stale-hover:   #E0A108;
    --map-failing:       #BC4A3C;   /* 3.00:1 */
    --map-failing-hover: #E36654;
    /* A cool slate rather than a true grey: under protanopia a neutral
       of this lightness collapsed to within dE 7.2 of the failing red,
       which is inside the warn band. This one is 11.0 away, and still
       reads as "nothing to say" (2.4% chroma, 1.71:1 — recessive on
       purpose, exactly as the light grey is). */
    --map-none:          #3C4C5D;
    --map-none-hover:    #627486;
    --map-mixed-ink:     #09AC87;
    /* Darker than every fill, the way white is lighter than every fill
       in light mode — a border the same colour as the ground would let
       neighbouring grey districts merge into one shape. */
    --map-border:        #12181F;
  }
}

.map-legend {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--radius-field); padding: var(--s2) var(--s3);
  font-size: var(--text-xs); color: var(--ink);
  display: flex; flex-direction: column; gap: var(--s1);
  box-shadow: var(--shadow-card);
}
.map-legend span { display: flex; align-items: center; gap: var(--s2); }
.map-legend i {
  width: 14px; height: 14px; border-radius: 3px; display: inline-block;
  border: 1px solid rgba(0, 0, 0, .15); flex: 0 0 auto;
}
/* The legend swatch carries the same second channel the map does: a
   `mixed` swatch is striped, not a fourth colour. Two backgrounds — the
   stripes over the fill — so it is the same construction as the SVG
   pattern and cannot drift from it. */
.map-legend i.map-swatch-hatch {
  background-image: repeating-linear-gradient(
    45deg,
    var(--map-mixed-ink) 0 2px,
    transparent 2px 5px);
}
/* And the glyph, so the legend is readable with no colour at all. */
.map-legend b {
  font-size: var(--text-xs); line-height: 1; width: 1em;
  text-align: center; color: var(--muted); font-weight: 700;
}
.map-alert { color: var(--danger); }

/* The state chip in a district popup: glyph + name, never colour alone.
   Ink stays ink (the data-viz rule that text wears text tokens); the
   coloured square beside it carries the identity. */
.map-state { display: inline-flex; align-items: center; gap: var(--s1);
             font-weight: 650; }
.map-state::before {
  content: ""; width: 10px; height: 10px; border-radius: 2px;
  background: var(--map-none); border: 1px solid rgba(0, 0, 0, .15);
}
.map-state-ok::before { background: var(--map-ok); }
.map-state-stale::before { background: var(--map-stale); }
.map-state-failing::before { background: var(--map-failing); }
.map-state-mixed::before {
  background: var(--map-failing);
  background-image: repeating-linear-gradient(
    45deg, var(--map-mixed-ink) 0 2px, transparent 2px 5px);
}
.map-state-none { color: var(--muted); }
.leaflet-popup-content { font-size: var(--text-sm); line-height: 1.5; }

/* --- alert settings: the explanation column (A2 review) ----------------
   Dan asked for the environment-variable column to be replaced by plain
   English, with room to be generous. These rows are a reading column, not
   a grid, so they get width and line height rather than compression. */
.control-doc { max-width: 62ch; }
.control-doc p { margin: 0 0 var(--s2); }
.control-doc dl { margin: 0; }
.control-doc dt {
  font-weight: 600; font-size: var(--text-xs); color: var(--muted);
  text-transform: uppercase; letter-spacing: .04em; margin-top: var(--s2);
}
.control-doc dd { margin: 0; font-size: var(--text-sm); }

/* --- legal pages (E5) --------------------------------------------------- */
.legal-body { max-width: 68ch; }
.legal-body h2 { margin-top: var(--s5); }
.legal-body h3 { margin-top: var(--s4); }
.app-footer a { color: var(--muted); }

/* --- letter template editor (D1 review, three-pane rebuild 7 Aug) -------

   Three panes on one screen: merge fields · the letter · the preview.
   Dan's instruction was that all three are visible at once, because a
   preview you have to scroll to find is a preview nobody checks. The
   fields column is deliberately narrow — it keeps its descriptions but
   gives its space to the two panes that do the work.

   Below 1100px the grid collapses to one column. That is the mobile
   fallback and it is meant to be plain: everything renders, nothing is
   pretty. This is a desktop tool by design. */
/* A workbench is not prose (Dan, 8 Aug). The 1180px app measure is right
   for reading and wrong for three working panes: on a 1920 or 2560 monitor
   it threw away most of the screen and left the letter and its proof both
   too narrow to judge. Pages opt in with {% block main_class %}main-wide.
   Still bounded — an unbounded grid on an ultrawide gives the middle pane
   a line length nobody can track — but bounded well past a laptop. */
.main-wide { max-width: 2200px; }

/* Rebuilt 12 Aug for Dan's "go to two columns - editor and preview…
   move the remaining options boxes underneath the editor (same width as
   editor box)". Two widths, not three:

     < 1100px   one column. The mobile fallback: everything renders,
                nothing is pretty. This is a desktop tool by design.
     >= 1100px  two columns — the letter (with every settings box under
                it, at its width) and the print proof beside it.

   The merge-field column is gone: it became the Insert field menu in the
   toolbar, which is where a list you read once and click occasionally
   belongs. The proof still drops full width below the editor at the
   narrow end, which is the responsive behaviour Dan asked to keep. */
.editor-2 { display: grid; gap: var(--s4); grid-template-columns: 1fr; }
.editor-2 .pane { min-width: 0; }
.editor-2 .pane-editor { display: flex; flex-direction: column; gap: var(--s4); }
.editor-2 .editor-settings {
  display: flex; flex-direction: column; gap: var(--s4);
}
.editor-2 .editor-settings .card, .editor-2 .pane-editor > .card { margin: 0; }
@media (min-width: 1100px) {
  .editor-2 {
    grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
    align-items: start;
  }
  /* Each pane scrolls inside itself, so the page as a whole does not: a
     long stack of settings must not push the proof off the screen. */
  .editor-2 .pane { max-height: calc(100vh - 190px); overflow-y: auto; }
}

/* --- Insert field, the tooltip hint, and the proof's loading state ------
   D1, 12 Aug. */

/* A <details> is the whole popover: it opens on click and with the
   keyboard, and Escape closes it (letter-editor.js). */
.insert-field { position: relative; }
.insert-field > summary {
  display: inline-flex; align-items: center; justify-content: center;
  list-style: none; cursor: pointer; padding: 0 var(--s3);
}
.insert-field > summary::-webkit-details-marker { display: none; }
.tool-wide { min-width: auto; white-space: nowrap; }
.insert-field[open] > summary { background: var(--surface-sunk); }
.insert-field-panel {
  position: absolute; z-index: 20; top: calc(100% + 4px); left: 0;
  width: min(420px, 90vw); max-height: 60vh; overflow-y: auto;
  padding: var(--s3); background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--radius-field);
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}

/* The "?" beside a field label. `title` carries the text — a real
   tooltip, so it works on a keyboard focus and needs no script. */
/* The house tooltip (Dan, 13 Aug: "readable shape box, not long and thin,
   with plan and post colours"). A native `title` is the long thin one —
   one line, at whatever width the browser likes, in the browser's own
   colours, and unreachable from a keyboard or a touch screen. This is the
   replacement, and docs/DESIGN-GUIDELINES.md says to use it everywhere.
   Markup: letters/templates/letters/_tip.html. */
.tip {
  position: relative;
  display: inline-flex; align-items: center; justify-content: center;
  width: 16px; height: 16px; margin-left: var(--s1);
  font-size: 11px; font-weight: 700; line-height: 1; cursor: help;
  color: var(--accent-soft-ink); background: var(--accent-soft);
  border: 1px solid var(--accent); border-radius: 50%;
  vertical-align: middle;
}
.tip-body {
  position: absolute; left: 50%; bottom: calc(100% + 8px);
  z-index: 20; visibility: hidden; opacity: 0;
  transform: translateX(-50%);
  /* A box, not a ribbon. 24em is about eight words to the line, which is
     the width prose is comfortable at; min-width stops a three-word tip
     rendering as a sliver. */
  width: max-content; max-width: min(24em, 72vw); min-width: 12em;
  padding: var(--s3) var(--s3);
  font-size: var(--text-xs); font-weight: 400; line-height: 1.45;
  text-align: left; white-space: normal; cursor: auto;
  color: var(--accent-ink); background: var(--accent);
  border-radius: var(--radius-field);
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);
}
/* The pointer, drawn as a rotated corner of the same colour so there is
   only one colour to change. */
.tip-body::after {
  content: ""; position: absolute; top: 100%; left: 50%;
  width: 10px; height: 10px; margin: -5px 0 0 -5px;
  background: var(--accent); transform: rotate(45deg);
}
.tip:hover > .tip-body, .tip:focus > .tip-body,
.tip:focus-within > .tip-body { visibility: visible; opacity: 1; }
@media (prefers-reduced-motion: no-preference) {
  .tip-body { transition: opacity 120ms ease-out; }
}
/* Near the left or right edge of a card the box would hang off it. The
   modifier is applied by the markup, not guessed at runtime. */
.tip.tip-right > .tip-body { left: auto; right: -8px; transform: none; }
.tip.tip-right > .tip-body::after { left: auto; right: 12px; }

/* The proof, while it is still rendering. The frame already holds the
   surface's aspect ratio (see _surface_proofs.html), so nothing moves
   when the image lands; until it does, the overlays stay hidden and the
   mark turns slowly. Slowly on purpose — Dan asked for "just spin it
   round slowly", and a fast spinner on a two-second render reads as
   panic. */
.proof-frame:not(.is-loaded) { background: var(--surface-sunk);
  border: 1px solid var(--line); border-radius: var(--radius-field); }
.proof-frame:not(.is-loaded) > img { visibility: hidden; }
.proof-frame:not(.is-loaded) > .zone { display: none; }
.proof-frame.is-loaded > .proof-loading { display: none; }
.proof-loading {
  position: absolute; inset: 0; display: flex; gap: var(--s3);
  flex-direction: column; align-items: center; justify-content: center;
  color: var(--muted);
}
.proof-loading .mark { color: var(--accent); animation: pp-spin 2.4s linear infinite; }
.proof-loading-text { font-size: var(--text-xs); }
@keyframes pp-spin { to { transform: rotate(360deg); } }
@media (prefers-reduced-motion: reduce) {
  .proof-loading .mark { animation-duration: 9s; }
}

.text-xs { font-size: var(--text-xs); }
.page-head.compact { margin-bottom: var(--s3); }
.page-head.compact p { font-size: var(--text-sm); margin-bottom: 0; }

.merge-field-list { list-style: none; margin: 0; padding: 0; }
.merge-field-list li { margin-bottom: var(--s2); line-height: 1.25; }
.mf-desc { display: block; font-size: var(--text-xs); color: var(--muted); }

/* A merge field is a chip everywhere it appears: in the list you click,
   in the letter you are writing, and in the preview. Same shape in all
   three, so "the thing I clicked" and "the thing in my letter" are
   visibly one thing. */
.mf-chip, .mf-pill {
  display: inline-block; font-family: var(--font-mono);
  font-size: var(--text-xs); font-weight: 650; line-height: 1.5;
  padding: 1px var(--s2); border-radius: var(--radius-pill);
  background: var(--accent-soft); color: var(--accent-soft-ink);
  border: 1px solid var(--accent);
}
.mf-chip { cursor: pointer; }
.mf-chip:hover { filter: brightness(0.97); }
.mf-pill {
  user-select: all;            /* select the chip whole, never half of it */
  white-space: nowrap; margin: 0 1px; cursor: default;
}
.mf-pill-unknown {
  background: var(--warn-bg); color: var(--warn-ink); border-color: var(--warn-ink);
}

.editor-toolbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2);
  padding: var(--s2); margin: var(--s3) 0 0;
  border: 1px solid var(--line); border-bottom: 0;
  border-radius: var(--radius-field) var(--radius-field) 0 0;
  background: var(--surface-sunk);
}
.editor-toolbar .tool {
  min-width: 34px; min-height: 30px; cursor: pointer;
  background: var(--surface); color: inherit; font-size: var(--text-sm);
  border: 1px solid var(--line); border-radius: var(--radius-field);
}
.editor-toolbar .tool:hover { background: var(--surface-sunk); }
.editor-toolbar .tool-sep { flex: 1; }
.editor-toolbar .tool-field {
  display: inline-flex; align-items: center; gap: var(--s1);
  white-space: nowrap; font-size: var(--text-xs); color: var(--muted);
}
.editor-toolbar select {
  font-size: var(--text-xs); padding: 2px var(--s2); min-height: 30px;
}

.letter-editor {
  min-height: 340px; padding: var(--s4); overflow-y: auto;
  border: 1px solid var(--line);
  border-radius: 0 0 var(--radius-field) var(--radius-field);
  background: var(--surface); white-space: pre-wrap;
}
.letter-editor:focus { outline: 2px solid var(--accent); outline-offset: -1px; }
.letter-editor ul, .letter-editor ol,
.letter-preview ul, .letter-preview ol {
  white-space: normal; margin: var(--s2) 0 var(--s2) var(--s5); padding: 0;
}
.editor-foot { display: flex; flex-wrap: wrap; gap: var(--s3);
  align-items: center; margin-top: var(--s3); }
.field-row { display: block; font-size: var(--text-sm); }
.field-row.inline { display: flex; align-items: center; gap: var(--s2); }
.field-row input[type="text"], .field-row input[type="url"] { width: 100%; }
.check-row { display: flex; align-items: center; gap: var(--s2);
  font-weight: 650; margin-bottom: var(--s3); }
/* Two optional extras. The ticks stay lined up on one row (Dan, 13 Aug:
   "line it up with the Include QR code checkbox") but each one now owns a
   column, so its settings hang directly under its own tick rather than
   under both of them (Dan, 14 Aug: "make the options for each appear
   under its own checkbox, not under all the checkboxes"). One column on a
   narrow settings pane, where two would squeeze the fields. The columns
   answer to the pane's own width, not the window's: this card is as wide
   as the editor above it, which is a different number at every breakpoint. */
.check-row-pair {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
  gap: var(--s4) var(--s6); align-items: start;
}
.check-row-pair > .extra { min-width: 0; }
.check-row-pair .check-row { margin-bottom: var(--s2); }
.qr-options, .watermark-options {
  padding-left: var(--s4); border-left: 2px solid var(--line);
}
/* In a column half the card's width, the size and line-up fields share
   the row rather than each claiming 150-220px and wrapping onto two.
   Only above 820px: below that .filters is already a column (see the
   max-width: 820px block), where a flex-basis is read as a row height and
   would space the two fields 120px apart. */
@media (min-width: 821px) {
  .qr-options .filters > label { flex: 1 1 120px; min-width: 0; }
  .qr-options .filters input:not([type="checkbox"]):not([type="radio"]),
  .qr-options .filters select { min-width: 0; max-width: none; width: 100%; }
}

/* A short list of labelled facts — the size an image must be, what gets
   trimmed, how it is cropped. Prose was what made the letterhead box
   "very cramped and hard to read" (Dan, 13 Aug): four numbers in a
   paragraph have to be re-read to be found, and in a list they do not. */
.spec-list {
  display: grid; grid-template-columns: max-content 1fr;
  gap: var(--s2) var(--s3);
  margin: var(--s3) 0 0; font-size: var(--text-xs); line-height: 1.45;
}
.spec-list dt { color: var(--muted); font-weight: 650; }
.spec-list dd { margin: 0; }
@media (max-width: 520px) {
  .spec-list { grid-template-columns: 1fr; gap: 0 0; }
  .spec-list dd { margin-bottom: var(--s2); }
}
.preview-heading { font-size: var(--text-sm); margin: var(--s4) 0 var(--s2); }

.letter-preview {
  white-space: pre-wrap; font-family: var(--font); margin: 0;
  background: var(--surface-sunk); border-radius: var(--radius-field);
  padding: var(--s4); min-height: 6em;
}

/* --- the allowance meter, and the locked value (869en5w3z) -------------
   Two components the upfront-payment workflow needed and the site had
   nowhere to put. Both are built from existing tokens only — no new
   colours, no new radii — because this was re-plumbing, not a redesign.

   The bar is a plain div rather than <progress>, whose rendering is the
   browser's and cannot be made to match the card it sits in on Windows,
   macOS and Android at once. The accessible name lives on the wrapper
   (role="img"), so a screen reader hears "18 of 30 included letters
   used" rather than a percentage nobody asked for. */
.meter {
  height: 10px; border-radius: var(--radius-pill); overflow: hidden;
  background: var(--surface-sunk); box-shadow: inset 0 0 0 1px var(--line);
  margin-bottom: var(--s2);
}
.meter-fill {
  height: 100%; background: var(--accent); border-radius: var(--radius-pill);
  /* A zero-width fill must still be invisible, not a 1px teal dot. */
  min-width: 0; transition: width .2s ease;
}
/* Allowance spent. Warn, not danger: nothing has gone wrong — the next
   letter simply costs money, which the line underneath says. */
.meter-fill.full { background: var(--warn-ink); }
.meter-line { margin: 0 0 var(--s1); font-size: var(--text-sm);
  display: flex; flex-wrap: wrap; gap: var(--s2); align-items: baseline; }
.meter-line b { font-size: var(--text-lg); font-weight: 700; color: var(--structure);
  font-variant-numeric: tabular-nums; }
/* The compact form: the same meter on a screen that already has a
   headline (browse, the confirm step). Card chrome would make it compete
   with the page's own first card. */
.meter-strip {
  border: 1px solid var(--line); border-left: 4px solid var(--accent);
  border-radius: var(--radius-field); padding: var(--s3) var(--s4);
  margin-bottom: var(--s4); background: var(--surface);
}
.meter-strip .meter-line b { font-size: var(--text-base); }
.topup-row { display: flex; flex-wrap: wrap; gap: var(--s2);
  align-items: center; margin-top: var(--s3); }
.topup-row .btn { width: auto; flex: 0 0 auto; }

/* A value withheld until it is paid for. Quiet on purpose: it appears on
   every row of a fifty-row table, and anything with a background colour
   at that density reads as an error state. */
.locked { display: inline-flex; align-items: center; gap: var(--s1);
  color: var(--muted); font-size: var(--text-sm); }
.locked-mark { font-size: 11px; opacity: .75; }
.locked-text { font-style: italic; }

/* Selection + action bar above and below the applications list (D4). */
.send-bar { flex-wrap: wrap; gap: var(--s3); margin: var(--s3) 0; align-items: center; }

/* --- D1 second pass: photographs, QR, and honest previews --------------- */

.band-editor {
  border: 1px solid var(--line);
  border-radius: var(--radius-field);
  padding: var(--s4);
  margin-bottom: var(--s4);
}
.band-thumbs { display: flex; flex-wrap: wrap; gap: var(--s3); margin: var(--s3) 0; }
.band-thumb { margin: 0; max-width: 180px; }
.band-thumb img {
  display: block; width: 100%; height: 100px;
  object-fit: cover;                 /* the same crop the print path does */
  border-radius: var(--radius-field);
  border: 1px solid var(--line);
}
.band-thumb figcaption { margin: var(--s2) 0; }
/* Resolution verdicts. "poor" has to be uncomfortable to look at: it is
   the difference between a good letter and £1.20 spent on a blurry one. */
.resolution-good { color: var(--muted); }
.resolution-ok   { color: #8a6d1f; }
.resolution-poor { color: #a2261d; font-weight: 600; }

.recipient-switcher {
  display: flex; flex-wrap: wrap; gap: var(--s4);
  align-items: flex-start; justify-content: space-between;
  margin-bottom: var(--s4);
}
.recipient-nav { display: flex; align-items: center; gap: var(--s2); }
.letter-address { margin: var(--s2) 0; line-height: 1.4; }

/* The proof, with the clear zone drawn over it. Percentage geometry so it
   stays correct at any rendered width — see letters/views.py CLEAR_ZONE. */
.proof-frame { position: relative; display: inline-block; max-width: 100%; }
/* Square corners, not rounded. Paper has square corners, and a letterhead
   that prints to the edge of the sheet had its own corners quietly clipped
   by the radius — which is exactly the "is this reaching the edge or not?"
   question Dan asked on 13 Aug. The hairline stays: it is what separates
   white paper from the white card behind it. */
.proof-frame img {
  display: block; max-width: 100%;
  border: 1px solid var(--line);
}
/* --- surfaces (869efge9g, 9 Aug) --------------------------------------
   Every page and every side of a piece, each an exact proof with its own
   reserved areas drawn on it. One rule set for all formats: a two-page
   letter and a postcard's two sides are the same problem, and giving
   them separate CSS is how the second one ends up subtly different from
   the first. Side by side where there is room, stacked where there is
   not — a postcard is read without being opened, so the picture is not a
   detail, and a letter's second page is not an appendix. */
.surface-proofs { display: flex; flex-wrap: wrap; gap: var(--s4); }
.surface-proofs .surface { flex: 1 1 320px; margin: 0; min-width: 0; }
.surface-proofs .proof-frame { display: block; }
.surface-proofs img { width: 100%; }
.surface-title {
  font-size: var(--text-sm); font-weight: 600; color: var(--muted);
  margin-bottom: var(--s2);
}

/* A reserved area. `zone-address` is a rule the client must obey;
   `zone-printer` is simply something that will be there. Two identical
   red boxes would read as two warnings. */
.zone { position: absolute; pointer-events: none; }
.zone-label {
  position: absolute; left: 0; bottom: 100%;
  font-size: 11px; line-height: 1.2;
  background: var(--surface); padding: 0 2px; white-space: nowrap;
}
/* A zone flush against the right edge of the card would push its caption
   off the proof. Anchor it the other way instead of shortening the words. */
.zone.label-right > .zone-label { left: auto; right: 0; }
.zone-address, .clear-zone {
  position: absolute;
  border: 2px dashed #a2261d;
  background: rgba(162, 38, 29, 0.06);
  pointer-events: none;
}
.zone-address > .zone-label { color: #a2261d; }
.zone-printer {
  border: 2px dashed #6b6f76;
  background: rgba(107, 111, 118, 0.10);
}
/* The trim line, drawn only on a letter whose letterhead runs to the edge
   of the paper. No fill — the whole page is inside it, and a tint over the
   whole proof would be a tint over the proof. Its label hangs *inside* the
   box for the same reason: there is nothing outside it to hang into. */
.zone-trim {
  border: 1px dashed var(--accent);
  background: none;
}
.zone-trim > .zone-label {
  bottom: auto; top: 0; left: 0; color: var(--accent-soft-ink);
  background: var(--accent-soft);
}
/* Under, not over: the printer's mark sits inside the address zone, and
   two labels stacked on the same edge overlap. */
.zone-printer > .zone-label { top: 100%; bottom: auto; color: #5C5852; }
.clear-zone-label {
  position: absolute; left: 0; bottom: 100%;
  font-size: 11px; line-height: 1.2; color: #a2261d;
  background: var(--surface); padding: 0 2px; white-space: nowrap;
}
/* --- not-production banner (869ee2b2q) --------------------------------
   Staging is the same code on the same machine as production, so the only
   reliable difference is that it says so. Deliberately not a design-system
   colour: it should look like it does not belong, because it doesn't. */
.env-banner {
  position: sticky; top: 0; z-index: 100;
  background: repeating-linear-gradient(
    135deg, #b3261e, #b3261e 12px, #8c1d17 12px, #8c1d17 24px);
  color: #fff; text-align: center;
  font-size: var(--text-sm); font-weight: 700; letter-spacing: .02em;
  padding: var(--s2) var(--s4);
}
@media print { .env-banner { display: none; } }

/* Stannp's own barcode. Deliberately a quieter grey than the address
   zone: the address zone is a rule the client must obey, this is simply
   something that will be there. Two identical red boxes would read as two
   warnings. */
.barcode-zone {
  position: absolute;
  border: 2px dashed #6b6f76;
  background: rgba(107, 111, 118, 0.10);
  pointer-events: none;
}
.barcode-zone-label {
  position: absolute; left: 0; top: 100%;
  font-size: 11px; line-height: 1.2; color: #5C5852;
  background: var(--surface); padding: 0 2px; white-space: nowrap;
}

/* --- the mark (869efdw9d) ----------------------------------------------
   Postmark, direction 02. Inline SVG so it inherits currentColor and
   costs no request; the raster copies are generated by
   scripts/make_favicons.py. The wordmark is set in the system stack —
   the product carries no webfont, deliberately (C2a). */
.mark { display: block; flex: 0 0 auto; }
.brand { display: inline-flex; align-items: center; gap: var(--s2); }
/* nowrap belts the &nbsp; braces in the markup: the name is one word to a
   reader and must never break as "Plan &" / "Post" (869efdw9d). */
.brand span { font-weight: 700; letter-spacing: -0.005em; color: var(--ink); white-space: nowrap; }
.brand:hover span { color: var(--accent-hover); }

/* --- public marketing pages (869efdw9d) --------------------------------
   Full-bleed sections with an inner .wrap, so a band's background can run
   edge to edge while its text keeps the same 1080px measure as the app.
   NO new colours: every pair below is an existing token pair with a
   measured ratio. The two that were not already recorded in this file:
   accent-soft on accent = 5.53:1 light, 5.66:1 dark. Both clear AA. */
.main-marketing { max-width: none; margin: 0; padding: 0; }
.wrap { max-width: 1080px; margin: 0 auto; padding: 0 var(--s4); }
.public-header { position: static; }
.public-nav a { font-size: var(--text-sm); }
.public-header .header-end { gap: var(--s2); }

.section { padding: var(--s8) 0; border-top: 1px solid var(--line); }
.section.plain { background: var(--ground); border-top: 0; }
.section.raised { background: var(--surface); }
.section.on-accent { background: var(--accent); color: var(--accent-ink); border-top: 0; }
.section.on-accent h2, .section.on-accent h3 { color: var(--accent-ink); }
.section.on-accent .lede, .section.on-accent .muted { color: var(--accent-soft); }
.section.on-accent a:not(.btn) { color: var(--accent-soft); }
/* Reversed buttons: solid = accent-ink ground with accent text (6.7:1
   light, 7.8:1 dark — the same pair as the band, swapped). */
.section.on-accent .btn:not(.quiet) {
  background: var(--accent-ink); color: var(--accent);
}
.section.on-accent .btn:not(.quiet):hover { background: var(--accent-soft); color: var(--accent-hover); }
.section.on-accent .btn.quiet {
  background: transparent; color: var(--accent-ink);
  box-shadow: inset 0 0 0 1.5px var(--accent-soft);
}
.section.on-accent .btn.quiet:hover { background: var(--accent-hover); color: var(--accent-ink); }
.section h2 { margin-top: 0; font-size: var(--text-2xl); }
.section-head { max-width: 62ch; margin-bottom: var(--s6); }

.eyebrow {
  font-size: var(--text-xs); font-weight: 700; letter-spacing: .09em;
  text-transform: uppercase; color: var(--accent); margin-bottom: var(--s3);
}
.on-accent .eyebrow { color: var(--accent-soft); }
.lede { font-size: var(--text-lg); color: var(--muted); max-width: 62ch; }

/* Hero: copy left, diagram right; one column under 900px, copy first. */
.hero { padding: var(--s8) 0; background: var(--ground); }
.hero-grid { display: grid; gap: var(--s7); align-items: center;
  grid-template-columns: 1fr; }
@media (min-width: 900px) {
  .hero-grid { grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr); }
}
.hero h1 { font-size: clamp(30px, 5.2vw, 46px); letter-spacing: -0.02em; }
.hero .lede { font-size: clamp(16px, 2.2vw, 20px); color: var(--ink); }
.hero-figure { max-width: 100%; }
.hero-figure svg { display: block; width: 100%; height: auto; }

.trust-strip {
  display: grid; gap: var(--s3);
  /* Fixed 2x2, not auto-fit: the four claims are a block, and auto-fit
     kept dropping the fourth onto a lonely second row. */
  grid-template-columns: repeat(2, 1fr);
  margin-top: var(--s6);
}
.trust-strip div { background: var(--surface); border-radius: var(--radius-card);
  box-shadow: var(--shadow-card); padding: var(--s3) var(--s4); }
.trust-strip b { display: block; font-size: var(--text-xl); font-weight: 700;
  color: var(--accent); font-variant-numeric: tabular-nums; line-height: 1.2; }
.trust-strip span { font-size: var(--text-xs); color: var(--muted); }

.grid-2 { display: grid; gap: var(--s4); grid-template-columns: 1fr; }
.grid-3 { display: grid; gap: var(--s4); grid-template-columns: 1fr; }
@media (min-width: 700px) { .grid-2 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 700px) { .grid-3 { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1000px) { .grid-3 { grid-template-columns: repeat(3, 1fr); } }

.feature { background: var(--surface); border-radius: var(--radius-card);
  box-shadow: var(--shadow-card); padding: var(--s5); }
.feature h3 { margin-bottom: var(--s2); }
.feature p { color: var(--muted); font-size: var(--text-sm); margin: 0; }
.feature-icon { color: var(--accent); margin-bottom: var(--s3); }
.feature-icon svg { display: block; }

.step { position: relative; background: var(--surface);
  border-radius: var(--radius-card); box-shadow: var(--shadow-card);
  padding: var(--s5); }
.step .n { display: inline-flex; align-items: center; justify-content: center;
  width: 34px; height: 34px; border-radius: var(--radius-pill);
  background: var(--accent-soft); color: var(--accent-soft-ink);
  font-weight: 700; font-size: var(--text-sm); margin-bottom: var(--s3); }
.step h3 { margin-bottom: var(--s2); }
.step p { color: var(--muted); font-size: var(--text-sm); margin: 0; }

/* --- the two ways to buy (869en5w2x) ------------------------------------
   The landing page's one job: two products, side by side, each with its
   own call to action. Same card furniture as .price-card further down the
   page, plus a footer that pins the CTA to the bottom edge — two cards
   whose buttons do not line up read as two different offers rather than
   one choice, and the pitches are never the same number of lines.

   The accent rule sits on the FREE card, not on the plan, and the solid
   button is there too. That is BUSINESS-CASE §9.7 ("the landing offer is
   the free tier, not a demo request"), not a mistake: the money is in the
   letters, and the free tier is what gets a builder as far as sending
   one. */
.ways { align-items: stretch; }
.way { display: flex; flex-direction: column; background: var(--surface);
  border-radius: var(--radius-card); box-shadow: var(--shadow-raised);
  padding: var(--s5); border-top: 3px solid transparent; }
.way.lead { border-top-color: var(--accent); }
.way .amount { font-size: 44px; font-weight: 700; color: var(--accent);
  line-height: 1.1; font-variant-numeric: tabular-nums; }
.way .per { font-size: var(--text-sm); color: var(--muted);
  display: block; margin-bottom: var(--s4); }
.way-pitch { font-size: var(--text-base); margin: 0; }
.way-cta { margin-top: auto; padding-top: var(--s5); }
.way-cta .waiting-list { margin-top: 0; }
.ways-foot { max-width: 72ch; margin-top: var(--s5); }

/* Pricing: two cards, then the worked invoice beside them. */
.price-card { background: var(--surface); border-radius: var(--radius-card);
  box-shadow: var(--shadow-raised); padding: var(--s5); }
.price-card .amount { font-size: 40px; font-weight: 700; color: var(--accent);
  line-height: 1.1; font-variant-numeric: tabular-nums; }
.price-card .per { font-size: var(--text-sm); color: var(--muted);
  display: block; margin-bottom: var(--s3); }
.price-card ul { margin: var(--s3) 0 0; padding-left: 1.15em;
  font-size: var(--text-sm); color: var(--muted); }
.price-card li { margin-bottom: var(--s2); }

.invoice { background: var(--surface); border-radius: var(--radius-card);
  box-shadow: var(--shadow-card); padding: var(--s5); }
.invoice .line { display: flex; justify-content: space-between; gap: var(--s4);
  padding: var(--s2) 0; border-bottom: 1px solid var(--line);
  font-size: var(--text-sm); font-variant-numeric: tabular-nums; }
.invoice .line:last-of-type { border-bottom: 0; }
.invoice .line.total { font-weight: 700; font-size: var(--text-lg);
  color: var(--accent); border-top: 2px solid var(--line-strong); border-bottom: 0; }

/* "Why people switch": claim on the left, what we do about it on the right. */
.compare { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
.compare th { text-align: left; padding: var(--s3) var(--s4);
  border-bottom: 1px solid var(--line); font-size: var(--text-xs);
  text-transform: uppercase; letter-spacing: .05em; color: var(--muted); }
.compare td { padding: var(--s3) var(--s4); border-bottom: 1px solid var(--line);
  vertical-align: top; }
.compare tbody tr:last-child td { border-bottom: 0; }
.compare td.ours { font-weight: 600; }

.faq { max-width: 72ch; }
.faq details { background: var(--surface); border-radius: var(--radius-card);
  box-shadow: var(--shadow-card); padding: var(--s3) var(--s5);
  margin-bottom: var(--s3); }
.faq summary { cursor: pointer; font-weight: 650; padding: var(--s2) 0;
  min-height: 44px; display: flex; align-items: center; }
.faq details p { color: var(--muted); font-size: var(--text-sm); }
.faq details > :last-child { margin-bottom: var(--s3); }

.cta-row { display: flex; flex-wrap: wrap; gap: var(--s3); margin-top: var(--s5); }
@media (max-width: 700px) { .cta-row .btn { width: 100%; } }

/* --- pre-launch waiting list (869ej4xwr) ------------------------------- */
/* The box that replaced "Create an account" on the front page. `min-width:0`
   on the input is the load-bearing rule: a flex item's default min-width is
   its content width, so without it the field refuses to shrink and the whole
   page scrolls sideways on a 320px phone — which test_front_page.py checks. */
.waiting-list { margin-top: var(--s4); }
.hero-waiting-list, .footer-waiting-list { max-width: 32rem; }
.waiting-list .actions { margin-top: var(--s2); flex-wrap: wrap; }
.waiting-list input[type="email"] { flex: 1 1 14rem; min-width: 0; }
@media (max-width: 480px) {
  .waiting-list .actions > * { flex: 1 1 100%; }
}
.on-accent .waiting-list label,
.on-accent .waiting-list .muted { color: var(--accent-soft); }
.on-accent .waiting-list input[type="email"] {
  background: var(--surface); color: var(--ink); border-color: var(--accent-ink);
}

.checklist { list-style: none; padding: 0; margin: 0; }
.checklist li { position: relative; padding-left: 28px; margin-bottom: var(--s3);
  font-size: var(--text-sm); }
.checklist li::before {
  content: ""; position: absolute; left: 4px; top: 6px;
  width: 7px; height: 12px; border: solid var(--accent);
  border-width: 0 2.5px 2.5px 0; transform: rotate(45deg);
}
.on-accent .checklist li::before { border-color: var(--accent-ink); }

@media (max-width: 820px) {
  .public-nav { display: none; }   /* the sections are all one scroll away */
  .public-header .header-end { flex-basis: auto; margin-left: auto; }
  .public-header .bar { flex-wrap: nowrap; }
  /* .btn goes full-width on phones for thumbs; the two header buttons
     must not, or the logged-out bar becomes a page of its own. */
  .public-header .btn { width: auto; }
  .brand span { font-size: var(--text-base); }
  .section, .hero { padding: var(--s7) 0; }
}

/* ---------------------------------------------------------------- brand page
   /brand/ — the public logo pack. Previews have to show the logo against the
   backgrounds it is actually approved for, because "white on teal" is
   meaningless as a filename and obvious as a swatch. */
.muted-note { color: var(--muted); font-size: var(--text-sm); }

.brand-strips { display: grid; gap: var(--s3); margin: var(--s4) 0 0;
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
.brand-strip { display: flex; flex-direction: column; align-items: center;
  justify-content: center; gap: var(--s3); padding: var(--s5);
  border: 1px solid var(--line); border-radius: var(--radius-card);
  min-height: 150px; }
.brand-strip.tall { min-height: 200px; }
.brand-strip img { max-width: 100%; height: auto; }
.brand-strip span { font-size: var(--text-xs); color: var(--muted); }
/* Literals, not tokens: these panels demonstrate the logo on fixed brand
   backgrounds, so they must NOT follow the dark-mode theme. A white logo
   shown on a dark-mode "stone" panel would be an invisible preview. */
.brand-strip.on-stone { background: #F6F4F1; }
.brand-strip.on-white { background: #FFFFFF; }
.brand-strip.on-teal { background: #14675F; }
.brand-strip.on-teal span { color: rgba(255, 255, 255, .8); }
.brand-strip.on-stone span, .brand-strip.on-white span { color: #5C5852; }

.brand-swatches { display: grid; gap: var(--s4); margin-top: var(--s4);
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
.brand-swatches i { display: block; height: 56px; border-radius: var(--radius-field);
  border: 1px solid var(--line-strong); margin-bottom: var(--s2); }
.brand-swatches b { display: block; font-size: var(--text-sm); }
.brand-swatches code { font-family: var(--font-mono); font-size: var(--text-xs); }
.brand-swatches span { display: block; font-size: var(--text-xs); color: var(--muted); }

.brand-rules { margin: var(--s4) 0 0; padding-left: var(--s5); }
.brand-rules li { margin-bottom: var(--s3); font-size: var(--text-sm); }

.brand-folder { font-size: var(--text-sm); text-transform: uppercase;
  letter-spacing: .06em; color: var(--muted); margin: var(--s5) 0 var(--s2); }
.brand-files { list-style: none; padding: 0; margin: 0; display: grid; gap: 2px;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); }
.brand-files li { display: flex; justify-content: space-between; gap: var(--s3);
  padding: var(--s2) var(--s3); border-radius: var(--radius-field);
  font-size: var(--text-sm); }
.brand-files li:hover { background: var(--surface-sunk); }
.brand-files span { color: var(--muted); font-size: var(--text-xs);
  white-space: nowrap; }

/* The two-step charge confirmation on "add a council" (869efdru3).
 * A native <dialog>: the browser gives focus trapping, Esc-to-close and
 * the top layer for free, so this is only paint. It lives inside the
 * add-council <form>, which is how its confirm button is the submit. */
.confirm-dialog {
  max-width: 34rem; width: calc(100% - var(--s5) * 2);
  border: 1px solid var(--line-strong); border-radius: var(--radius-card);
  background: var(--surface); color: inherit;
  padding: var(--s5); box-shadow: var(--shadow-card);
}
.confirm-dialog::backdrop { background: rgba(0, 0, 0, .45); }
.confirm-dialog h2 { font-size: var(--text-lg); }
.confirm-dialog ul { margin: 0 0 var(--s4); padding-left: var(--s5); }
.confirm-dialog li { margin-bottom: var(--s2); font-size: var(--text-sm); }
.confirm-dialog .strong { font-weight: 650; margin-bottom: var(--s2); }

/* Councils picked but not yet subscribed (869eg9yrb). Pinned to the
 * bottom of the viewport because the whole failure it guards against is
 * someone scrolling the list, forgetting the basket and browsing away —
 * a bar that scrolls out of sight guards nothing. */
.pending-bar {
  position: sticky; bottom: var(--s3); z-index: 20;
  display: flex; flex-wrap: wrap; gap: var(--s3); align-items: center;
  margin: var(--s4) 0; padding: var(--s3) var(--s4);
  background: var(--warn-bg); color: var(--warn-ink);
  border: 1px solid var(--line-strong); border-radius: var(--radius-card);
  box-shadow: var(--shadow-card); font-size: var(--text-sm);
}
.pending-bar span { flex: 1 1 14rem; }
.pending-bar[hidden] { display: none; }

/* The minerals-and-waste table's heading. Its own <h2> so the split
 * between "councils that do houses" and "councils that do quarries" is a
 * document structure a screen reader announces, not just a gap. */
.section-title { font-size: var(--text-base); margin: var(--s5) 0 var(--s2); }

/* --- Forecast against actual (869en5w9r) -----------------------------
 *
 * Charts are inline SVG rendered server-side (`staffroom/charts.py`);
 * nothing here positions anything, because every coordinate arrives
 * already computed. What lives here is the part that has to know about
 * the theme: the palette, which has a light value and a dark one.
 *
 * The four scenario curves are an ORDINAL ramp — good, base, bad,
 * runaway is an ordered scale, so the reader sees the order in the
 * colour without reading the legend. One hue (the brand accent, OKLCH
 * 185 degrees), four monotone lightness steps, severity mapped to
 * salience in BOTH modes: the worse the scenario, the louder it is
 * against its own background, which is why the ramp inverts under
 * prefers-color-scheme: dark rather than being flipped wholesale.
 *
 * Validated, not eyeballed, with the data-viz skill's ordinal checks
 * (monotone L, adjacent delta-L >= 0.06, pale end >= 2:1 on the card
 * surface, hue spread <= 40deg): all four pass in both modes.
 *
 * The ACTUAL line is not a step on that ramp. It wears ink, so it can
 * never be mistaken for a scenario: worst separation from any ramp step
 * is OKLab delta-E 21.4 simulated protan/deutan and 22.9 normal in
 * light, 15.5 / 20.9 in dark — clear of the 8 and 15 floors.
 */
.fc-good    { --fc: #4DAA9F; }   /* 2.78:1 on the card */
.fc-base    { --fc: #35958A; }   /* 3.61:1 */
.fc-bad     { --fc: #167F75; }   /* 4.85:1 */
.fc-runaway { --fc: #006B61; }   /* 6.41:1 */
.fc-actual  { --fc: #1C232C; }   /* 15.84:1 — ink, not a ramp step */

@media (prefers-color-scheme: dark) {
  .fc-good    { --fc: #25887E; }   /* 3.25:1 on the card */
  .fc-base    { --fc: #3F9E93; }   /* 4.33:1 */
  .fc-bad     { --fc: #57B4A9; }   /* 5.64:1 */
  .fc-runaway { --fc: #6FCABF; }   /* 7.22:1 */
  .fc-actual  { --fc: #F6F4F1; }   /* 12.69:1 */
}

.chart-wrap { margin: var(--s3) 0 var(--s4); }
.chart-svg { display: block; width: 100%; height: auto; overflow: visible; }

.chart-grid { stroke: var(--line); stroke-width: 1; }
.chart-zero { stroke: var(--line-strong); stroke-width: 1.5; }
.chart-axis { fill: var(--muted); font-size: 11px;
              font-family: var(--font); font-variant-numeric: tabular-nums; }
.chart-divider { stroke: var(--line-strong); stroke-width: 1;
                 stroke-dasharray: 3 4; }

.chart-line { stroke: var(--fc); stroke-width: 2;
              stroke-linecap: round; stroke-linejoin: round; }
/* A span the business case does not publish. Drawn straight because
 * there is nothing to draw, dashed because a straight line across four
 * unmodelled months must not look like four modelled ones. */
.chart-line-gap { stroke-dasharray: 5 4; opacity: .85; }
.chart-line-actual { stroke-width: 3; }
.chart-marker { fill: var(--fc); stroke: var(--surface); stroke-width: 2; }
.chart-endlabel { fill: var(--ink); font-size: 11px; font-family: var(--font);
                  font-variant-numeric: tabular-nums; }
/* Text wears text tokens, never the series colour — the mark beside it
 * carries the identity. The label is only nudged toward the hue enough
 * to pair with its line. */
.chart-endlabel.fc-actual { font-weight: 650; }
.chart-hit { fill: transparent; pointer-events: all; }

/* Two columns, not a wrapping flex row: the runaway case's summary is
 * two lines long and a flex row put its clickable "i" on a line of its
 * own, orphaned from the name it explains. */
.chart-legend { list-style: none; margin: 0; padding: 0;
                display: grid; gap: var(--s3); }
.chart-legend li { display: grid; gap: var(--s1) var(--s3);
                   grid-template-columns: minmax(9rem, max-content) 1fr;
                   align-items: baseline; }
.chart-legend-name { display: flex; align-items: center; gap: var(--s2); }
@media (max-width: 640px) {
  .chart-legend li { grid-template-columns: 1fr; }
}
.chart-swatch { display: inline-block; width: 22px; height: 4px;
                border-radius: 2px; background: var(--fc);
                flex: 0 0 auto; }

.spec-details { margin-top: var(--s3); font-size: var(--text-sm); }
.spec-details summary { cursor: pointer; color: var(--accent);
                        font-weight: 600; }
.spec-details[open] summary { margin-bottom: var(--s3); }

.forecast-note { margin-top: var(--s4); padding: var(--s3) var(--s4);
                 background: var(--surface-sunk); color: var(--ink);
                 border-radius: var(--radius-field); }
.forecast-note ul { margin: var(--s2) 0 0; padding-left: var(--s5); }
.forecast-note li { margin-bottom: var(--s2); }
.forecast-note li:last-child { margin-bottom: 0; }

/* Forced colours flattens every stroke to the same system colour, which
 * would leave four identical curves. Dash patterns are the fallback
 * identity channel; the legend, the endpoint labels and the table view
 * carry it the rest of the way. */
@media (forced-colors: active) {
  .chart-line { stroke: CanvasText; }
  .fc-good.chart-line    { stroke-dasharray: 1 5; }
  .fc-base.chart-line    { stroke-dasharray: 8 4; }
  .fc-bad.chart-line     { stroke-dasharray: 3 3; }
  .fc-runaway.chart-line { stroke-dasharray: 12 3 2 3; }
  .fc-actual.chart-line  { stroke-dasharray: none; stroke-width: 3.5; }
  .chart-marker { fill: CanvasText; stroke: Canvas; }
}

/* ---------------------------------------------------------------- place
 * The public council and area pages (869en5wnv). Same tokens as every
 * other screen, so light and dark both come for free; nothing here is a
 * literal colour.
 *
 * `.basis` is the load-bearing class. Every figure on a place page
 * carries the word that says where it came from — Measured, Declared,
 * No source — and it is styled as quiet metadata rather than as a
 * disclaimer, because it is a property of the number and not an apology
 * for it. The same vocabulary the stop-loss table uses.
 */
.place-page { padding: var(--s6) 0 var(--s8); }
.breadcrumbs { font-size: var(--text-sm); color: var(--muted);
               margin-bottom: var(--s4); }
.breadcrumbs a { color: var(--muted); }
.place-header h1 { margin: 0 0 var(--s3); }
.place-authority, .place-lede { color: var(--muted); max-width: 62ch;
                                margin: 0 0 var(--s4); }
.basis { display: inline-block; font-size: var(--text-xs);
         color: var(--muted); background: var(--surface-sunk);
         border-radius: 999px; padding: 1px var(--s2); margin-left: var(--s2);
         white-space: nowrap; }
.place-note { background: var(--surface-sunk); border-radius: var(--radius-card);
              padding: var(--s4); margin: var(--s4) 0; max-width: 72ch; }

.place-figures { display: grid; gap: var(--s3); margin: var(--s5) 0;
                 grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); }
.place-figures .figure { background: var(--surface); border-radius: var(--radius-card);
                         padding: var(--s4); display: flex; flex-direction: column;
                         gap: var(--s1); }
.place-figures .figure b { font-size: var(--text-2xl); color: var(--accent);
                           line-height: 1.1; }
.place-figures .figure span { font-size: var(--text-sm); color: var(--muted); }
.place-figures .figure .basis { align-self: flex-start; margin-left: 0; }

.place-trend { margin: var(--s5) 0; }
.place-trend figcaption { font-size: var(--text-sm); color: var(--muted);
                          margin-bottom: var(--s2); }
.trend-chart { max-width: 420px; display: block; }
.trend-bar { fill: var(--accent); }
.trend-axis { display: flex; justify-content: space-between; max-width: 420px;
              font-size: var(--text-xs); color: var(--muted); margin: var(--s1) 0 0; }

.proposal-list { list-style: none; margin: 0; padding: 0; }
.proposal-list li { background: var(--surface); border-radius: var(--radius-card);
                    padding: var(--s4); margin-bottom: var(--s2); }
.proposal-list .proposal { display: block; margin-bottom: var(--s2); }
.proposal-meta { display: flex; flex-wrap: wrap; gap: var(--s2);
                 align-items: center; font-size: var(--text-sm);
                 color: var(--muted); }

.place-limit { background: var(--surface); border-radius: var(--radius-card);
               padding: var(--s5); margin: var(--s5) 0; max-width: 72ch; }
.place-limit h2 { margin-top: 0; }
.place-honest { color: var(--muted); font-size: var(--text-sm); }

.place-link-list { list-style: none; margin: 0; padding: 0; display: grid;
                   gap: var(--s2); grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); }
.place-link-list.wide { grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); }
.place-link-list li { display: flex; justify-content: space-between;
                      align-items: center; gap: var(--s2);
                      padding: var(--s2) var(--s3); background: var(--surface);
                      border-radius: var(--radius-field); }
.place-link-list li > span { font-size: var(--text-xs); color: var(--muted); }

.place-cta { background: var(--surface-sunk); border-radius: var(--radius-card);
             padding: var(--s5); margin-top: var(--s6); }
.place-cta h2 { margin-top: 0; }

/* The staff publication table (869en5wnv rule 7). */
.pub-decision { display: flex; gap: var(--s1); flex-wrap: wrap; }
.pub-decision button { font-size: var(--text-xs); }

/* --- The editorial guides (869en5wux) ---------------------------------
 *
 * Three long-form pages plus a hub, reusing the place pages' shell
 * (`.place-page`, `.place-header`, `.place-figures`, `.basis`) so a
 * guide reads as the same site rather than as a microsite. What is new
 * here is what a place page has no use for: a wide comparison table
 * that has to survive a phone, a distribution chart, and a verdict box.
 *
 * The histogram's geometry is computed in `staffroom/charts.py` and
 * nothing below positions anything. What lives here is the part that
 * has to know about the theme, which is one colour: the bars are a
 * SINGLE series, so they take the brand accent token and no ramp. The
 * bins are ordered along the x-axis and that axis already carries the
 * ordering — painting them on a scale as well would encode the same
 * fact twice and invite a reader to hunt for a meaning that is not
 * there. `--accent` is 6.9:1 on white and 5.1:1 on the dark surface,
 * both far clear of the 3:1 floor for a non-text mark, and it is reused
 * rather than re-minted because a fifth teal nobody validated is how a
 * palette rots.
 */

.guide { max-width: none; }
.guide-body { max-width: 72ch; }
.guide-body h2 { margin-top: var(--s6); }
.guide-body h3 { margin-top: var(--s5); }
.guide-body ol, .guide-body ul { max-width: 72ch; }
.guide-body li { margin-bottom: var(--s2); }
.guide-meta { font-size: var(--text-sm); color: var(--muted); }

.guide-entry { flex-direction: column; align-items: flex-start;
               gap: var(--s1); padding: var(--s3) var(--s4); }
.guide-entry > span { font-size: var(--text-sm); }

.guide-plain-list { list-style: none; margin: var(--s3) 0; padding: 0;
                    max-width: 72ch; }
.guide-plain-list li { background: var(--surface);
                       border-radius: var(--radius-field);
                       padding: var(--s2) var(--s3); margin-bottom: var(--s2); }

.guide-sources { list-style: none; margin: var(--s3) 0; padding: 0;
                 max-width: 72ch; font-size: var(--text-sm); }
.guide-sources li { margin-bottom: var(--s3); }
.guide-sources li span { display: block; color: var(--muted); }

.guide-notes { max-width: 72ch; margin: var(--s3) 0; }
.guide-notes dt { font-weight: 650; margin-top: var(--s3); }
.guide-notes dd { margin: 0; color: var(--muted); font-size: var(--text-sm); }

/* A comparison table is genuinely wide and the alternative to scrolling
 * it is dropping columns, which drops the volumes a reader came for.
 * The row header is sticky so a name never scrolls out from under its
 * own numbers. */
.guide-table-scroll { overflow-x: auto; margin: var(--s4) 0 var(--s5); }
.guide-table { border-collapse: collapse; width: 100%;
               font-size: var(--text-sm); }
.guide-table th, .guide-table td { padding: var(--s2) var(--s3);
                                   text-align: left; vertical-align: top;
                                   border-bottom: 1px solid var(--line); }
.guide-table thead th { font-size: var(--text-xs); color: var(--muted);
                        text-transform: uppercase; letter-spacing: .04em;
                        white-space: nowrap; }
.guide-table tbody th { font-weight: 600; }
/* Only the market comparison needs a wide row header — a vendor name
 * plus its basis and tags. The five league tables carry a council name
 * and must not inherit it: 16rem of row header plus two columns
 * overflowed a 390px phone by 99px, caught by the e2e viewport test
 * rather than by reading the stylesheet. */
.guide-table.wide tbody th { min-width: 16rem; }
.guide-table td b { display: block; }
.guide-table td span { font-size: var(--text-xs); color: var(--muted); }
.guide-table td .none { font-style: italic; }
.guide-table tr.is-ours { background: var(--surface); }
.guide-table tr.is-ours tbody th { font-weight: 700; }
.guide-table tr.is-diy th, .guide-table tr.is-diy td { font-style: italic; }
.guide-table .tag { display: inline-block; font-size: var(--text-xs);
                    color: var(--muted); border: 1px solid var(--line);
                    border-radius: var(--radius-field);
                    padding: 0 var(--s1); margin-left: var(--s1);
                    font-style: normal; }
.guide-table .tag.warn { border-color: var(--accent); color: var(--accent); }

/* `min(100%, 320px)` rather than a bare 320px: `minmax(320px, 1fr)`
 * makes the track 320px wide even when the container is narrower than
 * that, which is how a two-up grid pushes a phone sideways. */
.guide-pair { display: grid; gap: var(--s4); margin: var(--s4) 0;
              grid-template-columns:
                repeat(auto-fit, minmax(min(100%, 320px), 1fr)); }
.guide-rank { overflow-x: auto; }
.guide-rank h3 { margin: 0 0 var(--s2); font-size: var(--text-base); }

.guide-table-view { max-width: 40rem; margin: 0 0 var(--s5); }
.guide-table-view summary { cursor: pointer; font-size: var(--text-sm);
                            color: var(--muted); }

.guide-sum ul { list-style: none; margin: var(--s4) 0; padding: 0;
                display: grid; gap: var(--s3);
                grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); }
.guide-sum li { background: var(--surface); border-radius: var(--radius-card);
                padding: var(--s4); display: flex; flex-direction: column;
                gap: var(--s1); }
.guide-sum li b { font-size: var(--text-2xl); color: var(--accent);
                  line-height: 1.1; }
.guide-sum li span { font-size: var(--text-sm); color: var(--muted); }

/* The CANNOT TELL box on the direct-mail guide. Deliberately above the
 * fold and deliberately not styled as a warning: it is the honest
 * answer, not an error. */
.verdict-box { background: var(--surface-sunk);
               border-radius: var(--radius-card); padding: var(--s5);
               margin: var(--s5) 0; max-width: 72ch; }
.verdict-label { display: flex; align-items: center; gap: var(--s2);
                 margin-top: 0; font-weight: 650; }

.hist-wrap { max-width: 42rem; }
.hist-svg { overflow: visible; }
.hist-bar { fill: var(--accent); }
.hist-value { fill: var(--ink); font-weight: 650; }
.hist-rule { stroke: var(--ink); stroke-width: 1.5; stroke-dasharray: 4 3; }
.hist-rule-label { fill: var(--ink); font-weight: 650; }

/* Forced-colours: identity must survive the palette being replaced
 * wholesale, and a single-series chart survives it by construction —
 * there is nothing to tell apart. The bars simply take the system's
 * own mark colour. */
@media (forced-colors: active) {
  .hist-bar { fill: CanvasText; }
  .hist-rule { stroke: CanvasText; }
}

/* --- front-page live preview (869endv2t) -------------------------------- */

/* One helper, added here because the preview's three controls carry
   labels a screen reader needs and a sighted visitor does not: a
   placeholder is not a label, and a visible one on three inline controls
   would double the height of the block that must not push the two ways
   to buy off a phone screen. */
.visually-hidden {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap;
}

.preview-form { margin-top: var(--s4); margin-bottom: var(--s4); }
.preview-label { margin: 0 0 var(--s2); font-size: var(--text-sm);
  color: var(--muted); }
.preview-label strong { color: var(--ink); }
/* One row on a 390px phone, and that is a measurement rather than a
   preference: at `flex: 1 1 11ch` the button dropped to a second row,
   the form grew to 183px, and the hero's own call to action was pushed
   61px below the fold — which 869endv2t explicitly forbids. Seven rems
   apiece fits input + select + button inside 358px with room to spare,
   and `flex-wrap` still rescues a 320px screen by wrapping rather than
   overflowing.

   `min-width: 0` because a flex child containing an <input> refuses to
   shrink below its intrinsic size otherwise, and the hero then overflows
   sideways — which `test_the_front_page_fits_every_viewport` fails on. */
.preview-controls { display: flex; flex-wrap: wrap; gap: var(--s2); }
.preview-controls input { flex: 1 1 7rem; min-width: 0; }
.preview-controls select { flex: 1 1 7rem; min-width: 0; }
.preview-controls .btn { flex: 0 0 auto; }
.preview-note:empty { display: none; }
.preview-note { margin: var(--s2) 0 0; font-size: var(--text-sm);
  color: var(--muted); }
.preview-note.bad { color: var(--danger); }

.preview-results[hidden] { display: none; }
.preview-figures {
  display: grid; gap: var(--s3); margin-bottom: var(--s4);
  grid-template-columns: repeat(2, 1fr);
}
@media (min-width: 700px) {
  .preview-figures { grid-template-columns: repeat(4, 1fr); }
}
.preview-figures div { background: var(--surface);
  border-radius: var(--radius-card); box-shadow: var(--shadow-card);
  padding: var(--s3) var(--s4); }
.preview-figures b { display: block; font-size: var(--text-xl);
  font-weight: 700; color: var(--accent);
  font-variant-numeric: tabular-nums; line-height: 1.2; }
.preview-figures span { font-size: var(--text-xs); color: var(--muted); }

/* No basemap, so the ground is a flat surface token rather than tiles —
   see the comment on the section in core/home.html for why there are no
   tiles on this page. */
.preview-map {
  height: 380px; border-radius: var(--radius); border: 1px solid var(--line);
  background: var(--surface-sunk); margin-bottom: var(--s3); z-index: 0;
}
@media (max-width: 640px) { .preview-map { height: 280px; } }

.preview-list { list-style: none; margin: 0 0 var(--s3); padding: 0;
  display: grid; gap: var(--s2); }
.preview-list li { background: var(--surface);
  border-radius: var(--radius-card); box-shadow: var(--shadow-card);
  padding: var(--s3) var(--s4); font-size: var(--text-sm); }
.preview-list .where { color: var(--muted); font-size: var(--text-xs);
  display: block; margin-top: var(--s1); }
.preview-foot { font-size: var(--text-xs); }
