/*
 * Shared component styling.
 *
 * Both server-rendered services load this. Every colour here is a var(--token)
 * from tokens.css — a raw hex in this file would be a colour that no theme can
 * change, which is exactly what the token system exists to prevent.
 */

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

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-ui);
  font-size: var(--font-size);
  line-height: var(--line-height);
}

/* High visibility is the theme's whole idea, so focus is the most on-brand
   element on the page, never the most muted. */
:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

a {
  color: inherit;
}

/* --- the stripe: the vest's own geometry, decoration only --- */
.stripe {
  height: 10px;
  background: repeating-linear-gradient(
    90deg,
    var(--stripe-light) 0 28px,
    var(--stripe-dark) 28px 32px
  );
}

/* --- header --- */
.masthead {
  background: var(--accent);
  color: var(--on-accent);
}

.masthead-inner {
  max-width: var(--page);
  margin: 0 auto;
  padding: 1rem var(--gutter);
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: baseline;
  justify-content: space-between;
}

.wordmark {
  font-size: 1.5rem;
  font-weight: var(--title-weight);
  letter-spacing: var(--title-tracking);
  text-decoration: none;
}

.tagline {
  font-size: 0.875rem;
  opacity: 0.75;
}

.languages {
  display: flex;
  gap: 0.5rem;
  font-size: 0.8125rem;
}

.languages a {
  text-decoration: none;
  padding: 0.125rem 0.5rem;
  border: 1px solid var(--stripe-dark);
  border-radius: var(--radius);
}

.languages a[aria-current="true"] {
  background: var(--on-accent);
  color: var(--accent);
}

/* --- navigation --- */
.nav {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
}

.nav-inner {
  max-width: var(--page);
  margin: 0 auto;
  padding: 0 var(--gutter);
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
}

.nav a {
  padding: 0.75rem 0.875rem;
  text-decoration: none;
  color: var(--text-muted);
  border-bottom: 3px solid transparent;
  font-size: 0.9375rem;
}

.nav a:hover {
  color: var(--text);
}

.nav a[aria-current="page"] {
  color: var(--text);
  border-bottom-color: var(--accent);
  font-weight: 600;
}

/* --- layout --- */
main {
  max-width: var(--page);
  margin: 0 auto;
  padding: var(--space-lg) var(--gutter) 4rem;
}

.prose {
  max-width: var(--measure);
}

/* --- two columns, where a page has a second thing worth seeing at once --- */

/* A reading column and whatever belongs beside it. The first track is the
   measure and the second takes the rest, so the text keeps the line length it
   is set for however wide the page gets.
 *
 * It collapses to one column well before the columns would get too narrow to
 * read: two cramped columns are worse than one comfortable one, and a form
 * that has become unusable on a tablet is not a layout, it is a bug.
 */
.split {
  display: grid;
  grid-template-columns: minmax(0, var(--measure)) minmax(0, 1fr);
  gap: var(--space-lg);
  align-items: start;
}

/* Stacked below this, and the number is arithmetic rather than taste: the
   reading column is 68ch, the gap is 2rem and the gutters take their share, so
   under about this width whatever sits beside the text is narrower than the
   map it usually holds. One comfortable column beats two cramped ones. */
@media (max-width: 1100px) {
  .split {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* The aside keeps its own heading and scrolls with the page rather than being
   pinned: somebody reading an example wants to stay with it, and a panel that
   followed them down the page while they were writing would be the opposite of
   what this form is for. */
.split > .prose {
  max-width: none;
}

/* The top of a column lines up with the top of what it sits beside, rather
   than carrying the gap a heading or a label needs when it follows a
   paragraph. */
.split-aside > h2:first-child,
.split-aside > label:first-child,
.split > .prose > label:first-child {
  margin-top: 0;
}

/* A map is not prose, so it is not held to a reading measure. Inside a form it
   spans whatever column it is in. */
.prose #map {
  max-width: none;
}

h1 {
  font-size: 2rem;
  line-height: 1.2;
  margin: 0 0 0.5rem;
}

h2 {
  font-size: 1.25rem;
  margin: 2.5rem 0 0.75rem;
}

.lede {
  font-size: 1.125rem;
  color: var(--text-muted);
  margin-top: 0;
}

.section-note {
  color: var(--text-muted);
  font-size: 0.9375rem;
}

/* --- cards --- */
.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(var(--grid-min), 1fr));
  gap: var(--space-md);
  margin-top: 1rem;
}

