/* ==========================================================================
   SEED Society Analytics - design tokens and components.

   Self-contained: no CSS framework is loaded, so the container has no runtime
   dependency on a CDN. The handful of Bootstrap-style class names used by the
   Django forms (.form-control, .form-select, .btn) are implemented here.

   Colour is declared once, as custom properties, and every component and chart
   reads from these roles. Dark mode is a deliberately chosen set of steps for
   the dark surface, not an automatic inversion.
   ========================================================================== */

:root {
  color-scheme: light;

  /* Surfaces and ink */
  --plane: #f9f9f7;
  --surface: #fcfcfb;
  --surface-sunken: #f3f2ee;
  --text-primary: #0b0b0b;
  --text-secondary: #52514e;
  --text-muted: #898781;
  --grid: #e1e0d9;
  --axis: #c3c2b7;
  --border: rgba(11, 11, 11, 0.1);
  --border-strong: rgba(11, 11, 11, 0.18);
  --shadow: 0 1px 2px rgba(11, 11, 11, 0.04), 0 8px 24px rgba(11, 11, 11, 0.05);

  /* Categorical series - fixed order, never cycled past slot 8. */
  --series-1: #2a78d6;
  --series-2: #eb6834;
  --series-3: #1baf7a;
  --series-4: #eda100;
  --series-5: #e87ba4;
  --series-6: #008300;
  --series-7: #4a3aa7;
  --series-8: #e34948;

  /* Status - reserved meanings, never used for a data series. */
  --status-good: #0ca30c;
  --status-warning: #fab219;
  --status-serious: #ec835a;
  --status-critical: #d03b3b;
  --status-unknown: #898781;
  --success-text: #006300;

  /* Spacing scale. Every gap, pad and margin below picks from this, so
     rhythm stays consistent instead of drifting into arbitrary pixel values. */
  --s1: 4px;
  --s2: 8px;
  --s3: 12px;
  --s4: 16px;
  --s5: 22px;
  --s6: 30px;
  --s7: 44px;

  /* How wide the page may get. Everything between a phone and this is fluid;
     past it the page centres, because a table wider than about this is harder
     to read, not easier. */
  --page-max: 2100px;
  --radius: 10px;
  --radius-sm: 6px;
  --font: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    --plane: #0d0d0d;
    --surface: #1a1a19;
    --surface-sunken: #131312;
    --text-primary: #ffffff;
    --text-secondary: #c3c2b7;
    --text-muted: #898781;
    --grid: #2c2c2a;
    --axis: #383835;
    --border: rgba(255, 255, 255, 0.1);
    --border-strong: rgba(255, 255, 255, 0.2);
    --shadow: none;
    --series-1: #3987e5;
    --series-2: #d95926;
    --series-3: #199e70;
    --series-4: #c98500;
    --series-5: #d55181;
    --series-6: #008300;
    --series-7: #9085e9;
    --series-8: #e66767;
    --success-text: #0ca30c;
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --plane: #0d0d0d;
  --surface: #1a1a19;
  --surface-sunken: #131312;
  --text-primary: #ffffff;
  --text-secondary: #c3c2b7;
  --text-muted: #898781;
  --grid: #2c2c2a;
  --axis: #383835;
  --border: rgba(255, 255, 255, 0.1);
  --border-strong: rgba(255, 255, 255, 0.2);
  --shadow: none;
  --series-1: #3987e5;
  --series-2: #d95926;
  --series-3: #199e70;
  --series-4: #c98500;
  --series-5: #d55181;
  --series-6: #008300;
  --series-7: #9085e9;
  --series-8: #e66767;
  --success-text: #0ca30c;
}

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

body {
  margin: 0;
  font-family: var(--font);
  font-size: 15px;
  line-height: 1.55;
  color: var(--text-primary);
  background: var(--plane);
  -webkit-font-smoothing: antialiased;
}

h1,
h2,
h3,
h4 {
  margin: 0 0 0.4em;
  line-height: 1.25;
  font-weight: 650;
  letter-spacing: -0.01em;
}
h1 { font-size: 1.75rem; }
h2 { font-size: 1.25rem; }
h3 { font-size: 1.02rem; }
p { margin: 0 0 1em; }
a { color: var(--series-1); }
a:hover { color: var(--text-primary); }

.muted { color: var(--text-muted); }
.secondary { color: var(--text-secondary); }
.small { font-size: 0.84rem; }
.nowrap { white-space: nowrap; }
.tabular { font-variant-numeric: tabular-nums; }
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

/* ---------------------------------------------------------------- shell --- */
/* The page fills the window rather than sitting in a 1200px column with half a
   widescreen monitor left empty either side. It keeps growing to --page-max,
   which exists so a 4K display does not stretch a table to three thousand
   pixels and make a row impossible to follow across.

   Widening this is safe for reading because prose carries its own measure:
   `.secondary` and `.section-sub` are capped in ch above, so paragraphs stay
   the same width no matter how wide the window gets. What gains the space is
   what should - cards, tables, charts, and the auto-fill grids, which simply
   fit another column in. */
.shell {
  width: min(100%, var(--page-max));
  margin-inline: auto;
  /* Gutters grow with the window too: cramped on a phone, generous on a
     desktop, without a breakpoint for each step. */
  padding: 0 clamp(14px, 1.8vw, 36px) 64px;
}

.topbar {
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  position: sticky;
  top: 0;
  z-index: 20;
}
.topbar-inner {
  width: min(100%, var(--page-max));
  margin-inline: auto;
  /* More above than below: the wordmark is cropped tight to its own
     ascenders, so an even gap reads as though it is hanging off the top of
     the bar. */
  padding: 14px clamp(14px, 1.8vw, 36px) 8px;
  display: flex;
  align-items: center;
  gap: 18px;
  flex-wrap: wrap;
}
.brand {
  display: flex;
  align-items: center;
  text-decoration: none;
  flex: none;
}

/* The wordmark. Two files rather than one: the artwork is white on
   transparent, which is invisible against the light header, so a second copy
   has its white recoloured to ink. The globe keeps its blue and green in both.

   Declared light-first, then swapped for the system preference, then swapped
   again for an explicit data-theme - so the toggle wins over the operating
   system in both directions. */
