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

/* The `hidden` attribute must always win. Several components carry a `display` (`.btn` is
   inline-flex, `.admin-btn` too), and an author `display` beats the UA `[hidden]{display:none}`
   — so a toggled-hidden element like #admin-link stayed visible for non-admins. `!important`
   restores the attribute's meaning everywhere at once. */
[hidden] { display: none !important; }

html { -webkit-text-size-adjust: 100%; }

body {
  font-family: var(--font);
  background: var(--bg);
  color: var(--text);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

/* Never outline:none. A keyboard user who cannot see the focus cannot use the app. */
:focus-visible {
  outline: 2px solid var(--vg-red);
  outline-offset: 2px;
  /* No border-radius here: the outline follows the element's own radius. Forcing 6px made
     a focused card (10px) or chip (999px) squarer than the element itself. */
}

/* Off-screen via clip-path, not via left:-999px — a negatively positioned element can add
   to the document's scroll width, which is exactly the bug this app had at 320px. */
/* The skip link is the first thing a keyboard user meets. It gets the same tap target as
   everything else — it is also the first thing a switch or touch user meets. */
.skip {
  position: absolute;
  top: var(--s2);
  left: var(--s4);
  background: var(--surface);
  padding: var(--s2) var(--s4);
  border-radius: var(--radius-sm);
  z-index: 100;
  clip-path: inset(50%);
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
}
.skip:focus { clip-path: none; }

/* ------------------------------------------------------------------ typography */

h1 {
  font-size: var(--t-2xl);
  font-weight: var(--fw-black);        /* Lato Black — the VG's signature weight */
  line-height: var(--lh-tight);
  letter-spacing: var(--ls-tight);
  text-wrap: balance;
}
h2 {
  font-size: var(--t-xl);
  font-weight: var(--fw-bold);
  line-height: var(--lh-snug);
  letter-spacing: var(--ls-tight);
  margin-bottom: var(--s3);
  text-wrap: balance;
}
/* Was 1rem — identical to body text. The card title is the most important thing in the
   grid and it disappeared between the chip and the muted line. */
h3 {
  font-size: var(--t-lg);
  font-weight: var(--fw-bold);
  line-height: var(--lh-snug);
}

/* Prose gets a reading width. Cards, tables and grids are exempt. */
main p, .panel p, .banner, .empty p {
  max-width: var(--measure);
  text-wrap: pretty;
}
.card p, .vcard p, .table p, .toast-msg, .row-actions { max-width: none; }

/* The reset sets margin:0 on everything, and nothing put it back — so the legal pages were
   a wall of text with invisible bullet points (padding:0 kills the list marker's room). */
main p + p,
main p + ul,
main ul + p,
.panel p + p { margin-top: var(--s3); }

.panel ul,
main ul { padding-left: var(--s5); list-style: disc; }
.panel li + li,
main li + li { margin-top: var(--s1); }

.muted { color: var(--text-muted); font-size: var(--t-sm); }
.hint { font-size: var(--t-xs); color: var(--text-muted); margin-top: var(--s1); max-width: var(--measure); }
.error-text { font-size: var(--t-sm); color: var(--danger); margin-top: var(--s1); }

/* Utility classes instead of style="" — the CSP forbids inline styles (style-src 'self'
   without unsafe-inline), so an inline attribute is not just untidy, it is ignored. */
.stack-tight { display: flex; gap: var(--s1); flex-wrap: wrap; align-items: center; }
.mt-2 { margin-top: var(--s2); }
.mt-3 { margin-top: var(--s3); }
.mt-4 { margin-top: var(--s4); }
.mb-4 { margin-bottom: var(--s4); }
.w-full { width: 100%; }
.break-all { word-break: break-all; }

/* ---------------------------------------------------------------------- header */

.topbar {
  background: var(--surface);
  border-bottom: 1px solid var(--border);
  box-shadow: var(--shadow-sm);
  position: sticky;
  top: 0;
  /* Above the overflow menu (30), below toasts (90) and the modal (100). An open <details>
     menu does not close on scroll, so a lower topbar would be covered by it. */
  z-index: 40;
}
/* The red edge is the VG signature. It sits on top so it reads as a brand band, not as a
   border that happens to be red. */
.topbar::before {
  content: '';
  display: block;
  height: 4px;
  background: var(--vg-red);
  box-shadow: 0 1px 0 rgba(177, 5, 15, .35);
}

.topbar-inner {
  max-width: var(--page);
  margin: 0 auto;
  padding: var(--s3) var(--s4);
  display: flex;
  align-items: center;
  gap: var(--s2);
}

.brand { display: flex; align-items: center; gap: var(--s3); text-decoration: none; color: inherit; }
.brand img { height: 40px; width: auto; }
.brand-text strong { display: block; font-size: var(--t-md); line-height: 1.2; }
.brand-text span { font-size: var(--t-2xs); color: var(--text-muted); }

.topbar-spacer { flex: 1; }

/* The topbar is a flex row that cannot wrap and whose buttons cannot shrink
   (white-space: nowrap). At 320px the brand plus three buttons wanted ~358px of the 296px
   available, and the WHOLE PAGE scrolled sideways. Below 640px the labels go and the
   buttons become icons; the aria-labels keep them usable. */
.topbar-inner { min-width: 0; }
.brand { min-width: 0; }
.brand-text { min-width: 0; overflow: hidden; }
.brand-text strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }

@media (max-width: 640px) {
  .brand-text span { display: none; }
  .topbar-inner { padding: var(--s2) var(--s3); gap: var(--s1); }
  .topbar-inner .btn { padding-inline: var(--s2); }
}

@media (max-width: 480px) {
  .brand img { height: 32px; }
  .brand-text { display: none; }              /* the crest alone carries the brand here */
  .admin-btn { border-left: none; padding-left: var(--s2); margin-left: 0; }
  /* .admin-btn .label is already hidden from the ≤640 rule above — no need to repeat it. */
  [data-action='logout'] .label { display: none; }
}

/* ------------------------------------------------------------------------ tabs */

.tabs {
  max-width: var(--page);
  margin: 0 auto;
  padding: 0 var(--s4);
  display: flex;
  gap: var(--s1);
  overflow-x: auto;
  scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
.tabs[hidden] { display: none; }
/* The scrollbar is hidden, so a fade is the only hint that there is more to the right. */
@media (max-width: 560px) {
  .tabs { mask-image: linear-gradient(to right, #000 88%, transparent); }
}

.tab {
  appearance: none;
  border: none;
  background: none;
  font: inherit;
  font-weight: var(--fw-bold);
  color: var(--text-muted);
  padding: var(--s3) var(--s4);
  min-height: var(--tap);
  cursor: pointer;
  border-bottom: 2px solid transparent;
  white-space: nowrap;
  display: flex;
  align-items: center;
  gap: var(--s2);
  transition: color var(--fast), border-color var(--fast), background var(--fast);
  border-radius: var(--radius-sm) var(--radius-sm) 0 0;
}
.tab:hover { color: var(--text); background: var(--surface-2); }
.tab[aria-selected='true'] { color: var(--vg-red); border-bottom-color: var(--vg-red); }

.tab .count {
  background: var(--border);
  color: var(--text);   /* grey-on-grey was 4.19:1 */
  border-radius: 999px;
  padding: 1px var(--s2);
  font-size: var(--t-2xs);
  font-weight: var(--fw-bold);
  transition: background var(--fast), color var(--fast);
}
.tab[aria-selected='true'] .count { background: var(--vg-red); color: #fff; }

/* ------------------------------------------------------------------------ main */

main {
  max-width: var(--page);
  margin: 0 auto;
  padding: var(--s5) var(--s4) var(--s7);
  width: 100%;
  flex: 1;
}

.page-head { margin-bottom: var(--s5); }
.page-head h1 { margin-bottom: var(--s1); }

.area-head {
  border-left: 3px solid var(--vg-red);
  padding: var(--s1) 0 var(--s1) var(--s3);
  margin: var(--s4) 0 var(--s5);
}
.area-head h1 { margin-bottom: 2px; }

/* The poll description, shown read-only to creator and participant while voting and in the
   Verlauf. `pre-line` keeps the line breaks the creator typed; a readable measure and wrap
   stop a pasted URL or a wall of text from breaking the layout. */
.poll-desc {
  margin: var(--s3) 0;
  white-space: pre-line;
  overflow-wrap: anywhere;
  line-height: 1.5;
  max-width: 70ch;
}

/* ----------------------------------------------------------------------- cards */

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(268px, 100%), 1fr));
  gap: var(--s4);
}

.card {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--e1);
  padding: var(--s4);
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  text-align: left;
  font: inherit;
  color: inherit;
  cursor: pointer;
  position: relative;
  overflow: hidden;
  transition: box-shadow var(--fast), transform var(--fast), border-color var(--fast);
}
/* A hairline that grows on hover: it signals "this whole card is the target" without
   moving anything else on the page. */