.card {
  background: var(--surface);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius);
  padding: var(--card-padding-y) var(--card-padding-x);
}

/* --- the doléance card --- */

/* Every card the same shape, whatever it holds.
 *
 * The grid already stretches its rows, so the work here is to make a card use
 * that height rather than sit at the top of it: a column, with the controls
 * pushed to the bottom by `margin-top: auto`. Two cards side by side then end
 * on the same line however much text is above, which is what stops a listing
 * looking like a ransom note — and a listing that cannot be scanned cannot be
 * read alongside the others, which is the entire point of the register.
 */
.message-card {
  display: flex;
  flex-direction: column;
  height: 100%;
}

.message-card .card-actions {
  margin-top: auto;
}

/* The excerpt is the link, so it has to read as text that happens to be
   clickable rather than as a link that happens to be long: the colour of the
   words around it, and an underline only when somebody is pointing at it or
   has tabbed to it. */
.card-excerpt {
  color: inherit;
  text-decoration: none;
}

.card-excerpt:hover {
  text-decoration: underline;
  text-decoration-color: var(--text-muted);
  text-underline-offset: 0.2em;
}

.card-excerpt:focus-visible {
  outline: 2px solid var(--accent-deep);
  outline-offset: 3px;
  border-radius: 2px;
}

/* Says the text was cut, in the place where it was cut. Without it a card
   ends on a half-sentence and reads as a mistake rather than as an excerpt. */
.card-more {
  color: var(--text-muted);
  white-space: nowrap;
}

.card-actions {
  display: flex;
  align-items: center;
  gap: 1rem;
  padding-top: 0.75rem;
  margin-top: 1rem;
  border-top: 1px solid var(--border);
}

.card-actions form {
  display: contents;
}

.card-action {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
  padding: 0;
  font: inherit;
  font-size: 0.875rem;
  color: var(--text-muted);
  background: none;
  border: none;
  cursor: pointer;
}

.card-action:hover {
  color: var(--text);
}

/* Pressed, in this browser. The count is a count of presses and the register
   has nobody to attribute one to, so this is a courtesy to the reader rather
   than a fact about the doléance. */
.card-action.like.pressed {
  color: var(--accent-deep);
}

.card-action.like.pressed .icon {
  fill: currentColor;
}

/* Kept, which is a different kind of fact from pressed above and is marked
   the same way on purpose — the gesture is the same gesture. The difference
   is where the truth lives: "pressed" is this browser's own recollection,
   because the register has nobody to attribute a like to, where "kept" came
   from the server with the card and is a row held for one person who asked
   for it.

   The filled icon is drawn in the markup rather than painted here, so the
   state reaches somebody who cannot see the colour: a shape, not a hue. */
.card-action.keep.kept {
  color: var(--accent-deep);
}

.card-meta {
  font-size: 0.8125rem;
  color: var(--text-muted);
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-bottom: 0.5rem;
}

.card-text {
  margin: 0;
  white-space: pre-wrap;
}

/* Historical texts: paper and ink, so the two eras read differently. */
.card.historical {
  background: var(--paper);
  color: var(--paper-text);
  border-color: var(--paper-border);
  font-family: var(--font-historical);
}

.card.historical .card-meta {
  color: color-mix(in srgb, var(--paper-text) 65%, transparent);
}

.subjects {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-xs);
  margin-top: 0.75rem;
}

.subject {
  font-size: 0.75rem;
  padding: 0.125rem 0.5rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-muted);
}

/* --- forms and buttons --- */
label {
  display: block;
  font-weight: 600;
  margin: 1.25rem 0 0.375rem;
}

.hint {
  font-weight: 400;
  color: var(--text-muted);
  font-size: 0.875rem;
}

input[type="text"],
input[type="date"],
input[type="time"],
input[type="number"],
input[type="email"],
select,
textarea {
  width: 100%;
  padding: 0.625rem 0.75rem;
  font: inherit;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
}