.brand-logo {
  display: block;
  /* Sized to the artwork's own 3.49:1 ratio, so "Seed The Future" stays
     legible rather than collapsing into a grey smudge. */
  width: 220px;
  height: 63px;
  background: url("../img/logo-light.cf98531ed8bf.png") left center / contain no-repeat;
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .brand-logo {
    background-image: url("../img/logo-dark.15e7f851a73d.png");
  }
}
:root[data-theme="dark"] .brand-logo {
  background-image: url("../img/logo-dark.15e7f851a73d.png");
}
:root[data-theme="light"] .brand-logo {
  background-image: url("../img/logo-light.cf98531ed8bf.png");
}
@media (max-width: 900px) {
  .brand-logo { width: 178px; height: 51px; }
}
@media (max-width: 430px) {
  .brand-logo { width: 140px; height: 40px; }
}

.nav {
  display: flex;
  gap: 2px;
  flex-wrap: wrap;
  align-items: center;
  /* No margin-left:auto here: .nav sits inside .nav-disclosure, which is the
     flex item of .topbar-inner and carries the push-right. */
}
.nav a {
  padding: 6px 11px;
  border-radius: var(--radius-sm);
  color: var(--text-secondary);
  text-decoration: none;
  font-size: 0.9rem;
}
.nav a:hover { background: var(--surface-sunken); color: var(--text-primary); }
.nav a[aria-current="page"] {
  color: var(--text-primary);
  background: var(--surface-sunken);
  font-weight: 600;
}
.nav .nav-sep {
  width: 1px;
  height: 20px;
  background: var(--border);
  margin: 0 6px;
}

.theme-toggle {
  border: 1px solid var(--border);
  background: transparent;
  color: var(--text-secondary);
  border-radius: var(--radius-sm);
  padding: 5px 9px;
  cursor: pointer;
  font-size: 0.86rem;
  font-family: inherit;
}
.theme-toggle:hover { color: var(--text-primary); border-color: var(--border-strong); }

/* --------------------------------------------------------------- cards --- */
.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--s4) var(--s5);
  box-shadow: var(--shadow);
}
.card + .card { margin-top: var(--s4); }
/* ...but not when the cards are grid cells. The grid's own gap already
   spaces them, and because this is an adjacent-sibling rule it skips the
   first card only - so every grid put its opening card a step higher than
   the rest of its row. Subtle per card, obvious across a page of them. */
.grid > .card + .card,
.tiles > .card + .card,
.status-grid > .card + .card { margin-top: 0; }
.card-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
  flex-wrap: wrap;
  margin-bottom: var(--s1);
}
.card-head h3 { margin: 0; flex: 1 1 auto; min-width: 0; }
.card-head .avatar { margin: 0; }
.card-head h2,
.card-head h3 { margin-bottom: 2px; }
.card-sub {
  color: var(--text-muted);
  font-size: 0.86rem;
  margin: 0 0 var(--s3);
}
/* A card whose last child is a paragraph should not carry its bottom margin
   as extra padding. */
.card > *:last-child { margin-bottom: 0; }

.grid { display: grid; gap: var(--s4); }
.grid-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
.grid-auto { grid-template-columns: repeat(auto-fill, minmax(230px, 1fr)); }
/* Headline tiles: five across on a wide screen, never orphaning one on a row
   of its own the way auto-fill did. */
.tiles {
  display: grid;
  gap: var(--s3);
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
}
/* For a set of six or so small counters, which at the wider minimum wrapped
   4 + 2 and left a hole across half a row. */
.tiles-small {
  display: grid;
  gap: var(--s3);
  grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
}

/* Tablet bands.
   Auto-fit alone picks column counts that divide badly at these widths: six
   counters wrapped 5 + 1, four tiles 3 + 1, five tiles 4 + 1 - each leaving a
   near-empty row. Pinning an even count through the awkward range keeps the
   rows balanced.

   The five-tile case is scoped by child count, so the four-tile grid on the
   resources page is not dragged to 3 + 1 along with it. Every narrower rule
   below repeats that :has() selector: without it the plain `.tiles` rule
   loses on specificity and five tiles would never collapse on a phone.
   Browsers without :has() fall back to plain auto-fit, which is untidy at
   two widths rather than broken. */