.card::after {
  content: '';
  position: absolute;
  left: 0; right: 0; top: 0;
  height: 3px;
  background: var(--vg-red);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform var(--fast);
}
.card:hover {
  box-shadow: var(--e2);
  transform: translateY(-2px);
  border-color: var(--border-strong);
}
.card:hover::after,
.card:focus-visible::after { transform: scaleX(1); }
.card:active { transform: translateY(0); }

.card.is-draft { border-style: dashed; }
/* An <h2> for the heading outline (h1 → h2, no skip), but visually the card title, not a
   section head: keep the compact --t-lg size and drop the section margin. */
.card-title { margin: var(--s1) 0 0; font-size: var(--t-lg); }

.card-new {
  border: 2px dashed var(--border);
  background: transparent;
  box-shadow: none;
  align-items: center;
  justify-content: center;
  min-height: 148px;
  color: var(--text-muted);
  font-weight: var(--fw-bold);
  gap: var(--s1);
}
.card-new::after { display: none; }
/* The darker red on hover, not the brand red: #e30613 on #fdecee is 4.28:1 — under the 4.5:1
   this bold-but-not-large label needs. --vg-red-dark on the same background is 6.35:1, and
   the border stays brand red, so the card still reads as "the red one". */
.card-new:hover {
  border-color: var(--vg-red);
  color: var(--vg-red-dark);
  background: var(--vg-red-soft);
  box-shadow: none;
  transform: none;
}

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
  font-size: var(--t-2xs);
  font-weight: var(--fw-bold);
  padding: 3px var(--s2);
  border-radius: 999px;
  background: var(--no-soft);
  color: var(--text-muted);
  width: fit-content;
  letter-spacing: .01em;
}
.chip.active { background: var(--vg-red); color: #fff; }
.chip.closed { background: var(--no); color: #fff; }
.chip.confidential { background: var(--vg-red-soft); color: var(--vg-red-dark); }
.chip.warn { background: var(--maybe-soft); color: var(--maybe); }
/* Brand red means "this is the app's accent" (a running poll). A user account being active
   is not an accent, it is a plain OK — so it gets the yes-colour, and a red chip stays
   meaningful. */
.chip.ok { background: var(--yes-soft); color: var(--yes); }
.chip.off { background: var(--no-soft); color: var(--text-muted); }

/* Generic modifiers. The user table used to borrow `.chip.confidential` for "Administrator"
   and `.chip.draft` for "Benutzer" — names from the poll domain that mean nothing about a
   person, and `.chip.draft` never even existed (it fell back to the neutral base by luck).
   The next person to "fix" that missing class would have silently changed what a role looks
   like. A colour vocabulary has to be named after the colour's job, not after one caller. */
.chip.accent { background: var(--vg-red-soft); color: var(--vg-red-dark); }
.chip.neutral { background: var(--n-100); color: var(--text-muted); }

.progress {
  height: 6px;
  background: var(--border);
  border-radius: 999px;
  overflow: hidden;
}
.progress > span {
  display: block;
  height: 100%;
  background: var(--vg-red);
  border-radius: 999px;
  transition: width var(--slow);
}

/* --------------------------------------------------------------------- buttons */

.btn {
  appearance: none;
  font: inherit;
  font-weight: var(--fw-bold);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: var(--s2) var(--s4);
  min-height: var(--tap);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  white-space: nowrap;
  /* So an <a class="btn"> (the static legal pages) looks like a button, not an underlined link. */
  text-decoration: none;
  transition: background var(--fast), border-color var(--fast), color var(--fast),
              box-shadow var(--fast), transform var(--fast);
}
.btn:active { transform: translateY(1px); }
/* Solid buttons lift a hair on hover and press back down — a small tactile cue that the surface
   is live. Ghost buttons stay flat (they are quiet by design). transform rides --fast, so
   prefers-reduced-motion neutralises it. */
.btn-primary:hover, .btn-secondary:hover { transform: translateY(-1px); }
.btn-primary:active, .btn-secondary:active { transform: translateY(1px); }

.btn-primary { background: var(--vg-red); color: #fff; }
.btn-primary:hover { background: var(--vg-red-dark); box-shadow: 0 0 0 4px var(--vg-red-ring); }

.btn-secondary { background: var(--surface); color: var(--text); border-color: var(--border-strong); }
.btn-secondary:hover { background: var(--surface-2); border-color: var(--text-muted); }

.btn-ghost { background: none; color: var(--text-muted); border: none; }
.btn-ghost:hover { color: var(--text); background: var(--n-100); }

/* "← Zurück" follows you down a long page: it rides a sticky strip that carries the PAGE's own
   background, so it reads as an extension of the header rather than a floating pill, and content
   scrolls cleanly under it. `--bg` is mode-aware — body.poll-editing tints it, so in edit mode
   the strip tints with the page automatically. `--topbar-h` is the top bar's live height
   (measured in app.js). The button inside stays a plain ghost button; the strip is the surface. */
.back-bar {
  position: sticky;
  top: var(--topbar-h);
  z-index: 30;
  background: var(--bg);
  border-bottom: 1px solid var(--border);
  padding: var(--s2) 0;
  margin-bottom: var(--s3);
}
/* The static legal pages have no JS to MEASURE the top bar, so the sticky offset falls back to
   the --topbar-h default (56px) while the real bar is 53px — a 3px strip of content then shows
   through between the two sticky elements as you scroll. The back link should still follow the
   scroll (stay sticky), so instead of removing the stick we pull it a few px UP: it then tucks
   just under the top bar (higher z-index, opaque), which covers the overlap — no gap can form,
   and only a sliver of the back-bar's top padding is hidden. */
body.legal .back-bar { top: calc(var(--topbar-h) - 6px); }

.btn.danger { color: var(--danger); }
.btn.danger:hover { background: var(--danger-soft); color: var(--danger-dark); }

.btn:disabled { opacity: .45; cursor: not-allowed; transform: none; box-shadow: none; }

/* ----------------------------------------------------------------------- forms */

label { display: block; font-weight: var(--fw-bold); font-size: var(--t-sm); margin-bottom: var(--s1); }
.field { margin-bottom: var(--s4); }
.field:last-child { margin-bottom: 0; }

input[type='text'], input[type='email'], input[type='password'],
input[type='number'], input[type='datetime-local'], textarea, select {
  width: 100%;
  font: inherit;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  padding: var(--s2) var(--s3);
  min-height: var(--tap);
  transition: border-color var(--fast), box-shadow var(--fast);
}
input:hover, textarea:hover, select:hover { border-color: var(--text-muted); }
input:focus, textarea:focus, select:focus {
  outline: 2px solid transparent;   /* invisible normally, real in forced-colors mode */
  outline-offset: 0;
  border-color: var(--vg-red);
  box-shadow: 0 0 0 3px var(--vg-red-ring);
}

/* Windows high-contrast discards box-shadow entirely — without this the focus ring is
   simply gone for the people who need it most. */
@media (forced-colors: active) {
  input:focus, textarea:focus, select:focus,
  .btn:focus-visible, .tab:focus-visible { outline: 2px solid Highlight; outline-offset: 2px; }
}
textarea { min-height: 112px; resize: vertical; font-family: inherit; line-height: 1.5; }

/* Placeholders are only ever a hint (every field has a real <label>), but the UA default is
   often below 4.5:1. Pin it to --text-muted (5.74:1 on white) so it stays legible. */
::placeholder { color: var(--text-muted); opacity: 1; }

/* A checkbox is not a text field — it must not inherit the 44px min-height. */
.check {
  display: flex;
  align-items: flex-start;
  gap: var(--s2);
  font-weight: var(--fw-bold);
  cursor: pointer;
  margin-bottom: var(--s1);
}
.check input {
  width: 18px;
  height: 18px;
  min-height: 0;
  margin-top: 3px;
  accent-color: var(--vg-red);
  flex: none;
  cursor: pointer;
}

/* A panel is a container, not an object: contour instead of shadow.
   And the OUTER gap must exceed the INNER padding — with 24 inside and 16 between, the eye
   reads four panels as one group, and the editor feels cramped despite the air inside. */
.panel {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  padding: var(--s5);
  margin-bottom: var(--s4);
  box-shadow: var(--e0);
}
@media (min-width: 900px) {
  .panel { padding: var(--s6); margin-bottom: var(--s5); }
}
@media (max-width: 560px) { .panel { padding: var(--s4); } }

/* ---------------------------------------------------------------------- banner */

.banner {
  background: var(--surface);
  border: 1px solid var(--border);
  border-left: 3px solid var(--info);
  background: var(--info-soft);
  border-radius: var(--radius);
  padding: var(--s3) var(--s4);
  margin-bottom: var(--s4);
  font-size: var(--t-sm);
}
.banner.warn { border-left-color: var(--maybe); background: var(--maybe-soft); }
.banner code {
  font-size: var(--t-xs);
  background: var(--surface);
  border: 1px solid var(--border);
  padding: 2px var(--s1);
  border-radius: 4px;
  display: inline-block;
  margin-top: var(--s1);
}

/* --------------------------------------------------- areas outside the workspace */

/* Set by app.js when leaving the poll workspace (the tab bar is hidden then). */
.topbar.in-area { box-shadow: none; }

.admin-btn {
  color: var(--text);
  border-left: 1px solid var(--border);
  border-radius: var(--radius-sm);   /* runder Hover wie beim Profil-Button, nicht eckig */
  padding-left: var(--s4);
  margin-left: var(--s1);
}
.admin-btn:hover { color: var(--text); background: var(--n-100); }

.profile-btn { gap: var(--s2); color: var(--text); font-weight: var(--fw-bold); padding-inline: var(--s2); }
/* A very long account name must not push the topbar past the viewport (no wrap on .btn).
   SCOPED to the profile button: the participant vote page has its OWN #who (the full-width
   "wer hat abgestimmt" foldout), and an unscoped rule clipped it to 22ch on the left. */
.profile-btn #who { max-width: 22ch; overflow: hidden; text-overflow: ellipsis; }
.profile-btn:hover { background: var(--surface-2); }
.profile-btn:hover .avatar { background: var(--vg-red-dark); }

.avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--vg-red);
  color: #fff;
  display: grid;
  place-items: center;
  font-size: var(--t-2xs);
  font-weight: var(--fw-bold);
  flex: none;
  transition: background var(--fast);
}

/* On a phone the name would push the buttons off the bar. The initials still say who is
   logged in — which is the entire point of showing it. */
@media (max-width: 640px) {
  .profile-btn #who { display: none; }
  /* Hide the WORD "Verwaltung", keep the cog. The old rule targeted `span:not([aria-hidden])`
     — but the icon slot has no aria-hidden, so it hid the icon TOO and left an empty 17px
     button: the whole admin entry vanished on a phone. The button carries aria-label, so the
     cog alone still has an accessible name. */
  .admin-btn .label { display: none; }
}

/* `minmax(0, 1fr)`, not `1fr`: a long unbreakable address (vorname.nachname@vg-…-…de) makes the
   value column's min-content ~250px wide and, with `1fr` == `minmax(auto,1fr)`, pushes the whole
   page into a horizontal scroll on a phone. The 0 floor plus overflow-wrap lets it wrap instead. */
.kv { display: grid; grid-template-columns: minmax(88px, auto) minmax(0, 1fr); gap: var(--s2) var(--s5); margin-bottom: var(--s3); }
.kv dt { font-weight: var(--fw-bold); font-size: var(--t-sm); color: var(--text-muted); }
.kv dd { font-size: var(--t-md); overflow-wrap: anywhere; }

/* ----------------------------------------------------------------------- table */

/* A long address (vorname.nachname@vg-altenkirchen-flammersfeld.de) has no break
   opportunity, so its min-content width blew the table past the viewport and the WHOLE page
   scrolled sideways. Two defences: let it break, and let the table scroll inside itself. */
.table-wrap { overflow-x: auto; }
.table { width: 100%; border-collapse: collapse; }
.table td { overflow-wrap: anywhere; }
.table th {
  text-align: left;
  font-size: var(--t-2xs);
  text-transform: uppercase;
  letter-spacing: .05em;
  color: var(--text-muted);
  padding: var(--s2) var(--s3);
  border-bottom: 1px solid var(--border-strong);
}
.table td { padding: var(--s3); border-bottom: 1px solid var(--border); vertical-align: middle; }
.table tbody tr { transition: background var(--fast); }
.table tbody tr:hover { background: var(--surface-2); }
.table tr:last-child td { border-bottom: none; }
.table select { min-height: var(--tap); padding: var(--s1) var(--s2); }

/* Edit mode: only the row being edited turns into a form. The rest of the table stays a
   table — that is what keeps a list of 30 people scannable. */
.table tr.editing { background: var(--vg-red-soft); }
.table tr.editing td { border-bottom-color: var(--vg-red); }

.cell-input {
  border: 1px solid var(--border-strong);
  background: var(--surface);
  padding: var(--s1) var(--s2);
  min-height: var(--tap);
  width: 100%;
  max-width: 260px;
  border-radius: var(--radius-sm);
  margin-bottom: var(--s1);
}
.cell-input.small { font-size: var(--t-sm); }

/* The pencil: quiet until hovered, and square, so it does not compete with the text buttons
   next to it. */
/* The app sets itself a 44px minimum (tokens.css) for anything you act with. The pencil and
   the kebab sit in a row whose menu contains "Zugang löschen" — that is not the place to
   undercut your own standard.

   This rule used to be followed three lines later by `min-height: 36px` on the same selector,
   which — same specificity, later in the file — simply won. The comment above said 44 and the
   browser did 36: the standard was declared, then quietly cancelled. Size lives HERE now, and
   nowhere else. */
.icon-btn,
.menu summary { min-height: var(--tap); min-width: var(--tap); }

.icon-btn {
  padding: 0;
  font-size: 1rem;
  border: 1px solid var(--border-strong);
}
.icon-btn:hover { border-color: var(--n-400); background: var(--n-50); color: var(--text); }

/* Visually hidden, still read by screen readers — an input without a label is a guess. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* A <td> must stay a table-cell.
   `display: flex` on the cell itself takes it OUT of the table layout: its border no longer
   lines up with the rest of the row, and the buttons stack instead of sitting side by side.
   The flexbox belongs to an inner wrapper, not to the cell. */
td.row-actions { white-space: nowrap; text-align: right; }
td.row-actions > * { vertical-align: middle; }
/* "Entfernen", "Link sperren", "Speichern" — small type, full tap target. Shrinking the box
   to match the type is how a row of destructive buttons ends up under the 44px minimum this
   app promises itself. Font size and hit area are two different decisions. */
.row-actions .btn { padding: var(--s1) var(--s2); min-height: var(--tap); font-size: var(--t-xs); }
.row-actions .btn + .btn,
.row-actions .btn + .menu,
.row-actions .menu + .btn { margin-left: var(--s1); }

/* Five columns are unreadable on a phone — the cells become stacked rows. */
@media (max-width: 900px) {
  .table thead { display: none; }
  .table tr { display: block; border-bottom: 1px solid var(--border); padding: var(--s3) 0; }
  .table tr:hover { background: transparent; }
  .table td { display: block; border: none; padding: var(--s1) 0; }
  td.row-actions { text-align: left; margin-top: var(--s2); white-space: normal; }

  /* A bare "3" or "✓" means nothing once the header row that named its column is gone.
     Most cells stay self-evident without one (a name, an e-mail, a status chip) — this only
     opts in the few that are a naked number or glyph. */
  .table td[data-label]::before {
    content: attr(data-label) ": ";
    font-weight: 600;
    color: var(--text-muted);
  }
}


/* -------------------------------------------------------------- empty/skeleton */

.empty {
  text-align: center;
  padding: var(--s7) var(--s4);
  color: var(--text-muted);
  background: var(--surface);
  border: 1px dashed var(--border-strong);
  border-radius: var(--radius);
}
.empty h2 { color: var(--text); }
.empty p { max-width: 46ch; margin-inline: auto; }

/* Skeletons mirror the shape of what is coming, so nothing jumps when it arrives. A spinner
   in the middle of the page cannot do that. */
.skeleton {
  background: linear-gradient(90deg, var(--border) 25%, var(--surface-2) 50%, var(--border) 75%);
  background-size: 200% 100%;
  animation: shimmer 1.3s infinite linear;
  border-radius: var(--radius);
}
.sk-card { height: 152px; }
.sk-line { height: 12px; border-radius: 999px; margin-bottom: var(--s2); }
.sk-line.short { width: 45%; }
.sk-line.medium { width: 70%; }
.sk-row { height: 56px; margin-bottom: var(--s2); border-radius: var(--radius-sm); }

@keyframes shimmer { to { background-position: -200% 0; } }

/* ----------------------------------------------------------------------- modal */

.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(16, 24, 40, .45);
  backdrop-filter: blur(3px);
  display: grid;
  place-items: center;
  padding: var(--s4);
  /* Scrollable, and the modal is height-capped below: a "Änderungen speichern?" dialog with
     several moved dates can grow past a phone's height, and a centred, non-scrollable modal
     then hides its own title and buttons off-screen — the flow dead-ends. */
  overflow: auto;
  z-index: 100;
  animation: fade-in .15s var(--ease);
}