textarea {
  min-height: 14rem;
  resize: vertical;
}

/* A rejected form and a saved one say so in the same place, above the fields,
   so the answer is where the person is already looking rather than wherever
   the browser happened to leave the page. */
.form-error,
.form-notice {
  padding: 0.75rem 1rem;
  border-radius: var(--radius);
  border: 1px solid;
  font-weight: 600;
}

.form-error {
  color: var(--error);
  border-color: var(--error);
}

.form-notice {
  color: var(--good);
  border-color: var(--good);
}

/* The live answer to "is this name free?" sits directly under the field it is
   about, and it is the field's own margin that puts it there. */
.name-status {
  margin: 0.375rem 0 0;
  font-size: 0.875rem;
  min-height: 1.25rem;
  color: var(--text-muted);
}

.name-status.taken {
  color: var(--error);
}

.name-status.free {
  color: var(--good);
}

/* A date or a time field is a row of small targets — 23 / 09 / 2026 — so it is
   set larger than the text fields around it.
 *
 * **The popup it opens is not ours and cannot be made to match.** The browser
 * exposes the field's own internals, and nothing else: the calendar itself is
 * a platform control drawn outside the document, at the platform's size. No
 * rule here reaches it, and none should be added in the belief that one might.
 *
 * Keeping it is a decision rather than a defeat. The native picker is fully
 * keyboard- and screen-reader-accessible on every platform for free, and on a
 * phone it is a great deal better than anything this project would write —
 * which is the device most people will use to announce a meeting. A calendar
 * of our own would match the page and would have to re-earn all of that.
 */
.datelike {
  font-size: 1.125rem;
  padding: 0.75rem;
}

/* The one part of the picker a page can touch: the button that opens it.
   Sized to the field rather than left at the browser's default, so the target
   matches the type beside it. */
.datelike::-webkit-calendar-picker-indicator {
  width: 1.25rem;
  height: 1.25rem;
  cursor: pointer;
}

/* A day of the month is two digits and should not be a field the width of the
   page; a time is five characters and should not be either. */
input[type="number"] {
  max-width: 8rem;
}

input[type="time"] {
  max-width: 12rem;
}

/* --- the recurrence form --- */

/* Everything is shown, and then hidden for the choices it does not belong to.
 *
 * That way round on purpose: a browser without `:has()` shows every field, and
 * the form still works because the backend reads only the fields that match
 * the choice. Revealing with `:has()` instead would mean such a browser could
 * never announce a recurring action at all — a hidden control is not a
 * degraded experience, it is a missing one.
 */
.action-form:has(.kind option[value="onetime"]:checked) .rhythm {
  display: none;
}

.rhythm:has(.repeat option[value="weekly"]:checked) .when-yearly,
.rhythm:has(.repeat option[value="weekly"]:checked) .when-dated,
.rhythm:has(.repeat option[value="monthly"]:checked) .when-weekly,
.rhythm:has(.repeat option[value="monthly"]:checked) .when-yearly,
.rhythm:has(.repeat option[value="yearly"]:checked) .when-weekly {
  display: none;
}

.rhythm:has(.monthly-mode option[value="day"]:checked) .by-weekday,
.rhythm:has(.monthly-mode option[value="weekday"]:checked) .by-day {
  display: none;
}

/* Signing out is a form rather than a link — a change of state should not be
 * something a prefetcher can trigger — so its button has to be talked back
 * into looking like the links it sits among. */
.nav-form {
  display: contents;
}

/* Matched to `.nav a` above rather than left to a browser's button defaults:
 * it sits in the same row and has to read as one of the same things. */
.nav-button {
  padding: 0.75rem 0.875rem;
  font: inherit;
  font-size: 0.9375rem;
  color: var(--text-muted);
  background: none;
  border: 0;
  border-bottom: 3px solid transparent;
  cursor: pointer;
}

.nav-button:hover {
  color: var(--text);
}

/* --- where an action happens --- */

/* Same direction as the rhythm above: shown, then hidden when the switch is
 * off. A browser without `:has()` shows the map always and the form is still
 * right — nothing is pinned, so nothing is posted, and the action keeps the
 * group's meeting place.
 */