@media (max-width: 1080px) {
  .tiles:has(> :nth-child(5)) { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 1023px) {
  .tiles-small { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 880px) {
  .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 620px) {
  .tiles-small { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .tiles:has(> :nth-child(5)) { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 430px) {
  .tiles,
  .tiles:has(> :nth-child(5)) { grid-template-columns: minmax(0, 1fr); }
}

.section { margin-top: var(--s6); }
.section > h2 { margin-bottom: var(--s1); }
.section-sub {
  color: var(--text-muted);
  font-size: 0.86rem;
  margin: 0 0 var(--s3);
  max-width: 78ch;
}
/* Page head. Every page opens with an h1 and an optional lead paragraph; the
   spacing lives here rather than as an inline style repeated on each one. */
.shell > h1 { margin-top: var(--s5); }
.shell > h1 + p { margin-top: calc(-1 * var(--s2)); }
.shell > h1 + p.secondary { max-width: 80ch; }

/* Utilities, deliberately few. Anything needed more than a handful of times
   becomes a component instead. */
.inline-form { display: inline; }
.flush { margin-bottom: 0; }
.push-right { margin-left: auto; }
.row-actions {
  display: flex;
  gap: var(--s3);
  align-items: center;
}
.card-compact { padding: var(--s3) var(--s4); }
.spaced { margin-top: var(--s3); }
.scroll-260 { max-height: 260px; overflow: auto; }
.narrow-page { max-width: 460px; margin: var(--s7) auto; }
.announcement,
.messages { margin-top: var(--s4); }
.back-link { margin: var(--s5) 0 var(--s1); }
.back-link + h1 { margin-top: 0; }
/* A table row that continues the one above it, with no rule between them. */
.cell-continued { padding-top: 0; }

/* Patch-note list items. */
.post-title { margin: 0 0 2px; }
.post-head { min-width: 0; }
.post-item + .post-item { margin-top: var(--s2); }
.post-item .card-head { margin-bottom: var(--s2); }
summary { cursor: pointer; }
/* Spacing for prose disclosures. Scoped away from the navigation, which is
   also a <details> but sits in a vertically-centred flex row where a top
   margin would knock it out of alignment with the brand. */
details:not(.nav-disclosure) > :not(summary) { margin-top: var(--s3); }
.empty { margin-top: var(--s6); }

@media (max-width: 960px) {
  .grid-3, .grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 680px) {
  .grid-2, .grid-3, .grid-4 { grid-template-columns: minmax(0, 1fr); }
  .shell { padding: 0 14px 48px; }
  /* The clamp above already narrows the gutter; this keeps the vertical
     padding tighter on a phone without re-fixing the horizontal one. */
  .topbar-inner { padding-block: 10px 6px; }
}

/* --------------------------------------------------------------- hero ---- */
.hero {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  gap: var(--s6);
  align-items: center;
  margin: var(--s5) 0 var(--s2);
  padding: var(--s5) var(--s6);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow);
}

/* The score gauge. A single number is the whole story here, so it gets one
   clear dial rather than a chart. */
.gauge { position: relative; width: 132px; height: 132px; flex: 0 0 auto; }
.gauge-svg { width: 100%; height: 100%; transform: rotate(0deg); }
.gauge-svg circle {
  fill: none;
  stroke-width: 9;
  stroke-linecap: round;
}
.gauge-track { stroke: var(--grid); }
.gauge-fill { transition: stroke-dasharray 600ms ease; }
.status-arc-critical { stroke: var(--status-critical); }
.status-arc-attention { stroke: var(--status-serious); }
.status-arc-watch { stroke: var(--status-warning); }
.status-arc-healthy,
.status-arc-excellent { stroke: var(--status-good); }
.status-arc-unknown,
.status-arc-dormant { stroke: var(--status-unknown); }

.gauge-label {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0;
}
.gauge-score {
  /* Proportional figures: tabular-nums makes a large number look loose. */
  font-size: 2.9rem;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.03em;
}
.gauge-unit { color: var(--text-muted); font-size: 0.74rem; margin-top: 2px; }

.hero-eyebrow {
  margin: 0 0 var(--s1);
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--text-muted);
}
.hero-verdict h1 {
  margin: 0 0 var(--s3);
  font-size: 1.6rem;
  line-height: 1.3;
  max-width: 34ch;
}
.hero-meta {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
  margin: 0;
  color: var(--text-muted);
  font-size: 0.84rem;
}

/* Three counters that double as jump links to the matching section. */
.hero-counts { display: flex; gap: var(--s2); flex: 0 0 auto; }
.count {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-width: 86px;
  padding: var(--s3) var(--s2);
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  text-decoration: none;
  color: var(--text-primary);
}
.count:hover { border-color: var(--border-strong); background: var(--surface-sunken); }
.count-n { font-size: 1.6rem; font-weight: 700; line-height: 1.1; }
.count-l {
  font-size: 0.72rem;
  color: var(--text-muted);
  text-align: center;
  margin-top: 2px;
}
.count-bad .count-n { color: var(--status-critical); }
.count-warn .count-n { color: #a8521f; }
.count-good .count-n { color: var(--success-text); }
/* A zero is good news: stop it shouting in red. */
.count.is-zero .count-n { color: var(--text-muted); }

:root[data-theme="dark"] .count-warn .count-n { color: var(--status-serious); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .count-warn .count-n { color: var(--status-serious); }
}

@media (max-width: 1040px) {
  .hero { grid-template-columns: auto minmax(0, 1fr); gap: var(--s5); }
  .hero-counts { grid-column: 1 / -1; }
}
@media (max-width: 620px) {
  .hero {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    text-align: center;
    padding: var(--s5) var(--s4);
    gap: var(--s4);
  }
  .hero-verdict h1 { font-size: 1.35rem; }
  .hero-meta { justify-content: center; }
  .hero-counts { width: 100%; }
  .count { flex: 1 1 0; min-width: 0; }
}

/* -------------------------------------------------------------- status --- */
.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 9px 2px 7px;
  border-radius: 999px;
  font-size: 0.78rem;
  font-weight: 600;
  border: 1px solid transparent;
  white-space: nowrap;
}
/* The glyph is a second channel: status is never colour alone. */
.chip-icon {
  width: 14px;
  height: 14px;
  flex: 0 0 14px;
  display: inline-grid;
  place-items: center;
  border-radius: 50%;
  font-size: 10px;
  font-weight: 800;
  line-height: 1;
  color: var(--surface);
}

.status-critical { color: var(--status-critical); border-color: color-mix(in srgb, var(--status-critical) 35%, transparent); background: color-mix(in srgb, var(--status-critical) 10%, transparent); }
.status-critical .chip-icon { background: var(--status-critical); }
.status-attention { color: #a8521f; border-color: color-mix(in srgb, var(--status-serious) 45%, transparent); background: color-mix(in srgb, var(--status-serious) 14%, transparent); }
.status-attention .chip-icon { background: var(--status-serious); }
.status-watch { color: #7a5600; border-color: color-mix(in srgb, var(--status-warning) 45%, transparent); background: color-mix(in srgb, var(--status-warning) 16%, transparent); }
.status-watch .chip-icon { background: var(--status-warning); }
.status-healthy,
.status-excellent { color: var(--success-text); border-color: color-mix(in srgb, var(--status-good) 35%, transparent); background: color-mix(in srgb, var(--status-good) 10%, transparent); }
.status-healthy .chip-icon,
.status-excellent .chip-icon { background: var(--status-good); }
.status-unknown { color: var(--text-muted); border-color: var(--border); }
.status-unknown .chip-icon { background: var(--status-unknown); }

:root[data-theme="dark"] .status-attention,
:root[data-theme="dark"] .status-watch { color: var(--text-primary); }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .status-attention,
  :root:not([data-theme="light"]) .status-watch { color: var(--text-primary); }
}

/* Left rule echoing the chip colour, for scanning a list of metrics. */
.rule-critical { border-left: 3px solid var(--status-critical); }
.rule-attention { border-left: 3px solid var(--status-serious); }
.rule-watch { border-left: 3px solid var(--status-warning); }
.rule-healthy,
.rule-excellent { border-left: 3px solid var(--status-good); }
.rule-unknown { border-left: 3px solid var(--status-unknown); }

/* ---------------------------------------------------------- stat tiles --- */
.tile {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--s3) var(--s4);
  display: flex;
  flex-direction: column;
  gap: var(--s1);
  min-width: 0;
}
.tile-label {
  font-size: 0.8rem;
  color: var(--text-secondary);
  font-weight: 600;
}
.tile-value {
  font-size: 1.85rem;
  font-weight: 700;
  line-height: 1.1;
  letter-spacing: -0.02em;
}
.tile-foot {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
  font-size: 0.8rem;
  color: var(--text-muted);
}

.trend { display: inline-flex; align-items: center; gap: 3px; font-weight: 600; }
.trend-good { color: var(--success-text); }
.trend-bad { color: var(--status-critical); }
.trend-neutral { color: var(--text-muted); }

/* Metric rows in the verdict lists.
   The row wraps: at phone width the name takes the first line and the value,
   trend and status chip drop to a second one. Without wrapping, the chip is
   pushed past the right edge and the page scrolls sideways. */
.metric-row {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px 12px;
  padding: var(--s2) var(--s3);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  min-width: 0;
}
.metric-row + .metric-row { margin-top: var(--s2); }
.metric-row .m-name {
  font-weight: 600;
  min-width: 0;
  /* Grows to fill, but may shrink to its content before the row wraps. */
  flex: 1 1 min(100%, 220px);
}
.metric-row .m-desc {
  color: var(--text-muted);
  font-size: 0.82rem;
  font-weight: 400;
  display: block;
  overflow-wrap: anywhere;
}
.metric-row .m-value {
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  white-space: nowrap;
  margin-left: auto;
}
.metric-row .trend { flex: 0 1 auto; min-width: 0; }

/* --------------------------------------------------------------- chart --- */
.chart-card { display: flex; flex-direction: column; }
.chart-frame {
  position: relative;
  /* Height includes room for the x-axis band, so the card never scrolls. */
  height: 260px;
}
.chart-frame.tall { height: 330px; }
.chart-frame.short { height: 150px; }
canvas { max-width: 100%; }

.chart-tools {
  display: flex;
  gap: 8px;
  align-items: center;
  margin-top: 10px;
}
.chart-tools .btn-quiet { margin-left: auto; }

.chart-table-wrap {
  margin-top: 12px;
  max-height: 320px;
  overflow: auto;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
}
.chart-table-wrap[hidden] { display: none; }

/* Small multiples: one plot per need, so identity never rides on colour. */
.multiples { display: grid; gap: 12px; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }
.multiple {
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: 10px 12px 6px;
  background: var(--surface);
}
.multiple h4 { font-size: 0.84rem; margin-bottom: 2px; }
.multiple .chart-frame { height: 84px; }
/* A need nobody currently has: shown for completeness, visually recessive. */
.multiple.is-dormant { opacity: 0.6; }

/* Sparkline inside a stat tile. Server-rendered SVG: no library, prints fine. */
.spark {
  width: 100%;
  height: 26px;
  margin: 2px 0;
  overflow: visible;
}
.spark polyline {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.5;
  stroke-linejoin: round;
  stroke-linecap: round;
  vector-effect: non-scaling-stroke;
}
.spark.tone-good { color: var(--status-good); }
.spark.tone-bad { color: var(--status-critical); }
/* --axis was too pale to read as a line at 26px tall. */
.spark.tone-neutral { color: var(--text-muted); }

/* Compact status grid: a dozen settled indicators in one card instead of a
   dozen full-width rows the reader has to scroll past. */
.status-grid {
  display: grid;
  gap: var(--s2);
  grid-template-columns: repeat(auto-fill, minmax(238px, 1fr));
}
.status-cell {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  background: var(--surface-sunken);
  border-radius: var(--radius-sm);
  min-width: 0;
}
.status-cell-name {
  font-size: 0.86rem;
  min-width: 0;
  /* Wrap rather than ellipsise: "Average need satisfaction" truncated to
     "Average need satisfacti…" tells the reader nothing the full name would
     not have, and the cell has room for a second line. */
  overflow-wrap: anywhere;
}
.status-cell-value {
  font-weight: 700;
  font-size: 0.86rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  flex: 0 0 auto;
}
/* Dormant cells carry a sentence, not a number, so they stack. */
.status-cell.stacked {
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
}
.status-cell.stacked .status-cell-value { font-weight: 400; }
.rule-dormant { border-left: 3px solid var(--status-unknown); }
.status-dormant {
  color: var(--text-muted);
  border-color: var(--border);
  background: var(--surface-sunken);
}
.status-dormant .chip-icon { background: var(--status-unknown); }

/* Positive confirmation, for when there is genuinely nothing wrong. */
.notice {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  padding: var(--s3) var(--s4);
  border-radius: var(--radius);
  border: 1px solid var(--border);
  background: var(--surface);
}
.notice-good { border-color: color-mix(in srgb, var(--status-good) 35%, transparent); }
.notice-icon {
  flex: 0 0 22px;
  width: 22px;
  height: 22px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--status-good);
  color: #fff;
  font-weight: 800;
  font-size: 12px;
  line-height: 1;
}
.notice div { min-width: 0; }
.notice strong { display: block; }

/* -------------------------------------------------------------- tables --- */
table {
  width: 100%;
  border-collapse: collapse;
  font-size: 0.88rem;
}
thead th {
  text-align: left;
  font-weight: 600;
  color: var(--text-secondary);
  font-size: 0.78rem;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  padding: 8px 10px;
  border-bottom: 1px solid var(--border);
  background: var(--surface);
  position: sticky;
  top: 0;
}
tbody td { padding: 7px 10px; border-bottom: 1px solid var(--grid); }
tbody tr:last-child td { border-bottom: none; }
tbody tr:hover { background: var(--surface-sunken); }
td.num, th.num { text-align: right; font-variant-numeric: tabular-nums; }
.table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius-sm); }