.modal {
  background: var(--surface);
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-lg);
  max-height: calc(100dvh - 2 * var(--s4));
  overflow: auto;
  width: 100%;
  max-width: 480px;
  padding: var(--s5);
  animation: modal-in .24s var(--ease-spring);
  border-top: 4px solid var(--vg-red);
}
/* Used to be the brand red — i.e. identical to the normal dialog. A danger dialog that
   looks like every other dialog is not a danger dialog. */
.modal.danger { border-top-color: var(--danger); }

.modal h2 { font-size: var(--t-xl); margin-bottom: var(--s3); }
.modal p { color: var(--text-muted); line-height: 1.55; }

.modal-list {
  list-style: none;
  margin: var(--s3) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}
.modal-list li {
  position: relative;
  padding-left: var(--s4);
  font-size: var(--t-sm);
  color: var(--text);
}
/* A consequence list should look like consequences, not like decoration. */
/* A bullet point is not a warning. */
.modal-list li::before {
  content: '';
  position: absolute;
  left: 0;
  top: .55em;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--n-400);
}

.modal-detail {
  margin-top: var(--s3);
  background: var(--surface-2);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  padding: var(--s3);
  font-size: var(--t-xs);
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  white-space: pre-wrap;
  word-break: break-word;
  color: var(--text);
}