.venue:has(.elsewhere:not(:checked)) .venue-map {
  display: none;
}

.venue {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0 1rem 1rem;
  margin-top: 1.5rem;
  min-width: 0;
}

.venue legend {
  font-weight: 600;
  padding: 0 0.375rem;
}

/* Shorter than the group's own map: this one is a correction to a place that
 * is usually already right, not the main act of the page. */
.venue .map {
  height: 18rem;
  border-radius: var(--radius);
  margin-top: 0.5rem;
}

.rhythm {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: 0 1rem 1rem;
  margin-top: 1.5rem;
  min-width: 0;
}

.rhythm legend {
  font-weight: 600;
  padding: 0 0.375rem;
}

/* The seven days sit in a row that wraps, rather than a column seven tall. */
.weekdays {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem 1.25rem;
  margin-top: 0.5rem;
}

.weekdays .checkbox {
  margin-top: 0;
}

/* --- data tables --- */

/* A table is the one thing on the page allowed to be wider than the page, so
   it gets its own scroller rather than the body acquiring one. */
.table-wrap {
  overflow-x: auto;
  margin-top: 1rem;
}

.data-table {
  width: 100%;
  border-collapse: collapse;
}

.data-table th,
.data-table td {
  padding: 0.625rem 0.75rem;
  text-align: left;
  vertical-align: middle;
  border-bottom: 1px solid var(--border);
}

.data-table th {
  font-size: 0.8125rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--text-muted);
}

/* Each control in a row is its own form post, and nothing about that should
   show: the forms collapse to nothing so the buttons sit where they look like
   they belong. */
.data-table form {
  display: contents;
}

/* The actions column is as wide as its buttons and no wider, so the name and
   the address get the rest of the page. */
.actions-column {
  width: 1%;
  white-space: nowrap;
}

/* Side by side and never wrapping: two icons stacked on top of each other read
   as two rows, which is exactly what a table must not do. */
.row-actions {
  display: flex;
  gap: 0.375rem;
  align-items: center;
}

/* The button and the anchor-styled-as-a-button have to sit on the same line,
   and .button carries a top margin for use in prose. */
.data-table .button {
  margin-top: 0;
}

.button.small {
  padding: 0.375rem 0.75rem;
  font-size: 0.875rem;
}

/* An address is data, not prose: monospace so two of them can be compared at a
   glance, and breakable anywhere so a long one widens nothing. */
.data-table .email {
  font-size: 0.875rem;
  overflow-wrap: anywhere;
}

/* A square control carrying one icon. The label lives in aria-label and
   title rather than on screen — which is the whole point of the icon — so the
   hit area has to be a comfortable size on its own. */
.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.25rem;
  height: 2.25rem;
  padding: 0;
  color: var(--text-muted);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  cursor: pointer;
}

.icon-button:hover {
  color: var(--text);
  border-color: var(--text-muted);
}

/* Destructive, so it says so before it is pressed rather than only in the
   confirmation afterwards. */
.icon-button.danger:hover {
  color: var(--error);
  border-color: var(--error);
}

.icon {
  display: block;
}

/* Inside the drawer the save and cancel controls are ordinary buttons again,
   side by side. */
.drawer-actions {
  display: flex;
  gap: 0.625rem;
  align-items: center;
}

/* The bar above a table, for the one control that acts on the table as a whole
   rather than on a row. */
.table-bar {
  display: flex;
  justify-content: flex-end;
  margin-top: 1rem;
}

/* A button carrying an icon and a word. The gap is what stops the two reading
   as one smudge at small sizes. */
.button .icon {
  display: inline-block;
  vertical-align: -0.15em;
  margin-right: 0.375rem;
}

/* --- the overlay --- */

/* Hidden until a fragment names it, or until the server says to open it
   because something in it was refused. Two selectors, one panel. */
.overlay {
  display: none;
}

.overlay:target,
.overlay.open {
  display: flex;
  position: fixed;
  inset: 0;
  z-index: 1200;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}

/* A link, not a decoration: clicking beside a panel closes it, and with no
   script that has to be something a browser already knows how to follow. */