/* --------------------------------------------------------------- forms --- */
label { font-weight: 600; font-size: 0.88rem; display: inline-block; margin-bottom: 4px; }
.form-control,
.form-select {
  width: 100%;
  padding: 8px 10px;
  font: inherit;
  font-size: 0.92rem;
  color: var(--text-primary);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
}
.form-control:focus,
.form-select:focus {
  outline: 2px solid var(--series-1);
  outline-offset: 1px;
  border-color: var(--series-1);
}
.form-row { margin-bottom: 14px; }
.form-row .helptext { display: block; color: var(--text-muted); font-size: 0.8rem; margin-top: 4px; }
/* Django's own help text is .helptext; hand-written rows use .form-help. Both
   sit under their label rather than continuing the sentence it started - as a
   direct child only, so the flex layouts that place help text themselves
   (.avatar-editor, .add-form) keep their own arrangement. */
.form-row > label { display: block; }
.form-row > .form-help {
  display: block;
  color: var(--text-muted);
  font-size: 0.8rem;
  margin-top: 2px;
  margin-bottom: 6px;
}
.form-check { display: flex; gap: 8px; align-items: flex-start; }
.form-check-input { margin-top: 4px; width: 16px; height: 16px; accent-color: var(--series-1); }
.form-check label { font-weight: 500; }
.errorlist {
  list-style: none;
  margin: 6px 0 0;
  padding: 0;
  color: var(--status-critical);
  font-size: 0.84rem;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 8px 15px;
  font: inherit;
  font-size: 0.9rem;
  font-weight: 600;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  cursor: pointer;
  text-decoration: none;
}
.btn-primary { background: var(--series-1); color: #fff; }
.btn-primary:hover { filter: brightness(0.93); color: #fff; }
.btn-quiet,
.btn-secondary {
  background: transparent;
  border-color: var(--border-strong);
  color: var(--text-secondary);
}
.btn-quiet:hover,
.btn-secondary:hover { color: var(--text-primary); background: var(--surface-sunken); }
.btn-sm { padding: 5px 10px; font-size: 0.82rem; }

/* ------------------------------------------------------------- filters --- */
/* One filter row above everything it scopes - never inside a chart card. */
.filterbar {
  display: flex;
  gap: var(--s3);
  align-items: flex-end;
  flex-wrap: wrap;
  padding: var(--s3) var(--s4);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  /* Matches the rhythm a .section would give it. A filter bar placed
     directly in the page flow was otherwise sitting flush against whatever
     came before it. */
  margin-top: var(--s6);
  margin-bottom: var(--s4);
}
/* ...unless a section is already providing that gap. */
.section > .filterbar:first-child { margin-top: 0; }
.filterbar .field { display: flex; flex-direction: column; gap: 3px; }
.filterbar .field .form-control,
.filterbar .field .form-select { width: auto; min-width: 170px; }
.pill-row { display: flex; gap: 6px; flex-wrap: wrap; }
.pill {
  padding: 5px 11px;
  border-radius: 999px;
  border: 1px solid var(--border-strong);
  color: var(--text-secondary);
  text-decoration: none;
  font-size: 0.84rem;
}
.pill:hover { background: var(--surface-sunken); color: var(--text-primary); }
.pill[aria-current="true"] {
  background: var(--text-primary);
  color: var(--surface);
  border-color: var(--text-primary);
}

/* ------------------------------------------------------------ messages --- */
.banner {
  padding: 11px 15px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  margin-bottom: 14px;
  font-size: 0.9rem;
}
.banner-success { border-color: color-mix(in srgb, var(--status-good) 40%, transparent); background: color-mix(in srgb, var(--status-good) 9%, transparent); }
.banner-error { border-color: color-mix(in srgb, var(--status-critical) 40%, transparent); background: color-mix(in srgb, var(--status-critical) 9%, transparent); }
.banner-warning { border-color: color-mix(in srgb, var(--status-warning) 45%, transparent); background: color-mix(in srgb, var(--status-warning) 12%, transparent); }
.banner-info { background: var(--surface-sunken); }

.freshness {
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  align-items: center;
  color: var(--text-muted);
  font-size: 0.82rem;
  padding: 10px 0 0;
}

.empty {
  text-align: center;
  padding: 48px 20px;
  color: var(--text-muted);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
}

/* Two rows on one hairline: the facts about the data on the left, the links
   on the right, and the standing disclaimer beneath both. Previously one flat
   run of spans where the build hash, the freshness and the Discord link all
   had the same weight and ran together. */
.footer {
  border-top: 1px solid var(--border);
  margin-top: var(--s7);
  padding-top: var(--s4);
  color: var(--text-muted);
  font-size: 0.82rem;
  display: flex;
  align-items: center;
  gap: var(--s3) var(--s4);
  flex-wrap: wrap;
}
/* A hairline between items, so they read as separate facts without needing a
   middot typed into the markup at every join. */
.footer > span + span::before {
  content: "";
  display: inline-block;
  width: 1px;
  height: 0.8em;
  margin-right: var(--s4);
  vertical-align: -1px;
  background: var(--border);
}
.footer a {
  color: var(--text-secondary);
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.footer a:hover {
  color: var(--text-primary);
  border-bottom-color: var(--border-strong);
}

.footer-legal {
  display: inline-flex;
  gap: var(--s4);
  margin-left: auto;
}
/* The separator belongs between the facts, not before the link group that is
   pushed to the far side. */
.footer > .footer-legal::before { display: none; }

@media (max-width: 680px) {
  .footer { gap: var(--s2) var(--s3); }
  .footer > span + span::before { margin-right: var(--s3); }
  .footer-legal { margin-left: 0; flex-basis: 100%; }
}

/* ---------------------------------------------------------- pagination --- */
.pager { display: flex; gap: 6px; align-items: center; justify-content: center; margin-top: 16px; flex-wrap: wrap; }

/* --------------------------------------------------------------- print --- */
@media print {
  .topbar, .chart-tools, .filterbar, .footer { display: none; }
  .card { break-inside: avoid; box-shadow: none; }
}

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

/* Chart range picker. Sits above the charts it scopes, per the rule that a
   filter belongs to everything below it rather than to one card. */
.range-picker {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
  margin-bottom: var(--s4);
}

/* Skip link: hidden until focused, first thing a keyboard reaches. */
.skip-link {
  position: fixed;
  left: -9999px;
  top: 0;
  z-index: 100;
  padding: var(--s2) var(--s4);
  background: var(--surface);
  color: var(--text-primary);
  border: 1px solid var(--border-strong);
  border-radius: 0 0 var(--radius-sm) 0;
  text-decoration: none;
}
.skip-link:focus { left: 0; }

/* Navigation.
   On a wide screen the <details> is transparent: the summary is hidden and
   the nav shows as a normal row. Below the breakpoint the summary becomes a
   Menu button and the links collapse behind it. Using <details> rather than
   JavaScript keeps it keyboard- and screen-reader-operable by default, and it
   still works if the page's script never loads. */
.nav-disclosure { margin-left: auto; }
.nav-toggle { display: none; }

@media (max-width: 900px) {
  .nav-toggle {
    display: inline-block;
    cursor: pointer;
    list-style: none;
    padding: 5px 11px;
    border: 1px solid var(--border-strong);
    border-radius: var(--radius-sm);
    color: var(--text-secondary);
    font-size: 0.88rem;
    font-weight: 600;
    user-select: none;
  }
  .nav-toggle::-webkit-details-marker { display: none; }
  .nav-toggle::after { content: " \25be"; }
  .nav-disclosure[open] .nav-toggle::after { content: " \25b4"; }
  .nav-disclosure .nav {
    position: absolute;
    /* Matches .topbar-inner's gutter, which is 20px until the 680px break. */
    right: 20px;
    left: 20px;
    margin-top: var(--s2);
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    padding: var(--s2);
    background: var(--surface);
    border: 1px solid var(--border-strong);
    border-radius: var(--radius);
    box-shadow: var(--shadow);
    z-index: 30;
  }
  .nav-disclosure .nav a { padding: var(--s2) var(--s3); }
  .nav .nav-sep { width: auto; height: 1px; margin: var(--s1) 0; }
}
/* Below 680px the topbar gutter narrows, so the panel follows it. This has to
   come after the block above, or the cascade would keep the wider value. */
@media (max-width: 680px) {
  .nav-disclosure .nav { right: 14px; left: 14px; }
}

/* An overflow disclosure: the rest of a capped list, opened in place. Styled
   as a quiet link so it reads as "more of the same", not a new section. */
.overflow > summary {
  cursor: pointer;
  margin-top: var(--s3);
  color: var(--accent);
  font-size: 0.875rem;
}
.overflow > summary:hover { text-decoration: underline; }
.overflow[open] > summary { margin-bottom: var(--s3); }
/* Only the first child: the generic `details > :not(summary)` rule above
   would otherwise stack its gap on top of `.metric-row + .metric-row`. The
   rows keep their own rhythm. */
.overflow > summary + * { margin-top: 0; }

/* ---- Accounts ---------------------------------------------------------- */
/* The landing page: the only thing a signed-out visitor sees, so it carries
   the whole first impression. */
.welcome { max-width: 62rem; }
.welcome .eyebrow {
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-size: 0.78rem;
  color: var(--text-muted);
  margin-bottom: var(--s1);
}
.welcome .lead {
  font-size: 1.05rem;
  color: var(--text-secondary);
  max-width: 46rem;
}
.welcome-card { margin-top: var(--s5); }
.welcome-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-top: var(--s3);
  align-items: center;
}
.plain-list { margin: 0; padding-left: 1.1rem; }
.plain-list li { margin-bottom: var(--s1); color: var(--text-secondary); }

.title-row {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
}

/* Watch toggle. A form so it works without JavaScript, but it should read as
   a single control rather than a form. */
.watch-form { display: inline; margin: 0; }
.watch-btn {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  background: none;
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: 3px 7px;
  cursor: pointer;
  color: var(--text-muted);
  font-size: 0.85rem;
  line-height: 1;
}
.watch-btn:hover {
  color: var(--text-primary);
  border-color: var(--border);
}
.watch-btn.is-watching { color: var(--accent); }

.row-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  align-items: center;
}
.row-actions form { display: flex; gap: 4px; align-items: center; margin: 0; }
.form-select-sm { padding: 3px 6px; font-size: 0.85rem; }
.btn-sm { padding: 3px 9px; font-size: 0.85rem; }
.row-actions .btn-sm { white-space: nowrap; }

/* ---- Header menus ------------------------------------------------------ */
/* Pushes everything after it to the right edge of the bar. */
.nav-grow { flex: 1 1 auto; }

.menu { position: relative; }
.menu > summary {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 6px 11px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  color: var(--text-secondary);
  font-size: 0.9rem;
  list-style: none;
  white-space: nowrap;
}
.menu > summary::-webkit-details-marker { display: none; }
.menu > summary::after {
  content: "\25be";
  font-size: 0.7em;
  opacity: 0.7;
}
.menu[open] > summary::after { content: "\25b4"; }
.menu > summary:hover { color: var(--text-primary); background: var(--surface); }
.menu[open] > summary { background: var(--surface); color: var(--text-primary); }

.menu-panel {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  z-index: 60;
  min-width: 13rem;
  display: flex;
  flex-direction: column;
  padding: var(--s1);
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.28);
}
.menu-panel-right { left: auto; right: 0; }
.menu-panel a,
.menu-signout {
  display: block;
  width: 100%;
  text-align: left;
  padding: 7px 10px;
  border: 0;
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-secondary);
  font: inherit;
  font-size: 0.9rem;
  cursor: pointer;
  white-space: nowrap;
}
.menu-panel a:hover,
.menu-signout:hover { background: var(--surface-2, var(--bg)); color: var(--text-primary); }
.menu-panel a[aria-current="page"] { color: var(--text-primary); font-weight: 600; }
.menu-panel form { margin: 0; }
.menu-head {
  padding: 6px 10px 8px;
  font-size: 0.78rem;
  color: var(--text-muted);
  text-transform: uppercase;
  letter-spacing: 0.05em;
}
.menu-sep {
  display: block;
  height: 1px;
  margin: var(--s1) 6px;
  background: var(--border);
}
.menu-name { font-weight: 600; color: var(--text-primary); }