/* The designed "what changes" list (see modal.js renderChanges). Each change is a card with a
   coloured mark: a clock for a moved time, a trash for a removed date. Reads as a summary you
   scan, not a log line you decode. */
.modal-changes {
  list-style: none;
  margin: var(--s3) 0 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
}
.mchange {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s2) var(--s3);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--surface-2);
  font-size: var(--t-sm);
  line-height: 1.35;
}
.mchange-mark {
  flex: none;
  display: grid;
  place-items: center;
  width: 26px;
  height: 26px;
  border-radius: 50%;
}
.mchange-mark svg { width: 15px; height: 15px; }
.mchange.moved .mchange-mark { background: var(--maybe-soft); color: var(--maybe); }
.mchange.removed .mchange-mark { background: var(--danger-soft); color: var(--danger); }
/* A value being fixed for the first time — a green check, like a confirmed "Ja". */
.mchange.set .mchange-mark { background: var(--yes-soft); color: var(--yes); }
.mchange-text { min-width: 0; }
.mchange-label { font-weight: var(--fw-bold); margin-right: var(--s1); }
.mchange-from { color: var(--text-muted); }
.mchange-arrow { color: var(--text-muted); margin: 0 var(--s1); font-weight: var(--fw-bold); }
.mchange-to { font-weight: var(--fw-bold); }
.mchange.removed s { color: var(--text-muted); text-decoration-color: var(--danger); }
.mchange-tag {
  margin-left: var(--s2);
  font-size: var(--t-2xs);
  font-weight: var(--fw-bold);
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--danger);
}

/* Right-aligned, DOM order preserved: the action(s) sit left, cancel rightmost — "Speichern …
   Abbrechen", the same order as every inline form. */
.modal-actions {
  display: flex;
  justify-content: flex-end;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-top: var(--s5);
}

/* A three-way choice needs room for three buttons on one line; the modal widens to fit them. */
.modal.choices { max-width: 560px; }
.modal-actions.choices { flex-wrap: nowrap; }

/* When the row cannot hold them, stack full-width — as a plain column (not reversed), so the
   top-to-bottom order matches the left-to-right one: recommended action first, cancel last. */
@media (max-width: 560px) {
  .modal-actions.choices { flex-direction: column; flex-wrap: wrap; }
  .modal-actions.choices .btn { width: 100%; }
}
@media (max-width: 480px) {
  .modal-actions { flex-direction: column; }
  .modal-actions .btn { width: 100%; }
}

.btn-danger { background: var(--danger); color: #fff; }
.btn-danger:hover { background: var(--danger-dark); box-shadow: 0 0 0 4px rgba(179, 38, 30, .2); }

@keyframes fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes modal-in {
  from { opacity: 0; transform: translateY(10px) scale(.98); }
  to { opacity: 1; transform: none; }
}

/* ----------------------------------------------------------------------- toast */

/* A stack, bottom-centred. Newest on top of the pile so it is closest to the eye. */
#toast-root {
  position: fixed;
  left: 50%;
  bottom: var(--s5);
  transform: translateX(-50%);
  z-index: 90;
  display: flex;
  flex-direction: column-reverse;
  gap: var(--s2);
  width: min(460px, calc(100vw - var(--s5)));
  pointer-events: none;
}
#toast-root > * { pointer-events: auto; }

/* On a phone the sticky votebar is ~100px tall; a toast 24px off the bottom sat exactly on
   top of "Antwort absenden". */
@media (max-width: 640px) {
  #toast-root { bottom: calc(var(--s5) + 96px); }
}
/* The participant page ALWAYS has that votebar, on every width — so in landscape and on a wide
   screen the toast must clear it too, not only below 640px. */
body.vote #toast-root { bottom: calc(var(--s5) + 96px); }

/* Edit mode has its OWN sticky bar at the bottom ("Bearbeitung aktiv · Fertig · Änderungen
   speichern"), on every width — so a toast sitting 24px off the bottom lands right on the save
   button. Lift the stack clear of it whenever the page is in edit mode. */
body.poll-editing #toast-root { bottom: calc(var(--s5) + 76px); }
@media (max-width: 560px) {
  body.poll-editing #toast-root { bottom: calc(var(--s5) + 128px); }
}

.toast {
  background: var(--surface);
  color: var(--text);
  border: 1px solid var(--border);
  border-left: 3px solid var(--text-muted);
  box-shadow: var(--shadow-lg);
  border-radius: var(--radius);
  padding: var(--s3) var(--s3) var(--s3) var(--s4);
  display: flex;
  align-items: center;
  gap: var(--s3);
  overflow: hidden;
  position: relative;
  animation: toast-in .26s var(--ease-spring);
  font-size: var(--t-sm);
}

/* Colour is the accent, the icon carries the meaning (WCAG 1.4.1). */
.toast.success { border-left-color: var(--yes); }
.toast.error { border-left-color: var(--danger); }
.toast.warn { border-left-color: var(--maybe); }
.toast.info { border-left-color: var(--text-muted); }

.toast-icon {
  width: 22px;
  height: 22px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: var(--t-2xs);
  font-weight: var(--fw-bold);
  color: #fff;
  flex: none;
}
.toast.success .toast-icon { background: var(--yes); }
.toast.error .toast-icon { background: var(--danger); }
.toast.warn .toast-icon { background: var(--maybe); }
.toast.info .toast-icon { background: var(--text-muted); }

.toast-msg { flex: 1; line-height: 1.4; }

.toast-count {
  display: inline-block;
  margin-left: var(--s1);
  background: var(--border);
  color: var(--text);
  border-radius: 999px;
  padding: 0 var(--s2);
  font-size: var(--t-2xs);
  font-weight: var(--fw-bold);
}

/* "Rückgängig" is the most time-critical button in the app — it is only there for a few
   seconds, and missing it means living with the action. It gets a full tap target. */
.toast-undo {
  appearance: none;
  border: none;
  background: none;
  font: inherit;
  font-weight: var(--fw-bold);
  color: var(--vg-red);
  cursor: pointer;
  padding: var(--s1) var(--s2);
  min-height: var(--tap);
  border-radius: var(--radius-sm);
  flex: none;
}
.toast-undo:hover { background: var(--vg-red-soft); color: var(--vg-red-dark); }

.toast-close {
  appearance: none;
  border: none;
  background: none;
  color: var(--text-muted);
  cursor: pointer;
  font-size: var(--t-xs);
  line-height: 1;
  padding: var(--s1);
  min-height: var(--tap);
  min-width: var(--tap);
  display: grid;
  place-items: center;
  border-radius: var(--radius-sm);
  flex: none;
}
.toast-close:hover { background: var(--surface-2); color: var(--text); }

.toast-bar {
  position: absolute;
  left: 0;
  bottom: 0;
  height: 2px;
  width: 100%;
  background: var(--border-strong);
}

@keyframes toast-in {
  from { opacity: 0; transform: translateY(10px); }
  to { opacity: 1; transform: none; }
}

/* ----------------------------------------------------------------------- login */

/* Auth pages (login, reset): the crest + title sit centred above the box, no topbar. The body
   is a flex column so the footer settles at the bottom of the viewport instead of below the
   fold, and .auth-main takes the space between and centres the box in it. */
body.auth { min-height: 100vh; display: flex; flex-direction: column; }
.auth-main { flex: 1; display: grid; place-items: center; padding: var(--s6) var(--s4); width: 100%; }
/* A real <h1> for the page outline, sized as the panel heading rather than a hero title. */
.auth-title { font-size: var(--t-xl); margin: 0 0 var(--s4); }

.login-box { width: 100%; max-width: 392px; }
.login-box .brand { justify-content: center; margin-bottom: var(--s5); flex-direction: column; text-align: center; gap: var(--s2); }
.login-box .brand img { height: 76px; }

/* The topbar hides the brand text on a phone (there the crest alone carries it). On the login
   card that text IS the title, so keep it visible — override the topbar's mobile rule. */
@media (max-width: 480px) {
  .login-box .brand-text { display: block; }
}

/* ---------------------------------------------------------------------- footer */

.site-footer {
  max-width: var(--page);
  margin: 0 auto;
  padding: var(--s5) var(--s4);
  border-top: 1px solid var(--border);
  font-size: var(--t-xs);
  color: var(--text-muted);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3) var(--s4);
  flex-wrap: wrap;
  width: 100%;
}

/* The two legal links are a GROUP, not two loose items.
   As flat children of the footer they wrapped individually: at 480px "Datenschutz" stayed up
   beside the authority's name while "Barrierefreiheit" dropped alone onto the next line. Now
   the pair moves as one — either both sit beside the name, or both go under it, together. */
.footer-links {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;   /* three legal links (Datenschutz/Barrierefreiheit/VG) overflow 320px in one row */
  gap: var(--s4);
}