.overlay-backdrop {
  position: absolute;
  inset: 0;
  background: rgb(0 0 0 / 55%);
}

.overlay-panel {
  position: relative;
  width: 100%;
  max-width: 32rem;
  max-height: calc(100vh - 2rem);
  overflow-y: auto;
  padding: 1.25rem 1.5rem 1.5rem;
  background: var(--bg);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius);
}

.overlay-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.overlay-header h2 {
  margin: 0;
  font-size: 1.125rem;
}

/* The first field sits right under the heading rather than a label's usual
   distance from the previous paragraph. */
.overlay-panel form > label:first-of-type {
  margin-top: 1rem;
}

/* Confirmed, in the one colour the palette keeps for "this is fine". */
.verified {
  color: var(--good);
  font-weight: 600;
}

/* The edit row opens beneath the contact it belongs to, on :target — no
   script, and the address bar says which row is open, so a reload or a shared
   link comes back to the same place. */
.contact-edit {
  display: none;
}

.contact-edit:target {
  display: table-row;
}

.contact-edit td {
  background: var(--surface);
}

/* Inside the drawer the fields are ordinary stacked form fields again, so the
   `display: contents` above must not reach them. */
.contact-edit form {
  display: block;
  padding: 0.5rem 0 1rem;
}

/* The drawer is the full width of the table and its fields should not be:
   an address typed into a field the width of a page is harder to read back. */
.contact-edit input[type="text"],
.contact-edit input[type="email"] {
  max-width: var(--measure);
}

/* Visually hidden, still read aloud: the actions column has no visible
   heading because the buttons say what they do, but a screen reader announcing
   an empty column header is worse than a redundant word. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
}

/* A credential shown once. It has to survive being read off a screen and
   retyped, so it wraps at every character rather than running off the page. */
.receipt-value {
  display: inline-block;
  max-width: 100%;
  padding: 0.5rem 0.625rem;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  font-size: 0.875rem;
  overflow-wrap: anywhere;
}

.checkbox {
  display: flex;
  gap: 0.625rem;
  align-items: flex-start;
  margin-top: 1.5rem;
  font-weight: 400;
}

.checkbox input {
  margin-top: 0.3rem;
}

.button {
  display: inline-block;
  margin-top: 1.5rem;
  padding: 0.75rem 1.5rem;
  font: inherit;
  font-weight: 700;
  /* Black on yellow, never white on yellow: at ~18:1 this is the one way
     round that is legible. */
  color: var(--on-accent);
  background: var(--accent);
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
}

.button:hover {
  background: var(--accent-deep);
}

/* --- map --- */
#map {
  height: 520px;
  max-width: 100%;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  margin-top: 1rem;
  background: var(--surface);
}

/* The register's map sits above the list it filters, so it is shorter: the
   point is to see both at once rather than scroll between them. */
.register-map {
  height: 380px;
}

/* A doléance in a marker popup. Leaflet styles the container itself, so this
   only has to make the contents read like the cards below the map. */
.map-popup {
  max-width: 22rem;
}

.map-popup .card-meta {
  margin: 0 0 0.35rem;
}

.map-popup .card-text {
  margin: 0;
  /* Somebody's own words, so line breaks in them are preserved rather than
     collapsed into a paragraph they did not write. */
  white-space: pre-wrap;
}

/* The cluster count is the number of doléances written in a place — the one
   thing the map is there to say — so it is given the accent rather than the
   plugin's default blue, which belongs to no theme here. */
.marker-cluster-small div,
.marker-cluster-medium div,
.marker-cluster-large div {
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
}

.marker-cluster-small,
.marker-cluster-medium,
.marker-cluster-large {
  background: color-mix(in srgb, var(--accent) 45%, transparent);
}

/* --- empty state --- */
.empty {
  border: 1px dashed var(--border);
  border-radius: var(--radius);
  padding: 2rem;
  text-align: center;
  color: var(--text-muted);
}

/* --- page footer ---
   Scoped to the page's own footer: a bare `footer` selector also caught the
   provenance footer inside every card, painting it as a highlighted panel. */