/* ---- Avatars ----------------------------------------------------------- */
.avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  object-fit: cover;
  background: var(--border);
  color: var(--text-secondary);
  font-weight: 700;
  letter-spacing: 0.02em;
  flex: none;
  overflow: hidden;
}
.avatar-sm { width: 24px; height: 24px; font-size: 0.65rem; }
.avatar-md { width: 44px; height: 44px; font-size: 1.05rem; }
.avatar-lg { width: 96px; height: 96px; font-size: 2rem; }
.avatar-initials { user-select: none; }

.avatar-editor {
  display: flex;
  gap: var(--s4);
  align-items: flex-start;
  flex-wrap: wrap;
}
.avatar-editor .avatar-actions {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  /* Keeps the explanation from stretching the whole card width and pushing
     the picture onto its own line. */
  flex: 1 1 22rem;
  min-width: 0;
  max-width: 34rem;
}
.avatar-editor form { display: flex; flex-wrap: wrap; gap: var(--s2); align-items: center; }
.avatar-editor .form-help { flex: 1 1 100%; margin: 0; }
.avatar-editor input[type="file"] { font-size: 0.85rem; color: var(--text-secondary); min-width: 0; }

/* ---- Drop zones -------------------------------------------------------- */
/* Built by static/js/uploads.js around whatever file input it finds, so none
   of this exists unless the script ran. The input itself is still the control:
   it lies over the target at zero opacity, which is why a click anywhere here
   opens the picker and why the keyboard reaches it without any help. */