/* Long, unbreakable strings — a pasted URL, a very long address, a single-word title — must
   wrap instead of forcing a horizontal page scroll. Belt-and-suspenders for the surfaces that
   render user data or legal-page links. */
.summary-head, .banner, .who-line, .identity strong { overflow-wrap: anywhere; }
main a[href^="mailto:"], main a[href^="http"] { overflow-wrap: anywhere; }

/* "Datenschutz" and "Barrierefreiheit" are legally required links, so they have to be
   REACHABLE — a 20px-tall line of small type is not a target you hit on a phone with cold,
   gloved hands. Padding, not font size: the footer stays quiet, the target does not. */
.site-footer a {
  color: inherit;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
}
.site-footer a:hover { color: var(--text); text-decoration: underline; }

/* On a phone the name and the links stack — and then both are centred, because a left-aligned
   pair under a left-aligned name reads like a list someone forgot to finish. */
@media (max-width: 560px) {
  .site-footer { justify-content: center; text-align: center; }
}

/* --------------------------------------------------- participant entry */

/* Two fields plus a button, side by side on desktop, stacked on a phone. The single-person
   form is the default because that is the common case; the bulk paste hides in a
   <details> so it does not dominate a panel most people use one line at a time. */
.p-add {
  display: grid;
  grid-template-columns: 1fr 1fr auto;
  gap: var(--s3);
  align-items: end;
}
.p-add .field { margin-bottom: 0; }
.p-add .btn { min-height: var(--tap); }

@media (max-width: 640px) {
  .p-add { grid-template-columns: 1fr; }
  .p-add .btn { width: 100%; }
}

/* A disclosure toggle is a control: full tap target, like every other control. */
.bulk summary {
  cursor: pointer;
  font-weight: var(--fw-bold);
  font-size: var(--t-sm);
  color: var(--vg-red);
  padding: var(--s2) 0;
  min-height: var(--tap);
  display: flex;
  align-items: center;
  list-style: none;
}
.bulk summary::-webkit-details-marker { display: none; }
.bulk summary::before { content: '▸ '; }
.bulk[open] summary::before { content: '▾ '; }
.bulk summary:hover { color: var(--vg-red-dark); }

/* Motion is decoration everywhere, not just on the cards: the modal and toast entrances get
   the same treatment as the flip, so "reduziert Bewegung" means what it says. */
@media (prefers-reduced-motion: reduce) {
  .modal, .modal-backdrop, .toast { animation: none; }
}

/* A button that reads as a link. Used for the withdrawal action on the vote page, which must
   look like a considered choice, not like a primary button next to "Absenden". */
/* A button that reads as a link.
   NOT `font: inherit` — inside a .hint it would inherit 0.82rem grey and look like
   unstyled text. It sets its own size and weight, so it looks the same wherever it lands. */
/* Looks like a link, acts like a button — so it needs the tap target of a button. The one on
   the participant page is "Teilnahme widerrufen": small type on purpose (it must not compete
   with "Antwort absenden"), but a 34px-tall target for an irreversible action is a trap. */
.linklike {
  appearance: none;
  border: none;
  background: none;
  font-family: var(--font);
  font-size: var(--t-sm);
  font-weight: var(--fw-bold);
  padding: var(--s2) 0;
  min-height: var(--tap);
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  text-decoration: none;
  color: var(--vg-red);
  border-bottom: 1px solid transparent;
  transition: color var(--fast), border-color var(--fast);
}
.linklike:hover { color: var(--vg-red-dark); border-bottom-color: currentColor; }
.linklike.danger { color: var(--danger); }
.linklike.danger:hover { color: var(--danger-dark); }

/* Right-aligned under the submit button — the conventional spot for "forgot password". */
/* "Passwort vergessen?" sits under the centred Anmelden button, so it is centred too — pushed
   into the right corner it reads as an afterthought and no longer belongs to the form above. */
.form-aside { display: flex; justify-content: center; margin-top: var(--s2); }

.login-note {
  text-align: center;
  font-size: var(--t-xs);
  color: var(--text-muted);
  margin-top: var(--s4);
  line-height: 1.5;
}

.sep { border: none; border-top: 1px solid var(--border); margin: var(--s4) 0; }

/* ----------------------------------------------------- table toolbar / menu */

.table-toolbar {
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding-bottom: var(--s3);
  margin-bottom: var(--s2);
  border-bottom: 1px solid var(--border);
}
.table-toolbar h2 { margin: 0; display: flex; align-items: center; gap: var(--s2); }

.count-badge {
  background: var(--border);
  color: var(--text);
  border-radius: 999px;
  padding: 1px var(--s2);
  font-size: var(--t-2xs);
  font-weight: var(--fw-bold);
}

.filter-input {
  width: min(220px, 40vw);
  min-height: var(--tap);
  padding: var(--s1) var(--s3);
  font-size: var(--t-sm);
  /* type='search' is NOT in the base input rule above, so without these it falls back to the
     UA look (square/native). Match the other fields: rounded, same border and surface. */
  appearance: none;
  color: var(--text);
  background: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
}

@media (max-width: 720px) {
  .table-toolbar { flex-wrap: wrap; }
  .table-toolbar .topbar-spacer { display: none; }
  .filter-input { width: 100%; order: 3; }
  .table-toolbar .btn { order: 2; }
}

/* The "new" row is the same form as the edit row, tinted so it reads as "not saved yet". */
.table tr.is-new { background: var(--yes-soft); }
.table tr.is-new td { border-bottom-color: var(--yes); }

/* Overflow menu built on <details>: open/close, Escape and focus come from the browser.
   No JS state, nothing for the CSP to block. */
.menu { position: relative; display: inline-block; }
.menu summary::-webkit-details-marker { display: none; }

/* The trigger is a <summary>, not a .btn — it needs the look spelled out, not inherited.
   Its size comes from the .icon-btn/.menu summary rule above (44px); setting a width here
   again is what used to undercut it. */
.menu summary {
  list-style: none;
  cursor: pointer;
  display: grid;
  place-items: center;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius-sm);
  background: none;
  color: var(--text-muted);
  font-size: 1rem;
  transition: color var(--fast), border-color var(--fast), background var(--fast);
}
.menu summary:hover {
  border-color: var(--n-400);
  color: var(--text);
  background: var(--n-50);
}
.menu[open] summary { border-color: var(--vg-red); color: var(--vg-red); background: var(--vg-red-soft); }

.menu-items {
  position: absolute;
  right: 0;
  top: calc(100% + var(--s1));
  z-index: 30;
  min-width: 220px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--shadow-lg);
  padding: var(--s1);
  display: flex;
  flex-direction: column;
  animation: menu-in .12s var(--ease);
}

/* "Zugang löschen" lives in here. It gets the full tap target like everything else that acts. */
.menu-item {
  appearance: none;
  border: none;
  background: none;
  font: inherit;
  font-size: var(--t-sm);
  text-align: left;
  padding: var(--s2) var(--s3);
  border-radius: var(--radius-sm);
  cursor: pointer;
  color: var(--text);
  min-height: var(--tap);
  white-space: nowrap;
  display: flex;
  align-items: center;
}
.menu-item:hover:not(:disabled) { background: var(--surface-2); }
.menu-item.danger { color: var(--danger); }
.menu-item.danger:hover:not(:disabled) { background: var(--danger-soft); color: var(--danger-dark); }
.menu-item:disabled { opacity: .4; cursor: not-allowed; }

@keyframes menu-in {
  from { opacity: 0; transform: translateY(-4px); }
  to { opacity: 1; transform: none; }
}

/* On a phone the table is a stack of blocks, so an absolutely-positioned menu would fly
   off screen. Let it flow inline instead. */
@media (max-width: 900px) {
  .menu-items { position: static; box-shadow: none; margin-top: var(--s2); min-width: 0; }
}

/* A collapsible panel whose summary is the heading. */
.foldout > summary {
  list-style: none;
  cursor: pointer;
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: var(--tap);
}
.foldout > summary::-webkit-details-marker { display: none; }
.foldout > summary::before { content: '▸'; color: var(--vg-red); font-size: var(--t-xs); }
.foldout[open] > summary::before { content: '▾'; }
.foldout-title { font-size: var(--t-lg); font-weight: var(--fw-bold); letter-spacing: -.005em; }
.foldout > summary:hover .foldout-title { color: var(--vg-red); }


/* ------------------------------------------------------------------------ print */

/* A creator wants to take the result to the meeting. Without this they get the topbar, the
   tabs, the sticky bar and — worse — only the FRONT of every card, because the back is
   hidden behind backface-visibility. */