body > footer {
  border-top: 1px solid var(--border);
  background: var(--surface);
}

.footer-inner {
  max-width: var(--page);
  margin: 0 auto;
  padding: 1.5rem var(--gutter);
  color: var(--text-muted);
  font-size: 0.875rem;
}

.placeholder-badge {
  display: inline-block;
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  padding: 0.125rem 0.375rem;
  border: 1px solid currentColor;
  border-radius: 3px;
}

@media (max-width: 640px) {
  h1 {
    font-size: 1.5rem;
  }
  #map {
    height: 360px;
  }
}

/* --- console-only --- */
.warning {
  background: var(--accent);
  color: var(--on-accent);
  font-weight: 600;
  padding: 0.75rem 1rem;
  border-radius: var(--radius);
  margin: 0 0 1.5rem;
}

/* --- theme toggle --- */
.theme-toggle {
  font: inherit;
  font-size: 1rem;
  line-height: 1;
  padding: 0.25rem 0.5rem;
  color: var(--on-accent);
  background: transparent;
  border: 1px solid var(--stripe-dark);
  border-radius: var(--radius);
  cursor: pointer;
}

.theme-toggle:hover {
  background: var(--accent-deep);
}

/* --- theme library manager --- */
.theme-bar {
  display: flex;
  gap: var(--space-sm);
  flex-wrap: wrap;
  align-items: center;
  margin: 1.5rem 0 0.5rem;
}

.theme-bar .button {
  margin-top: 0;
}

.button.secondary {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
}

.button.secondary:hover {
  border-color: var(--accent-deep);
  background: var(--surface);
}

.button.small {
  margin-top: 0;
  padding: 0.35rem 0.75rem;
  font-size: 0.875rem;
}

.button.danger {
  background: var(--surface);
  color: var(--error);
  border: 1px solid var(--border);
}

.button.danger:hover {
  border-color: var(--error);
  background: var(--surface);
}

.themes {
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
  margin-top: 1rem;
}

.theme-card {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}

.theme-card.active {
  border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}

.theme-name {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-weight: 600;
  flex: 1;
  min-width: 12rem;
}

.badge {
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  font-weight: 700;
  padding: 0.125rem 0.375rem;
  border-radius: 3px;
  color: var(--on-accent);
  background: var(--accent);
}

.badge.builtin {
  color: var(--text-muted);
  background: transparent;
  border: 1px solid var(--border);
}

.warnpill {
  font-size: 0.75rem;
  color: var(--warn);
  border: 1px solid var(--warn);
  border-radius: 3px;
  padding: 0.05rem 0.375rem;
  cursor: help;
}

.swatches {
  display: flex;
  gap: 4px;
}

.swatch {
  width: 18px;
  height: 18px;
  border-radius: 4px;
  border: 1px solid var(--border);
}

.theme-actions {
  display: flex;
  gap: 0.5rem;
  align-items: center;
  flex-wrap: wrap;
}

.masthead-controls {
  display: flex;
  gap: 0.75rem;
  align-items: center;
}

/* --- masthead identity --- */
.masthead-identity {
  display: flex;
  align-items: center;
  gap: 0.75rem;
}

/* The default mark is drawn in currentColor, so it follows the palette
   without being an asset at all; an uploaded logo replaces it. */
.masthead-identity .logo {
  width: 40px;
  height: 40px;
  color: var(--on-accent);
  flex: none;
}

/* --- theme images --- */
.theme-assets {
  flex-basis: 100%;
  border-top: 1px solid var(--border);
  margin-top: 0.75rem;
  padding-top: 0.75rem;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}

.asset-row {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  flex-wrap: wrap;
}

.asset-label {
  flex: 1;
  min-width: 14rem;
  display: flex;
  flex-direction: column;
}

.asset-slot {
  font-weight: 600;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.875rem;
}

/* --- historical passages --- */
/* The date and the document are what a reader needs before the passage, so
   the header is the emphasised band and the provenance below is not. */