.dropzone { width: 100%; min-width: 0; }
.avatar-editor .dropzone { flex: 1 1 100%; }

.dropzone-target {
  position: relative;
  display: block;
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface-sunken, var(--surface));
  transition: border-color 0.12s ease, background-color 0.12s ease;
}
.dropzone-target:hover { border-color: var(--series-1); }

.dropzone-target input[type="file"] {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  opacity: 0;
  cursor: pointer;
}

.dropzone-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  padding: var(--s4) var(--s3);
  text-align: center;
  pointer-events: none; /* the input underneath takes every click */
}
.dropzone-icon { width: 26px; height: 26px; color: var(--text-muted); margin-bottom: var(--s1); }
.dropzone-title { font-size: 0.9rem; font-weight: 600; color: var(--text-secondary); }
.dropzone-hint { font-size: 0.8rem; color: var(--text-muted); }

/* Dragged over, and focused from the keyboard. The focus ring is drawn on the
   wrapper because the input it belongs to is invisible - without this, tabbing
   to an upload would land on nothing a sighted user can see. */
.dropzone.is-over .dropzone-target {
  border-color: var(--series-1);
  border-style: solid;
  background: var(--surface);
}
.dropzone.is-over .dropzone-icon,
.dropzone.is-over .dropzone-title { color: var(--series-1); }
.dropzone.is-focus .dropzone-target {
  outline: 2px solid var(--series-1);
  outline-offset: 2px;
  border-color: var(--series-1);
}