@media print {
  .topbar, .tabs, .votebar, #toast-root, #modal-root, .site-footer,
  .flip-btn, .btn, .menu, .table-toolbar .btn, .filter-input { display: none !important; }

  body { background: #fff; }
  main { padding: 0; max-width: none; }

  .panel, .card, .vcard-front, .vcard-back { box-shadow: none; border: 1px solid #999; }

  /* Print BOTH faces, stacked, instead of one hidden behind the other. */
  .vcard-inner { transform: none !important; display: block; }
  .vcard-front, .vcard-back {
    position: static;
    backface-visibility: visible;
    transform: none !important;
    opacity: 1 !important;
    page-break-inside: avoid;
    margin-bottom: var(--s3);
  }
  .vcard-back[inert] { display: none; }   /* the hidden face carries `inert` */

  .vote-grid, .grid { display: block; }
  .table { page-break-inside: auto; }
  tr { page-break-inside: avoid; }
}

/* Two-cell inline form (label+input, then a button). The participant entry uses .p-add with
   three columns; reusing it here stretched the single button across half the panel. */
.inline-form {
  display: grid;
  grid-template-columns: minmax(220px, 1fr) auto;
  gap: var(--s3);
  align-items: end;
}
.inline-form .field { margin-bottom: 0; }
@media (max-width: 560px) {
  .inline-form { grid-template-columns: 1fr; }
  .inline-form .btn { width: 100%; }
}


/* ---------------------------------------------------------------- polish */

/* Proportional digits make every live counter jitter horizontally on each update. */
.chip, .count, .count-badge, .toast-count, .bar, .answer-line,
.table td, .vcard-time, .kv dd, .progress + p, .muted {
  font-variant-numeric: tabular-nums;
}

/* Browser blue in a red app. */
::selection { background: var(--vg-red-soft); color: var(--vg-red-dark); }

/* The topbar is sticky (~90px). Without this the skip link lands behind it. */
#main { scroll-margin-top: calc(var(--s7) + var(--s5)); }

/* Vertical flow inside a panel: siblings get space, the first child never does. */
.panel > * + * { margin-top: var(--s4); }
.panel > * + h2 { margin-top: var(--s6); }
.panel > h2 + * { margin-top: var(--s3); }
.panel > .field + .field { margin-top: 0; }   /* .field carries its own margin */

/* Empty states used to be text only — a tinted mark lifts them from "nothing here" to
   "something could be here". */
.empty::before {
  content: '';
  display: block;
  width: 44px;
  height: 44px;
  margin: 0 auto var(--s4);
  border-radius: 50%;
  border: 2px dashed var(--n-300);
}

/* -------------------------------------------------------------------- icons */

/* One grid, one stroke, currentColor — so a hover tints the icon and its label together.
   Replaces five Unicode blocks with five different stroke weights and baselines. */
.icon {
  width: 1.15em;
  height: 1.15em;
  flex: none;
  vertical-align: -.18em;   /* optically on the baseline, not mathematically */
}
.icon-lg { width: 1.75em; height: 1.75em; }
.icon-slot { display: inline-flex; }

/* The tally line (✓ 5 · ~ 2 · ✗ 1) needs its icons aligned with the numbers. */
.tally { display: flex; align-items: center; gap: var(--s1); flex-wrap: wrap; }
.tally .icon { width: 1em; height: 1em; }

/* Answer buttons: icon and label are one unit. */
.seg-opt span .icon { width: 1.1em; height: 1.1em; }

/* Deleting is destructive — the trash icon says so, and so does its hover. */
.flip-btn.danger:hover {
  border-color: var(--danger);
  color: var(--danger);
  background: var(--danger-soft);
  transform: none;   /* the rotate belongs to "flip", not to "delete" */
}

/* Icon + label as one unit in the topbar. */
.admin-btn, [data-action='logout'] { display: inline-flex; align-items: center; gap: var(--s2); }

/* Fixed column widths.
   Without them the browser derives column widths from the CONTENT — so switching a cell
   from text to an input still shifts the whole row sideways, which is exactly what the
   pencil must not do. */
.table-fixed { table-layout: fixed; }
.table-fixed .col-person { width: auto; }
.table-fixed .col-role { width: 190px; }
.table-fixed .col-status { width: 150px; }
.table-fixed .col-num { width: 90px; }
.table-fixed .col-actions { width: 210px; }

/* The inputs fill their cell instead of defining its width. */
.table-fixed .cell-input { max-width: 100%; }

@media (max-width: 900px) {
  /* Stacked layout below this breakpoint — colgroup widths would fight it. */
  .table-fixed { table-layout: auto; }
  .table-fixed colgroup { display: none; }
}

/* A table whose rows carry a dropdown must NOT be a scroll container.
   `overflow-x: auto` drags the y axis along with it (a computed `visible` next to a scrolling
   axis becomes `auto`), so the absolutely positioned menu turns into scrollable overflow: a
   scrollbar appears and the panel jumps the moment you open it.
   Dropping the container is safe precisely here — this table is `table-fixed` with fixed
   column widths, and long addresses break inside their cell via `overflow-wrap: anywhere`.
   It never needs to scroll sideways, so it must not offer to. */
.table-wrap.has-menus { overflow: visible; }

/* ── Versand-Checkliste ────────────────────────────────────────────────────────────────
   Sits inside the sticky send bar, because that is where the question "why is the button
   dead?" is actually asked. Every requirement stays listed whether it is met or not: a list
   that drops finished items reorders itself while you read it, and the line you were about
   to act on jumps away. Fixed rows turn the checklist into progress rather than nagging. */
.votebar.sendbar {
  flex-direction: column;
  align-items: stretch;
  gap: var(--s3);
}

.sendbar-actions {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
}
.sendbar-actions .spacer { flex: 1; }

.readiness {
  border: 1px solid var(--border);
  border-left: 3px solid var(--maybe);
  background: var(--maybe-soft);
  border-radius: var(--radius);
  padding: var(--s3) var(--s4);
}
.readiness.ready {
  border-left-color: var(--yes);
  background: var(--yes-soft);
}

.readiness-top {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--s3);
}
.readiness-head {
  margin: 0;
  min-width: 0;
  font-size: var(--t-xs);
  font-weight: var(--fw-bold);
  color: var(--text);
}

/* "Ausblenden": these are hints, not a gate — pressing "Versenden" brings them back. Sits in
   the panel's top corner, pulled into the padding so it reads as a close control, not a form
   button competing with the send action. */
.readiness-close {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s1);
  min-height: 24px;               /* WCAG 2.5.8 (AA) target size */
  margin: calc(var(--s1) * -1) calc(var(--s2) * -1) 0 0;
  padding: var(--s1) var(--s2);
  border: none;
  background: none;
  font: inherit;
  font-size: var(--t-2xs);
  font-weight: var(--fw-bold);
  color: var(--text-muted);
  cursor: pointer;
  border-radius: var(--radius-sm);
  transition: background var(--fast), color var(--fast);
}
.readiness-close:hover { background: rgba(16, 24, 40, .06); color: var(--text); }
.readiness-close:focus-visible { outline: 2px solid var(--vg-red); outline-offset: 2px; }
.readiness-close svg { width: 14px; height: 14px; flex: 0 0 auto; }

.readiness-list {
  list-style: none;
  margin: var(--s2) 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: var(--s1) var(--s4);
}
.readiness-list li {
  display: flex;
  align-items: center;
  gap: var(--s2);
  font-size: var(--t-xs);
  line-height: 1.4;
}

/* Done items step back: they are context now, not a task. The open ones keep full contrast
   so the eye lands on what is left to do. */
.readiness-list li.done { color: var(--text-muted); }
.readiness-list li.open { color: var(--text); font-weight: var(--fw-bold); }

/* The marker carries the state on its own — colour alone would be invisible to a colour-blind
   or greyscale-printing user. Filled check = done, hollow ring = open. */
.readiness-mark {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  display: grid;
  place-items: center;
  border-radius: 50%;
}
.readiness-list li.done .readiness-mark {
  background: var(--yes);
  color: #fff;
}
.readiness-list li.done .readiness-mark svg { width: 11px; height: 11px; }
.readiness-list li.open .readiness-mark {
  border: 2px dashed var(--border-strong);
}

/* The whole bar is sticky, so on a phone it must not eat the screen: one column, and the
   satisfied items collapse away — there, space is worth more than a stable list. */
/* One column on a phone — but the finished items STAY. Hiding them (as this did) made the
   list shrink and reorder with every completed step, which is the exact behaviour the full
   list was built to avoid, on the screen where losing your place is easiest. They step back
   instead: smaller, quieter, still in their row. */
@media (max-width: 640px) {
  .readiness-list { grid-template-columns: 1fr; }
  .readiness { padding: var(--s2) var(--s3); }
  .readiness-list li.done { font-size: var(--t-2xs); opacity: .75; }
  .readiness.ready .readiness-list { display: none; }

  /* On a phone the head line is long; the labelled button would push it into a second row.
     Drop to an icon-only ✕ with a finger-sized hit area, still in the top corner. */
  .readiness-close span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .readiness-close {
    margin: calc(var(--s1) * -1) calc(var(--s1) * -1) 0 0;
    min-width: 40px;
    min-height: 40px;
    justify-content: center;
  }
  .readiness-close svg { width: 18px; height: 18px; }
}

/* ── Passwortfelder ────────────────────────────────────────────────────────────────────
   A password box nobody can read is where typos live — and this form asks for the same
   secret twice for exactly that reason. The eye sits INSIDE the field, so it cannot be
   mistaken for a form action, and the input keeps padding on the right so the text never
   runs under it. */