.card-header {
  /* Bleeds to the card edge, so it follows whatever padding the structure
     theme sets rather than a value copied from it. */
  margin: calc(-1 * var(--card-padding-y)) calc(-1 * var(--card-padding-x)) 0.875rem;
  padding: var(--space-sm) var(--card-padding-x);
  background: color-mix(in srgb, currentColor 6%, transparent);
  border-bottom: 1px solid color-mix(in srgb, currentColor 12%, transparent);
  border-radius: var(--radius) var(--radius) 0 0;
}

.card-header .card-meta {
  margin-bottom: 0;
}

/* What the passage is taken from, beside the date — before the passage
   itself, so a reader knows what they are reading. */
.card-document {
  margin: 0.25rem 0 0;
  font-size: 0.875rem;
  font-style: italic;
  font-weight: 600;
}

.card-title {
  font-size: 1rem;
  margin: 0 0 0.5rem;
  font-weight: 700;
}

.card-provenance {
  margin-top: 1rem;
  padding-top: 0.625rem;
  border-top: 1px solid currentColor;
  border-top-color: color-mix(in srgb, currentColor 18%, transparent);
  font-size: 0.75rem;
}

/* The location reads as a label, not a heading. */
.card-place {
  margin: 0;
  font-size: 0.8125rem;
  font-weight: 600;
  opacity: 0.75;
}

.card-source {
  margin: 0.25rem 0 0;
  font-size: 0.75rem;
  opacity: 0.6;
}

/* --- moving through the corpus --- */
.corpus-nav {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  margin-top: 1rem;
}

.corpus-nav .button {
  font-size: 1.125rem;
  line-height: 1;
  padding: 0.25rem 0.75rem;
}

.corpus-position {
  font-size: 0.875rem;
  color: var(--text-muted);
  font-variant-numeric: tabular-nums;
}

/* Marks a passage the reader is not seeing in the words it was written in. */
.translated-badge {
  font-size: 0.6875rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  opacity: 0.65;
}

/* One library per section on the theme page. */
.library + .library {
  margin-top: var(--space-lg);
}

/* A structure previews as its values, where a palette previews as chips. */
.sample {
  font-size: 0.6875rem;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  padding: 0.125rem 0.375rem;
  border: 1px solid var(--border);
  border-radius: 3px;
  color: var(--text-muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 14rem;
}

/* The colourway a theme is shown in, chosen on its row. */
.theme-colors {
  width: auto;
  min-width: 9rem;
  padding: 0.3rem 0.5rem;
  font-size: 0.875rem;
}

/*
 * The model's own sentence about a submission, on the curation queue and the
 * dropped sample.
 *
 * Boxed and set off rather than run in with the metadata, because it is the
 * one thing on the card that explains a decision instead of stating it — and
 * because it is frequently *wrong*, which is exactly what a curator is there
 * to notice. A score of 30 cannot be argued with; "reads as advertising for a
 * named company" can.
 *
 * The left rule rather than a fill: the card already carries the contributor's
 * own words, and a machine's note must not compete with them for attention.
 */
.reason {
  margin: 0.5rem 0;
  padding: 0.5rem 0.75rem;
  border-left: 3px solid var(--accent-deep);
  background: var(--bg);
  color: var(--text-muted);
  font-size: 0.875rem;
  border-radius: 0 3px 3px 0;
}

.reason .reason-label {
  display: block;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--text-muted);
  margin-bottom: 0.125rem;
}

/* A named refusal is a harder fact than a score, so it reads as one. */
.reason.refused {
  border-left-color: var(--warn);
}

/*
 * The subject filter — a disclosure holding checkboxes, used identically on
 * every page that offers one.
 *
 * It replaces a <select multiple>, which could not show the hierarchy, hid a
 * long vocabulary behind a four-row scrollbar, and required a modifier key to
 * pick more than one thing. Being <details> rather than a scripted widget is
 * what keeps it working on the pages that are plain forms.
 */
.subject-filter {
  position: relative;
  margin-bottom: 1rem;

  /*
   * Above the map. Leaflet stacks its panes from 200 and puts its controls at
   * 1000, so anything that opens over a map has to clear that — the panel was
   * at 20 and the zoom buttons drew straight through it.
   *
   * Set here rather than on the panel: this creates one stacking context for
   * the whole filter, so the panel's own z-index is a local matter and cannot
   * be outbid by something inside the map later.
   */
  z-index: 1100;
}