.dropzone-list {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-top: var(--s2);
}
.dropzone-list:empty { margin-top: 0; }

.dropzone-file {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  padding: 3px 4px 3px 10px;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: var(--surface);
  font-size: 0.8rem;
}
.dropzone-file-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-width: 18rem;
}
.dropzone-file-size { color: var(--text-muted); flex: none; }
.dropzone-file-remove {
  flex: none;
  width: 18px;
  height: 18px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--text-muted);
  font: inherit;
  line-height: 1;
  cursor: pointer;
}
.dropzone-file-remove:hover { background: var(--border); color: var(--text-primary); }
.dropzone-file-remove:focus-visible { outline: 2px solid var(--series-1); outline-offset: 1px; }

.dropzone-message {
  margin: var(--s2) 0 0;
  font-size: 0.8rem;
  color: var(--status-warning, var(--text-secondary));
}

@media (max-width: 900px) {
  /* In the collapsed phone panel the bar is a column, so a floating panel
     would land on top of the links below it. Let them stack instead. */
  .nav-grow { display: none; }
  .menu { width: 100%; }
  .menu > summary { width: 100%; }
  .menu-panel {
    position: static;
    border: 0;
    box-shadow: none;
    padding-left: var(--s3);
    background: none;
    min-width: 0;
  }
}
.who { display: inline-flex; align-items: center; gap: 8px; }

/* ---- Member listings --------------------------------------------------- */
/* A claim measured against the exports. Deliberately not a status colour:
   these are not health verdicts, and reusing the status palette would make a
   member's boast look like a reading from the game. */
.claim-note { font-size: 0.82rem; }
.claim-strong { color: var(--series-1, var(--accent)); font-weight: 600; }
.claim-good { color: var(--text-secondary); }
.claim-muted { color: var(--text-muted); }
/* A level the exports cannot account for. Neutral, not alarming - the export
   may simply be stale. */
.claim-beyond {
  color: var(--text-primary);
  border-bottom: 1px dotted var(--border-strong, var(--border));
}

/* ---- Icons ------------------------------------------------------------- */
/* Inherit the surrounding colour and sit on the text baseline, so one set
   works in light and dark and nothing needs a second variant. */
.icon {
  display: inline-block;
  vertical-align: -0.15em;
  flex: none;
  color: var(--text-muted);
}
.icon-sm { width: 1em; height: 1em; margin-right: 0.4em; }
.icon-md { width: 1.25em; height: 1.25em; margin-right: 0.45em; }
.metric-row:hover .icon,
.tile:hover .icon { color: var(--text-secondary); }

/* ---- Data freshness ---------------------------------------------------- */
.freshness { color: var(--text-secondary); }
.freshness.is-stale { color: var(--status-attention-fg, var(--text-primary)); font-weight: 600; }
.stale-banner { display: flex; gap: 8px; flex-wrap: wrap; align-items: baseline; }
.banner-warning {
  background: var(--surface);
  border: 1px solid var(--border-strong, var(--border));
  border-left: 3px solid #d08c26;
  color: var(--text-primary);
}
/* An in-game office, not a site permission. Deliberately unlike the status
   chips, which mean something measured. */