.pw-wrap { position: relative; display: block; }
.pw-wrap input { width: 100%; padding-right: 48px; }

/* Edge/IE inject their OWN reveal (and clear) control inside the field the moment you type —
   a second eye right beside ours (.pw-eye), doing the exact same job. Suppress the native one
   so there is exactly one reveal control on the page: the app's. */
input[type='password']::-ms-reveal,
input[type='password']::-ms-clear { display: none; }

/* The app's 44px tap minimum applies here too — a 36px eye was the one control that undercut
   it. It sits inside a 44px-tall field, so a full-height target fits without changing layout. */
.pw-eye {
  position: absolute;
  right: 2px;
  top: 50%;
  transform: translateY(-50%);
  width: var(--tap);
  height: var(--tap);
  display: grid;
  place-items: center;
  border: none;
  background: none;
  border-radius: var(--radius-sm);
  color: var(--text-muted);
  cursor: pointer;
  transition: color var(--fast), background var(--fast);
}
.pw-eye:hover { color: var(--text); background: var(--n-50); }
.pw-eye:focus-visible { outline: 2px solid var(--vg-red); outline-offset: 1px; }

/* The password checklist reuses .readiness/.readiness-list from the send bar — same pattern,
   same meaning. It only needs to sit closer to the fields and stack in one column, because
   its lines are sentences about what you are typing right now, not a status board. */
#pw-checks { margin: var(--s3) 0; }
#pw-checks .readiness-list { grid-template-columns: 1fr; }

/* A button that reads as a link: used where an explanation ends in "go here" and a full
   button would shout louder than the sentence around it. */
.btn-link {
  background: none;
  border: none;
  padding: 0;
  color: var(--vg-red);
  font: inherit;
  text-decoration: underline;
  cursor: pointer;
}
.btn-link:hover { text-decoration: none; }

/* A note inside the readiness panel: not "you forgot something" (that is the checklist) but
   "this is complete and probably wrong". It needs its own voice, or a warning would read as
   a missing requirement at the exact moment nothing is missing. */
.readiness-note {
  display: flex;
  gap: var(--s2);
  align-items: flex-start;
  margin-top: var(--s3);
  padding-top: var(--s3);
  border-top: 1px solid var(--border);
  font-size: var(--t-xs);
  line-height: 1.45;
}
.readiness-warnings {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  min-width: 0;
}
/* Each warning its own bullet: the round marker separates two problems that otherwise read as
   one grey run-on. */
.readiness-warnings li {
  position: relative;
  padding-left: var(--s3);
}
.readiness-warnings li::before {
  content: '';
  position: absolute;
  left: 0;
  top: .55em;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--danger);
}
.rw-problem { display: block; }
.rw-fix { display: block; margin-top: 2px; color: var(--text-muted); }
.readiness-note-mark {
  flex: 0 0 auto;
  width: 16px;
  height: 16px;
  display: grid;
  place-items: center;
  border-radius: 50%;
  background: var(--danger);
  color: #fff;
  font-weight: var(--fw-bold);
  font-size: var(--t-2xs);
}

/* "Complete, but warned" must not look like "all clear": the panel stays amber and the note
   carries the red mark. Green here would be the UI telling a comfortable lie. */
.readiness.warned { border-left-color: var(--danger); background: var(--danger-soft); }

/* ── Bearbeitungsmodus ─────────────────────────────────────────────────────────────────
   A running poll opens read-only. Everything behind this switch reaches other people — a
   reminder mail, a revoked link, a moved date that deletes votes, closing the poll — and
   none of it should sit one stray click away from someone who came to read the numbers. */
.editbar {
  padding: var(--s3) var(--s4);
  margin-bottom: var(--s5);
  border: 1px solid var(--border);
  border-left: 3px solid var(--n-400);
  border-radius: var(--radius);
  background: var(--surface);
}
.editbar-row {
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
}
.editbar-mode { display: flex; align-items: flex-start; gap: var(--s3); min-width: 0; }
.editbar-icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--surface-2);
  color: var(--text-muted);
}
.editbar.on .editbar-icon { background: var(--vg-red); color: #fff; }
.editbar .spacer { flex: 1; }
.editbar strong { font-size: var(--t-sm); }
.editbar .hint { margin-top: 2px; }

/* The controls the mode unlocks, named where it is switched on — otherwise you flip the
   switch and then have to hunt down the page for what appeared. */
.editbar-list {
  list-style: none;
  margin: var(--s3) 0 0;
  padding: var(--s3) 0 0;
  border-top: 1px solid var(--vg-red-soft);
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: var(--s1) var(--s4);
  font-size: var(--t-xs);
  color: var(--text-muted);
}
.editbar-list li { margin: 0; }
.editbar-list strong { color: var(--text); }

/* Switched on, the bar changes colour — the mode has to be visible from anywhere on the page,
   not just at the switch you flipped ten scrolls ago. */
.editbar.on {
  border-left-color: var(--vg-red);
  background: var(--vg-red-soft);
}

.chip.editing-chip { background: var(--vg-red); color: #fff; }

/* The whole page tints in edit mode. `--bg` drives the body canvas; overriding it here washes
   everything behind the (white) panels without touching their contrast. The top bar grows a
   red underline so the mode is announced at the top of the viewport too — top and bottom
   (the sticky action bar) frame the page, and there is no scroll position from which you
   cannot tell you are editing. */
body.poll-editing { --bg: var(--edit-bg); }
body.poll-editing .topbar { box-shadow: inset 0 -3px 0 var(--vg-red); }

/* The sticky edit action bar: the mode indicator that never scrolls away, and the one staged
   action in the whole app — "Änderungen speichern". Sticky like the participant vote bar, so
   the save button is reachable from anywhere instead of stranded at the foot of a long page. */
.edit-actionbar {
  position: sticky;
  bottom: 0;
  z-index: 10;
  display: flex;
  align-items: center;
  gap: var(--s3);
  flex-wrap: wrap;
  margin-top: var(--s5);
  padding: var(--s3) var(--s4);
  background: rgba(253, 236, 238, .95);   /* --vg-red-soft, translucent over the tint */
  backdrop-filter: blur(8px);
  border-top: 2px solid var(--vg-red);
  border-radius: var(--radius) var(--radius) 0 0;
  box-shadow: 0 -2px 12px rgba(16, 24, 40, .06);
}
.edit-actionbar .spacer { flex: 1; }
.edit-actionbar-info { display: flex; align-items: center; gap: var(--s2); min-width: 0; }
.edit-actionbar strong { font-size: var(--t-sm); display: block; line-height: 1.2; }
.edit-sub { font-size: var(--t-xs); color: var(--danger-dark); }

/* The live dot. A slow pulse says "active" without a label; prefers-reduced-motion kills it
   (the global rule zeroes animation-duration), leaving a plain solid dot. */
.edit-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--vg-red);
  flex: none;
  animation: edit-pulse 2s var(--ease) infinite;
}
@keyframes edit-pulse {
  0%, 100% { box-shadow: 0 0 0 0 var(--vg-red-ring); }
  50% { box-shadow: 0 0 0 5px transparent; }
}

@media (max-width: 560px) {
  .edit-actionbar { padding: var(--s2) var(--s3); }
  .edit-actionbar .btn { flex: 1 1 auto; }
}

/* The "not yet informed" reminder: after saving without mailing, the participants still see
   the old dates. This is not an error (info tone would undersell it) and not a catastrophe
   (danger would cry wolf) — it is a job left half-done, so it is amber, and it carries the
   button that finishes it. */
.notice-pending {
  border-left-color: var(--maybe);
  background: var(--maybe-soft);
}

/* The end of the road for a poll. "Umfrage beenden" used to float in a sticky bar; a rare,
   irreversible action does not belong one careless scroll-flick away. It lives here now, in a
   bordered box at the very bottom you have to arrive at on purpose. */
.danger-zone {
  margin-top: var(--s6);
  padding: var(--s4);
  border: 1px solid var(--danger);
  border-left: 3px solid var(--danger);
  border-radius: var(--radius);
  background: var(--danger-soft);
}
.danger-zone h3 { margin: 0 0 var(--s1); font-size: var(--t-sm); color: var(--danger-dark); }
.danger-zone p { margin: 0 0 var(--s3); font-size: var(--t-xs); color: var(--text-muted); }
/* Separates the two irreversible actions in a running poll's danger zone (beenden / löschen)
   so the second does not read as a caption of the first. */
.danger-sep { border: none; border-top: 1px solid var(--danger); opacity: .35; margin: var(--s4) 0; }

/* ── Ergebnis-Übersicht ────────────────────────────────────────────────────────────────
   The ranking existed in the data and never reached the screen: the creator had to scan a
   grid of date-ordered cards to work out which one won and by how much. */
.summary-head {
  font-size: var(--t-lg);
  font-weight: var(--fw-bold);
  margin-bottom: var(--s4);
  text-wrap: balance;
}