.subject-filter-label {
  margin: 0 0 0.25rem;
  font-weight: 600;
}

.subject-filter-menu > summary {
  display: flex;
  /* Clicking a summary otherwise selects its text, which looks like a bug. */
  user-select: none;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--surface);
  cursor: pointer;
  list-style: none;
}

/* Safari draws its own triangle through the flex layout. */
.subject-filter-menu > summary::-webkit-details-marker {
  display: none;
}

.subject-filter-menu > summary::after {
  content: "▾";
  color: var(--text-muted);
}

.subject-filter-menu[open] > summary::after {
  content: "▴";
}

.subject-filter-menu > summary:focus-visible {
  outline: 2px solid var(--accent-deep);
  outline-offset: 2px;
}

/*
 * Floated over the page rather than pushing it down: on the register the map
 * sits directly beneath, and a filter that reflowed it on every open would
 * move the thing the reader is looking at.
 */
.subject-filter-options {
  position: absolute;
  z-index: 1;
  left: 0;
  right: 0;
  max-height: 20rem;
  overflow-y: auto;
  margin-top: 0.25rem;
  padding: 0.25rem 0;
  border: 1px solid var(--border);
  border-radius: 4px;
  background: var(--surface);
  box-shadow: 0 6px 18px rgb(0 0 0 / 12%);
}

/*
 * These are <label>s, and the global label rule makes every label a bold block
 * with a 1.25rem top margin — which turned each option into a tall heading
 * with a gap above it. Everything that rule sets has to be undone here, not
 * only `display`.
 */
.subject-option {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  margin: 0;
  padding: 0.3rem 0.75rem;
  font-weight: 400;
  cursor: pointer;
}

.subject-option:hover {
  background: var(--bg);
}

.subject-option input {
  flex: none;
}

/* The subject hierarchy, indented by depth.
 *
 * Seven steps because the backend caps the tree at six and emits a root at
 * zero. It is a class per step rather than one inline style per row: a
 * Content-Security-Policy strict enough to be worth setting refuses inline
 * styles, and this is the only place in the project that used one.
 */
.subject-option.depth-1 { padding-left: 1.6rem; }
.subject-option.depth-2 { padding-left: 2.7rem; }
.subject-option.depth-3 { padding-left: 3.8rem; }
.subject-option.depth-4 { padding-left: 4.9rem; }
.subject-option.depth-5 { padding-left: 6rem; }
.subject-option.depth-6 { padding-left: 7.1rem; }

/* --- pills, and the few things the account page needs -------------------- */

/* A pill is a fact about a row, not a control: a device that is synced, a
   notification that has not been read, a role somebody holds. It has to be
   readable at a glance beside text, which is why it borrows the border colour
   rather than a fill — a page with six filled pills on it reads as a warning. */
.pill {
  display: inline-block;
  font-size: 0.75rem;
  padding: 0.05rem 0.375rem;
  border: 1px solid var(--border);
  border-radius: 3px;
  color: var(--text-muted);
  white-space: nowrap;
}

/* Only for the answer that is good news — a synced passkey, the device you are
   holding. Accent rather than green: this theme has one positive colour and it
   is the vest's. */
.pill.ok {
  color: var(--on-accent);
  background: var(--accent);
  border-color: var(--accent);
}

/* Recovery codes, shown once. Monospace and one per line, because they are
   copied onto paper by hand by somebody who has just lost their phone. */
.code-list {
  list-style: none;
  margin: 0.75rem 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr));
  gap: 0.375rem;
}

.code-list li {
  margin: 0;
}

/* A field and its own submit button on one line, for renaming a device in
   place. Each row of that table is its own form post; this keeps the pair from
   stacking. */
.inline-form {
  display: flex;
  align-items: center;
  gap: 0.375rem;
}

.inline-form input {
  margin: 0;
  min-width: 0;
}

.pagination {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  margin: 1.5rem 0;
}

/* An unread notification. A left edge rather than a background: the card is
   already a surface, and a second one inside it flattens the type. */
.card.unread {
  border-left: 3px solid var(--accent);
}