.chip-game {
  background: none;
  border: 1px solid var(--border-strong, var(--border));
  color: var(--text-secondary);
  font-weight: 600;
}
.check-grid { display: flex; flex-direction: column; gap: var(--s2); }
.check-grid .form-check { display: flex; gap: 8px; align-items: flex-start; }
.check-grid .form-check span { display: flex; flex-direction: column; gap: 2px; }
.member-name { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; }
.member-name .chip-game { font-size: 0.72rem; padding: 1px 6px; }

/* ---- Profile ----------------------------------------------------------- */
/* Identity first: a picture, a name, what they are. The upload form is a
   setting and lives with the other settings, not at the top of the page. */
.profile-header {
  display: flex;
  align-items: center;
  gap: var(--s4);
  flex-wrap: wrap;
  padding-bottom: var(--s4);
  margin-bottom: var(--s2);
  border-bottom: 1px solid var(--border);
}
.profile-identity { flex: 1 1 14rem; min-width: 0; }
.profile-identity h1 { margin: 0 0 var(--s1); }
.profile-meta {
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-wrap: wrap;
  margin: 0;
  font-size: 0.88rem;
}

/* Chips in a row need their own gap - relying on whitespace between inline
   elements gives an uneven rag once they wrap. */
.chip-row { display: flex; flex-wrap: wrap; gap: 6px; }

.discord-handle { display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; }

/* Separates two stanzas inside one card - the Discord card holds the verified
   account and the free-text handle, which are different things and should not
   read as one paragraph. */
hr.rule {
  border: 0;
  border-top: 1px solid var(--border);
  margin: var(--s4) 0;
}

/* ---- Legal pages ------------------------------------------------------- */
/* Prose, so it gets a readable measure rather than the full page width. */
.legal { max-width: 46rem; }
.legal h2 { margin-bottom: var(--s3); }
.legal-list {
  margin: 0;
  padding-left: 1.2rem;
  color: var(--text-muted);
  font-size: 0.86rem;
}
.legal-list li { margin-bottom: var(--s2); }
.legal-list li:last-child { margin-bottom: 0; }

/* One question per row, closed. Somebody arrives with a single question and
   should be able to see every question at once to find theirs, rather than
   scrolling a wall of answers they did not ask for. */
.faq {
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
  margin-bottom: var(--s2);
}
.faq > summary {
  cursor: pointer;
  padding: var(--s3) var(--s4);
  font-weight: 600;
  font-size: 0.92rem;
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--s3);
  border-radius: var(--radius);
}
.faq > summary::-webkit-details-marker { display: none; }
/* The marker is drawn rather than inherited, so it rotates and so it does not
   differ between browsers. */
.faq > summary::before {
  content: "";
  flex: none;
  width: 0;
  height: 0;
  border-left: 5px solid currentColor;
  border-top: 4px solid transparent;
  border-bottom: 4px solid transparent;
  color: var(--text-muted);
  transition: transform 0.15s ease;
}
.faq[open] > summary::before { transform: rotate(90deg); }
.faq > summary:hover { background: var(--surface-sunken); }
.faq > summary:focus-visible {
  outline: 2px solid var(--series-1);
  outline-offset: -2px;
}
.faq-body {
  padding: 0 var(--s4) var(--s4);
  color: var(--text-secondary);
  font-size: 0.88rem;
  line-height: 1.6;
}
.faq-body p { margin: 0 0 var(--s3); }
.faq-body p:last-child { margin-bottom: 0; }
.faq-body ol,
.faq-body ul { margin: 0 0 var(--s3); padding-left: 1.3rem; }
.faq-body li { margin-bottom: var(--s2); }
.faq-body li:last-child { margin-bottom: 0; }
.faq-body code {
  background: var(--surface-sunken);
  padding: 1px 5px;
  border-radius: 4px;
  font-size: 0.85em;
}
@media (prefers-reduced-motion: reduce) {
  .faq > summary::before { transition: none; }
}

/* Pushed to the end of the footer row, so the build hash and the data
   freshness - the things somebody actually checks - stay leftmost. */

/* Under the footer row rather than in it: it is a standing statement about
   what this site is, not another link somebody scans past. */
.footer-disclaimer {
  width: min(100%, var(--page-max));
  margin-inline: auto;
  padding: var(--s3) 0 var(--s5);
  color: var(--text-muted);
  font-size: 0.76rem;
  line-height: 1.6;
  opacity: 0.85;
}
@media (max-width: 680px) {
  .footer-disclaimer { padding-inline: 14px; }
}

/* "Add a seedling" and "Add a business" used to be cells in the same grid as
   the things they add, which made every card in their row as tall as a form.
   A seedling with one skill came out 240px tall and mostly empty. They now sit
   below their grid as one full-width card, laid out along a row so the card
   stays shallow and still reads as an action rather than another entry. */
.add-form {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  /* Top-aligned, so every label starts on one line and every input starts on
     the next. Bottom-aligning instead lined up the feet of the fields, and
     since only one of them carries help text underneath, that pushed its input
     higher than its neighbour - the same kind of near-miss this page already
     had elsewhere. */
  align-items: flex-start;
}
.add-form .form-row {
  flex: 1 1 13rem;
  min-width: 0;
  margin-bottom: 0;
}
.add-form .form-row label {
  display: block;
  font-size: 0.85rem;
  line-height: 1.3;
  color: var(--text-secondary);
  margin-bottom: 4px;
}
.add-form .form-help {
  display: block;
  color: var(--text-muted);
  font-size: 0.8rem;
  margin-top: 4px;
}
/* Dropped by exactly one label - the height above plus its margin - so the
   button meets the inputs rather than floating level with their labels. */
.add-form .add-submit {
  flex: 0 0 auto;
  margin-top: calc(0.85rem * 1.3 + 4px);
}

/* An irreversible action, set apart from the settings it sits below rather
   than sharing their grid - it should not look like one more preference. */
.danger-zone { border-left: 3px solid #b4483c; }
.btn-danger {
  background: #b4483c;
  border-color: #b4483c;
  color: #fff;
}
.btn-danger:hover { background: #9c3d33; border-color: #9c3d33; }