.rank-list { list-style: none; margin: 0; padding: 0; }
.rank-list li + li { margin-top: var(--s1); }

/* The whole row is the hit area, not a link buried inside it — a 4px target in a 700px row
   is a target nobody hits on a phone. */
.rank-hit {
  width: 100%;
  display: grid;
  grid-template-columns: 28px minmax(0, 1fr) minmax(80px, 140px) auto 20px;
  align-items: center;
  gap: var(--s3);
  padding: var(--s2) var(--s3);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  background: none;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  transition: background var(--fast), border-color var(--fast);
}
.rank-hit:hover { background: var(--n-50); border-color: var(--border); }

.rank-row.lead .rank-hit { background: var(--yes-soft); border-color: var(--yes); }
.rank-row.lead .rank-hit:hover { background: var(--yes-soft); border-color: var(--yes); }

.rank-no {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--n-100);
  color: var(--text-muted);
  font-size: var(--t-2xs);
  font-weight: var(--fw-bold);
}
.rank-row.lead .rank-no { background: var(--yes); color: #fff; }

.rank-when {
  font-weight: var(--fw-bold);
  font-size: var(--t-sm);
  display: flex;
  align-items: center;
  gap: var(--s2);
  flex-wrap: wrap;
  min-width: 0;
}

/* Two segments, one bar: the solid "Ja" sits ON TOP of the lighter "Ja + wenn nötig". A
   single mixed bar would let six reluctant maybes out-measure five real yeses. */
.rank-bar {
  position: relative;
  height: 8px;
  border-radius: 999px;
  background: var(--n-100);
  overflow: hidden;
}
.rank-bar i {
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  border-radius: 999px;
}
.rank-bar i.maybe { background: var(--maybe); }
.rank-bar i.yes { background: var(--yes); }

.rank-tally {
  display: flex;
  gap: var(--s2);
  align-items: center;
  font-size: var(--t-xs);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.rank-tally span { display: inline-flex; align-items: center; gap: 2px; }
.rank-tally .yes { color: var(--yes); }
.rank-tally .maybe { color: var(--maybe); }
.rank-tally .no { color: var(--no); }
.rank-tally svg { width: 12px; height: 12px; }

/* On a phone the bar is the first thing to go: the numbers next to it say the same thing
   exactly, and a 40px bar says nothing at all. */
@media (max-width: 560px) {
  .rank-hit { grid-template-columns: 24px minmax(0, 1fr) auto 20px; }
  .rank-bar { display: none; }
  /* A big poll ("✓ 128 − 88 ✗ 55 – 12") kept nowrap would push the row past the viewport on a
     narrow phone; let the tally wrap instead of forcing a horizontal page scroll. */
  .rank-tally { white-space: normal; flex-wrap: wrap; }
}

/* The rank, repeated on the card, so the two views can be read against each other. */
.rank-pill {
  display: grid;
  place-items: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  background: var(--n-100);
  color: var(--text-muted);
  font-size: var(--t-2xs);
  font-weight: var(--fw-bold);
  flex: 0 0 auto;
}
.rank-pill.lead { background: var(--yes); color: #fff; }

/* Clicking a row scrolls to its card — which is useless if you then cannot tell WHICH card
   you landed on. A brief ring says "this one". */
.vcard.pinged { outline: 2px solid var(--vg-red); outline-offset: 3px; border-radius: var(--radius); }
@media (prefers-reduced-motion: reduce) {
  .rank-hit, .vcard.pinged { transition: none; }
}



/* The end of the story. It sits at the top of the result view and looks like a conclusion,
   not like another statistic — this is the answer everyone gave up an evening for. */
.final-panel { border-left: 3px solid var(--yes); }
.final-when {
  font-size: var(--t-xl);
  font-weight: var(--fw-black);
  line-height: var(--lh-snug);
  margin-top: var(--s1);
}

/* ── Ergebnis: eine Sektion, zwei Lesarten ─────────────────────────────────────────────
   Cards by default (that is how you read a schedule), a list to decide. One headline above
   both, so the two views can never tell a different story about the same numbers. */
.result-head {
  display: flex;
  align-items: flex-start;
  gap: var(--s3);
  flex-wrap: wrap;
  margin-bottom: var(--s4);
}
.result-head .spacer { flex: 1; }
.result-head .summary-head { margin-bottom: 0; }

.viewswitch {
  display: inline-flex;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: var(--surface);
  flex: 0 0 auto;
}
.viewswitch-btn {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s2) var(--s3);
  min-height: var(--tap);
  border: none;
  background: none;
  font: inherit;
  font-size: var(--t-xs);
  font-weight: var(--fw-bold);
  color: var(--text-muted);
  cursor: pointer;
  transition: background var(--fast), color var(--fast);
}
.viewswitch-btn + .viewswitch-btn { border-left: 1px solid var(--border); }
.viewswitch-btn:hover { background: var(--n-50); color: var(--text); }
.viewswitch-btn.on { background: var(--vg-red-soft); color: var(--vg-red-dark); }
.viewswitch-btn svg { width: 16px; height: 16px; }

/* On a phone the labels go: two icons in a 2-button switch are unambiguous, and the row
   they sit in is already carrying a headline. */
@media (max-width: 520px) {
  .viewswitch-btn span { display: none; }
}

/* ── Ergebniskarte: klappt auf, statt sich zu drehen ───────────────────────────────────
   A flipped card cannot be read next to its neighbours — the moment you turn one over you
   lose sight of what you were comparing it against. Unfolding keeps the numbers on screen
   and lets several cards be open at once, which is the whole point: "who can do Friday but
   not Saturday" is a question about two cards. */
.rcard {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  box-shadow: var(--e1);
  overflow: hidden;
  transition: border-color var(--fast), box-shadow var(--fast);
  align-self: start;   /* a grid item must not stretch to the height of its tallest sibling */
}
.rcard:hover { border-color: var(--border-strong); box-shadow: var(--e2); }
.rcard.best { border-color: var(--yes); }
.rcard.open { box-shadow: var(--e2); }

.rcard-head {
  display: flex;
  align-items: center;
  gap: var(--s3);
  width: 100%;
  padding: var(--s3) var(--s4);
  border: none;
  background: none;
  font: inherit;
  text-align: left;
  cursor: pointer;
  min-height: var(--tap);
}
.rcard-head:hover { background: var(--n-25); }

.rcard-when { display: flex; flex-direction: column; min-width: 0; flex: 1; }

.rcard-chev {
  flex: 0 0 auto;
  color: var(--text-muted);
  display: grid;
  place-items: center;
  transition: transform var(--fast);
}
.rcard-chev svg { width: 18px; height: 18px; }
/* The chevron points where the content will go, so it turns when the content arrives. */
.rcard.open .rcard-chev { transform: rotate(180deg); }

.rcard-body { padding: 0 var(--s4) var(--s4); }
.rcard-detail { margin-top: var(--s3); padding-top: var(--s3); border-top: 1px solid var(--border); }
.rcard-detail[hidden] { display: none; }

.rcard.pinged { outline: 2px solid var(--vg-red); outline-offset: 3px; }

@media (prefers-reduced-motion: reduce) {
  .rcard-chev { transition: none; }
}

/* A list row unfolds in place, exactly like a card. Sending someone to the other view to
   answer "who?" would make the list a dead end. */
.rank-chev {
  display: grid;
  place-items: center;
  color: var(--text-muted);
  transition: transform var(--fast);
}
.rank-chev svg { width: 16px; height: 16px; }
.rank-row.open .rank-chev { transform: rotate(180deg); }

.rank-detail {
  padding: var(--s2) var(--s3) var(--s3) calc(28px + var(--s3) * 2);
  border-left: 2px solid var(--border);
  margin: 0 0 var(--s2) var(--s3);
}
.rank-detail[hidden] { display: none; }

@media (prefers-reduced-motion: reduce) {
  .rank-chev { transition: none; }
}

/* The drafts tab is the only empty state with something to offer, so its button lives inside
   the box. `.empty` centres its paragraphs with `margin-inline: auto`; a button is not a
   paragraph, so it needs the centring said out loud. */
.empty .btn { display: inline-flex; align-items: center; gap: var(--s2); }

/* Wer noch nicht abgestimmt hat: sichtbar, aber zurückgenommen.
   These names are the answer to "who do I remind?", so they belong in the card — but they are
   not a result, and giving them the same weight as the votes would make an unanswered poll
   look busier than it is. Grey, with the label carrying the meaning: colour alone would be
   invisible to anyone who cannot see it. */
.who-line.pending { opacity: .72; }
.who-line.pending .muted-strong {
  color: var(--text-muted);
  font-weight: var(--fw-bold);
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
}
.who-line.pending svg { width: 14px; height: 14px; }

/* Der Balken klebte am „Bester Termin"-Abzeichen.
   `.stack-tight` has no bottom margin (it is a row of chips, usually the last thing in a
   header), and `.bars` had none either — so with a badge present the two touched and the bar
   read as a cut-off underline of the badge rather than as a chart. */
.rcard-badges { margin-bottom: var(--s3); }
.rcard-body .bars { margin-top: var(--s2); }
