/* Grillliste — the Dienstplan layout.
 *
 * Mobile first, in the literal sense: everything below is written for a
 * 360-px-wide phone and widened by the two min-width blocks at the end. The
 * QR code is the front door, so the phone is the design and the desktop is
 * the variant.
 *
 * Dark is the default and light is a switch, not a preference query: the
 * button at the top right sets data-theme on <html> and the choice is
 * remembered. prefers-color-scheme is honoured for anyone who has not chosen.
 *
 * Fonts are served from this host. A German site that pulls its fonts from
 * Google hands every visitor's address to Google on every page, and this one
 * has no reason to call anybody at all.
 */

@font-face {
  font-family: "Archivo";
  src: url("../fonts/archivo-var-latin.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "JetBrains Mono";
  src: url("../fonts/jetbrains-mono-var-latin.woff2") format("woff2");
  font-weight: 100 800;
  font-style: normal;
  font-display: swap;
}

/* Dark, the default. */
:root {
  --surface: #0f141a;
  --ink: #e7edf3;
  --ink-2: #9aa8b6;
  --hair: #26313d;
  --band: #151c24;

  /* THE DEFAULT ACCENT, AND IT IS „glut“ SINCE G-90. A page with no
   * data-accent wears what this block says, so the default lives here as well
   * as in the table below — and the two have to agree or the site would be
   * orange by setting and green by stylesheet. */
  --accent: #f0913c;
  --accent-paper: #9a3412;
  --mark-dot: #f0913c;
  --accent-soft: #3d2410;
  --on-accent: #1a0f04;
  --red: #f2777a;
  --red-soft: #3a1c1e;
  --input: #141b23;
  --chevron: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none' stroke='%239aa8b6' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M1 1.5 6 6.5 11 1.5'/></svg>");
  --input-line: #324050;
  --bubble: #e7edf3;
  --bubble-ink: #0f141a;
  --tint-0: #2f6f4a;
  --tint-1: #6b5330;
  --tint-2: #35566f;
  --tint-3: #5c3a44;
  --tint-4: #4a4a63;
  --on-tint: #eef4f9;
  /* THE ÜBERSICHT'S STATES (G33 §1): nothing needs attention, something
   * waits, something failed. Chosen with the dataviz validator against this
   * surface: each clears 3:1 as a graphical object, and the three stand
   * ΔE 17.8 apart for a full-colour reader. Red against green comes to ΔE
   * 7.6 for a red-green colour-blind one, which is why a state is never
   * colour alone: the dot's shape and the sub-line say it too. */
  --state-ok: #3aaf6a;
  --state-warn: #eeb02c;
  --state-crit: #ff7a86;
  /* THE SPACING SCALE. One rhythm, 4 px, and every gap in the site is a
   * step on it. The operator's words about the organiser area were "almost
   * every control / label is too close to each other", and the cause was
   * that every margin had been chosen on its own: 6, 10, 12, 14, 18, 26 px,
   * each reasonable next to the one before it and none of them agreeing.
   *
   * Named by step rather than by size, so a rule reads as "two steps of
   * air" and not as a number somebody nudged. Anything that needs a value
   * off this scale needs a reason in a comment.
   */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 24px;
  --space-6: 32px;
  --space-7: 48px;
  --space-8: 64px;

  --sans: "Archivo", system-ui, "Segoe UI", sans-serif;
  --mono: "JetBrains Mono", ui-monospace, "Cascadia Mono", Consolas, monospace;
  color-scheme: dark;
}

/* Light, whether preferred or chosen. Only the tokens change, and the chosen
 * one is written last so an explicit switch beats the system preference in
 * both directions. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --surface: #ffffff;
    --ink: #0f172a;
    --ink-2: #475569;
    --hair: #e2e8f0;
    --band: #f1f5f9;

    /* The default again, in light. 7.31:1 on white — see G-90. */
    --accent: #9a3412;
    --accent-paper: #9a3412;
    --mark-dot: #9a3412;
    --accent-soft: #ffedd5;
    --on-accent: #ffffff;
    --red: #b91c1c;
    --red-soft: #fee2e2;
    --input: #ffffff;
    --chevron: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M1 1.5 6 6.5 11 1.5'/></svg>");
    --input-line: #cbd5e1;
    --bubble: #0f172a;
    --tint-0: #166534;
    --tint-1: #92400e;
    --tint-2: #1e4e79;
    --tint-3: #9f1239;
    --tint-4: #4338ca;
    --on-tint: #ffffff;
    --bubble-ink: #ffffff;
    /* Against white: 3:1 each, ΔE 16.9 apart; see the dark block. */
    --state-ok: #15803d;
    --state-warn: #a16207;
    --state-crit: #9f1239;
    color-scheme: light;
  }
}

:root[data-theme="light"] {
  --surface: #ffffff;
  --ink: #0f172a;
  --ink-2: #475569;
  --hair: #e2e8f0;
  --band: #f1f5f9;

  /* And once more for the theme chosen by hand rather than by the system. */
  --accent: #9a3412;
  --accent-paper: #9a3412;
  --mark-dot: #9a3412;
  --accent-soft: #ffedd5;
  --on-accent: #ffffff;
  --red: #b91c1c;
  --red-soft: #fee2e2;
  --input: #ffffff;
  --chevron: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8' fill='none' stroke='%23475569' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M1 1.5 6 6.5 11 1.5'/></svg>");
  --input-line: #cbd5e1;
  --bubble: #0f172a;
  --bubble-ink: #ffffff;
  --tint-0: #166534;
  --tint-1: #92400e;
  --tint-2: #1e4e79;
  --tint-3: #9f1239;
  --tint-4: #4338ca;
  --on-tint: #ffffff;
  --state-ok: #15803d;
  --state-warn: #a16207;
  --state-crit: #9f1239;
  color-scheme: light;
}

/* THE ACCENTS.
 *
 * Five of them, and the one above is the first: a page with no data-accent
 * wears the default, so the base block and the default's entry in this table
 * say the same thing twice. Since G-90 that default is the orange rather than
 * the green — "looks better than the green" (the operator, 2026-09-12) — and
 * green is still here, still selectable, simply no longer first. Every one is
 * written out rather than computed, because a hue rotation of a colour that
 * clears 4.5:1 does not clear it any more.
 *
 * Each is a pair, not a colour. Dark needs a bright accent on a near-black
 * page, light needs a deep one on white, and the ink that goes on top flips
 * with it. Every one of the twenty values below was measured before it was
 * written down — accent on surface, accent on band, on-accent on accent, ink
 * on the soft tone — and the worst pair in the set is 4.89:1. AppearanceTests
 * reads them back out of this file and measures them again, so a value nudged
 * later by eye fails a test rather than a person.
 *
 * ON [data-accent], NOT ON :root[data-accent]. The attribute names an accent
 * wherever it appears, so the same block that colours the whole page colours
 * one label of the form that chooses it — which is why the swatches on the
 * settings pages are var(--accent) and not a second copy of this table. The
 * light blocks carry the theme selector twice for that reason: once for the
 * root that has both attributes, once for a labelled element under it.
 *
 * The three parts come in the same order as the themes above: dark, then
 * preferred light, then chosen light.
 */
/* The green. No longer the page's default, so this is the only place it is
 * written — and it is still what the label marked "gruen" on the two settings
 * pages needs, or that label would inherit whichever accent the page is
 * wearing and draw the wrong circle. */
[data-accent="gruen"] {
  --accent: #3fbf6f;
  --accent-paper: #166534;
  --mark-dot: #3fbf6f;
  --accent-soft: #123a24;
  --on-accent: #08150d;
}

/* The orange, again. Not for the page — :root already says this since G-90 —
 * but for the label that names it on the settings pages, the same way the
 * green needed its own block while it was the default. */
[data-accent="glut"] {
  --accent: #f0913c;
  --accent-paper: #9a3412;
  --mark-dot: #f0913c;
  --accent-soft: #3d2410;
  --on-accent: #1a0f04;
}

[data-accent="senf"] {
  --accent: #d8b23a;
  --accent-paper: #854d0e;
  --mark-dot: #d8b23a;
  --accent-soft: #382d0f;
  --on-accent: #17130a;
}

[data-accent="see"] {
  --accent: #45b6d8;
  --accent-paper: #0e7490;
  --mark-dot: #45b6d8;
  --accent-soft: #0e3441;
  --on-accent: #05161c;
}

[data-accent="beere"] {
  --accent: #e4739b;
  --accent-paper: #9d174d;
  --mark-dot: #e4739b;
  --accent-soft: #40182a;
  --on-accent: #1b0810;
}

@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"])[data-accent="gruen"],
  :root:not([data-theme="dark"]) [data-accent="gruen"] {
    --accent: #166534;
    --accent-paper: #166534;
    --mark-dot: #166534;
    --accent-soft: #dcfce7;
    --on-accent: #ffffff;
  }

  :root:not([data-theme="dark"])[data-accent="glut"],
  :root:not([data-theme="dark"]) [data-accent="glut"] {
    --accent: #9a3412;
    --accent-paper: #9a3412;
    --mark-dot: #9a3412;
    --accent-soft: #ffedd5;
    --on-accent: #ffffff;
  }

  :root:not([data-theme="dark"])[data-accent="senf"],
  :root:not([data-theme="dark"]) [data-accent="senf"] {
    --accent: #854d0e;
    --accent-paper: #854d0e;
    --mark-dot: #854d0e;
    --accent-soft: #fef3c7;
    --on-accent: #ffffff;
  }

  :root:not([data-theme="dark"])[data-accent="see"],
  :root:not([data-theme="dark"]) [data-accent="see"] {
    --accent: #0e7490;
    --accent-paper: #0e7490;
    --mark-dot: #0e7490;
    --accent-soft: #cffafe;
    --on-accent: #ffffff;
  }

  :root:not([data-theme="dark"])[data-accent="beere"],
  :root:not([data-theme="dark"]) [data-accent="beere"] {
    --accent: #9d174d;
    --accent-paper: #9d174d;
    --mark-dot: #9d174d;
    --accent-soft: #fce7f3;
    --on-accent: #ffffff;
  }
}

:root[data-theme="light"][data-accent="gruen"],
:root[data-theme="light"] [data-accent="gruen"] {
  --accent: #166534;
  --accent-paper: #166534;
  --mark-dot: #166534;
  --accent-soft: #dcfce7;
  --on-accent: #ffffff;
}

:root[data-theme="light"][data-accent="glut"],
:root[data-theme="light"] [data-accent="glut"] {
  --accent: #9a3412;
  --accent-paper: #9a3412;
  --mark-dot: #9a3412;
  --accent-soft: #ffedd5;
  --on-accent: #ffffff;
}

:root[data-theme="light"][data-accent="senf"],
:root[data-theme="light"] [data-accent="senf"] {
  --accent: #854d0e;
  --accent-paper: #854d0e;
  --mark-dot: #854d0e;
  --accent-soft: #fef3c7;
  --on-accent: #ffffff;
}

:root[data-theme="light"][data-accent="see"],
:root[data-theme="light"] [data-accent="see"] {
  --accent: #0e7490;
  --accent-paper: #0e7490;
  --mark-dot: #0e7490;
  --accent-soft: #cffafe;
  --on-accent: #ffffff;
}

:root[data-theme="light"][data-accent="beere"],
:root[data-theme="light"] [data-accent="beere"] {
  --accent: #9d174d;
  --accent-paper: #9d174d;
  --mark-dot: #9d174d;
  --accent-soft: #fce7f3;
  --on-accent: #ffffff;
}

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

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

body {
  margin: 0;
  background: var(--surface);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 16px;
  line-height: 1.45;
}

/* THE COLUMN EVERY PAGE'S BLOCKS STAND IN, and until 2026-09-11 it had no
 * gap: the only thing holding two blocks apart was the padding-top a `.stack`
 * happens to carry, so anything that is not a stack — a hint, a table, a list
 * — touched whatever came before it. That is what "the filter button is
 * clipped" was: six pixels between a button and the sentence under it.
 *
 * One step of the scale, here, rather than a margin on each block: a margin
 * per block is how the numbers stopped agreeing the first time. */
.page {
  max-width: 760px;
  margin: 0 auto;
  padding: 16px 14px 8px;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.page-wide {
  max-width: 1080px;
}

a {
  color: var(--accent);
}

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

/* Keyboard users get to the form without walking the header. */
.skip {
  position: absolute;
  left: -9999px;
}

.skip:focus {
  position: static;
  display: inline-block;
  margin-bottom: 8px;
}

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

/* IT WRAPS. At 320 px the lockup and the tools do not fit on one line, and
 * without this the pair was pushed out of its own bar — measured at 286 to
 * 313 in a bar that ends at 306. Nothing else changes: above 320 there is
 * room and it never wraps.
 *
 * FLEX-END RATHER THAN SPACE-BETWEEN since G15 item 4. „Today's bar spreads
 * the two apart, which put the login on the far left edge.“ It did, and only
 * on the landing page: that is the one page with no lockup, so space-between
 * had two children to spread instead of three and sent them to opposite ends
 * of the screen. The tools are one group now and the bar places the group;
 * where there is a lockup, `.topbar .brand`'s auto margin still keeps the
 * left and pushes the group right, which is the same rule rather than a
 * second one. */
.topbar {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  padding-bottom: 14px;
}

/* THE PAIR. 8 px apart, which is the smallest gap on the scale that is still
 * a gap — G-78's finding was two controls whose targets met at zero, and this
 * is the same corner of the same bar. */
.topbar-tools {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* The old text brand's type rules are gone: the name is drawn now. What
 * is left of this selector is above, with the mark. */

.theme {
  font: inherit;
  font-size: 16px;
  width: 44px;
  height: 44px;
  border-radius: 6px;
  border: 1px solid var(--input-line);
  background: var(--input);
  color: var(--ink);
  cursor: pointer;
}

/* ---- the motion vocabulary ------------------------------------------- */

/* "Add motion and responsiveness" (the operator, 2026-09-11).
 *
 * FOUR THINGS MOVE ON THIS SITE, and the list is short on purpose: the
 * guidance is to animate one or two things per view and that too many
 * animations cause distraction and motion sickness. Every one of these is a
 * response to something a person did, or an arrival — none of them loops, and
 * none of them is tied to scrolling.
 *
 *   1. A section arrives once, when the page does.
 *   2. A card lifts under the pointer, so a card that can be opened says so.
 *   3. A button gives under a press, because a control that does not move
 *      under a thumb feels like one that did not register.
 *   4. The mascot's bubble pops, which it already did (G-22).
 *
 * ALL OF IT IS INSIDE `no-preference`. Not "reduced motion turns it off" —
 * the other way round: nothing here is declared at all unless somebody has
 * left the preference alone. A rule written the first way has to be
 * remembered twice and is forgotten once.
 *
 * And every one of these animates `transform` or `opacity` only. Animating a
 * width or a height is work for the layout engine on every frame; these are
 * work for the compositor and nothing else. */
@media (prefers-reduced-motion: no-preference) {
  /* 1. THE SECTION ARRIVES. Once, 260 ms, and it is over — short enough that
   *    somebody who has seen the page a hundred times is not waiting for it. */
  .panel {
    animation: lg-rise 260ms ease-out both;
  }

  /* 2. THE CARD LIFTS. Only for a pointer that can hover: on a touch screen
   *    :hover sticks after a tap and the card would stay lifted. */
  @media (hover: hover) {
    .rows-cards .row {
      transition: transform 160ms ease-out, box-shadow 160ms ease-out;
    }

    .rows-cards .row:hover {
      transform: translateY(-2px);
      box-shadow: 0 6px 18px rgb(0 0 0 / 18%);
    }
  }

  /* 3. THE BUTTON GIVES. Faster going down than coming back, because a press
   *    is an event and its release is a recovery — exit faster than enter
   *    reads as mechanical rather than springy. */
  .button,
  .submit,
  .signin,
  .theme,
  .copy,
  .mascot-pause {
    transition: transform 120ms ease-out;
  }

  .button:active,
  .submit:active,
  .signin:active,
  .theme:active,
  .copy:active,
  .mascot-pause:active {
    transform: scale(0.97);
    transition-duration: 60ms;
  }
}

/* ---- empty states, and the line icons they use ----------------------- */

/* One drawing idea for every icon on this site: a 20-box, stroked at 1.5,
 * `currentColor`, no fill. Sized by whatever is around it. */
.line-icon {
  width: 20px;
  height: 20px;
  flex: none;
  display: block;
}

/* A LIST WITH NOTHING IN IT YET.
 *
 * "Cleaner empty / load states with icons and animations" (the operator,
 * 2026-09-11). What was here was one grey sentence where a list should be:
 * true, and no help at all to somebody who has just made an account.
 *
 * Centred, because there is nothing beside it to align to, and generous with
 * height so the page does not look broken — an empty state that is three
 * lines tall reads as a list that failed to load. */
.empty {
  display: grid;
  justify-items: center;
  align-content: center;
  gap: var(--space-4);
  padding: var(--space-7) var(--space-4);
  text-align: center;
}

.empty .line-icon {
  width: 32px;
  height: 32px;
  color: var(--ink-2);
  opacity: 0.7;
}

.empty-line {
  margin: 0;
  max-width: 34ch;
  color: var(--ink-2);
  font-size: 15px;
}

/* It arrives rather than appearing, once, and only for somebody who has not
 * asked for less motion. Nothing loops and nothing moves on scroll. */
@media (prefers-reduced-motion: no-preference) {
  .empty {
    animation: lg-rise 260ms ease-out both;
  }
}

@keyframes lg-rise {
  from {
    opacity: 0;
    transform: translateY(6px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

/* ---- the progress line for a navigation ------------------------------ */

/* THE WAIT, WHEN THERE IS ONE.
 *
 * Every page here is server-rendered, so a click on a link is a round trip
 * with nothing on the screen to say so. This is a line across the top of the
 * viewport while that happens.
 *
 * TWO TIMINGS, and they are the whole design. It appears only after 150 ms,
 * because a navigation that answers faster than that is not a wait and a bar
 * that flashes for one frame is noise. Once shown it stays for at least
 * 300 ms, because a bar that appears and vanishes in 40 ms reads as a glitch.
 * Both live in nav-progress.js, which is where the numbers are explained.
 *
 * Fixed, so it cannot move the page under the reader: a progress element in
 * the flow would push every page down by its own height at the moment the
 * reader is least able to cope with it. */
.nav-progress {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  width: 100%;
  transform-origin: 0 50%;
  transform: scaleX(0);
  background: var(--accent);
  z-index: 50;
  pointer-events: none;
}

.nav-progress[hidden] {
  display: none;
}

@media (prefers-reduced-motion: no-preference) {
  .nav-progress {
    transition: transform 200ms ease-out;
  }
}

/* Under a reduced-motion preference it still says the same thing; it simply
 * arrives at each width rather than travelling to it. */
/* ---- a section: a heading above a card ------------------------------- */

/* "The admin dashboard tab headers are still not centered properly between
 * the lines. I'd rather have the headers float above a card with the content
 * inside it." (the operator, 2026-09-11.)
 *
 * The old pattern was a rule, a heading and a rule: `.hdr` drew a hairline
 * under the page title and every `h2` sat between one block and the next with
 * nothing but margin to say which side it belonged to. That is what "not
 * centred between the lines" is — it was not centred between them, and it
 * could not be, because the space above a heading and the space below it were
 * set by two different rules that never agreed.
 *
 * A heading that stands above a card needs no rules at all: the card's edge
 * says where the section starts, and the heading belongs to it by sitting on
 * top of it. Nothing has to be centred between anything.
 *
 * The heading is flush with the card's left edge rather than indented to its
 * padding: an outdented label reads as a tab on a folder, which is what the
 * operator asked for by the word "float". */
.panel {
  display: grid;
  gap: var(--space-3);
  align-content: start;
}

.panel-head {
  margin: 0;
  font-size: 15px;
  font-weight: 700;
  letter-spacing: -0.01em;
  color: var(--ink-2);
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.panel-body {
  background: var(--band);
  border: 1px solid transparent;

  /* THE PHONE'S VALUES ARE THE BASE. At 360 px the card is the column, and
   * the page already has side padding, so the card's own inset has to be
   * something a phone can afford — a 24-px ring inside a 20-px ring leaves a
   * form 272 px wide. This was written the other way round, as a
   * `max-width: 480px` override, which G-1 forbids and for a reason: a
   * viewport of 480.5 px matched neither that block nor anything else. */
  border-radius: 8px;
  padding: var(--space-4);
  display: grid;
  gap: var(--space-4);
  align-content: start;
}

@media (min-width: 481px) {
  .panel-body {
    border-radius: 10px;
    padding: var(--space-5);
  }
}

/* A card whose content is already a grid of cards draws no second box around
 * them: the rows are the card. */
.panel-body-plain {
  background: none;
  border-color: transparent;
  padding: 0;
}

/* The page title keeps its own space but loses its rule: the cards under it
 * are what separate it from the page now. */
.hdr {
  padding-bottom: var(--space-2);
}

/* THE COLLISION. This row holds a page title and the button that acts on
 * it. Without `flex-wrap` neither could give way at 360 px, so "Neue
 * Veranstaltung" sat on top of "Veranstaltungen" — the clipping in the
 * operator's screenshots. Wrapping is the fix, and the gap is a step on
 * the scale so the wrapped row does not touch the title. */
.hdr-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: flex-end;
  gap: var(--space-3) var(--space-4);
}

/* WHERE THE SECOND THING IS TALL, THE TEXT STARTS AT THE TOP.
 *
 * „There is a lot of dead space … between the group banner and/or event image
 * and the time/place and header.“ (the operator, 2026-09-13.)
 *
 * `flex-end` above is right for a row of a title and the button that acts on
 * it: they share a baseline. It is wrong for the guest's page, where the
 * second thing is a 192 px mascot — the text was pushed to the BOTTOM of his
 * height, leaving 121 px of nothing between the picture above and the first
 * word. Measured at 1280 px: the row began at 108 and the kicker at 229.
 *
 * A modifier and not a change to the rule, because every other row that uses
 * it wants the baseline it has. */
.hdr-row-top {
  align-items: flex-start;
}

/* Once it has wrapped, the title should not be squeezed to make room for
 * a button that is now on another line. */
.hdr-row > :first-child {
  flex: 1 1 auto;
  min-width: 0;
}

/* AND ITS TEXT COLUMN GIVES WAY RATHER THAN PUSHING THE MASCOT OFF THE ROW.
 *
 * `.hdr-row > :first-child` is `flex: 1 1 auto`, so its basis is its own
 * max-content width — the longest line in it laid out without wrapping. That
 * was the title, about 380 px, and the mascot sat beside it. Moving the
 * deadline line into this column made the longest line a whole sentence, about
 * 560 px, so the column plus the gap plus 192 px of mascot no longer fitted the
 * 732 px this page has and the row wrapped: the mascot dropped underneath and
 * sat alone in a field of nothing.
 *
 * `1 1 0` with `min-width: 0` says the column may be as narrow as the row
 * needs, and the sentence wraps instead. Only on this row — every other header
 * is a title and a button, where sizing to the content is what keeps the button
 * its own width.
 *
 * AFTER the rule it overrides, not before it. Written above, it lost at equal
 * specificity and changed nothing — measured twice, identical numbers. The
 * third time in this stylesheet this session (G-91's nav band, G-93's flash
 * margin), and the reason to read the file rather than the diff. */
.hdr-row-top > :first-child {
  flex: 1 1 0;
  /* NOT `min-width: 0`, WHICH IS WHAT THE BASE RULE SETS. A basis of 0 with
   * nothing to stop it shrinking meant the row could never wrap: at 360 px the
   * text column was 187 px with the mascot's 96 px beside it, so the title
   * broke over three lines and the deadline sentence over four while the
   * mascot sat in the space they needed. 14 rem is wide enough that the row
   * still holds together at 900 px — where wrapping was the fault this
   * modifier was added to fix — and narrow enough that a phone drops the
   * mascot underneath and gives the words the whole column. */
  min-width: 14rem;
}

/* ---- the deadline ---------------------------------------------------- */

/* THE CHECKBOX, THE DATE AND THE HINT AS ONE THING.
 *
 * Three controls that answer one question, so they are one block in the grid
 * of fields rather than three that happen to sit next to each other.
 *
 * AND NO INDENT ON THE DATE OR THE HINT. „The deadline controls aren't quite
 * fully aligned.“ (the operator, 2026-09-13.) They were indented 30 px to sit
 * under the checkbox's own words, which lined them up with nothing: measured,
 * the box at 34, its words at 67, the date field at 60, and every other field
 * on the form at 30 — three left edges where the form has one. The checkbox
 * above is what says the date belongs to it; it does not need the date moved
 * sideways to say so a second time.
 *
 * Both halves of that were written as separate comments, one of them still
 * describing the indent and one of them attached to the rules that had just
 * been deleted, which is two ways of telling the next reader to put it back. */
.deadline {
  display: grid;
  gap: var(--space-2);
}

/* THE STATE, BESIDE THE KICKER, ON A GUEST'S PAGE.
 *
 * The pill is the organiser's `.tag`, reused rather than redrawn: a guest and
 * an organiser looking at the same list should not have to learn two shapes
 * for the same fact. This only puts it on the kicker's line and keeps it from
 * being stretched by it. */
.kicker-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
}

.kicker-row > .tag {
  flex: none;
}

/* THE DEADLINE'S OWN LINE, under the host line.
 *
 * `.notice`'s box while the list is open — the accent-soft ground, because
 * „you can still sign up until Friday“ is something to act on. After it has
 * passed the same line is plain grey: a passed deadline is not an invitation,
 * and leaving it in the accent would make the page look open at a glance. */
/* THE CARD OVER A CLOSED LIST.
 *
 * „a card laid over the form area … The overlay is not dismissable and has no
 * close control; the page is not scrollable behind it in a way that hides it
 * (position it at the top of the form area, not centred in a tall page).“
 * (PKG-G16 §2.)
 *
 * So it is IN the flow, at the top of the form, and not positioned at all:
 * a card that floats over a long list is a card that scrolls away from it,
 * and one centred in the viewport is a card that covers the thing the guest
 * came to read. This one is the first thing in the form area and stays there.
 *
 * The form behind it is disabled rather than hidden — the offers and the
 * guest's own quantities are still what they came for — so this is dimmed
 * along with it: the eye should land on the sentence and then on a list that
 * plainly cannot be used. */
.deadline-over {
  display: grid;
  gap: var(--space-2);
  margin: 0 0 var(--space-5);
  padding: var(--space-4) var(--space-5);
  border: 1px solid var(--hair);
  border-left: 3px solid var(--ink-2);
  border-radius: 10px;
  background: var(--band);
  color: var(--ink);
}

.deadline-over h2 {
  margin: 0;
  font-size: 19px;
}

.deadline-over p {
  margin: 0;
  font-size: 16px;
  line-height: 1.5;
}

.deadline-over .kicker {
  color: var(--ink-2);
}

/* A DISABLED FORM LOOKS DISABLED. Browsers grey a disabled control's own text
 * and leave everything else alone, so without this the list behind the card
 * read as ordinary and only stopped working when somebody tried it. */
/* THE FIELDSET IS THE FORM'S GRID, because it took the form's children.
 *
 * `.entry` is a grid with a step of gap between the fields, the list and the
 * total. Wrapping those three in one fieldset in G16 made them children of a
 * plain block instead, so every gap between them became zero — measured, the
 * name and e-mail boxes ended at 543.5 and the list began at 543.5. „The
 * name/mail boxes now clip into the list table.“ (the operator, 2026-09-13.)
 *
 * So the element that holds them lays them out, with the display and the gap
 * the form has. A wrapper that changes the spacing of what it wraps is not a
 * wrapper. */
.entry-fields {
  display: grid;
  gap: var(--space-5);
  margin: 0;
  padding: 0;
  border: 0;
  min-width: 0;
}

.entry-fields[disabled] {
  opacity: 0.55;
}

/* NO MARGIN OF ITS OWN. On the sign-up page this box is a child of `.page`,
 * a flex column with a 24 px gap, and flex items do not collapse margins — a
 * top margin here would add to that gap and push it away from the form it
 * governs. Inside a header it still needs one; that rule is below, where the
 * context that wants it can ask. */
.deadline-line {
  margin: 0;
  padding: 10px 14px;
  border-radius: 6px;
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  color: var(--ink);
  font-size: 15px;
}

/* The change page still draws it inside its header, under the intro line. */
.hdr .deadline-line {
  margin-block-start: var(--space-3);
}

.deadline-line-over {
  background: transparent;
  border-color: var(--hair);
  color: var(--ink-2);
}

.kicker {
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink-2);
  letter-spacing: 0.04em;
}

/* A LITTLE MORE AIR ABOVE THE TITLE. „There should be a very slightly larger
 * gap between the date/time/where, title and where the list is from.“ (the
 * operator, 2026-09-13.) Measured on the guest's page: 6 px from the kicker to
 * the title and 8 px from the title to the host line, which reads as one
 * block of four lines rather than three things. 10 and 12 — one notch, not a
 * new rhythm. */
h1 {
  font-size: 26px;
  font-weight: 700;
  margin: 10px 0 0;
  letter-spacing: -0.02em;
  line-height: 1.1;
  text-wrap: balance;
}

h2 {
  font-size: 19px;
  font-weight: 700;
  margin: 26px 0 10px;
  letter-spacing: -0.01em;
}

/* A subheading inside a section. The Datenschutz page is the only thing on
 * this site deep enough to need one — purposes, bases and periods under the
 * organisers' section — and it is set clearly smaller than an h2 so the
 * numbered sections stay the page's structure. */
h3 {
  font-size: 15px;
  font-weight: 600;
  margin: var(--space-5) 0 var(--space-2);
  letter-spacing: 0;
}

/* THE COLUMN'S GAP IS THE WHOLE GAP.
 *
 * Flex items do not collapse margins, so a block that carries a top margin of
 * its own gets that margin AND the column's 24 px — a heading 50 px below the
 * thing above it, on three pages, with the stylesheet claiming one step of the
 * scale. These four are the blocks that sit directly in the column and bring a
 * margin with them; inside a card or a stack their own margins still apply. */
.page > h2,
.page > .sub,
.page > .notice,
.page > .banner {
  margin-top: 0;
}

/* The descriptor under the mark. It is a second string on purpose: it says
 * what the site is for, and what it is for widened past barbecues without
 * the name having to change. Set in the mono face and letterspaced so it
 * reads as a subtitle to the wordmark rather than as the first line of the
 * page's prose, which is what .sub below is.
 *
 * No top margin of its own since G15: `.lockup` is a grid and the gap between
 * its two rows is the grid's. A margin and a gap stacked is how a spacing
 * scale stops being one.
 *
 * THE INDENT IS A MARGIN AND NOT PADDING, and the difference is measurable:
 * with padding the element's own left edge stays under the mark and only the
 * text moves, so „the descriptor's left edge equals the wordmark's“ — which is
 * what G15 asks a test to assert — would have been false of the boxes while
 * looking right on the screen. Measured at 360 px: padding put the box at 14
 * and the wordmark at 78. A margin moves the box, so the two edges are the
 * same edge and the assertion is about what a person sees. */
.descriptor {
  margin: 0 0 0 calc(var(--mark-width) + var(--space-3));
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--ink-2);
}

.sub {
  margin: 12px 0 0;
  font-size: 15px;
  color: var(--ink-2);
  max-width: 60ch;
}

.banner {
  margin: 14px 0 0;
  padding: 12px 14px;
  border-radius: 6px;
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  color: var(--ink);
  font-size: 15px;
}

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

.entry,
/* A GRID ITEM WILL NOT SHRINK BELOW ITS CONTENT by default, which means it
 * refuses to be narrower than what is in it — so one long unbroken thing
 * inside a stack pushes the whole column open. That is how the entry page
 * reached 655 px in a 320-px viewport the moment it was given a link line:
 * the line knows how to shrink and its parent would not let it.
 *
 * Declared on the children rather than on the stack itself, because it is
 * the items that have the rule this overrides. */
.stack {
  display: grid;
  gap: var(--space-5);
}

.stack > * {
  min-width: 0;
}

.fields {
  display: grid;
  gap: 14px;
}

.field {
  display: grid;
  gap: 6px;
}

/* A BUTTON IN A FIELD ROW LINES UP WITH THE CONTROL, NOT THE COLUMN.
 *
 * „When creating a new event, the button in step 2 'Vorlage übernehmen' is too
 * large (height) next to the dropdown.“ (the operator, 2026-09-14.)
 *
 * It was not too large; it was stretched. `.fields` is a grid and its rows
 * stretch, so a field holding only a button grew to the height of the field
 * beside it — a label AND a select, about seventy pixels. It keeps its own
 * height now and sits at the bottom of its cell, level with the control it
 * acts on. */
.field-control {
  align-self: end;
}

/* A FILTER ROW: STACKED, THEN TWO COLUMNS, THEN ONE LINE (G31 §7).
 *
 * „The filter rows at 390 stack with no overflow and 44 px targets; at 1280
 * one line.“ The controls are the site's own — 46 px inputs and selects, a
 * 46 px button — so the targets are met by what is already here, and the
 * row only decides where they stand.
 *
 * ONE LINE FROM 1100 PX, where the admin column is 1052 px wide. The text
 * takes a larger share, the days and times their control's own width, the
 * button its own, and the selects what is left — `min-width: 0` is what lets
 * a select with a long event title in it shrink instead of pushing the row
 * wider than the page. Between 640 and 1100 the row is the two columns every
 * other form here has at that width. */
.filter-row {
  display: grid;
  gap: var(--space-3);
}

/* THE LIST IS NARROWED: one line saying so and what it does not narrow,
 * and the way back — inside the sign-up panel, which is what the filter
 * replaces (see `_EntriesFiltered`). */
.filter-note {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2) var(--space-3);
}

.filter-actions,
.filter-reset {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* On a phone the buttons are full width, like every action row here. */
.filter-actions > *,
.filter-reset > *,
.filter-note > .button {
  flex: 1 1 100%;
}

@media (min-width: 640px) {
  .filter-row {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .filter-row > .filter-text,
  .filter-row > .filter-actions,
  .filter-row > .filter-reset {
    grid-column: 1 / -1;
  }

  .filter-actions > *,
  .filter-reset > *,
  .filter-note > .button {
    flex: none;
  }
}

/* The fields stretch to the line's height and put their control at the
 * bottom, so the labels stand level along the top — a date input is two
 * pixels taller than a text box, and aligning the fields by their ends pushed
 * its label up. The reset link wraps onto a line of its own under the row. */
@media (min-width: 1100px) {
  .filter-row {
    display: flex;
    flex-wrap: wrap;
    align-items: stretch;
  }

  .filter-row > .field {
    flex: 1 1 0;
    min-width: 0;
    grid-template-rows: auto 1fr;
    align-items: end;
  }

  .filter-row > .filter-text {
    flex-grow: 1.6;
  }

  /* A DATE OR A TIME FIELD IS AS WIDE AS THE ENGINE DRAWS ITS CONTROL, and
   * no guess of it holds: Chrome on Windows drew a date input 162 px wide and
   * a date-and-time one about 220; Chrome on the CI's Linux drew the second
   * 242 px, 18 px wider than the 14 rem it had been given, and it ran into the
   * select beside it (caught by `NoControlIsClippedAtAnyWidth`). The field
   * takes its content's width, and the text field and the selects share
   * what is left. */
  .filter-row > .filter-day,
  .filter-row > .filter-time {
    flex: 0 0 auto;
  }

  /* AND A NARROW FIELD REALLY IS NARROW. A field is a one-column grid, and
   * an `auto` column is never narrower than its widest unbreakable word —
   * „Veranstaltung“ — so with wider date controls the column stayed wider
   * than the field and the select in it stood 1.8 px out. The column may
   * shrink now, the select shortens its own text as selects do, and a label
   * that no longer fits ends in „…“ rather than running into its neighbour. */
  .filter-row > .field {
    grid-template-columns: minmax(0, 1fr);
  }

  .filter-row > .field > span {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  .filter-row > .filter-actions {
    flex: none;
    flex-wrap: nowrap;
  }

  .filter-row > .filter-reset {
    flex-basis: 100%;
  }
}

/* THE ÜBERSICHT, „ZUSTAND IN DER LEISTE" (G33 §1).
 *
 * Headers and content are told apart by weight, size and the band, not by
 * colour alone: a card's header band is the band colour with a hairline
 * under it and its title at 14 px semibold; the body is 13 px; a label is
 * 11 px mono in small capitals with letter-spacing; a figure is 28 px
 * semibold with tabular digits. */
.dash-head {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2) var(--space-4);
}

.dash-head h1 {
  margin: 0;
}

.dash-lbl,
.dash-asof,
.dash-grp {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-2);
  white-space: nowrap;
}

/* THE FIGURES, AS ONE PIECE (the period switch answers it in place): its
 * parts spaced as the page column spaces its children. */
.dash-figures {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

/* THE STRIP. One bordered row of six equal cells, hairlines and no gaps:
 * the one-pixel gap over a hairline background IS the hairline, between
 * cells and between rows alike. Six across from 1100 px, where every label
 * and sub-line fits its cell; three across below that; two on a phone —
 * never a wrapped label. */
.dash-strip {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1px;
  background: var(--hair);
  border: 1px solid var(--hair);
  border-radius: 10px;
  overflow: hidden;
}

@media (min-width: 640px) {
  .dash-strip {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}

@media (min-width: 1100px) {
  .dash-strip {
    grid-template-columns: repeat(6, minmax(0, 1fr));
  }
}

.dash-cell {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  min-width: 0;
  padding: 14px 16px 16px;
  background: var(--surface);
}

.dash-cell .dash-lbl,
.dash-sub {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.dash-cell .dash-lbl {
  padding-right: 14px;
}

.dash-fig {
  font-size: 28px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
}

.dash-sub {
  font-size: 12px;
  color: var(--ink-2);
}

/* A STATE, THREE WAYS: the 3 px edge and the dot in its colour, and the
 * dot's shape — a circle when nothing needs attention, a triangle when
 * something waits, a square when something failed — because the colours
 * alone do not survive a red-green colour-blind reader (see the tokens). */
.dash-cell.is-ok { --state: var(--state-ok); }
.dash-cell.is-warn { --state: var(--state-warn); }
.dash-cell.is-crit { --state: var(--state-crit); }

.dash-cell:is(.is-ok, .is-warn, .is-crit) {
  box-shadow: inset 0 3px 0 var(--state);
}

.dash-dot {
  position: absolute;
  top: 14px;
  right: 14px;
  width: 10px;
  height: 10px;
  background: var(--state);
}

.is-ok .dash-dot {
  border-radius: 50%;
}

.is-warn .dash-dot {
  clip-path: polygon(50% 0, 100% 100%, 0 100%);
}

/* A CARD: a header band, a body, a foot. */
.card {
  min-width: 0;
  background: var(--surface);
  border: 1px solid var(--hair);
  border-radius: 10px;
}

.card-hd {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2) var(--space-3);
  padding: 12px 16px;
  background: var(--band);
  border-bottom: 1px solid var(--hair);
  border-radius: 10px 10px 0 0;
}

.card-hd h2 {
  margin: 0;
  font-size: 14px;
  font-weight: 600;
}

.card-bd {
  padding: 14px 16px 16px;
  font-size: 13px;
}

.card-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-2) var(--space-3);
  margin-top: var(--space-3);
}

.card-foot .hint {
  font-size: 12px;
}

/* THE PERIOD SWITCH: a segmented control, each segment a real button of
 * 44 px, the chosen one filled with the accent's soft wash. */
.seg {
  display: inline-flex;
  border: 1px solid var(--hair);
  border-radius: 8px;
  overflow: hidden;
}

.seg button {
  min-height: 44px;
  padding: 0 14px;
  border: 0;
  border-left: 1px solid var(--hair);
  background: var(--surface);
  color: var(--ink-2);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}

.seg button:first-of-type {
  border-left: 0;
}

.seg button[aria-pressed="true"] {
  background: var(--accent-soft);
  color: var(--ink);
  font-weight: 600;
}

/* THE CHART. See `ChartView` for the marks: the accent is the only colour on
 * it, a day with nothing on it is a hairline stub, and the current bar wears
 * an outline in the text colour. Its height is the SVG's own attribute,
 * `ChartView.Height`: written here as well, the two could drift apart and
 * stretch every bar's rounded top. */
.chart-bars {
  display: block;
  width: 100%;
  overflow: visible;
}

.chart-hit {
  fill: transparent;
}

.chart-bar {
  fill: var(--accent);
}

.chart-bar.is-zero {
  fill: var(--hair);
}

.chart-bar.is-current {
  stroke: var(--ink);
  stroke-width: 2px;
  paint-order: stroke;
}

.chart-col:hover .chart-bar {
  opacity: 0.75;
}

.chart-col:hover .chart-hit {
  fill: color-mix(in srgb, var(--ink) 8%, transparent);
}

.chart-base {
  stroke: var(--hair);
  stroke-width: 1;
}

.chart-axis {
  display: flex;
  justify-content: space-between;
  gap: var(--space-2);
  margin-top: 6px;
  font-family: var(--mono);
  font-size: 11px;
  color: var(--ink-2);
}

.chart-axis > span {
  white-space: nowrap;
}

/* THE TWO CARDS, equal width side by side, stacked on a phone. */
.dash-cards {
  display: grid;
  gap: var(--space-4);
}

@media (min-width: 768px) {
  .dash-cards {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

.dash-table {
  width: 100%;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
}

/* The columns are named for a screen reader and by the band's label for
 * the eye, so the header row takes no room. */
.dash-table thead th {
  padding: 0;
  height: 0;
}

.dash-table tbody th {
  text-align: left;
  font-weight: 400;
}

.dash-table tbody th,
.dash-table td {
  padding: 7px 0;
  border-top: 1px solid var(--hair);
}

.dash-table tbody tr:first-child th,
.dash-table tbody tr:first-child td {
  border-top: 0;
}

.dash-table td {
  padding-left: 14px;
  text-align: right;
  font-family: var(--mono);
}

/* THE INVENTORY: grouped label/value rows, the values mono and right-aligned,
 * each group under a label with a hairline above it after the first. */
.dash-grp {
  margin: var(--space-2) 0 var(--space-2);
  padding-top: var(--space-2);
  border-top: 1px solid var(--hair);
  font-weight: 400;
}

.dash-grp:first-child {
  margin-top: 0;
  padding-top: 0;
  border-top: 0;
}

.kv {
  display: grid;
  gap: var(--space-2);
  margin: 0;
}

.kv > div {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-4);
  align-items: baseline;
}

.kv dt {
  color: var(--ink-2);
}

.kv dd {
  margin: 0;
  font-family: var(--mono);
  font-size: 15px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  text-align: right;
  white-space: nowrap;
}

/* THE CARDS' QUIET LINK: „Als Tabelle ›", 44 px tall for a finger. */
.link-quiet {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--accent);
  font-size: 13px;
  font-weight: 500;
  text-decoration: none;
  white-space: nowrap;
}

.link-quiet:hover {
  text-decoration: underline;
}

/* „ALS TABELLE" (G33 §3): over the page, fixed, so nothing under it moves —
 * G29's success dialog's overlay and card, which are one rule for both (see
 * `dialog.success`); only the card is wider. Drawn open by the server it is
 * not in the top layer, so the dim is the dialog's own background; opened by
 * `dialog.js` it is, and the same background dims the same way. A dialog
 * NOT open is hidden here as well as by the browser: the overlay's `display`
 * outranks the browser's own `dialog:not([open])`.
 *
 * AND THE PAGE BEHIND IT IS NOT LOCKED, unlike behind the success dialog:
 * taking the page's scrollbar away where a desktop draws one re-lays every
 * card a scrollbar wider, which is the „the cards to either side also
 * enlarge" this dialog exists to end. */
dialog.table-dialog:not([open]) {
  display: none;
}

dialog.table-dialog::backdrop {
  background: transparent;
}

.table-dialog-head {
  margin: 0 0 var(--space-3);
  font-size: 16px;
}

/* THE TABLE SCROLLS, NOT THE PAGE: seven columns are wider than a phone. */
.table-dialog-scroll {
  max-height: min(60vh, 34rem);
  overflow: auto;
}

.table-dialog-scroll .grid-table {
  font-size: 13px;
}

.table-dialog-scroll .grid-table th,
.table-dialog-scroll .grid-table td {
  white-space: nowrap;
}

/* The headings may take two lines, so seven columns fit the card on a desk;
 * a number never wraps. */
.table-dialog-scroll .grid-table thead th {
  white-space: normal;
  vertical-align: bottom;
}

.table-dialog-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

/* AND A FIELD THAT IS HIDDEN IS HIDDEN.
 *
 * `[hidden]` is a rule in the BROWSER's stylesheet and any author `display`
 * outranks it — G-32 for the drop zone's preview, G-93 for the confirmation
 * panel, and here for the deadline's date field, which `deadline.js` hides by
 * setting the attribute. Measured in Chromium before this rule: attribute set,
 * `display: grid`, 51.19 px tall, unchanged by unticking the box.
 *
 * On `.field` rather than on that one element, because the class is what
 * carries the `display` that outranks it — every field on this site had the
 * same hole and only one of them had anything trying to use it. */
.field[hidden] {
  display: none;
}

.field > span,
.field > label {
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
}

.field em {
  font-style: normal;
  font-weight: 400;
  opacity: 0.75;
}

/* Required, said in the one way a form is allowed to say it without a
   sentence. aria-hidden in the markup: the input's own `required` is what a
   screen reader announces, and two announcements of one fact is noise. */
.req {
  color: var(--accent);
  font-weight: 700;
}

/* `file` is in this list since G11 item 4. It was the one input without a
 * width of its own, so it kept its intrinsic one — the button and the
 * file name the browser draws — and ran 4 px off a 320-px screen, taking the
 * account page's document to 341. */
input[type="text"],
input[type="email"],
input[type="password"],
input[type="number"],
input[type="date"],
input[type="time"],
input[type="datetime-local"],
select,
textarea {
  font: inherit;
  font-size: 16px;
  width: 100%;
  background: var(--input);
  color: var(--ink);
  border: 1px solid var(--input-line);
  border-radius: 6px;
  padding: 12px;
  min-height: 46px;
}

textarea {
  min-height: 88px;
  resize: vertical;
}

/* THE ARROW IS DRAWN, AND IT SITS WHERE THE PADDING SAYS.
 *
 * „Generally on all dropdowns, the arrowhead facing downward sits awkwardly
 * close to the right edge of the control.“ (the operator, 2026-09-14.)
 *
 * A native select draws its own arrow hard against the frame and does not read
 * the element's padding, so there was nothing to adjust — and every browser
 * draws a different one, so one control looked like three across three
 * machines. `appearance: none` takes the browser's away; this draws the site's
 * own, on the space step everything else here is spaced on.
 *
 * The chevron is a palette token for the reason every colour is: `--ink-2` is
 * two different colours in the two themes, and `var()` cannot be interpolated
 * into a `url()`. The right padding leaves room for the arrow plus the same
 * step again, so the longest option never runs under it. */
select {
  appearance: none;
  background-image: var(--chevron);
  background-repeat: no-repeat;
  background-position: right var(--space-3) center;
  padding-right: calc(var(--space-3) + 12px + var(--space-3));
}

input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  border-color: var(--accent);
}

.check {
  display: flex;
  align-items: center;
  gap: 10px;
  font-size: 15px;
  min-height: 44px;
}

.check input {
  width: 20px;
  height: 20px;
  accent-color: var(--accent);
}

/* A CLUB AND WHAT THIS PERSON IS IN IT, ONE ROW EACH.
 *
 * „So if I want someone to be organiser in one group and admin in another I
 * can't set it up from the start." (the operator, 2026-09-22.)
 *
 * A PHONE STACKS AND A DESK ALIGNS. At 390 the select sits under the club it
 * is about, which is the only arrangement that leaves the name room to be a
 * name. At 640 and up the rows become a two-column grid — and it is the
 * OUTER element that is the grid, with each row `display: contents`, so that
 * every select lands in one column whatever the names beside them are. A grid
 * per row would align each select against its own name and leave a ragged
 * column, which is the thing the operator's „in one column" rules out.
 */
.group-roles {
  display: grid;
  gap: var(--space-2);
}

/* AKTUELL AND VERGANGEN, UNDER A CLUB'S HEADING.
 *
 * „in the events tab, have a sort of tab switch between live events and past
 * events below the headline (group specific)." (the operator, 2026-09-22.)
 *
 * TWO BUTTONS' WORTH OF SHAPE AND NO NEW ONE. The site already draws a
 * two-state control as a pair of buttons where the quiet one is the state you
 * are not in — the users page's „Gelöschte anzeigen" — so these are `.button`
 * and `.button-quiet` links with a row of their own. They wrap rather than
 * shrink, because a target under 44 px is a target a thumb misses.
 */
.tabs {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin-bottom: var(--space-3);
}

/* WHAT A CLUB HAS DONE, IN A CARD SMALLER THAN AN EVENT'S.
 *
 * „a total number of events and sign ups across all events as stats in a new
 * smaller card below the group header inside of the Events tab … + in the
 * groups tab." (the operator, 2026-09-22.)
 *
 * THE NUMBERS WRAP AS A GROUP AND NEVER INSIDE ONE. „340" on one line and
 * „Anmeldungen" on the next is a figure that has come apart from what it
 * counts — so each figure is its own inline-block with `nowrap` on it, and
 * the line breaks between them. The same rule `.row-counts` has on the club
 * cards, for the same reason.
 */
.figures {
  border: 1px solid var(--hair);
  border-radius: 8px;
  padding: var(--space-2) var(--space-3);
  margin-bottom: var(--space-3);
  background: var(--band);
}

.figures-line {
  margin: 0;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink-2);
  display: flex;
  flex-wrap: wrap;
  gap: 2px var(--space-3);
}



/* ONE PILL PER FIGURE.
 *
 * „Currently kinda just looks thrown together. And doesn't wrap well on
 * mobile. Make the stats into individual pills inside the card with sensible
 * Icons." (the operator, 2026-09-22.)
 *
 * The middle-dots this replaces were drawn with `::before` so that a
 * separator could never be left at the end of a wrapped line — which solved
 * half of it: the DOT stayed with its figure and the figure itself still
 * wrapped anywhere, so „Größte: Sommerfest 2026, 58 Anmeldungen" broke across
 * two lines with nothing to say where one figure ended and the next began. A
 * box is its own separator. */
.figure-pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: 0;
  padding: 0;
  list-style: none;
}

.figure-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* 0 rather than auto, so a pill that has to wrap inside itself may — a flex
   * item's floor is its content otherwise, and one long title would push the
   * row wider than the phone. */
  min-width: 0;
  padding: 4px 10px;
  border: 1px solid var(--hair);
  border-radius: 999px;
  background: var(--surface);
  font-family: var(--mono);
  font-size: 12px;
  line-height: 1.45;
  color: var(--ink-2);
}

/* THE ICON IS THE ACCENT AND THE WORDS ARE NOT. It is what says which figure
 * this is at a glance; the number is what you read second. */
.figure-pill .line-icon {
  width: 14px;
  height: 14px;
  color: var(--accent);
}

/* THE ONE FIGURE WITH A TITLE IN IT. Every other pill is a number and a word
 * and fits on any screen; this one carries whatever the club called its
 * evening — and a club that writes one long word („Sommerfestamdeich…") has
 * a title with no break in it at all, which is the case this rule is for. */
.figure-pill-wide span {
  overflow-wrap: anywhere;
}

/* WHAT IS MISSING IS SAID QUIETLY. It is a caveat about the number above it,
 * not a figure of its own. */
.figure-pill-quiet {
  border-style: dashed;
  background: none;
}

/* THE MOMENT SOMETHING WORKED, OVER THE PAGE IT HAPPENED ON.
 *
 * „Perhaps an overlay, with the background less visible, stating the success
 * with an edit button … So the user has to clearly push past the success
 * overlay to reach any other control." (the operator, 2026-09-22.)
 *
 * A `<dialog open>` RENDERED BY THE SERVER IS NOT A MODAL ONE. `open` gives
 * the element and its semantics; the top layer and `::backdrop` belong to
 * `showModal()`, which needs script — and nothing on this site needs script.
 * So the dim is this element's own background, the card sits inside it, and
 * what makes the page behind unreachable is the `inert` attribute the layout
 * puts on everything else.
 *
 * THE PAGE STAYS RECOGNISABLE THROUGH IT. A solid sheet would be a new page;
 * what the operator asked for is the same page, less visible.
 */
dialog.success,
dialog.table-dialog {
  position: fixed;
  inset: 0;

  /* ABOVE EVERY OTHER FIXED THING, AND 50 WAS NOT. `.nav-progress` is fixed
   * at 50 as well and lives inside `.page`, which the layout renders AFTER
   * this — and a tie in one stacking context is broken by document order, so
   * the progress bar painted over the top of the overlay during the very
   * navigation the overlay's own button starts. */
  z-index: 100;
  display: grid;
  place-items: center;
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  margin: 0;
  padding: var(--space-4);
  border: 0;
  background: color-mix(in srgb, var(--surface) 72%, transparent);
  color: var(--ink);

  /* The dialog scrolls, not the page behind it: a long entry on a short
   * phone has to be reachable, and `inert` has taken the page's scrolling
   * away from the keyboard. */
  overflow: auto;
  overscroll-behavior: contain;
}

.success-card,
.table-dialog-card {
  width: 100%;
  max-width: 28rem;
  border: 1px solid var(--hair);
  border-radius: 12px;
  padding: var(--space-4);

  /* The card is the BAND and the dim is the page's own colour, so the card
   * reads as a thing lying on the page rather than as a hole in it. */
  background: var(--band);
  box-shadow: 0 18px 48px rgb(0 0 0 / 35%);
}

/* THE TABLE'S CARD IS WIDER: seven columns. After the shared rule, which
 * it has to outrank. */
.table-dialog-card {
  max-width: 48rem;
}

.success-heading {
  margin: 0 0 var(--space-3);
  font-size: 20px;
  line-height: 1.25;
}

.success-line {
  margin: 0 0 var(--space-2);
  font-size: 15px;
}

.success-mail {
  color: var(--ink-2);
  font-size: 14px;
}

.success-card .actions {
  margin-top: var(--space-4);
}

/* NOTHING BEHIND IT MOVES. `inert` stops the keyboard and the pointer; this
 * stops the page scrolling under the overlay on a phone, where a tap that
 * lands on the dim would otherwise drag the list about behind it. */
body:has(dialog.success) {
  overflow: hidden;
}

/* A PAST ROW IS A RECORD, so it is quieter than a card: no banner, no link
 * line, and the two things left to do with it at the foot. */
.row-past .row-actions {
  padding-top: var(--space-2);
}

.group-role {
  display: grid;
  gap: var(--space-1);
  min-width: 0;
}

.group-role-choice select {
  width: 100%;
}

@media (min-width: 640px) {
  .group-roles {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: center;
    column-gap: var(--space-4);
  }

  .group-role {
    display: contents;
  }

  .group-role-choice select {
    width: auto;
    min-width: 12rem;
  }
}

/* A radio group as a row of chips. Two of these are the whole of the
 * appearance settings, and at 360 px they wrap rather than shrink: a target
 * under 44 px is a target somebody's thumb misses.
 *
 * fieldset and legend are used for what they are for — the group has a name
 * and a screen reader reads it with each option — with the browser's own
 * border and padding taken off, because they are from 1997. */
/* AN EVEN GRID ON A PHONE, A ROW AT THE DESK.
 *
 * „the round buttons for settings are wrapping and are distributed unevenly on
 * a mobile device.“ (the operator, PKG-G19 §4.) Content-width pills in a
 * wrapping flex row put „Wie das Gerät“ and „Dunkel“ on one line and „Hell“
 * alone on the next, and the five accents came out two, two and one — every
 * break at a width nobody chose, because the words chose it.
 *
 * `auto-fit` with a floor rather than a column count per breakpoint: the count
 * is then a consequence of the room, which is the same thing a breakpoint
 * table says with three numbers that have to be kept true. 8.5rem is the
 * longest of these labels („Wie das Gerät“) plus its radio and padding, so the
 * floor is a measurement and not a guess.
 *
 * A LAST ROW WITH FEWER TILES IS NOT RAGGED. They are still the width of the
 * ones above; what was ragged was pills of five different widths ending
 * wherever their words did. */
.choice {
  border: 0;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: var(--space-2);
}

/* At the desk the pills take their own width again and sit in a row: there is
   room for all of them, and stretching five accents across 1100 px would turn
   a set of choices into a toolbar. „It looks good on Desktop“ is not something
   to spend.

   600 IS THE ONE BOUNDARY. This package first wrote the phone rules as
   `max-width: 599px` against the desk rules' `min-width`, which is two
   numbers, not one: a viewport of 599.5 CSS px — routine under Windows display
   scaling and at most zoom steps — matched NEITHER, so a reader at that width
   got the un-fixed phone layout and the un-fixed desk layout at once. Every
   phone rule in this package is the base now and every desk rule is a
   `min-width: 640px` override — 640 being the width this sheet already calls
   its own — which is also what G-1 says this sheet is. */
@media (min-width: 640px) {
  .choice {
    display: flex;
    flex-wrap: wrap;
  }
}

.choice legend {
  padding: 0;
  margin-bottom: var(--space-2);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
  /* Flex, so the row's icon sits on the words' centre rather than on their
   * baseline — a 16-px square hung off a 13-px baseline sits low enough to
   * look like a mistake. */
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

/* THE SETTINGS ROWS' ICONS.
 *
 * "The settings should have some icons" (the operator, 2026-09-11). Drawn in
 * the same line style as everything else here, and the same colour as the
 * legend they belong to, so they read as part of the label and not as five
 * decorations somebody added to a form. */
.setting-icon {
  width: 16px;
  height: 16px;
  flex: none;
  display: block;
}

h2 .setting-icon {
  width: 18px;
  height: 18px;
  display: inline-block;
  vertical-align: -2px;
  margin-right: var(--space-2);
  color: var(--ink-2);
}

/* THE FLAGS.
 *
 * One height, and the width follows the flag: Germany is 5:3 and the United
 * Kingdom is 2:1, so they are not the same shape and pretending otherwise
 * would mean drawing one of them wrong.
 *
 * A hairline around them because both have white in them, and on the light
 * theme a white edge against a white row has no edge at all. */
.flag {
  height: 12px;
  width: auto;
  flex: none;
  display: block;
  border-radius: 2px;
  box-shadow: 0 0 0 1px var(--hair);
}

.choice em {
  font-style: normal;
  font-weight: 400;
  opacity: 0.75;
}

.choice-option {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: 0 var(--space-3);
  border: 1px solid var(--input-line);
  border-radius: 999px;
  background: var(--input);
  font-size: 15px;
  cursor: pointer;
}

.choice-option input {
  width: 18px;
  height: 18px;
  margin: 0;
  accent-color: var(--accent);
}

/* The chosen one, said twice: the radio is checked and the chip carries the
 * accent. :has() is what makes that possible without a line of script; where
 * it is not supported the radio alone still says it. */
.choice-option:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.choice-option:focus-within {
  border-color: var(--accent);
}

/* The swatch is the accent itself: this label carries data-accent, so
 * var(--accent) inside it resolves to that accent's own value in whichever
 * theme the page is wearing. */
.swatch {
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--accent);
  border: 1px solid var(--hair);
}

/* Hidden from people, visible to a bot filling every field it finds. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.error {
  color: var(--red);
  font-size: 14px;
  margin: 0;
}

.error-summary {
  background: var(--red-soft);
  border: 1px solid var(--red);
  border-radius: 6px;
  padding: 12px 14px;
  margin: 0 0 4px;
}

.error-summary ul {
  margin: 0;
  padding-left: 20px;
}

.hint {
  color: var(--ink-2);
  font-size: 14px;
  margin: 0;
}

/* A CONTROL THAT FOLLOWS A SENTENCE.
 *
 * "The 'Nochmal schicken' button is too close to the above label" (the
 * operator, 2026-09-11). `.hint`, `.error` and `.notice` carry no bottom
 * margin — they are usually the last thing in their block — and a form has
 * none of its own, so a button placed directly after one sits on the words.
 *
 * One step of the scale (G7), on the form rather than on the paragraph,
 * because it is the arrival of a control that needs the air and not the
 * sentence that needs a tail. Three places had it: the resend on a guest's own
 * entry, the open/close switch on an event, and the withdraw on the change
 * page. The operator found one of them. */
.after-text {
  margin-top: var(--space-4);
}

/* ---- the item table ------------------------------------------------- */

/* ONE GRID TEMPLATE FOR ALL FOUR ITEM KINDS — priced, free listed, free
 * counted, and any of them with a note. The kinds differ in what they put in
 * the cells, never in where the cells are, so every row lines up with every
 * other row.
 *
 * It used to be two templates and they disagreed. A counted free item is not
 * `.item-free` (that meant "free and uncounted"), so on wide screens it took
 * the template with no `free` area while `.free` still asked for one — a named
 * area that does not exist auto-places instead of erroring, which is why the
 * badge and the stepper landed on separate lines with nothing aligned.
 *
 * --qty-col is shared by the header row and the item rows so the two cannot
 * drift, and it is wide enough for the stepper: at 140px the buttons overflowed
 * their cell and squeezed the price column. */
.table {
  --qty-col: 152px;
  border: 1px solid var(--hair);
  border-radius: 8px;
  overflow: hidden;
}

.thead {
  display: none;
}

.item {
  display: grid;
  grid-template-columns: 28px 1fr auto;
  grid-template-areas:
    "icon name  price"
    "icon qty   qty";
  column-gap: 12px;
  /* No row gap: the stepper supplies its own, so a row that has no stepper
     collapses to one line instead of carrying an empty second one. */
  row-gap: 0;
  align-items: center;
  padding: 10px 12px;
  border-top: 1px solid var(--hair);
}

.items > .item:first-child {
  border-top: 0;
}

.items > .item:nth-child(even) {
  background: var(--band);
}

.icon {
  grid-area: icon;
  font-size: 20px;
  text-align: center;
  width: 28px;
}

.name {
  grid-area: name;
  font-size: 16px;
  font-weight: 500;
}

.name small {
  display: block;
  font-size: 12px;
  color: var(--ink-2);
  font-weight: 400;
}

/* The price and the "Frei" badge are the same cell: one column, one edge to
   read down. Right-aligned and never wrapped — a shortened price is a wrong
   price. */
.price {
  grid-area: price;
  justify-self: end;
  text-align: right;
  white-space: nowrap;
  font-family: var(--mono);
  font-size: 14px;
}

.qty {
  grid-area: qty;
  justify-self: end;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding-top: 8px;
}

/* A free listed item has no stepper at all; without this its empty cell would
   still push the row taller than its neighbours. */
.qty:empty {
  padding-top: 0;
}

/* 44 px on a phone, because that is a thumb. The wide layout sizes them to
   the row instead — see the min-width block. */
.qty button {
  background: var(--input);
  border: 1px solid var(--input-line);
  border-radius: 6px;
  color: var(--ink);
  width: 44px;
  height: 44px;
  font: inherit;
  font-size: 20px;
  line-height: 1;
  cursor: pointer;
  flex: 0 0 auto;
}

.qty button:hover {
  border-color: var(--accent);
  color: var(--accent);
}

/* The quantity is a real number input, kept visible: the two buttons write to
 * it, so there is one value on the page and one thing to read out. The native
 * spinners come off because the buttons are the 44-px version of them. */
.qty input[type="number"] {
  width: 3.6em;
  min-height: 44px;
  padding: 12px 4px;
  text-align: center;
  font-family: var(--mono);
  font-size: 15px;
  font-variant-numeric: tabular-nums;
  appearance: textfield;
  -moz-appearance: textfield;
}

.qty input[type="number"]::-webkit-outer-spin-button,
.qty input[type="number"]::-webkit-inner-spin-button {
  -webkit-appearance: none;
  margin: 0;
}

.free {
  grid-area: price;
  justify-self: end;
  white-space: nowrap;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--accent);
  background: var(--accent-soft);
  padding: 4px 8px;
  border-radius: 4px;
}

/* ---- the sticky total ----------------------------------------------- */

.sticky {
  position: sticky;
  bottom: 0;
  background: var(--surface);
  border-top: 2px solid var(--ink);
  padding: 14px 0 8px;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 12px;
}

.total {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 14px;
  font-size: 14px;
  color: var(--ink-2);
}

.total strong {
  font-family: var(--mono);
  font-size: 26px;
  color: var(--ink);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
}

/* Two columns where there is room for two. At 320 px „Speichern“ beside
 * „Angebot hinzufügen“ does not fit — measured at two pixels over — and a
 * button squeezed until its label is cut is the operator’s original
 * complaint from G7. Below that width they stack. */
.actions {
  display: grid;
  grid-template-columns: 1fr;
  gap: 12px;
  align-items: center;
}

/* TWO COLUMNS WHERE TWO FIT, AND 360 px IS NOT WHERE THAT IS.
 *
 * This pair went side by side from 360 px up, which gives each of them about
 * 160 px — and „Angebot hinzufügen“ needs 180. So the Angebote editor's footer
 * drew a two-line button beside a one-line one, which is the „uneven button
 * wrapping“ the operator saw on page after page (PKG-G19 §5).
 *
 * THE ROW SHAPE IS 640's, AND THERE IS NO RULE HERE. This block first said 600,
 * and 600 was a third shape nobody asked for: `.actions { display: flex }` in
 * the `min-width: 640px` block below already turns the footer into a row, so a
 * two-column GRID existed only between 600 and 639 — forty pixels wide, on no
 * device, while the comment claimed one boundary. Stacked below 640, a row at
 * 640, which is the width this sheet calls its own. */

.actions-single {
  grid-template-columns: 1fr;
}

/* The primary action is first in the source so that Enter reaches it, and
 * second on the screen because that is where the eye finishes. Two lines of
 * CSS rather than a keyboard handler nobody would find again. */
.actions-save-first > :first-child {
  order: 2;
}

.submit,
.button {
  font: inherit;
  font-weight: 600;
  font-size: 16px;
  background: var(--accent);
  color: var(--on-accent);
  border: 0;
  border-radius: 6px;
  padding: 13px 22px;
  min-height: 46px;
  cursor: pointer;
  text-align: center;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* WHAT EACH LOOK'S STATES ARE MADE OF, named once per LOOK rather than once
 * per control.
 *
 * The four values below were written out in full three times over — `.theme`
 * in the topbar, `.button-quiet` here, `.signin` two thousand lines down —
 * each with a comment saying „the same four as“ the others, which is a
 * stylesheet admitting a copy rather than removing one. The accent set was
 * written twice, `.submit`/`.button` and `.cta`, differing in one line.
 *
 * Order is load-bearing and is why this block sits HERE: `.button-danger`,
 * `.paypal` and `.icon-button` override the base set and follow it, and a
 * quiet button is `button button-quiet`, so the quiet set has to come after
 * the base one. All of it is one rule's distance apart now instead of two
 * thousand lines, which is the only reason that is checkable at all. */
.submit,
.button,
.cta {
  --btn-hover: color-mix(in srgb, var(--accent) 80%, var(--ink));
  --btn-active: color-mix(in srgb, var(--accent) 60%, var(--ink));
  --btn-line: var(--ink-2);
  --btn-ink: var(--on-accent);
}

/* Borderless at rest and it grows none: the one thing the call to action does
   not share with the buttons. */
.cta {
  --btn-line: transparent;
}

/* A LITTLE, because these labels are the ink itself. */
.button-quiet,
.signin,
.theme {
  --btn-hover: color-mix(in srgb, var(--input) 92%, var(--ink));
  --btn-active: color-mix(in srgb, var(--input) 84%, var(--ink));
  --btn-line: var(--ink-2);
  --btn-ink: var(--ink);
}

.button-quiet {
  background: var(--input);
  color: var(--ink);
  border: 1px solid var(--input-line);
}

.button-danger {
  /* AWAY FROM ITS LABEL, NOT TOWARD IT, AND IT KEEPS ITS RED. This variant
     is the odd one: its label is `--red`, not the ink, so mixing the ground
     toward the ink walks it toward the text — measured by the review at
     3.82:1 light and 3.59:1 dark, under the floor the guard beside this
     declares and did not then measure. `--surface` is the page behind it,
     which is the direction that opens the gap in both themes. And the border
     stays red: this is the one control on the site that destroys access, and
     it was dropping its only colour cue at the moment of commitment. */
  --btn-hover: color-mix(in srgb, var(--red-soft) 72%, var(--surface));
  --btn-active: color-mix(in srgb, var(--red-soft) 38%, var(--surface));
  --btn-line: var(--red);
  --btn-ink: var(--red);
  background: var(--red-soft);
  color: var(--red);
  border: 1px solid var(--red);
}

/* A WORD WHERE A BUTTON IS NOT. The same shape as a state pill and not one of
 * them: a state pill says what an account IS, this says why something it
 * cannot do is not offered.
 *
 * It stood in the actions row at first, where the button would have been, and
 * that made it read as a control — a row of things to press, one of which is a
 * word. It sits beside the name now (PKG-G18 §1), INSIDE `.row-title`, and
 * every declaration here is the cost of that move.
 *
 * The title is 600 and `overflow-wrap: anywhere`, and both are inherited, so
 * folded in bare this was the one bold pill on the site and the only one an
 * uppercase German compound could break inside. It is not a flex container
 * either — so the `align-self: center` this rule carried while it stood among
 * the buttons did nothing at all here, and the head row's `gap` never reaches
 * a grandchild: the whole space before it was the one collapsed space Razor
 * emits between the name and the tag. `inline-block` is what the other pills
 * get from being flex items, and it is what makes the padding hold the line
 * open rather than paint over the line above. */
.tag-reason {
  display: inline-block;
  margin-inline-start: var(--space-2);
  font-weight: 400;
  overflow-wrap: normal;
}

/* ---- what a button does when you touch it --------------------------- */

/* "Buttons currently have no hover feedback; they need one." (PKG-G17 §2.)
 * A pointer over any of these changed nothing at all.
 *
 * ONE SET OF STATE RULES, and two custom properties are what make it one:
 * every variant declares the ground it wears when hovered and when pressed,
 * and the three rules below just use them. Nothing is written per page.
 *
 * THE COLOURS ARE MIXED TOWARD `--ink`, AND THE AMOUNTS WERE MEASURED RATHER
 * THAN PICKED. All ten accent/theme pairs, three tries:
 *
 *   toward `--ink` at 12 % — invisible on the light theme's green, which is
 *     already dark: 0.015 of mean channel distance, one part in seventy;
 *   toward `currentColor`, the label, because a label is guaranteed to
 *     contrast with its own ground — always visible, and it moves the ground
 *     TOWARD the label's contrast floor: the light theme's sea fell to 4.26:1
 *     hovered and the dark theme's berry to 4.27:1 pressed;
 *   toward `--ink` at 20 % and 40 % for the accent ground, 8 % and 16 % for
 *     the soft ones — visible everywhere and contrast goes UP on the accent
 *     ground, because the ink is further from the accent than the label is.
 *
 * TWO AMOUNTS, NOT ONE, and that is the third try's own correction. A quiet
 * button's label IS `--ink`, so the same 40 % that is safe on an accent moved
 * that ground onto its own label and measured 4.31:1. A ground is shifted
 * toward the ink by a lot when the ink is not what is written on it, and by a
 * little when it is.
 *
 * NOTHING MOVES. Only `background` and `border-color` change — no padding, no
 * border WIDTH. The press already scales to 0.97 under
 * `prefers-reduced-motion: no-preference`; that is older than this package, it
 * is a paint transform, and it changes no layout.
 *
 * HOVER IS GATED ON A POINTER THAT HOVERS. A touch browser leaves `:hover`
 * stuck on the last thing tapped, so the pressed state — `:active`, which
 * touch does fire — is outside the query and the hover is inside it. */
@media (hover: hover) {
  .submit:hover:not(:disabled),
  .button:hover,
  .button-danger:hover,
  .paypal:hover,
  .cta:hover,
  .signin:hover,
  .theme:hover,
  .icon-button:hover:not(:disabled) {
    background: var(--btn-hover);
    border-color: var(--btn-line);
    color: var(--btn-ink);
  }
}

/* The keyboard gets the same change as the pointer, ON TOP OF the outline the
 * stylesheet already gives every focusable control: on a filled button that
 * outline is an accent ring against an accent ground, which is the one place
 * it reads weakly. */
.submit:focus-visible:not(:disabled),
.button:focus-visible,
.button-danger:focus-visible,
.paypal:focus-visible,
.cta:focus-visible,
.signin:focus-visible,
.theme:focus-visible,
.icon-button:focus-visible {
  background: var(--btn-hover);
  border-color: var(--btn-line);
  color: var(--btn-ink);
}

/* PRESSED, and outside the hover query so a finger gets it too. */
.submit:active:not(:disabled),
.button:active,
.button-danger:active,
.paypal:active,
.cta:active,
.signin:active,
.theme:active,
.icon-button:active:not(:disabled) {
  background: var(--btn-active);
  border-color: var(--btn-line);
  color: var(--btn-ink);
}

.paypal {
  font-weight: 500;
  color: var(--accent);
  /* The same shape as the danger button: an accent label on an accent-soft
     ground, so the ground moves toward the page and away from the words. */
  --btn-hover: color-mix(in srgb, var(--accent-soft) 72%, var(--surface));
  --btn-active: color-mix(in srgb, var(--accent-soft) 38%, var(--surface));
  --btn-line: var(--accent);
  --btn-ink: var(--accent);
  background: var(--accent-soft);
  padding: 12px 14px;
  border-radius: 6px;
  text-decoration: none;
  font-size: 14px;
  min-height: 46px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  text-align: center;
}

.paypal[aria-disabled="true"] {
  opacity: 0.45;
  pointer-events: none;
}

/* ---- lists and tables in the organiser area ------------------------- */

.rows {
  display: grid;
  gap: var(--space-3);
}

.row {
  border: 1px solid var(--hair);
  border-radius: 8px;
  padding: var(--space-4);
  display: grid;
  gap: var(--space-2);
  align-content: start;
}

/* THE TWO LIST PAGES ARE A GRID OF CARDS, and the four others are not.
 *
 * The operator asked for cards on the dashboard; the same shape suits the
 * templates beside it. It is a modifier on the container and not a second set
 * of classes because `.row` is the shape of six pages, and four of them have
 * to stay one per line: the item editors are ordered lists with up and down
 * buttons in them, and a two-column grid of those would be nonsense.
 *
 * The card is filled rather than outlined here, which is the whole visual
 * difference between "a row with a border" and "a card". The grid itself
 * starts at the first breakpoint — at 360 there is room for exactly one. */
.rows-cards .row {
  background: var(--band);
  border-color: transparent;
}

/* A CARD'S HEAD WRAPS, AND ITS TITLE MAY BREAK A WORD.
 *
 * A flex item is `min-width: auto` by default, which means it cannot be made
 * narrower than its own longest word — so a card holding
 * „Jahreshauptversammlung“ beside a „Geschlossen“ chip had a floor of 313 px
 * and grew the whole document to 361 in a 320-px viewport. Measured; the
 * dashboard was the page that did it.
 *
 * Two changes, because either alone leaves a case: the chip drops under the
 * title when there is no room for both, and a single word longer than the
 * card breaks rather than pushing it open. German compounds are why the
 * second one is not theoretical. */
.row-head {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--space-2) var(--space-3);
}

.row-title {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* A ROW THAT LEADS WITH A FACE.
 *
 * "Also render user avatars in the Benutzer tab" (the operator, 2026-09-11).
 * The disc goes first and the name takes the room that is left, so the roles
 * chip keeps its end of the row at every width.
 *
 * Centred rather than on the baseline: a 36-px circle hung off the baseline of
 * a 15-px name sits most of its own height too low. */
.row-head-avatar {
  align-items: center;
}

.row-head-avatar .row-title {
  margin-right: auto;
}

/* The numbers and the state chip on one line at the head's right-hand end,
 * which is where the head already puts whatever comes last. `baseline` rather
 * than `center` so the chip sits on the same line as the words beside it
 * rather than floating in the middle of them. */
.row-corner {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
  justify-content: flex-end;
}

.row-title {
  font-weight: 600;
  font-size: 16px;
}

.rows-cards .row-title {
  font-size: 17px;
}

.row-meta {
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink-2);
}

/* THE FOOT OF A CARD: who made it, and what can be done to it.
 *
 * „the 'Deaktivieren' button hangs awkwardly in the air rather than being
 * anchored to the bottom right corner of the card.“ (the operator,
 * 2026-09-14.) It was a row of its own ABOVE the provenance line, aligned
 * right against nothing — so it floated in the middle of the card with a
 * sentence underneath it.
 *
 * Phone-first: they stack, the sentence first, because an action is full width
 * there. At the desk they share the line, the sentence at the left end and the
 * action hard against the right, which is the corner the operator asked for. */
.row-foot {
  display: grid;
  gap: var(--space-2);
  margin-block-start: var(--space-2);
}

.row-foot .provenance {
  margin: 0;
}

.row-foot .row-actions {
  padding-top: 0;
}

@media (min-width: 640px) {
  .row-foot {
    grid-template-columns: 1fr auto;
    align-items: center;
  }

  /* THE CONTROL IS IN THE SECOND TRACK EVEN WHEN THE FIRST IS EMPTY.
   *
   * „The Deaktivieren button on the origin group (which has no creator/creation
   * date) sits slightly further inward than on the others.“ (the operator on
   * the live G21-a.)
   *
   * Eight pixels, and the eight are this grid's column gap. The club the Groups
   * migration seeded records no author, so `_Provenance` draws nothing and the
   * foot has ONE child: it auto-placed into the `1fr` track, the `auto` track
   * collapsed to zero — and the gap between two tracks is there whether or not
   * either holds anything. So the control right-aligned inside a track that
   * stops one gap short of the card.
   *
   * Pinning it to the second track is what makes „the right edge“ mean the
   * card's right edge in both cases: with a sentence beside it nothing moves,
   * and without one the empty `1fr` takes the whole line and the gap falls
   * where nobody can see it. */
  .row-foot > .row-actions {
    grid-column: 2;
  }
}

/* A COUNT IS ONE VALUE AND BREAKS NOWHERE. „4 Mitglieder, 1 Listen“ beside a
   state pill is a group, and the group may wrap — half of „1 Listen“ may not.
   The corner it sits in still wraps as a whole, which is what puts it under
   the name on a phone. */
.row-counts {
  white-space: nowrap;
}

/* Eight pixels is the floor for two things a thumb has to hit separately,
 * and these wrap rather than shrink. */
.row-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  padding-top: var(--space-1);
}

/* AND ON A PHONE THEY STACK, FULL WIDTH, AGAINST ONE EDGE.
 *
 * „The buttons aren't aligned properly for mobile view.“ (the operator,
 * PKG-G19 §1.) Wrapping content-width buttons put three on a line and the
 * fourth under them at an offset nobody chose — the event page's „QR-Code
 * Bearbeiten Angebote“ with „Drucken“ orphaned below — and `.row-actions-end`
 * sent a lone button to the far side of its card, which is the „sort of
 * floating around“ the operator described.
 *
 * THE SHAPE `.actions` HAS HAD SINCE G8. The form footer was already a grid;
 * this is the same answer for the row that sits inside a card, so the area has
 * one way of laying buttons out instead of two.
 *
 * ONE COLUMN AND NOT TWO, although two would be shorter on a five-button card.
 * „Exactly two“ cannot be asked in CSS here: a `<prompt>` renders its trigger
 * AND its popover as siblings, so a row holding one confirmation has two
 * children and would take the two-column branch. A rule that is wrong on the
 * clubs page is worse than a card that is taller.
 *
 * A form and a `<prompt>` are grid items, so their own control has to take the
 * cell rather than its text; the popover is excluded because it is in the top
 * layer when it is anywhere at all. `.row-actions-icons` keeps its own shape —
 * a grip and two arrows stacked would be a column of squares. */
.row-actions:not(.row-actions-icons) {
  display: grid;
  grid-template-columns: 1fr;
}

/* THE CONTROL INSIDE A FORM, and only that.
 *
 * A grid item already fills its track — which is why `.actions` stacks with no
 * width declaration at all — so the five other selectors this started with were
 * redundant or dead: `details` and `summary` match no element on the site, no
 * `.submit` sits in a row, and every `.row-form` is inside a `<prompt>`. The
 * one control that does NOT fill its track is a button nested in a plain form,
 * which is the Deaktivieren switch on the users page.
 *
 * `> form >` and not a descendant selector: `<prompt>` renders its popover as a
 * SIBLING inside this row, so a descendant selector reached into the open
 * confirmation and stretched its buttons across the panel — defeating
 * `.prompt > form .button { justify-self: start }` two hundred lines down,
 * while the comment here claimed the popover was excluded. Only the first
 * selector ever excluded it. */
.row-actions:not(.row-actions-icons) > form > .button,
.row-actions:not(.row-actions-icons) > form > .submit {
  width: 100%;
}

@media (min-width: 640px) {
  .row-actions:not(.row-actions-icons) {
    display: flex;
  }

  .row-actions:not(.row-actions-icons) > form > .button,
  .row-actions:not(.row-actions-icons) > form > .submit {
    width: auto;
  }
}

/* THE ONE BUTTON ON A ROW GOES WHERE THE EYE ALREADY IS.
 *
 * „The Deaktivieren button should be in the right corner instead of the left.“
 * (the operator, 2026-09-13.) On the clubs page the row is a name on the left,
 * the counts and the state pill on the right, and then one control — which sat
 * under the NAME, diagonally opposite the state it acts on.
 *
 * A modifier and not a change to the rule: a row with several controls reads
 * left to right and keeps doing so, which is the users page. This is for the
 * row that has one. */
.row-actions-end {
  justify-content: flex-end;
}

.row-actions .button,
.row-actions .submit {
  font-size: 14px;
  padding: 9px 14px;
  min-height: 44px;
}

/* A destructive action opens instead of firing: <details> is the confirmation,
 * and it costs no script. The marker goes because the summary is styled as a
 * button and a triangle inside one reads as damage. */
.row-actions details {
  display: inline-block;
}

.row-actions summary {
  list-style: none;
  cursor: pointer;
}

.row-actions summary::-webkit-details-marker {
  display: none;
}

/* A QUESTION ASKED IN A POPUP.
 *
 * "Buttons are aligned horribly. Also, the confirmation should be a popup
 * prompt, not a control expansion." … "Same goes for the rename." (the
 * operator, 2026-09-12.)
 *
 * THE HISTORY IS WORTH THE LINES, because this is the third arrangement.
 * First the confirmation was a `<details>` that opened where it stood, which
 * repacked the wrapping row of buttons around it — press one, and the one you
 * were reaching for next has moved (G-80). Then `order: 1` on the open
 * disclosure, which measuring rejected: taking an item out of the middle of a
 * wrapping row repacks it wherever the open one ends up. Then a stacked column
 * of its own, which stopped the repacking and produced the column the operator
 * called horrible.
 *
 * A POPOVER IS IN THE TOP LAYER, so it is not in the row's flow at all. The
 * buttons share one line again and nothing moves when one is pressed — which
 * is what all three attempts were trying to buy.
 *
 * `::backdrop` dims what is behind it, so the question reads as a question
 * rather than as a card that happens to be on top. */
/* THE PANEL, AND THE ONE DECLARATION THAT KEPT IT ON THE SCREEN.
 *
 * "Going over to 'Gruppen' shows a stuck pop up from the get go." … "Same
 * issue on the 'Benutzer' site." (the operator, 2026-09-12.)
 *
 * TWICE MIS-DIAGNOSED, AND THE CAUSE IS THE LINE BELOW THIS COMMENT.
 *
 * A closed popover is hidden by the UA stylesheet's
 * `[popover]:not(:popover-open) { display: none }`. That rule is in the USER
 * AGENT origin, and ANY author declaration outranks it — which is the same
 * trap as `[hidden]`, written down in this repository in G-32 and walked into
 * again here. `display: grid` on `.prompt` therefore un-hid every panel on the
 * page, and `position: fixed` put each one over the page with no way to
 * dismiss it, because nothing was open and so nothing could be closed.
 *
 * G-86 said the markup was right, and it was. G-91 then measured the served
 * page, found `popover=""` and `popovertarget` correct, and concluded that the
 * operator's browser must not implement the API — an inference, not a
 * measurement, and wrong. Measured properly, in the Chrome this suite drives:
 * `CSS.supports('selector(:popover-open)')` is **true**, the panel reports
 * `:popover-open` **false**, and it still computes to
 * `display: grid; position: fixed` with an `offsetHeight` of 203. The gate
 * G-91 added was a no-op and the defect shipped unchanged.
 *
 * So the fix is inside the gate: say `display: none` for a closed panel in
 * AUTHOR origin, where it beats the rule above rather than losing to it. The
 * declarations here are the panel's look, which both states want, and the
 * ungated `display: grid` is what a browser without the API needs to lay the
 * panel out in the flow. */
.prompt {
  display: grid;
  gap: var(--space-3);
  padding: var(--space-4);
  border: 1px solid var(--hair);
  border-radius: 10px;
  background: var(--band);
  color: var(--ink);
}

/* AND THE MODAL, ONLY WHERE THERE IS A TOP LAYER TO PUT IT IN.
 *
 * `selector(:popover-open)` is true exactly when the browser implements the
 * API, so the two states cannot disagree. With support: closed is hidden by
 * the first rule below — in author origin, which is the whole point — and open
 * is the modal. Without support: neither rule applies, `:popover-open` is not
 * a selector the browser knows, and the panel above stands in the flow, which
 * is what the `<details>` it replaced looked like when open. */
@supports selector(:popover-open) {
  /* THE ONE THAT WAS MISSING. `.prompt:not(:popover-open)` is 0,2,0 against
     the 0,1,0 of `.prompt`, so it wins on specificity as well as on origin —
     and it is inside the gate because a browser without the API would drop it
     as an unknown selector, which is exactly the behaviour wanted there. */
  .prompt:not(:popover-open) {
    display: none;
  }

  .prompt {
    position: fixed;
    inset: 0;
    margin: auto;
    width: min(28rem, calc(100vw - 2 * var(--space-4)));
    max-height: calc(100vh - 2 * var(--space-4));
    overflow: auto;
    padding: var(--space-5);
    border-radius: 12px;
    background: var(--surface);
    box-shadow: 0 12px 40px rgb(0 0 0 / 45%);
  }

  .prompt::backdrop {
    background: rgb(0 0 0 / 45%);
  }

}

/* The form inside gets the same stacking as any other small form, so a prompt
 * with a field in it looks like the rest of the site rather than like a
 * dialog somebody bolted on. */
/* A CONFIRMATION'S ANSWERS SIT AT THE BOTTOM RIGHT.
 *
 * „Both buttons should be anchored towards the right bottom corner (with a
 * slight gap of course), not float around.“ (the operator on the live G20,
 * PKG-G21 §6.) And on the avatar dialog beside it: „If they have an image,
 * there is a button between the two that lets me delete it, and then the
 * alignment makes sense, but it does not format properly whenever they do have
 * an image already. Also, the drag area is smaller than the window and is
 * uneven.“ (§8.)
 *
 * ONE RULE, AND IT HAS TO WORK ACROSS TWO ELEMENTS. The answers are never
 * siblings by nature: the confirm is inside the `<form>` that posts and the
 * cancel is outside it, because it cancels with `popovertarget` and has
 * nothing to post. `display: contents` on the form is what puts them in one
 * formatting context — the form generates no box, so its children become
 * items of `.prompt` alongside the cancel. The form still posts; submission is
 * a DOM relationship, not a layout one, and `PromptTests` drives a real one
 * through the browser because that is the thing this trick is suspected of
 * breaking.
 *
 * A WRAPPING FLEX ROW AND NOT A GRID. The grid this replaces could put two
 * answers side by side and nothing else: a third — the avatar dialog's „Bild
 * entfernen“ — took the second column and pushed the cancel onto a line of its
 * own, which is the stacking G20-a was written to remove, reappearing later in
 * the same package. In a flex row the three place themselves:
 *
 *   • anything that is a whole control rather than an answer — the sentence,
 *     the drop zone, a tick, a field — takes `flex: 1 0 100%` and therefore a
 *     line of its own, full width, which is also what §8 asks for the drop
 *     zone („the drag area is smaller than the window and is uneven“: it is
 *     the dialog's own padding on every side now, because the item is the
 *     content box);
 *   • `justify-content: flex-end` puts whatever is left of the last line
 *     against the right edge of that same content box;
 *   • `order` fixes which is which, because the DOM order is the order the
 *     two forms happen to be written in and not the order they are read.
 *
 * The primary sits last because it is the one being reached for; the cancel
 * before it; and a third, quieter action goes to the far left with an auto
 * margin, so the pair on the right does not move when it appears or goes. */
.prompt {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  align-items: center;
}

/* A FORM IN A DIALOG, AND NOT A FORM WEARING A PARTICULAR CLASS.
 *
 * This whole layout used to be keyed on `.row-form`, and two of the site's
 * eleven dialogs do not carry it: the entry's delete on the event page and the
 * template card's. Without the class the form stayed a box and was ONE flex
 * item, so the two answers of a destructive confirmation laid out worse than
 * before §6 — measured at 1280: „Abbrechen“ alone at the top right and
 * „Endgültig löschen“ at the bottom LEFT, 297 px and 53 px away from it.
 *
 * The condition was never „has this class“; it is „is the form that this
 * dialog's body is“. `> form` says that, and cannot be forgotten by the next
 * caller. Every dialog on the site is the tag helper's own div with the body
 * as its first child, so the selector is exact rather than broad. */
.prompt > form {
  display: contents;
}

/* A LINE OF ITS OWN, FULL WIDTH. `1 0 100%` rather than `width: 100%`: a flex
 * basis of the whole line is what forces the break, and a zero shrink keeps it
 * there when the answers beside it would otherwise pull it up. */
.prompt > form .hint,
.prompt > .hint,
.prompt > p,
.prompt .drop-zone,
.prompt .check,
.prompt .field {
  flex: 1 0 100%;
  min-width: 0;

  /* AHEAD OF EVERY ANSWER, and this is not decoration. `order` is a property
   * of the whole flex container, not of one row of it: the cancel's `-1`
   * below put it in front of the DROP ZONE as well, so the avatar dialog laid
   * out as sentence, cancel, drop zone, save — the cancel on a line of its
   * own, 178 px above the button it belongs beside. Measured, not guessed:
   * the two came back with bottoms of 383 and 561. Everything that is a whole
   * control rather than an answer is pinned before all of them; equal values
   * keep their document order, so several of these stay in the order they are
   * written. */
  order: -10;
}

/* The answers keep the size of their own words — a button stretched across a
 * dialog reads as a banner. */
.prompt > form .button,
.prompt > form .submit,
.prompt-close {
  flex: none;
}

/* THE ORDER THEY ARE READ IN, NOT THE ORDER THEY ARE WRITTEN IN.
 *
 * The cancel is last in the markup because the tag helper appends it after the
 * body; it is second-to-last here. The primary keeps the default 0 and
 * therefore the end of the line.
 *
 * AND THAT MAKES TAB ORDER DISAGREE WITH THE EYE, which is a real cost and is
 * written down rather than left to be re-discovered. In the avatar dialog the
 * markup is save → remove → cancel (the save first so that Enter reaches it,
 * G-68; the cancel last because the tag helper appends it), and the screen
 * reads remove | cancel | save — so Tab goes rightmost, then leftmost, then
 * the middle one. WCAG 2.4.3 is about meaning and operability rather than
 * geometry, and all three are answers to one question with their names on
 * them, so nothing here is unoperable or unclear — but it is a trade and not
 * an oversight, and undoing it means either Enter firing the wrong button or
 * the tag helper inserting its cancel in the middle of a caller's body. It is
 * in the G21 report for the operator to weigh. */
.prompt-close {
  order: -1;
}

/* AND WHERE THREE DO NOT FIT, ALL THREE STACK.
 *
 * Two answers fit across a phone's dialog — about 310 px of content at 390 —
 * and stay side by side there, which is what §6 asks for („the same at phone
 * width until the dialog is narrower than the two buttons side by side“).
 * Three do not: measured, the greedy wrap put „Bild entfernen“ and
 * „Abbrechen“ on one line and left „Bild speichern“ alone underneath, which
 * splits the pair — the one thing the pair must not do.
 *
 * So the dialog that HAS a third answer stacks all of them full width below
 * the site's own breakpoint, which is what `.row-actions` has done at the same
 * 640 since G19. `:has()` rather than a class on the panel: the condition is
 * exactly „this dialog has a third answer“, and the tag helper does not know
 * what a caller's body contains. Phone-first, so the stack is the base. */
.prompt:has(.prompt-aside) > form .button,
.prompt:has(.prompt-aside) > form .submit,
.prompt:has(.prompt-aside) .prompt-close {
  flex: 1 0 100%;
}

@media (min-width: 640px) {
  .prompt:has(.prompt-aside) > form .button,
  .prompt:has(.prompt-aside) > form .submit,
  .prompt:has(.prompt-aside) .prompt-close {
    flex: none;
  }
}

/* A THIRD ACTION, ON THE OTHER SIDE. „Bild entfernen“ is not one of the two
 * answers — it is a way out of the state the dialog is about — so it sits at
 * the far left and the auto margin is what holds the pair on the right exactly
 * where it is in the two-button state. Nothing shifts between the two except
 * this button's presence, which is what §8 asks for. */
.prompt-aside {
  order: -2;
  margin-inline-end: auto;
}
/* A PILL WITH ITS TEXT IN THE MIDDLE OF IT, BOTH WAYS.
 *
 * The operator's screenshot of an Angebot row: „13× bestellt" sitting off
 * centre in its badge.
 *
 * THE VERTICAL IS WHAT THEY SAW, AND IT MEASURED NINE PIXELS. An inline box's
 * padding does not make the line box any taller, so the pill that is painted
 * around the text is placed by the line's metrics rather than around the
 * words — and on a row whose other children are 44 px buttons, that is where
 * the nine came from. A flex box has a height of its own and centres its
 * content in it.
 *
 * THE HORIZONTAL WAS UNDER A PIXEL AND IS CORRECTED ANYWAY. `letter-spacing`
 * applies after the LAST letter too, so the text's box is 0.06em wider than
 * its glyphs and the words sit half a space left of centre — 0.66 px at this
 * size, which the guard's one-pixel tolerance would not have caught either
 * way. The avatar's initials had the same thing at 9 pt where it was visible,
 * and carry the same `text-indent`; it is written here because it is the same
 * bug, not because it is what was photographed. */
.tag {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-indent: 0.06em;
  text-transform: uppercase;
  padding: 3px 7px;
  border-radius: 4px;
  background: var(--band);
  color: var(--ink-2);
  border: 1px solid var(--hair);
}

/* NOT `.tag-reason`, WHICH IS THE SAME ELEMENT AND WANTS THE OTHER BOX.
 * `span class="tag tag-reason"` carries both, and `.tag` is declared 650 lines
 * later at the same weight — so an unqualified `display: inline-flex` here
 * silently won and left `.tag-reason`'s `inline-block`, and the eight lines
 * explaining why it is `inline-block`, describing something that no longer
 * shipped. A reason chip is a run of words that has to hold its line open;
 * this centring is for a pill with two words in it. */
.tag:not(.tag-reason) {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

.tag-open {
  color: var(--accent);
  background: var(--accent-soft);
  border-color: var(--accent);
}

.grid-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 15px;
}

.grid-table th,
.grid-table td {
  text-align: left;
  padding: 10px 8px;
  border-bottom: 1px solid var(--hair);
  vertical-align: top;
}

.grid-table th {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-2);
  background: var(--band);
}

.grid-table .num {
  font-family: var(--mono);
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* WHAT SOMEBODY ORDERED, AS BLOCKS RATHER THAN AS ONE STRING.
 *
 * „those never show up on the sign-ups and shopping list, so it is ambiguous"
 * (the operator, of two Angebote called „Steak"). A joined string has nowhere
 * to put a second line, so each ordered line is its own box and the note sits
 * under its name — and the „·" that used to separate them is the gap now,
 * because a separator between two-line items reads as a third item. */
.line-items {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-4);
}

.line-item {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.line-item-note,
.item-note {
  display: block;
  color: var(--ink-2);
  font-size: 0.85em;
  overflow-wrap: anywhere;
}

/* THE CLUBS A PERSON RUNS, UNDER THEIR NAME.
 *
 * „Assigned groups and roles in those groups should be displayed on a user in
 * the 'Benutzer' tab (take mobile views into account)." (the operator,
 * 2026-09-21.)
 *
 * The same wrapping shape as the ordered lines above, with the corner's gap
 * rather than that one's: `--space-4` separates items that are two lines tall
 * each, and a chip is one line. Its own row under the head rather than inside
 * it, because `.row-head` puts whatever comes last against the far edge —
 * which is where the role and the state pills are, and a club is not one of
 * those.
 *
 * WRAPPING IS THE WHOLE RULE AT 390 px. „Grillfreunde Musterstadt ·
 * Gruppenadmin" is wider than a third of a phone, so three of them are three
 * lines; without the wrap they would be one line as wide as the names are and
 * the card would scroll sideways. Measured in `MemberChipsTests`. */
.member-chips {
  gap: var(--space-1) var(--space-2);
}

/* AN ICON AND THE WORDS BESIDE IT, ON ONE LINE.
 *
 * „The icons and text aren't aligned properly in the shopping list of an
 * event." (the operator, 2026-09-20, on the live G25.)
 *
 * `.item-icon` is an inline-flex box of 22 px and the name is a text node, so
 * the two met on a BASELINE inside a cell whose `vertical-align` is `top`: the
 * icon's box hung from the top of the line and its drawing rode above the
 * middle of the words. Nothing about that is fixed by nudging a margin, because
 * it moves the moment the type does.
 *
 * A flex row instead — the shape `.item` already has on the sign-up page — so
 * the two are centred against each other whatever either of them is. When a
 * note follows the name (G26 §5) the text becomes two lines and the icon
 * centres on the block, which is why the guard measures it against the TEXT
 * rather than against the cell. */
.item-line {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.item-line-text {
  min-width: 0;
  overflow-wrap: anywhere;
}

/* AND THE COLUMNS BESIDE IT SIT ON THE SAME LINE. `grid-table`'s cells are
 * top-aligned for the Protokoll, where every cell is a sentence; a row of this
 * one is a single line and a count that belongs level with it. */
.shopping-grid td {
  vertical-align: middle;
}

.scroll-x {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
}

/* ---- the mascot ----------------------------------------------------- */

/* The sprite's own geometry and timeline are generated: see
 * wwwroot/mascot/grillmeister.css. What is here is the chrome around it. */

/* The box the mascot occupies is the generated stylesheet's, not this one's
 * (DECISIONS G-22): it knows the art's own resolution and picks integer
 * multiples of it. This used to scale it by 0.8 on phones, which was a
 * workaround for a fixed 136-px box and would now land the sprite on a
 * fractional pixel grid. */
/* The bubble hangs outside both of these on purpose, so neither may clip
 * it. `visible` is the default; it is written down because it is now load
 * bearing, and a later `overflow: hidden` here would silently cut the
 * bubble in half instead of breaking something obvious. */
.hdr-mascot {
  flex: 0 0 auto;
  overflow: visible;
}

.mascot {
  overflow: visible;
}

/* The four layers are one picture to a screen reader; this is the element
 * that says so, and it has to fill the box the layers are positioned in. */
.mascot-art {
  position: absolute;
  inset: 0;
}

/* THE BUBBLE NEVER CHANGES THE LAYOUT AND IS NEVER CLIPPED.
 *
 * It was `left: 40px; top: -6px; white-space: nowrap`: a desktop offset
 * and a promise never to wrap. A long word ran off the right of a 360 px
 * phone, the document grew sideways, and the operator got a scrollbar.
 *
 * Absolute, so it takes no space in the flow and the header does not move
 * when he speaks. `left` and `top` are written by mascot.js from the two
 * bounding boxes and the viewport width, clamped so it can never leave the
 * screen; the zeroes here are only what it starts at. The size is the
 * stylesheet's half: as wide as the words up to 12 rem, then it wraps, and
 * a single unbroken 40-character word breaks anywhere rather than pushing
 * the box wider than that. */
.bubble {
  position: absolute;
  /* A picture of somebody talking is not a thing to press. Without this it is
   * a box over the page that swallows the first tap aimed at whatever is
   * under it. */
  pointer-events: none;
  left: 0;
  top: 0;
  width: max-content;
  max-width: 12rem;
  overflow-wrap: anywhere;
  padding: 7px 11px;
  border-radius: 8px;
  font-size: 13px;
  font-weight: 600;
  line-height: 1.35;
  background: var(--bubble);
  color: var(--bubble-ink);
  z-index: 1;
}

/* Declared here rather than exempted below — see the motion vocabulary. */
@media (prefers-reduced-motion: no-preference) {
  .bubble {
    animation: bubble-pop 0.18s ease-out;
  }
}

/* The tail follows the bubble. Which side it points from is whichever
 * side mascot.js found room on, so it is written on the element rather
 * than assumed here. */
.bubble::after {
  content: "";
  position: absolute;
  border: 8px solid transparent;
}

.bubble[data-side="above-right"] {
  transform-origin: left bottom;
}

.bubble[data-side="above-right"]::after {
  left: 12px;
  bottom: -8px;
  border-bottom: 0;
  border-top-color: var(--bubble);
}

.bubble[data-side="above-left"] {
  transform-origin: right bottom;
}

.bubble[data-side="above-left"]::after {
  right: 12px;
  bottom: -8px;
  border-bottom: 0;
  border-top-color: var(--bubble);
}

.bubble[data-side="right"] {
  transform-origin: left center;
}

.bubble[data-side="right"]::after {
  left: -8px;
  top: 50%;
  margin-top: -8px;
  border-left: 0;
  border-right-color: var(--bubble);
}

.bubble[data-side="left"] {
  transform-origin: right center;
}

.bubble[data-side="left"]::after {
  right: -8px;
  top: 50%;
  margin-top: -8px;
  border-right: 0;
  border-left-color: var(--bubble);
}

@keyframes bubble-pop {
  from {
    transform: scale(0.6);
    opacity: 0;
  }

  to {
    transform: scale(1);
    opacity: 1;
  }
}

/* THE PAUSE CONTROL, WHICH IS NOT A CARD.
 *
 * Two monospace letters in a bordered box read as something gone wrong —
 * "there is a 'II' in a card inside the mascot's area for some reason"
 * (the operator, 2026-09-11). It is the control G-22 promised anybody who
 * does not want a looping animation, so it stays; what goes is the box.
 *
 * A round disc, quiet at rest and plain on hover or focus. Not hidden
 * altogether: a control that only exists on hover does not exist on a phone,
 * and this one is for the person most likely to be on one. */
/* THE PAUSE CONTROL, UNDER THE PICTURE AND OUT OF IT.
 *
 * It used to be a 26-px disc in the bottom corner of his own box, and it could
 * not be clicked at all: the generated sheet gives the smoke layer z-index 3
 * and the fire 2 so they draw over the figure, both fill the whole box, and
 * the button sat at 1 underneath them. Measured with
 * `document.elementFromPoint` at the button's own centre, which returned
 * `div.mascot-layer.m-smoke`.
 *
 * Out of the frame it cannot be covered by anything in it, which is a fix by
 * construction rather than one more z-index. And out here it can afford words,
 * which is the other half of what the operator said: a glyph in a circle does
 * not read as a control.
 *
 * Quiet, because it is furniture next to the picture it belongs to — but a
 * real target: 44 px of row on a phone, where quiet is not the same as hard to
 * hit. */
.hdr-mascot {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--space-1);
}

.mascot-pause {
  appearance: none;
  border: 1px solid transparent;
  border-radius: 999px;
  background: none;
  color: var(--ink-2);
  font-family: var(--sans);
  font-size: 12px;
  line-height: 1;
  padding: var(--space-2) var(--space-3);
  min-height: 32px;
  cursor: pointer;
}

@media (prefers-reduced-motion: no-preference) {
  .mascot-pause {
    transition: color 0.15s ease-out, border-color 0.15s ease-out;
  }
}

.mascot-pause:hover,
.mascot-pause:focus-visible {
  color: var(--ink);
  border-color: var(--hair);
}

/* WHO IS SIGNED IN, IN THE TOP BAR (G33 §2).
 *
 * A disc, the name over the role, and the way out, beside the theme switch
 * and as tall as it. It was a bordered card while it shared a row with the
 * links, so that it would not read as three more of them; up here it is
 * apart already, and a box would be a second frame round the switch's
 * neighbour. */
.account {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.account .avatar {
  width: 32px;
  height: 32px;
  font-size: 12px;
}

/* THE WAY OUT: a quiet button the height of the switch, in the tab row's
 * type — furniture, not the page's action. */
.account .button {
  min-height: 44px;
  padding: 0 var(--space-3);
  font-size: 14px;
  font-weight: 500;
}

/* The disc. Fixed size and never squashed: a flexed circle becomes an
 * ellipse, which reads as a rendering fault. */
.avatar {
  flex: 0 0 auto;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.02em;
  /* LETTER-SPACING APPLIES AFTER THE LAST LETTER TOO, so two initials in a
   * centred box sit half a space left of centre — visible at 36 px and the
   * first thing the operator noticed about the disc. The indent puts that
   * half back. */
  text-indent: 0.02em;
  color: var(--on-tint);
  background: var(--tint-0);
}

/* Five discs, picked from the name so a person keeps the same one. They are
 * the site's own greens and ambers rather than a new palette. */
.avatar[data-tint="1"] { background: var(--tint-1); }
.avatar[data-tint="2"] { background: var(--tint-2); }
.avatar[data-tint="3"] { background: var(--tint-3); }
.avatar[data-tint="4"] { background: var(--tint-4); }

/* The picture fills the disc and is cropped to it, whatever shape it came
 * in: the upload keeps the picture as it was sent — nothing here re-encodes
 * it — so a wide photograph is cropped by the browser rather than squashed. */
.avatar img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: 50%;
  display: block;
}

/* The one on the account page, where it is the subject rather than a label. */
.avatar-large {
  width: 72px;
  height: 72px;
  font-size: 24px;
}

.avatar-form {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  flex-wrap: wrap;
}

.avatar-fields {
  display: grid;
  gap: var(--space-3);
  flex: 1 1 260px;
  min-width: 0;
}

.avatar-actions {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
}

/* The browser's own control, in the site's type. Not rebuilt: a file picker
 * reimplemented in CSS is a file picker that stops working with a keyboard,
 * and this one is opened twice a year. */
/* ON A PHONE THE ACCOUNT IS THE WAY OUT. Beside the brand there is room for
 * „Abmelden" and the switch and no more — with the disc as well the bar was
 * two rows at 360 and 375 px. The disc is decoration (`aria-hidden`); the
 * name and the role are still read, by the same rules as
 * `.visually-hidden`, which cannot be put in a media query from the markup.
 * Phone first (G-1): this is the base, and the 640 block below draws them. */
.account .avatar {
  display: none;
}

.account-who {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

@media (min-width: 640px) {
  .account .avatar {
    display: inline-flex;
  }

  .account-who {
    position: static;
    display: flex;
    flex-direction: column;
    width: auto;
    height: auto;
    min-width: 0;
    margin: 0;
    overflow: visible;
    clip-path: none;
    white-space: normal;
    line-height: 1.25;
  }
}

/* A LONG NAME ENDS IN AN ELLIPSIS, and the whole of it is the title: an
 * address stands in for a name nobody has set, and it must not push the
 * switch off the bar. */
.account-name {
  max-width: 12rem;
  font-size: 14px;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.account-role {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.04em;
  color: var(--ink-2);
}


/* ---- the tag editor (mascot words) ---------------------------------- */

.tags {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  padding: 4px 0;
}

.tags li {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: var(--band);
  border: 1px solid var(--hair);
  border-radius: 6px;
  padding: 6px 6px 6px 12px;
  font-family: var(--mono);
  font-size: 14px;
}

.tags button {
  font: inherit;
  font-size: 16px;
  line-height: 1;
  width: 32px;
  height: 32px;
  border: 0;
  border-radius: 4px;
  background: transparent;
  color: var(--ink-2);
  cursor: pointer;
}

.tags button:hover {
  color: var(--red);
  background: var(--red-soft);
}

.tag-add {
  display: flex;
  gap: 10px;
  align-items: stretch;
}

.tag-add input {
  flex: 1 1 auto;
}

ul.tags {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* ---- the QR page ---------------------------------------------------- */

.qr {
  display: grid;
  gap: 14px;
  justify-items: center;
  text-align: center;
  padding: 18px 0;
  /* THE SAME ONE DECLARATION `.row-title` CARRIES, and for the same reason: a
   * title is 120 characters of whatever somebody typed, and German writes a
   * single word of 47 (‚Donaudampfschiffahrtsgesellschaftskapitänsmütze‘).
   * Without it such a word does not wrap, and the sheet the guard measured
   * wanted 409 mm across a page that has 180 — on the screen it is the same
   * word running off the side of a phone. Inherited, so it reaches the title,
   * the date line and the call together; the address below them breaks harder
   * still, for the reason written over `.qr-link`. */
  overflow-wrap: anywhere;
}

/* THE CODE'S OWN PRESENTATION, AND ONLY THE CODE'S.
 *
 * „This looks accidental/bad. The avatar is tiny in a white box and, even on
 * the white backdrop of the print page, has edges.“ (the operator, on the live
 * G21.)
 *
 * The white field, the 12 px of quiet zone and the rounded corners are how a QR
 * code is presented — and `.qr img` was written when the code was the only
 * picture on this page. PKG-G21 §9 put the club's avatar inside `.qr`, so the
 * club's photograph got the code's treatment: `height: auto` broke the square
 * crop, `border-radius: 8px` beat the disc's `50%`, and 12 px of padding inside
 * a 36 px disc left twelve pixels of picture in the middle of a white box. That
 * is the operator's sentence exactly.
 *
 * `> img` is the same scoping the print rule already has — and that is the
 * point: the print rule was corrected for this a package ago and its screen
 * twin, twelve hundred lines away, was not. The code is the only direct child
 * of `.qr` that is a picture; everything else is nested. */
.qr > img {
  width: 100%;
  max-width: 320px;
  height: auto;
  background: #ffffff;
  padding: 12px;
  border-radius: 8px;
}

/* AND `break-all` STAYS ON THE STANDALONE ADDRESS, which is not an oversight.
 *
 * The review asked why §2's ellipsis was not put on the class itself, since the
 * QR page and the issued password draw a `.qr-link` outside any `.copy-line`
 * and still break mid-character. Because those two are the opposite case: they
 * are addresses somebody READS — the QR page is a poster that gets printed, and
 * the password is typed in once by hand — and there is no copy button beside
 * either to hold what an ellipsis would hide. Trimming a value whose only
 * purpose is to be read off the screen removes the page's reason to exist.
 *
 * Measured, not argued: with `nowrap` here the QR page's document went 339 px
 * wide in a 320 px viewport, because a block that cannot wrap sizes its own
 * track. The ellipsis belongs where the value is carried away by a button, and
 * that is `.copy-line`. */
/* The lockup at the head of the sheet. Centred like everything else on it,
 * and a flex row of its own so the mark and the name sit on one baseline the
 * way they do in the bar. */
.qr-brand {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  justify-self: stretch;
  color: var(--ink);
}

/* What to do with the code, for the person standing in front of it. On the
 * screen it is a line of the preview; on paper it is the instruction, which is
 * why it is set heavier there and not here. */
.qr-call {
  margin: 0;
  font-weight: 600;
}

.qr-link {
  font-family: var(--mono);
  font-size: 13px;
  word-break: break-all;
}

/* THE ADDRESS IN A BOX OF ITS OWN.
 *
 * „Place the link into a visibly distinct box which when clicked auto selects
 * the link for manual ctrl+v as a backup to the copy button.“ (the operator,
 * 2026-09-13.)
 *
 * The copy button is the first way; this is the one for when it cannot work —
 * the clipboard API needs a secure context, so an organiser on a plain-http
 * test host gets the fallback, which until now was „select this by hand,
 * carefully, it breaks across lines“.
 *
 * A field's ground and border rather than a new shape: it holds a value
 * somebody takes away, which is what every input on this site looks like. The
 * pointer says it is not text to read past. */
.link-box {
  padding: 6px 10px;
  border: 1px solid var(--input-line);
  border-radius: 6px;
  background: var(--input);
  cursor: pointer;
}

.link-box:hover {
  border-color: var(--ink-2);
}

/* A RING OF ITS OWN, because the site's is scoped to elements. The global
 * focus outline names `a, button, input, select, textarea, summary`; this box
 * is a span made focusable by script, so it matched none of them and was the
 * one control on the site a keyboard reached with nothing to show for it. */
.link-box:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-color: var(--ink-2);
}

/* THE LINK LINE AND ITS COPY BUTTON.
 *
 * "There should be a copy icon button at the end of the link line of an event
 * entry to copy to clipboard" (the operator, 2026-09-11). An organiser reads
 * this address off a screen and sends it to a group chat.
 *
 * A row rather than a block, so the button sits at the END of the line the
 * operator described and not under it. On a card the address is allowed to
 * run out of room — the button copies the whole thing from its own attribute,
 * so what is trimmed here is only what the eye sees.
 *
 * It wraps so the label can take its own line on a phone; nothing else in here
 * is allowed to wrap, which is what the address's `nowrap` below is for. */
.copy-line {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
}

.copy-label {
  flex: none;
}

/* AN ELLIPSIS AFTER ALL, AND WHAT MAKES IT SAFE.
 *
 * „The event link box turns into a large cube. It should sit in its own line
 * with the label above it and copy icon button at the end (text truncated if
 * necessary).“ (the operator, PKG-G19 §2.)
 *
 * G-92 chose the whole address over an ellipsis, because with one card per row
 * there was width for it and half an address is no address. On a 390-px screen
 * there is not: `break-all` turned a sixty-character URL into three stacked
 * rows of characters inside a bordered box, 71 px tall. That is the cube.
 *
 * WHAT HIDING PART OF A VALUE OWES. All of it stays reachable three ways, two
 * of which work on a touch screen: the button copies it, clicking the box
 * selects it, and `title` carries it (copy.js). The rendered text was never
 * what gets copied — `data-copy` has been the source since this partial was
 * written, exactly so a CSS ellipsis could never trim the clipboard. */
.copy-line .qr-link {
  /* `1 1 0` and not `1 1 auto`: with `auto` the basis is the address's own
     width, which on a phone is wider than the line — so the box took the whole
     row and pushed the copy button onto the next one, which is not „at the
     end of the line“. A zero basis lets it shrink to whatever the button
     leaves. */
  flex: 1 1 0;
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* 32 px of drawn button, and 44 of target where a thumb has to hit it — see
 * the phone rule above. This comment used to claim the 44 was already here
 * „because this is used on a phone“; 44 was the ROW's height, which is padding
 * around a 32-px button and not a bigger one to press. The same sentence was
 * wrong about the reorder icons, and PKG-G19 §1 and §2 measured both. */
.copy {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--hair);
  border-radius: 6px;
  background: var(--surface);
  color: var(--ink-2);
  cursor: pointer;
}

/* THE LABEL TAKES THE LINE ABOVE ON A PHONE — „in its own line with the label
   above it“ — which is what gives the address the whole width to be trimmed
   from rather than half of it, and the button the 44 px its own comment
   promised. After `.copy` rather than before it: both are one class, so the
   later rule is the one that wins. At the desk the label is beside it and
   there is room for both; „it looks good on Desktop“ is not something to
   spend. */
/* The label takes the line above it on a phone — „in its own line with the
   label above it“ — which gives the address the whole width to be trimmed from
   rather than half of it. Phone-first, with the desk as the override: G-1's
   rule, and this package wrote three `max-width` blocks against it. */
.copy-label {
  flex: 1 0 100%;
}

@media (min-width: 640px) {
  .copy-label {
    flex: none;
  }
}

/* The same pointer question as the reorder icons. */
@media (pointer: coarse) {
  .copy {
    width: 44px;
    height: 44px;
  }
}

.copy:hover,
.copy:focus-visible {
  color: var(--ink);
  border-color: var(--accent);
}

.copy-icon {
  width: 16px;
  height: 16px;
  display: block;
}

/* WHAT THE BUTTON LOOKS LIKE WHILE IT IS SAYING SOMETHING.
 *
 * A review said the icon was not hiding and the button was overflowing. Half
 * right, and the half that was wrong is worth writing down, because the first
 * fix for it would have done nothing at all.
 *
 * MEASURED IN THE BROWSER: after a copy the icon reports no `hidden` attribute
 * (`attr:false`), `display:block`, and a width of NOUGHT; the word beside it is
 * 43 px wide inside a 32 px button. Two facts in that.
 *
 * First, `el.hidden = true` on an SVG element sets a property and no attribute.
 * `hidden` is reflected by HTMLElement, and an `<svg>` is not one — so no
 * `[hidden]` rule, of any specificity, was ever going to hide it. It collapsed
 * to nothing by flex, which is luck rather than design. So the state is a class
 * on the button now, and nothing here depends on `hidden` reflecting anywhere.
 *
 * Second, the overflow is real and it is the WORD, not the icon: a fixed 32-px
 * button cannot hold 43 px of "Kopiert". While it is speaking it sizes to what
 * it is saying. */
.copy .copy-said {
  display: none;
}

.copy.copy-saying .copy-icon {
  display: none;
}

.copy.copy-saying .copy-said {
  display: inline;
}

.copy.copy-saying {
  width: auto;
  padding: 0 var(--space-2);
}

/* What it says for its two seconds. Sized so the button does not change width
 * when the icon is swapped for the word, which would shove the line about. */
.copy-said {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.02em;
  color: var(--accent);
}

/* ---- the mark ---------------------------------------------------------- */

/* For text that belongs to a screen reader and not to the eye: the page's
 * h1 is the name, and the name is also drawn in the mark above it. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* The header's mark. Sized in `em` off the topbar's own font size so it
 * keeps step with the rest of the row rather than being a fixed picture. */
/* THE LOCKUP IS ONE LINE.
 *
 * `align-items: baseline` with the mark opted out of it by `align-self:
 * center` — two rules disagreeing about the same row, which is what the
 * operator was looking at.
 *
 * Centred, and not on a typographic baseline, because neither of these is
 * type: both are outlines with a descender in them (the `g` in each), so a
 * true baseline sits the monogram low by the depth of its own hook. Their
 * optical centres are what makes them read as one line, and that is what the
 * guard measures. */
.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  text-decoration: none;
  color: var(--ink);
}

.wordmark {
  height: 20px;
  width: auto;
  display: block;
}

/* THE MONOGRAM, IN FRONT OF THE NAME.
 *
 * "There is still no brand logo on the landing page / dashboard" (the
 * operator, on the live G8). The wordmark is the name set in the site's own
 * Archivo and outlined; a person does not read lettering as a logo, and they
 * are right not to.
 *
 * Sized in `em` against whatever the wordmark beside it is set to, so the two
 * stay in proportion at every breakpoint without a second set of numbers to
 * keep in step. The glyphs are `currentColor` like the wordmark; the dot takes
 * the accent, which is the one part of the mark the person chose themselves.
 *
 * The baseline alignment of `.brand` would hang it off the wordmark's
 * baseline, but the monogram has a descender of its own — `align-self` puts it
 * back on the optical centre. */
.mark {
  height: 1.6em;
  width: auto;
  display: block;
  flex: none;
}

/* THE DOT IS THE ONE COLOURED THING ON A PRINTED SHEET, and it reads its own
 * token rather than `--accent` so that it can stay that way. `@media print`
 * gives `--accent` a paper value — dark enough for „GRATIS" and a badge border
 * to survive a photocopier — and there is nowhere to read the event's accent
 * back from once that has happened, because the accent is chosen per event.
 * `--mark-dot` is declared beside `--accent` everywhere `--accent` is
 * declared, which is why it is a mirrored value and not `var(--accent)`:
 * custom properties substitute at USE time, so the derived form would resolve
 * to the paper colour and the dot would print dark orange on every sheet. */
.mark-dot {
  fill: var(--mark-dot);
}

/* The header sets its own, because the anchor has no font size of its own to
 * measure against that would give the right answer. */
.topbar .mark {
  height: 26px;
}

/* THE LOCKUP: THE MARK AND THE NAME ON A LINE, THE DESCRIPTOR UNDER THE NAME.
 *
 * G15 item 2. It used to be a row that wrapped, on the grounds that at 360 px
 * the mark and the wordmark together are wider than the screen — which is
 * true of a wordmark at its natural width and not true of one that is told
 * what width is left. So the wordmark takes what the mark and the gap do not
 * (`max-width`, scaling by its own aspect ratio) and the row never breaks;
 * the mark keeps its size, because it is the smaller of the two and shrinking
 * it buys nothing.
 *
 * That matters beyond the look: the descriptor is indented by the mark's
 * width plus the gap so that it starts under the name rather than under the
 * monogram, and an indent computed from a mark that has wrapped away is an
 * indent into empty space. A test measures the descriptor's left edge against
 * the wordmark's at desktop and at phone widths and fails if they differ. */
.lockup {
  display: grid;
  gap: var(--space-3);

  /* THE MONOGRAM'S BOX, AND WHY IT IS NOT A SQUARE.
   *
   * `_Mark.cshtml` draws on `viewBox="150 104 285 350"`, so the artwork is
   * taller than it is wide — 285 by 350 — and an SVG with a height and no
   * width is exactly that shape. Forcing `width: 52px` on it (which the
   * mock-up did, because its own copy of the mark was square) makes the box
   * a square and `preserveAspectRatio` centres the drawing inside it: the
   * ink then starts 4.83 px in at the small size and 8.54 px in at the
   * large one, so the monogram looked indented from the text below it and
   * the descriptor lined up with an edge that had nothing on it.
   *
   * So the height is the number and the width follows from the artwork.
   * These two live on the lockup rather than on the mark because the
   * descriptor is the mark's sibling and needs the same figure to know how
   * far to indent — one number, used twice, instead of two that have to be
   * kept equal. A mark redrawn at another ratio fails
   * `TheDescriptorStartsWhereTheNameStarts` rather than drifting quietly. */
  --mark-height: 52px;
  --mark-width: calc(var(--mark-height) * 285 / 350);
}

.landing-mark {
  margin: 0;
  line-height: 0;
  display: flex;
  align-items: center;
  gap: var(--space-3);
}

.landing-mark .mark {
  height: var(--mark-height);
  flex: 0 0 auto;
}

.landing-mark .wordmark {
  height: 40px;
  width: auto;
  min-width: 0;
  max-width: calc(100% - var(--mark-width) - var(--space-3));
  display: block;
  color: var(--ink);
}

/* ---- the way in, in the public header ------------------------------- */

/* Small and quiet: this header belongs to the guests, who are almost
 * everyone who sees it, and the control is for the two organisers. It is
 * still a 44-px target on a phone, because quiet is not the same as hard
 * to hit.
 *
 * AND IT IS THE SHAPE OF THE THING NEXT TO IT, since G15 item 4: the same
 * 44 px, the same `--input` ground, the same `--input-line` border, the same
 * 6 px radius as the theme switch. Two controls that do the same kind of job
 * in the same corner read as a pair when they are one shape and as an
 * accident when one has a border and the other does not. The ink goes to
 * `--ink` with the border: the quiet version was a borderless label, and a
 * bordered box in `--ink-2` reads as disabled rather than as quiet.
 *
 * No negative margin pulling it into the theme button. It cancelled the
 * topbar's gap exactly, so the two tap targets met at zero — which is what
 * G14 was about, in the header, on every public page. The instrument's
 * overlap check wants more than a pixel of overlap and so did not see it; a
 * person would. */
.signin {
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.04em;
  color: var(--ink);
  text-decoration: none;
  padding: 0 var(--space-4);
  margin-right: 0;
  border: 1px solid var(--input-line);
  border-radius: 6px;
  background: var(--input);
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}

/* The brand takes the room that is left, so the control keeps its corner
 * at every width instead of being pushed onto a second line. */
.topbar .brand {
  margin-right: auto;
}

/* ---- the landing page ------------------------------------------------ */

/* Phone first, and nothing on it but the lockup, four paragraphs, one button
 * and the mascot. No hero gradient and no illustration behind the text: the
 * page is a notice, and a notice is read from the top. */
.landing {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);

  /* THE PHONE'S PADDING IS THE BASE, and the page ends a step earlier on one:
   * there is nothing under the mascot to separate him from. See the
   * `min-width` block below for the wide value — this was a
   * `max-width: 699px` override until PKG-G21 §5, which left 699.5 px
   * matching neither side. */
  padding: var(--space-6) 0;
}

.landing h1 {
  margin: 0;
}

/* A FULL STEP OF AIR AFTER THE LOCKUP.
 *
 * G15 item 2 asks for `--space-5` BEYOND the landing's own gap, so this is a
 * margin on top of that gap rather than a larger gap: the lockup and the text
 * are further apart than the text and the mascot, which is the point — the
 * name is a heading and everything after it is the page. */
.landing-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: var(--space-6);
  margin-top: var(--space-5);
}

/* 60 characters is where a line stops being comfortable to read. The gap is a
 * full step rather than the old one: the paragraphs are grouped into blocks
 * now and the space between two blocks has to be plainly more than the space
 * between two paragraphs inside one. */
.landing-text {
  display: grid;
  gap: var(--space-5);
  max-width: 60ch;
  font-size: 16px;
  line-height: 1.55;
}

.landing-text p {
  margin: 0;
}

/* THE LEAD. One sentence saying what the site is, at a size that reads as an
 * opening rather than as the first of many.
 *
 * It takes the column's full width, which it already does. `max-width: none`
 * was written here to say "escape the 60ch measure", and a grid child cannot
 * escape its parent's width — `.landing-text` is 480 px wide with the mascot
 * beside it and about 584 px without him, both measured, and the lead is
 * whichever of those its parent is. The declaration did nothing at all, so it
 * is gone rather than left looking as though it did. */
.lead {
  /* 19 px IS THE BASE AND 21 THE WIDE ONE: 21 px over a 320 px column is four
   * words a line. */
  font-size: 19px;
  line-height: 1.4;
  font-weight: 500;
  letter-spacing: -0.005em;
}

/* THE TWO BLOCKS, ONE PER READER.
 *
 * The page answers two different people and used to leave the reader to work
 * out which sentence was theirs. A rule down the left says where one answer
 * starts and the other stops without a heading in the prose — the eyebrow is
 * a label on the margin, not a title over a section.
 *
 * `--accent-soft` at rest and `--accent` under the pointer: enough to notice
 * and not enough to be a control. Nothing here is clickable, so the hover is
 * the only thing that moves and it moves a colour, not a position. */
.block {
  display: grid;
  gap: var(--space-3);
  padding-left: var(--space-4);
  border-left: 2px solid var(--accent-soft);
}

.block:hover {
  border-left-color: var(--accent);
}

.eyebrow {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
}

.landing-data {
  color: var(--ink-2);
  font-size: 15px;
}

/* THE ONE BUTTON ON THE PAGE. Filled, because it is the only thing here
 * anybody clicks, and 40 px rather than 44 because it sits in prose rather
 * than in a bar of controls — the target is the whole label and the padding
 * around it, and it is nowhere near another target.
 *
 * `justify-self: start` so it is the width of its words: a grid child
 * stretches by default and a button as wide as the column reads as a banner. */
.cta {
  justify-self: start;
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-top: var(--space-1);
  padding: 0 var(--space-4);
  height: 40px;
  border-radius: 6px;
  background: var(--accent);
  color: var(--on-accent);
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: 0.04em;
  text-decoration: none;
}

/* The arrow leans into the direction it points, 2 px, under the pointer only.
 * Inside `no-preference` with everything else that moves (see the motion
 * vocabulary above): a transform, nothing else, and nothing that loops.
 *
 * AND INSIDE THE HOVER GATE, which PKG-G18 §3 folded the CTA's GROUND into and
 * left this behind. The ground stopped sticking on a phone and the arrow did
 * not: tapping the landing page's one button left it translated 2 px until
 * something else was tapped. Two hover rules on one control, one gated. */
@media (prefers-reduced-motion: no-preference) {
  .cta svg {
    transition: transform 150ms ease-out;
  }

  @media (hover: hover) {
    .cta:hover svg {
      transform: translateX(2px);
    }
  }
}

/* Under the text on a phone, centred, because a 192 px picture pushed to one
 * side of a 360 px screen reads as a mistake. Beside it from 700 px — see
 * below — where `justify-self` is overridden for the column he stands in. */
.landing-mascot {
  align-self: flex-start;
  justify-self: center;
}

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

/* THE FOOTER, AND THE SITE HAS ONE OF IT.
 *
 * G15 item 5. A hairline, then a row — the copyright on the left, the two
 * legal links on the right — then the maker's line, smaller, under both. It
 * is written once, in `_Layout`, and the organiser area renders inside that
 * layout, so „the same footer on every public page and the organiser area“ is
 * a property of where the markup lives rather than an agreement three files
 * have to keep.
 *
 * The hairline is what the footer gained: it used to be a row of small text
 * that the page simply ran out into, with `margin-top: auto` as the only thing
 * separating it from the content above. */
.foot {
  margin-top: auto;
  border-top: 1px solid var(--hair);
  padding: var(--space-4) 0 var(--space-2);
  display: grid;
  gap: var(--space-2);
  color: var(--ink-2);
  font-size: 12px;
  font-family: var(--mono);
}

.foot-row {
  /* A COLUMN ON A PHONE, which is the base. `column` and not a wrap, so the
   * copyright is above the links at every width narrow enough to break at all
   * — a wrapped flex row with `space-between` puts a single surviving child
   * in the middle, which reads as neither left nor right. */
  display: flex;
  flex-direction: column;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: var(--space-2) var(--space-5);
}

/* THE TWO LINKS SHARE A ROW AT EVERY WIDTH.
 *
 * The row itself breaks on a phone — the links go under the copyright, which
 * is what should happen. What must not happen is the break landing BETWEEN
 * Impressum and Datenschutz, which is what the operator saw in G8, so they
 * are a group that does not wrap and neither of them breaks inside itself.
 * Together they are about 162 px in this 12 px monospace, which fits the
 * 328 px of content a 360 px phone leaves. */
.foot-links {
  display: flex;
  flex-wrap: nowrap;
  gap: var(--space-5);
}

.foot-links a {
  white-space: nowrap;
}

/* THE LINKS LOOK LIKE LINKS WITHOUT BEING POINTED AT.
 *
 * G15 §5 said they inherit the footer's colour and underline on hover, and
 * that is how they shipped. The report of that package raised it as the one
 * decision worth a second look: at rest they were not distinguished from the
 * copyright beside them at all, and on a phone there is no hover to find out
 * with. The operator's answer (G16 §4): „a permanent thin underline in the
 * footer's own colour, on every page“.
 *
 * The colour still inherits, which is the half of §5 that was right: four
 * links in the accent under a page whose one real button is in the accent
 * would make the button look like one of them.
 *
 * `text-decoration` rather than the border this used: a border is drawn at the
 * bottom of the BOX and an underline follows the text, so a link that wraps
 * gets one line under each of its lines rather than one under the whole
 * block. `text-underline-offset` keeps it off the descenders. */
.foot a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.15em;
}

.foot a:hover,
.foot a:focus-visible {
  color: var(--ink);
}

/* The maker's line is the quietest thing on the page and says so twice, with
 * its size and with its opacity. */
.foot-eng {
  font-size: 11px;
  opacity: 0.8;
}

/* ---- the landing page, wider than a phone --------------------------- */

/* THE MASCOT STANDS BESIDE THE TEXT FROM 700 PX.
 *
 * G15 item 3: a 208 px column to the right of the words, top-aligned. 700 and
 * not the site's own 640, because the package says 700 and because that is
 * where it stops costing the text: 208 px plus a 32 px gap out of the 660 px
 * a 700 px viewport leaves is 420 px of prose, which at 17 px is still inside
 * the 60ch measure. At 640 it would be 360 and the paragraphs would break
 * shorter than they read well.
 *
 * `align-items: start` on the grid rather than on him: he is as tall as he is
 * and the text is as long as it is, and a grid whose rows stretch would put
 * him in the middle of a column of empty space. */
@media (min-width: 700px) {
  /* ON THE CLASS, NOT ON THE GRID. The mascot can be turned off in the site
   * settings, and a 208 px track plus a 32 px gap were being held open for him
   * whether he was there or not: measured at 1280 px with him off, the text
   * column was 480 px of an available 720 and the page had 240 px of nothing
   * down its right-hand side — while `Index.cshtml`'s own comment said the
   * text "simply takes the width".
   *
   * The page knows whether it is drawing him, so it says so in the class and
   * this rule asks. `:has(.landing-mascot)` would read better and would be one
   * more thing whose support is assumed rather than measured, which is the
   * mistake that cost two attempts at the confirmation panel. */
  .landing-grid-mascot {
    grid-template-columns: minmax(0, 1fr) 208px;
    align-items: start;
  }

  .landing-mascot {
    justify-self: start;
  }
}

/* AND THE TEXT STEPS UP WITH THE ROOM. One size larger for the lead and the
 * paragraphs once there is a column to put them in, and the page gets its
 * lower margin back — the three phone values are the base, above. */
@media (min-width: 700px) {
  .landing {
    padding-bottom: var(--space-7);
  }

  .lead {
    font-size: 21px;
  }

  .landing-text {
    font-size: 17px;
  }
}

/* And a row once there is width for one. */
@media (min-width: 700px) {
  .foot-row {
    flex-direction: row;
  }
}

/* ---- the organiser area's tab row ----------------------------------- */

/* ONE ROW AT EVERY WIDTH, AND IT NEVER WRAPS (G33 §2).
 *
 * „Konto" alone on a second line was found on 2026-09-12, twice, and again
 * when G31 §6 added an eighth tab; each fix was less padding for another band
 * of widths, and the account card beside the links was what they were short
 * of. The card is in the top bar now (`.account`), and the row does not wrap
 * at all: where it does not fit it scrolls sideways, and `admin-tabs.js` puts
 * the current tab in view.
 *
 * THE HAIRLINE IS A SHADOW, NOT A BORDER. A scroller clips what it holds to
 * its padding box and a border is outside that, so the current tab's
 * underline, drawn over a border, would be cut away. An inset shadow is
 * inside the box and under the tabs, and the underline lies on top of it. */
.nav {
  display: flex;
  gap: var(--space-1);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  box-shadow: inset 0 -1px 0 var(--hair);
}

/* UP TO THE TOP BAR, AS ONE HEADER. The bar's own air under it is for the
 * public pages, where the page starts there; here 12 px between the bar and
 * the row, as the mock has it, and the column's 24 under the row. */
.topbar:has(+ .nav) {
  padding-bottom: 0;
}

.topbar + .nav {
  margin-top: calc(var(--space-3) - var(--space-5));
}

/* THE TABS. Quiet ink, until one is current: that one gets the accent
 * underline and the weight — not a filled box, which every mock dropped.
 *
 * EACH TAB IS AS WIDE AS ITS BOLD SELF. Under the label, at no height, is the
 * same label set bold, so the tab that is current being bold moves none of
 * the others from one page to the next. `/ ""` keeps that copy out of the
 * link's name; a browser that cannot read it drops the declaration and draws
 * no copy at all, which costs a pixel or two of movement and nothing else. */
.nav a {
  flex: none;
  display: inline-flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--space-2);
  font-size: 14px;
  font-weight: 500;
  white-space: nowrap;
  color: var(--ink-2);
  text-decoration: none;
  border-bottom: 2px solid transparent;
}

.nav a::after {
  content: attr(data-label) / "";
  height: 0;
  overflow: hidden;
  visibility: hidden;
  font-weight: 600;
}

/* THE GAP BETWEEN THE GROUPS: the work, the site's running, your account. */
.nav .nav-group-first {
  margin-left: var(--space-3);
}

@media (hover: hover) {
  .nav a:hover {
    color: var(--ink);
    border-bottom-color: var(--input-line);
  }
}

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

/* THE RING INSIDE THE TAB, because outside it the scroller would cut it off. */
.nav a:focus-visible {
  outline-offset: -2px;
}

.notice {
  margin: 14px 0 0;
  padding: 12px 14px;
  border-radius: 6px;
  background: var(--accent-soft);
  border: 1px solid var(--accent);
  font-size: 15px;
}

/* WHAT JUST HAPPENED, AT THE BOTTOM OF THE SCREEN THE READER IS ON.
 *
 * „The notification/confirmation … should be a toast at the bottom“ (the
 * operator, 2026-09-22.) This used to be the first child of the page column,
 * which was the right place for exactly as long as every action ended with a
 * jump to the top of the page. G30 §1 ends those jumps — so the message has
 * to come to the reader instead of waiting where they used to land.
 *
 * FIXED, SO THE PAGE DOES NOT MOVE. Nothing reflows when it arrives and
 * nothing reflows when it goes: a sentence that pushed the list down would
 * move the row the reader is about to press.
 *
 * BOTTOM-CENTRE ON A PHONE, BOTTOM-RIGHT WHERE THERE IS ROOM. On 390 px the
 * full width less the page's own gutters is the only honest choice — a
 * narrower box would either clip a two-line sentence or float in the middle
 * of nothing. */
.toast {
  position: fixed;
  left: var(--space-4);
  right: var(--space-4);
  bottom: var(--space-4);

  /* CLEAR OF THE BOTTOM BAR, WHERE A PAGE HAS ONE. The sign-up form and the
   * guest's own entry page end in a `.sticky` total that sits at the bottom
   * of the viewport while the list scrolls under it; a toast at `bottom: 16px`
   * covers the button somebody is reaching for. Measured at 390 px in the
   * tallest state that bar has — German, a total line, a sponsored line and
   * the PayPal button — and `PhoneToastTests` fails if the toast's top ever
   * comes below the bar's. */
  z-index: 60;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  margin: 0;
  padding: 12px 14px;
  border-radius: 8px;
  border: 1px solid var(--accent);
  background: var(--accent-soft);
  box-shadow: 0 8px 28px rgb(0 0 0 / 28%);
  font-size: 15px;
}

/* WHERE THERE IS ROOM FOR A PAGE BESIDE IT, IT TAKES THE CORNER. 640 is
 * this site's own breakpoint and not a number invented for this (G-1). A box
 * the full width of a 1280 px window for one short sentence reads as a banner
 * across the bottom of the page rather than as something that will go away by
 * itself. */
@media (min-width: 640px) {
  .toast {
    left: auto;
    right: var(--space-5);
    bottom: var(--space-5);
    width: min(28rem, calc(100vw - 2 * var(--space-5)));
  }
}

/* CLEAR OF A BOTTOM BAR WHEREVER THERE IS ONE, at either width — the rule
 * above sets `bottom` too and this one outranks it on specificity, which is
 * deliberate rather than lucky: a bar is a bar on a wide screen as well.
 *
 * 181 px IS MEASURED, NOT ESTIMATED. That is the tallest `.sticky` gets at
 * 390 px: German, an amount to pay so the total line is drawn, and a PayPal
 * handle so a second control stands beside „Eintragen" on a row of its own.
 * The gutter is added to it, which is the same air the toast keeps from every
 * other edge. `ToastTests.OnAPhoneTheToastClearsTheBottomBar` fails the day
 * that bar grows, and it measures the bar's own box rather than repeating
 * this number. */
body:has(.sticky) .toast {
  bottom: calc(181px + var(--space-4));
}

/* TWO LINES AT MOST, AND NEITHER OF THEM CLIPPED. „{0} als bezahlt markiert“
 * with a long name is one line at 1280 and two at 390; a third would be a
 * sentence that wants a page rather than a toast, and the names that produce
 * one are already trimmed where they are stored. Wrapping rather than
 * ellipsis: half a guest's name is worse than a taller box. */
.toast-line {
  margin: 0;
  overflow-wrap: anywhere;
}

/* THE WAY OUT, IN THE CORNER OF THE MESSAGE.
 *
 * „Give notifications a dismiss button.“ (the operator, 2026-09-13.) A link
 * and not a button since G30 §2: without script a button here would be a
 * control that does nothing, and the rule this site keeps is that such a
 * control is not drawn at all. A link has somewhere to go — this same
 * address, which reloads to the same place with the flash already spent. */
.toast-close {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 44px;
  height: 44px;
  margin: -10px -8px -10px 0;
  padding: 0;
  /* Transparent rather than none, so the hover below has something to colour —
   * the arrangement `.nav a` already uses. */
  border: 1px solid transparent;
  border-radius: 6px;
  background: none;
  color: inherit;
  cursor: pointer;
}

.toast-close:hover,
.toast-close:focus-visible {
  border-color: var(--accent);
}

@media (prefers-reduced-motion: no-preference) {
  .toast {
    animation: toast-in 220ms ease-out both;
  }

  .toast-going {
    animation: toast-out 200ms ease-in both;
  }
}

/* AND WITH MOTION TURNED DOWN IT STILL GOES. `toast.js` removes the element
 * outright in that case; this class is only ever on it while the fade runs. */
@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(12px);
  }

  to {
    opacity: 1;
    transform: none;
  }
}

@keyframes toast-out {
  from {
    opacity: 1;
  }

  to {
    opacity: 0;
  }
}

/* ---- wider than a phone --------------------------------------------- */

@media (min-width: 640px) {
  .wordmark {
    height: 22px;
  }

  .topbar .mark {
    height: 28px;
  }

  /* THE LOCKUP AT SIZE. 92 and 72 from 640 px up, 52 and 40 below it, which
     is what the page already did. One number changes: the mark's height. Its
     width and the descriptor's indent are both written in terms of that, so
     they cannot fall out of step with it — which is what three numbers in
     three rules invites. */
  .lockup {
    --mark-height: 92px;
  }

  .landing-mark .wordmark {
    height: 72px;
  }

  .descriptor {
    font-size: 12px;
  }
  .page {
    padding: 26px 20px 10px;
  }

  h1 {
    font-size: 30px;
  }

  /* ONE EVENT PER ROW, AT EVERY WIDTH.
   *
   * "Events should be one row at a time not 2 side by side, that way the
   * invite link doesn't get truncated and the buttons don't have to wrap."
   * (the operator, 2026-09-12.)
   *
   * G11 made these two columns because two cards fit, which is a fact about
   * the grid and not about the card: what is IN one is a full public link and
   * five actions, and neither fits in half of a 1040-px column. Two columns
   * bought a second card per screenful and paid for it with an address that
   * ended in „…“ — on a card whose whole job is to hand somebody that
   * address. The gap stays the wider one; only the count changes. */
  .rows-cards {
    grid-template-columns: 1fr;
    gap: var(--space-4);
  }

  .fields {
    grid-template-columns: 1fr 1fr;
  }

  .field-wide {
    grid-column: 1 / -1;
  }

  .thead {
    display: grid;
    grid-template-columns: 28px 1fr auto var(--qty-col);
    gap: 12px;
    padding: 8px 12px;
    background: var(--band);
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink-2);
  }

  .item {
    grid-template-columns: 28px 1fr auto var(--qty-col);
    grid-template-areas: "icon name price qty";
  }

  /* One row, so the stepper stops carrying the phone layout's top spacing. */
  .qty {
    padding-top: 0;
  }

  /* Sized to the row rather than to a thumb: there is a pointer here, and a
     44-px button is what overflowed the column in the first place. */
  .qty button {
    width: 36px;
    height: 36px;
    font-size: 18px;
  }

  .qty input[type="number"] {
    width: 3.4em;
    min-height: 36px;
    padding: 8px 4px;
  }

  .sticky {
    flex-direction: row;
    justify-content: space-between;
    align-items: center;
    flex-wrap: wrap;
  }

  /* AND THE ROW ENDS AT THE RIGHT.
   *
   * „The 'Neue Gruppe' button should sit on the right side, not the left.“
   * (the operator on the live G21-a.)
   *
   * This was the last place on the site where an answer sat at the left. A
   * card's foot ends at the right (`.row-actions-end`), a dialog's answers end
   * at the right (§6 of this package) — and a form's footer was a flex row
   * with no justification at all, so the pair landed wherever the words ran
   * out and every card had its controls against a different edge.
   *
   * THE SHEET ALREADY SAID SO IN WORDS, twenty lines above: the primary is
   * „second on the screen because that is where the eye finishes“. That was
   * written about the order of the two while the pair as a whole sat at the
   * left — and the eye finishes at the edge of the card, not at the edge of
   * the pair.
   *
   * ONLY FROM 640 UP, because only here is it a row. Below it the footer is a
   * one-column grid and every button takes the whole line, which is what keeps
   * a 390-px card from growing the uneven wrapping of PKG-G19 §5. */
  .actions {
    display: flex;
    gap: 12px;
    justify-content: flex-end;
  }

  /* THE WAY BACK IS NOT ONE OF THE ANSWERS.
   *
   * „The Zurück button sits fine, the Weiter button should be on the opposite
   * side.“ (the operator on the live G21-a.) With the footer ending at the
   * right the two had become a pair in the corner, and going back is not the
   * neighbour of going on — it is the other direction.
   *
   * The auto margin eats the free space before `justify-content` gets any, so
   * one declaration puts this button at the far left AND leaves the primary
   * exactly where every other footer's primary is. It is the same move
   * `.prompt-aside` makes for „Bild entfernen“ in a dialog, for the same
   * reason: what is not an answer does not stand with the answers.
   *
   * ONLY FROM 640 UP. Below it the footer is a one-column grid whose items
   * stretch, and an auto margin there would shrink this one to its words and
   * strand it at the left of a stack of full-width buttons. */
  .actions-aside {
    margin-inline-end: auto;
  }
}

/* A STEPPED FORM'S NAVIGATION IS NOT PART OF THE BODY'S RHYTHM.
 *
 * „…with the angebot hinzufügen button should have a bit of space between it
 * and that row.“ (the operator on the live G21-a.) On step 2 the creation
 * wizard ends with two action rows in a row — „Angebot hinzufügen“, which adds
 * to what is on the page, and then Zurück/Weiter, which leaves it — and at the
 * form's own 24-px rhythm the three buttons read as one cluster of three.
 *
 * So the step row is set off by the next step of the scale: 24 of gap and 24
 * of margin is 48, `--space-7`, which is the distance this sheet uses between
 * things that are not the same thing. Written as a margin and not as a bigger
 * gap, because only the last row is leaving. */
.actions-steps {
  margin-block-start: var(--space-5);
}

/* THE PAGE BOX, AND THE SECOND ONE THIS SITE TURNS OUT TO NEED.
 *
 * A4 portrait with a 15 mm margin, which is what every ordinary page gets when
 * somebody presses Ctrl+P on it — the Datenschutz text, the Impressum, a
 * sign-up page, the audit log.
 *
 * AND ONE MARGINLESS BOX FOR THE TWO SHEETS THIS SITE DESIGNS. The browser
 * draws its own header and footer — the timestamp, the address — inside the
 * page box's margin, so a box with no margin has nowhere to put them. That is
 * how the operator's „Timestamp, 'QR Code - Lecker Grillen' at the top serves
 * no purpose“ and „the list print … contains some unnecessary clutter“ were
 * answered, and the inset those two sheets need is their own padding instead.
 *
 * IT WAS ONE BOX WITH NO MARGIN AT ALL, WHICH TOOK THE INSET OFF EVERY PAGE.
 * `@page` is in the stylesheet the whole site loads, so „no margin“ reached
 * the Datenschutz page too and printed its text flush to all four paper edges,
 * inside the band most printers cannot reach. Two boxes is what the comment
 * that used to stand here called a per-page exception; it is nothing of the
 * kind — it is one box for pages and one for sheets, and only two elements on
 * the site are sheets. */
@page {
  size: A4 portrait;
  margin: 15mm;
}

/* AND THE TWO SHEETS' BOX IS NOT HERE ANY MORE (G31 §8). It was `@page sheet`
 * with no margin, reached through `page: sheet` — a named page, which Safari
 * does not support and Firefox only recently does, so an engine that ignored
 * it printed the sheets in the 15 mm box above: the browser's header and
 * footer on them, and a second, empty poster page. Each sheet's own
 * stylesheet now carries a plain `@page { margin: 0 }` — `print.css` for the
 * list, `qr-print.css` for the poster — which every engine applies, because
 * only that page loads it. This box stays for every other page: the legal
 * pages print with their inset, and `PageBoxTests` holds it. */

@media print {
  /* ---- PAPER IS WHITE AND INK IS BLACK, WHATEVER THE SCREEN WAS ----------
   *
   * „the sheet's hairlines use theme variables the print block never
   * redefines, so with the dark theme active they print near-white on white"
   * (G23's report, and G-102).
   *
   * THE DARK THEME IS THE DEFAULT. A bare `:root` is the dark one — `--ink:
   * #e7edf3` — and `prefers-color-scheme: light` is what switches it, so an
   * organiser who never touched the setting prints nearly-white text unless
   * their system says otherwise. The failure runs BOTH ways: in the dark theme
   * `--ink` is near-white and the text goes, and in the light one `--hair` is
   * `#e2e8f0` and the rules between the sign-ups go instead. One is a
   * legibility bug and the other is a sheet with no structure, and neither is
   * visible to anybody testing in the theme that happens to work.
   *
   * REDEFINED ONCE, HERE, RATHER THAN PATCHED PER RULE. The sheet already had
   * one hairline set by hand — `.person`'s `#cccccc` — which is how this class
   * of bug looks while it is being fixed one rule at a time: the one somebody
   * noticed is right and the five they did not are still on variables. Every
   * rule that reads a colour keeps reading its variable; the variables mean
   * something else on paper.
   *
   * `:root:root` IS A SPECIFICITY BUMP AND NOTHING ELSE, and it is deliberate.
   * The themes are written at (0,2,0) — `:root[data-theme="light"]` for the
   * chosen one and `:root:not([data-theme="dark"])` inside a
   * `prefers-color-scheme` query for the system one — so a plain `:root` here
   * loses to both however late it appears. Measured, before the second colon:
   * the row rules still printed `#e2e8f0` for anybody whose machine says
   * light. Naming the theme selectors instead would work until somebody adds
   * a third shape; asking for the same weight and winning on order does not
   * depend on knowing what they are.
   *
   * A TOKEN AND ITS GROUND ARE ONE DECISION, AND THE FIRST VERSION OF THIS
   * BLOCK TOOK HALF OF IT. Six tokens were redefined here — the inks and the
   * page grounds — and the grounds those inks are actually painted on were
   * left on the screen theme, which is the same class of bug as the one above
   * and was introduced by fixing it. Measured on `/e/{token}` in the default
   * dark theme, before this paragraph existed:
   *
   *   `.avatar`         9.80:1 -> 2.14:1   black initials on `--tint-3`
   *   `.banner`        12.21:1 -> 1.46:1   black on `--accent-soft`
   *   `.deadline-line` 12.21:1 -> 1.46:1   the same
   *   `input`          14.71:1 -> 1.21:1   black on `--input`
   *
   * — while the thing this block exists for, the body text, went 1.18:1 to
   * 21:1. Right direction, half a palette. So the pairs are here together:
   * every ground an ink in this block lands on is white, and `--on-accent`
   * and `--bubble-ink` are ink because their fills are gone. That also fixes
   * the case nobody had looked at, which is the DEFAULT one: Chrome prints
   * with „Background graphics“ unticked, so a white-on-tint disc or a
   * white-on-accent button label lost its fill and printed white on white.
   *
   * `--accent` IS HERE SINCE PKG-G25 §5, AND IT IS THE CLUB'S SINCE G26.
   *
   * G24 left it out because the mark's dot reads it, the dot is the one
   * coloured thing on a printed sheet (§9 of PKG-G21), and the accent is
   * chosen per event — so there was nowhere to read it back from once this
   * block had overwritten it. The cost was recorded rather than paid: „GRATIS",
   * the „OFFEN" chip and the border round a banner printed at about 2.4:1 in
   * the dark theme and about 7:1 in the light one.
   *
   * `--mark-dot` is that somewhere. It is declared beside `--accent` in all
   * eighteen places `--accent` is declared, and mirrored rather than derived:
   * a custom property substitutes at USE time, so `--mark-dot: var(--accent)`
   * would resolve to the paper value below and every poster would print the
   * same dark orange.
   *
   * AND IT IS ONE COLOUR PER CLUB RATHER THAN ONE FOR EVERYBODY. G25 shipped a
   * single `#9a3412` and recorded the trade: „a club whose icon set is green
   * prints this red-brown". `--accent-paper` is declared beside `--accent` in
   * all eighteen places, and it is not a new palette — it is the LIGHT theme's
   * value for that accent, which is that accent drawn for a white background,
   * chosen for one. Paper is a white background.
   *
   * Every one of the five clears the 3:1 the non-text floor asks of it, which
   * `PrintColourTests` measures per accent rather than taking on trust. */
  :root:root {
    --ink: #000000;
    --ink-2: #333333;

    /* A RULE, NOT TEXT, so it is not held to the text floor — but WCAG 1.4.11
     * asks 3:1 of a non-text thing that carries meaning, and a row separator
     * does. `#999999` was here first and measures 2.849:1, which is under it;
     * this is 3.03:1. The floor in the guard is the standard's 3, not a number
     * picked to admit whatever grey was already shipped. */
    --hair: #949494;

    /* THE PAPER IS WHITE AND NOTHING ELSE IS FILLED, which is one rule rather
     * than two and is the one `QrSheetTests` already held the club's disc to:
     * „a printer is not asked to fill it". A white fill and no fill look the
     * same on white paper, but they are not the same request — and asking is
     * what makes the output depend on „Background graphics", which is the
     * switch that made a white-on-tint disc vanish in the first place. So the
     * page ground is white and every ground ON it is transparent. */
    --surface: #ffffff;
    --band: transparent;
    --input: transparent;
    --input-line: #949494;
    --accent-soft: transparent;
    --red-soft: transparent;
    --bubble: transparent;
    --tint-0: transparent;
    --tint-1: transparent;
    --tint-2: transparent;
    --tint-3: transparent;
    --tint-4: transparent;

    /* AND THE INKS THAT GO WITH THEM. `--on-tint`, `--on-accent` and
     * `--bubble-ink` are white or near-white on screen because they sit on a
     * filled shape; the shape is gone, so they are ink. */
    --on-tint: #000000;
    --on-accent: #000000;
    --bubble-ink: #000000;

    /* `--red` IS TEXT ONLY — no rule in either stylesheet fills with it — so
     * it can simply be the dark red the light theme already uses. At `#f2777a`
     * a failed sign-up printed its reason at 2.73:1. */
    --red: #b91c1c;

    /* AND THE ACCENT, WHICH G24 LEFT OUT AND SAID SO.
     *
     * It was left out because the mark's dot reads it and the dot is the one
     * coloured thing on a printed sheet (§9 of PKG-G21) — and because the
     * accent is chosen per event, there was nowhere to read it back from once
     * this block had overwritten it. `--mark-dot` is that somewhere now, so
     * the cost G24 recorded is paid: „GRATIS" on a printed sign-up page, the
     * „OFFEN" chip on an organiser's card and the border round a banner were
     * printing at about 2.4:1 in the dark theme, which is a colour a
     * photocopier does not keep.
     *
     * ONE PAPER ACCENT PER CLUB SINCE G26 §3. This said the opposite until
     * then — one colour for everybody, with the eighteen declarations named as
     * the alternative not taken — and the eighteen are there now: see
     * `--accent-paper` at the top of this block and beside every `--accent` in
     * the palette. The dot keeps `--mark-dot`, which is the SCREEN accent and
     * a different thing again. */
    --accent: var(--accent-paper);

    /* AND THE WIDGETS THE BROWSER DRAWS ITSELF. `color-scheme` sits in the
     * same `:root` block as the six tokens this started with and was the
     * seventh declaration in it; without this line a checkbox, a radio and a
     * select keep their dark-mode livery on a white sheet. */
    color-scheme: light;
  }

  /* `.crumbs` is here because the QR page is the one page in this area whose
   * output is a printed sheet, and its header, its action row and its hint all
   * carry `no-print` for that reason. The trail replaced a kicker that lived
   * INSIDE that header, so it inherited nothing and „Veranstaltungen ›
   * Sommergrillen › QR-Code“ was printed across the top of the poster. */
  /* `.admin-bar` IS THE ONE THAT WAS MISSING, and it cost a page. The
   * organiser area's own navigation bar comes from `_AdminLayout`, which the
   * QR page uses and the printable list does not — so a hide-list assembled
   * while looking at the list had no reason to name it, and the poster
   * printed 27 mm of navigation above itself and spilled onto a second
   * sheet. Measured by printing the page to PDF and counting the pages,
   * which is the only thing that sees it: `@page` is not applied by print
   * MEDIA emulation, so every layout guard here reported one page while
   * Chromium produced two. Since G33 §2 there is no `.admin-bar`: the tabs
   * are `.nav` alone, and the account card is `.account`, inside `.topbar`. */
  /* `.toast` IS THE NEXT ONE THAT WAS MISSING, one element along from
   * `.admin-bar`. The QR page's own „Mit Logo“ button sets a flash and
   * redirects — it is the only other control on the page — so the obvious
   * sequence is press the toggle, then press print, and the poster came out
   * with an accent-bordered banner reading „QR-Code mit Monogramm“ above the
   * brand, on a sheet this package measured as one page with millimetres to
   * spare. A hide-list assembled by looking at a page is a list of what
   * happened to be on it. */
  .topbar,
  .nav,
  .crumbs,
  .foot,
  .toast,
  .row-actions,
  .no-print {
    display: none !important;
  }

  /* NOTHING IS EXACTLY ONE PAGE TALL. A root that is the height of the page
   * plus any fraction of a millimetre is the classic trailing blank sheet,
   * and this site had it: `min-height: 100vh` on the page column resolves
   * against the page box when printing, so the document came out 297.1 mm on
   * a 297 mm page and Chromium started a second one. Measured by counting the
   * pages of a real PDF — `@page` is not applied by print MEDIA emulation, so
   * every layout guard reported one page while the printer produced two. */
  html,
  body {
    height: auto;
    min-height: 0;
  }

  /* AND THE COLUMN THEY HOLD, which is where the 100vh actually is. The page
   * column is screen furniture: a 760 px measure, 14 px of side padding, a gap
   * between blocks and a minimum of one screen. On paper the inset is the
   * sheet's own and the column is not a screen tall — left in, it made the
   * document exactly the height of the page plus a tenth of a millimetre, and
   * Chromium answered that with a blank second sheet. */
  .page {
    display: block;
    max-width: none;
    min-height: 0;
    padding: 0;
    gap: 0;
  }

  /* The page ground and the page's ink, from the block above — spelled as the
   * variables so that changing what ink means changes this too. They were
   * `#ffffff` and `#000000` written out, which is the per-rule patching this
   * block replaces, left standing in the file that replaces it. */
  body {
    background: var(--surface);
    color: var(--ink);
  }

  /* ---- the QR poster on A4 -------------------------------------------
   *
   * REDESIGNED after the operator printed one and sent the sheets back:
   * „it is split on two pages … the logo isn’t centered between the lines and
   * shouldn’t have lines to begin with … the logged in user is printed with a
   * log out button to boot … a huge gap between the event header and the QR
   * code which also causes the printed link backup to be shifted to a second
   * page … Fix it. Re-design it properly.“ (2026-09-14.)
   *
   * Every one of those was a thing this stylesheet did:
   *
   * • THE ACCOUNT CARD was `.admin-bar`, which the hide-list did not name
   *   because the list it was written against does not draw one. 27 mm of
   *   navigation, printed above the poster and pushing it over the edge.
   * • THE TIMESTAMP AND THE URL are the browser’s own header and footer. They
   *   are drawn in the page box’s MARGIN, so a page box with no margin has
   *   nowhere to put them — see `@page`. The sheet carries its own inset now.
   * • THE LINES round the lockup were a rule of this sheet’s own and the
   *   account bar’s border under it. Both gone.
   * • THE HUGE GAP was the code growing to fill a sheet pinned to the height
   *   of the page. The sheet is its natural height now and the code is a
   *   fixed 100 mm, which is what put the call and the link on sheet two.
   *
   * The whole thing is a block flow with margins, like the printable list
   * beside it. Nothing here is measured against the page, so nothing here can
   * be a millimetre too tall for it. */
  .qr {
    display: block;
    height: auto;
    padding: 16mm 15mm;
    gap: 0;
    text-align: center;
    color: var(--ink);
  }

  /* Centred, and without the rule it used to carry. */
  .qr-brand {
    display: flex;
    justify-content: center;
    align-items: center;
    gap: 3mm;
    border: 0;
    padding: 0;
    margin: 0;
    color: var(--ink);
  }

  .qr .mark {
    height: 11mm;
  }

  .qr .wordmark {
    height: 8mm;
  }

  /* THE DOT KEEPS THE ACCENT, AND THE PRINTER IS TOLD SO.
   *
   * „the mark's dot prints in the accent while the rest stays ink“ (PKG-G21
   * §9). It used to be forced to black here, on the argument that a printer's
   * guess at a screen colour is a grey dot in the middle of the name — which
   * is what happens when the browser is left to decide whether a colour is
   * worth ink. `print-color-adjust: exact` is how a document says it is: the
   * one coloured thing on an otherwise black sheet is the mark's dot, and it
   * is the mark. Everything else on the poster stays ink. */
  .qr .mark {
    print-color-adjust: exact;
    -webkit-print-color-adjust: exact;
  }

  .qr .mark-dot {
    fill: var(--mark-dot);
  }

  /* THE CLUB'S LOCKUP, sized for the sheet rather than for a line of body
   * text. Same partial and same sentence as the sign-up page; on paper it
   * sits under the brand and over the title, which is the order somebody
   * reads a poster in: whose site, whose club, which barbecue. */
  /* `.qr .qr-host` AND NOT `.qr-host`, WHICH NEVER APPLIED AT ALL. The
   * element is `class="event-host qr-host"`, and `.event-host` is one class
   * too — same specificity — defined 860 lines further down the sheet, so it
   * won on every property the two share whatever medium was being drawn.
   * Measured on the rendered sheet: the club line printed at 14 px with a
   * 12 px top margin, which are the screen's values, not the 12 pt and 6 mm
   * written here. `@media print` adds nothing to specificity; only the
   * selector does. */
  .qr .qr-host {
    justify-content: center;
    margin: 6mm 0 0;
    font-size: 12pt;

    /* `--ink` AND NOT `--ink-2`, which is what this line reads on screen: the
     * host line is small, it is 12 pt on paper, and the sheet is read across a
     * room. A deliberate darkening, not a colour patch. */
    color: var(--ink);
  }

  /* AND THE DISC IS DRAWN IN INK, because a printer is not asked to fill it.
   *
   * A club with no uploaded picture gets initials in `--on-tint` — very nearly
   * white — on a tinted disc, and Chrome's print dialog has „Background
   * graphics“ unticked by default: the disc goes and the near-white letters
   * stay, so the poster prints a 9 mm hole beside „Eine Liste von …“. The dot
   * in the mark is the one coloured thing on this sheet (§9) and it says so
   * with `print-color-adjust`; everything else, this included, is ink. */
  .qr .qr-host .avatar {
    width: 9mm;
    height: 9mm;
    font-size: 9pt;
  }

  /* THE OUTLINE IS FOR THE LETTERS AND NOT FOR THE PHOTOGRAPH.
   *
   * „even on the white backdrop of the print page, has edges“ (the operator).
   * It was on every disc, which put a ring round a club's picture for no
   * reason — a photograph is already a shape and does not need a box drawn
   * round it. Initials do: two black letters with no fill and no edge on a
   * white sheet read as stray type rather than as a mark. So the outline is
   * asked for exactly where the fill was taken away and nothing replaced it. */
  .qr .qr-host .avatar:not(:has(img)) {
    border: 0.3mm solid var(--ink);
  }

  .qr h2 {
    /* 22 pt AND NOT 26, because the sheet has to hold the worst title the
     * fields allow: 120 characters, which is six lines at 180 mm of measure,
     * on top of a place of 160. At 26 pt that worst case was a second sheet —
     * measured by printing it, not by adding the rules up. */
    margin: 14mm 0 0;
    font-size: 22pt;
    line-height: 1.1;
    letter-spacing: -0.01em;
  }

  .qr .row-meta {
    margin: 4mm 0 0;
    font-size: 12pt;

    /* Darkened from `--ink-2` for the same reason as the host line. */
    color: var(--ink);
  }

  /* THE CODE, AT A SIZE THAT IS READ FROM ACROSS A ROOM — and at ONE size.
   * It used to take whatever height the sheet had left, which is what drew
   * the „huge gap“ the operator saw and what pushed the two lines under it
   * onto a second sheet. 100 mm of picture is about 80 mm of code once the
   * quiet zone the standard asks for is taken off it, which a phone reads
   * from several metres. */
  /* `.qr > img` AND NOT `.qr img`: THE SHEET HAS TWO PICTURES ON IT NOW.
   * §9 put the club's lockup inside `.qr`, and a club with an uploaded avatar
   * draws an `<img>` in it — which then took the code's geometry, including a
   * 12 mm top margin inside a 9 mm disc, and printed the photograph floating
   * below its own circle. The code is the only direct child that is a picture;
   * everything else is nested. Invisible to the suite, because every seeded
   * event belongs to the default club and the default club has no photo. */
  .qr > img {
    display: block;
    width: 100mm;

    /* AND THE SCREEN'S CAP IS LIFTED. `.qr img` carries `max-width: 320px`
     * for the preview, which is 84.7 mm — so every width set here was
     * silently ignored and the code printed at the size of the thumbnail.
     * It cost an afternoon: a sweep of print widths from 90 to 130 mm
     * changed the page count not at all, because none of them ever applied. */
    max-width: none;
    height: auto;
    margin: 12mm auto 0;
    padding: 0;
    background: none;
    border-radius: 0;
  }

  /* THE DEADLINE CARD, NEARER THE CODE THAN THE TEXT ABOVE IT (G31 §9).
   *
   * „The deadline card should sit a little lower towards the QR code; it's
   * currently too close to the text above.“ (the operator.) It had no margin
   * of its own on the sheet, so it stood right under the date line, and
   * the code's 12 mm stood between it and the code. Measured on the
   * printed PDF on 2026-09-28, the tallest poster: 1.7 mm from the date
   * line's lowest ink to the card, 12.3 mm from the card to the code's box.
   * The rule: the gap above at least twice the gap below, the gap below at
   * least 4 mm. The gap below is to the code's BOX, not its first dark
   * module: the box holds the quiet zone the standard asks for, white on
   * white paper, and nothing may stand in it.
   *
   * ONLY WHEN THERE IS A CARD. A poster with no deadline keeps its 12 mm
   * between the date line and the code, which is what the sibling selector
   * is for. */
  .qr .deadline-line {
    margin: 10mm 0 0;
  }

  .qr > .deadline-line + img {
    margin-top: 5mm;
  }

  .qr-call {
    margin: 10mm 0 0;
    font-size: 15pt;
    font-weight: 700;
  }

  .qr-link {
    margin: 3mm 0 0;
    font-size: 10pt;
    color: var(--ink);
  }
}

/* ---- the Angebot icons and their picker ------------------------------ */

/* One size wherever an icon is drawn beside a line of a list. The line and
 * glyph sets are `currentColor`; the pixel set carries the mascot's palette in
 * its own fills, which is why nothing here sets a colour. */
.item-icon {
  display: inline-flex;
  flex: none;
  width: 22px;
  height: 22px;
}

.item-icon svg {
  width: 100%;
  height: 100%;
  display: block;
}

/* A PICKER, NOT A DROP-DOWN.
 *
 * "I'd like to have custom icons for the Posten with 2-3 different icon style
 * sets an organiser can choose from. With the option to leave blank (no
 * icon)." (the operator, 2026-09-11.)
 *
 * An icon is chosen by looking at it, and a select shows one at a time behind
 * a tap. Radios so the keyboard and the screen reader get the same list the
 * eye does, wrapped, each one a 44-px target. */
/* THE SAME GRID AS `.choice`, for the same reason and by the same measurement.
   This was a character-for-character copy of `.choice` as it stood before §4,
   so the icon picker in the Angebote editor kept the ragged distribution §4
   removed everywhere else — a dozen content-width pills breaking where their
   words end, on a page the walk visits. The tiles probe never saw it: it asks
   for `.choice`. */
.icon-choice {
  border: 0;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(8.5rem, 1fr));
  gap: var(--space-2);
}

@media (min-width: 640px) {
  .icon-choice {
    display: flex;
    flex-wrap: wrap;
  }
}

.icon-choice legend {
  padding: 0;
  margin-bottom: var(--space-2);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
}

.icon-option {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 44px;
  padding: 0 var(--space-3);
  border: 1px solid var(--input-line);
  border-radius: 999px;
  background: var(--input);
  font-size: 14px;
  cursor: pointer;
}

.icon-option:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-soft);
}

.icon-option input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.icon-option:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* "No icon" needs something to stand where the others draw: an empty ring,
 * which reads as a choice rather than as a missing picture. */
.icon-none {
  width: 22px;
  height: 22px;
  border: 1.5px dashed var(--input-line);
  border-radius: 50%;
  flex: none;
}

/* ---- choosing a picture ---------------------------------------------- */

/* "The Choose file / drag drop area isn't styled, just the default control
 * style." (the operator, 2026-09-12.)
 *
 * THE ZONE IS A LABEL WRAPPING THE REAL INPUT. That is what makes a pointer
 * anywhere on it open the picker with no script, and it is why the focus ring
 * is drawn with `:focus-within` — the thing that takes focus is the hidden
 * input inside, and the ring has to appear around what the eye thinks is the
 * control.
 *
 * Dashed, in the site's hairline, because a dashed edge is the one border
 * everybody reads as „put something here“ rather than as a box. */
/* A CONTROL AND THE BUTTON THAT SAVES IT ARE TWO THINGS.
 *
 * "Buttons too close." (the operator, 2026-09-12, of the club's picture
 * forms.) This class had no rule at all — it was doing nothing but naming the
 * element — so „Bild speichern“ sat flat against the bottom edge of the drop
 * zone above it, reading as part of the same control. It is used by every
 * small form that stacks one field over one button. */
.row-form {
  display: grid;
  gap: var(--space-3);
  justify-items: start;
}

.drop-zone {
  position: relative;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-4);
  border: 1.5px dashed var(--input-line);
  border-radius: 10px;
  background: var(--input);
  color: var(--ink-2);
  cursor: pointer;
  min-height: 44px;
}

.drop-zone:hover,
.drop-zone:focus-within {
  border-color: var(--accent);
  color: var(--ink);
}

.drop-zone:focus-within {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

/* WHILE SOMETHING IS OVER IT. Set by picker.js, so it only ever appears where
 * a drop would actually be taken. */
.drop-zone[data-over] {
  border-color: var(--accent);
  border-style: solid;
  background: var(--accent-soft);
  color: var(--ink);
}

/* VISUALLY HIDDEN, NOT GONE. `display: none` or `visibility: hidden` would
 * take the input out of the tab order, and then the only way to choose a file
 * would be a pointer. One pixel, clipped, still focusable. */
.drop-zone .drop-zone-input {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.drop-zone-icon {
  flex: none;
  width: 28px;
  height: 28px;
}

.drop-zone-words {
  display: grid;
  gap: 2px;
  min-width: 0;
}

.drop-zone-limits {
  font-style: normal;
  font-size: 12px;
  color: var(--ink-2);
}

/* THE WORDING THE SCRIPT EARNS. „oder hierher ziehen“ is only true once
 * picker.js has marked the zone; both sentences are in the markup and this
 * chooses between them, so a scriptless browser is never told it can drag. */
.drop-zone-call-drag {
  display: none;
}

.drop-zone[data-drag] .drop-zone-call-plain {
  display: none;
}

.drop-zone[data-drag] .drop-zone-call-drag {
  display: inline;
}

/* ONE PICTURE SLOT, AND THE DRAWING IS WHAT IT SHOWS WHILE IT IS EMPTY.
 *
 * "Why isn't the placeholder image getting replaced by the uploaded image as a
 * preview instead? In the current state the name is almost entirely cut off
 * because it sits so far at the edge." (the operator, 2026-09-12.)
 *
 * The first version put the glyph on the left and a second thumbnail against
 * the right edge, with the file's name in the gap between them — two pictures
 * in one control and the name in the narrowest place on the row. The glyph and
 * the preview share this box now, and the name has moved under the words. */
.drop-zone-thumb {
  position: relative;
  flex: none;
  width: 44px;
  height: 44px;
  display: grid;
  place-items: center;
  border-radius: 8px;
  overflow: hidden;
}

.drop-zone-icon {
  width: 28px;
  height: 28px;
}

/* `[hidden]` is a rule in the BROWSER's stylesheet and an author's `display`
 * outranks it, so anything that sets a display on these two has to hide them
 * again itself. The first version of this file declared `display: flex` on the
 * wrapper and un-hid an element the markup had marked hidden: every zone drew
 * an empty bordered square waiting for a picture nobody had chosen. Same trap
 * as the file input's `width` (G-82) — a rule written, right, and outranked. */
.drop-zone-preview[hidden],
.drop-zone-name[hidden] {
  display: none;
}

/* THE PREVIEW FILLS THE SLOT AND COVERS THE GLYPH UNDER IT. Absolutely
 * positioned rather than swapping the glyph off with `:has()`: the slot keeps
 * its size either way, so nothing on the row moves when a picture arrives. */
.drop-zone-preview {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  background: var(--band);
}

/* The file's own name, which can be long. A line of its own under the small
 * print, so it has the control's width rather than the corner of it. */
.drop-zone-name {
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink);
  overflow-wrap: anywhere;
}

/* ---- the icon SET picker --------------------------------------------- */

/* "The icon selection is broken in its current state, not only does it say
 * 'Wie die Site' as the first option but it also doesn't show the icons as a
 * preview so it's impossible to tell what they look like before choosing
 * them." (the operator, 2026-09-12.)
 *
 * ONE SET PER ROW, not chips in a wrapping line. The Angebot picker wraps
 * because it is twenty-six small choices and the eye scans for one drawing;
 * this is three choices that have to be COMPARED, and six drawings each. Side
 * by side they would either be too small to read or would wrap mid-set, which
 * is the one arrangement that makes comparing impossible. */
.icon-set-choice {
  border: 0;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
}

.icon-set-choice legend {
  padding: 0;
  margin-bottom: var(--space-2);
  font-size: 13px;
  font-weight: 600;
  color: var(--ink-2);
}

.icon-set-option {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  padding: var(--space-3);
  border: 1px solid var(--input-line);
  border-radius: 12px;
  background: var(--input);
  font-size: 15px;
  cursor: pointer;
}

.icon-set-option:has(input:checked) {
  border-color: var(--accent);
  background: var(--accent-soft);
}

/* The radio is hidden and the row says which is chosen, the same way the
 * Angebot picker does. `:focus-visible` is what puts the keyboard back. */
.icon-set-option input {
  position: absolute;
  opacity: 0;
  width: 0;
  height: 0;
}

.icon-set-option:has(input:focus-visible) {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

.icon-set-name {
  font-weight: 600;
}

/* THE DRAWINGS, at the size they are actually used. Bigger would flatter a
 * set that looks worse small, which is the size every one of them is seen at
 * on the sign-up form. They wrap on a narrow screen rather than scrolling
 * sideways: six icons over two lines still compares. */
.icon-set-sample {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

.icon-set-sample .item-icon {
  width: 24px;
  height: 24px;
  color: var(--ink-2);
}

/* ---- an Angebot: one line, or every field ---------------------------- */

/* "When configuring a new Angebot, all the options should show. But then
 * there should be an 'Add' button which adds it to the list only showing the
 * icon, name and price … The screen is too cluttered when adding 3-4+
 * Angebote." (the operator, 2026-09-12.)
 *
 * A closed row is one line: the icon, the name, the price, and the icon
 * buttons at the end. The name takes the slack so the price stays where the
 * eye can run down the column and compare.
 *
 * THE ROW ITSELF IS NOT DRAGGABLE ANY MORE, only the grip — "I'm unable to
 * select text inside a 'Angebot' because it causes it to drag the entry
 * instead". Which also means a closed row's text is selectable, and there is
 * nothing here that prevents it. */
.offer-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
}

/* The fields, when a row is open, take the whole width and the buttons drop
 * under them; a closed line shares its row with them. */
.offer-row > .fields {
  flex: 1 1 100%;
}

.offer-line {
  flex: 1 1 auto;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  min-width: 0;
}

.offer-line .item-icon {
  flex: none;
  width: 22px;
  height: 22px;
  color: var(--ink-2);
}

.offer-name {
  flex: 1 1 auto;
  min-width: 0;
  font-weight: 600;
  overflow-wrap: anywhere;
}

.offer-price {
  flex: none;
  font-family: var(--mono);
  font-size: 13px;
  color: var(--ink-2);
}

/* „GESPONSERT" BESIDE THE PRICE ON A CLOSED LINE. Quiet, because it is a
 * qualification of the number rather than a second number — and in the body
 * face, not the mono one, so it does not read as part of the amount. */
/* A NOTE BESIDE A FIELD. It spans the fields grid so it sits under the field
 * it is about rather than beside the next one, and it is pulled up by one step
 * because the grid's row gap is meant for two fields, not for a field and its
 * footnote. */
.help {
  grid-column: 1 / -1;
  margin-block-start: calc(var(--space-2) * -1);
}

/* THE „i" THE OPERATOR ASKED FOR, drawn rather than written. The trigger's
 * words are what a screen reader announces and „i" is not a sentence, so the
 * icon is a `::before` on the button: visible, and not in the accessible name.
 *
 * `.button-quiet` already gives 44 px of target and the site's button
 * behaviour; this only makes it read as a note rather than as an action. */
.help > button {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-weight: 400;
  font-size: 14px;
  color: var(--ink-2);
}

.help > button::before {
  content: "i";
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  flex: none;
  border: 1px solid var(--hair);
  border-radius: 999px;
  font-style: italic;
  font-weight: 700;
  line-height: 1;
}

.help-steps {
  margin: 0;
  padding-inline-start: var(--space-5);
  color: var(--ink-2);
  font-size: 14px;
  line-height: 1.55;
  text-align: start;
}

.help-steps > li + li {
  margin-block-start: var(--space-3);
}

/* AN ANGEBOT THAT IS OFF THE MENU. Muted rather than hidden — the people who
 * already chose it have to be able to see what they chose — and quiet enough
 * that the rows still on offer are what the eye lands on. The name keeps full
 * contrast: this is a row somebody may still be reading their own order off,
 * so it is the row's STANDING that is dimmed, not its legibility. */
.item-unavailable > .price,
.item-unavailable > .qty {
  color: var(--ink-2);
}

/* IT SAYS SO UNDER THE NAME rather than in the price's cell, which `.free` and
 * `.price` share — two badges in one grid area drew one on top of the other. */
.item-gone {
  color: var(--ink-2);
  font-weight: 600;
}

/* THE FOOT OF A SHOPPING LIST: one number and up to two qualifications of it.
 * The number keeps the table's mono face and alignment; the notes are quiet,
 * on their own lines, and in the body face so they cannot be misread as part
 * of the amount. */
.shop-total {
  display: block;
}

.shop-note {
  display: block;
  margin-block-start: var(--space-1);
  font-family: var(--sans);
  font-size: 12px;
  font-weight: 400;
  color: var(--ink-2);
  white-space: nowrap;
}

/* „Alle Angebote sind gesponsert", where the amount would be. Quiet and not
 * a number: it is the ABSENCE of one, so it must not read as a total of zero —
 * which is the whole of the operator's finding. */
.total-sponsored {
  margin: 0;
  padding-block: var(--space-2);
  color: var(--ink-2);
  font-size: 15px;
}

.offer-sponsored {
  flex: none;
  font-size: 12px;
  color: var(--ink-2);
  border: 1px solid var(--hair);
  border-radius: 999px;
  padding: 1px 8px;
  white-space: nowrap;
}

/* The line under a field that says what somebody ELSE will see — the sponsored
 * Angebot's „Frei", under the price that contradicts it. A hint about the
 * field's meaning rather than about its format, so it sits under the control
 * and not in the label. */
.field-note {
  display: block;
  margin-block-start: var(--space-1);
  font-size: 13px;
  font-style: normal;
  color: var(--ink-2);
}

/* The buttons sit at the end of a closed line and under an open one, which is
 * `margin-left: auto` in both cases: with the line beside them it pushes them
 * right, and with the fields above them the row has wrapped anyway. */
.offer-row > .row-actions-icons {
  margin-inline-start: auto;
  padding-top: 0;
}

/* ---- the reorder controls -------------------------------------------- */

/* "'Nach oben' 'Nach unten' 'Entfernen' should all be icons instead of text.
 * With the ability to drag/drop to reorder." (the operator, 2026-09-11.)
 *
 * 40 px of drawn button, and 44 where a thumb has to hit it. The icon is small
 * because a list of them would otherwise shout; this comment used to say the
 * TARGET was 44 „because this is used on a phone“, and it was not — 44 was the
 * ROW's height, which is padding around a 40-px button and not a bigger one to
 * press. PKG-G19 §1 measured it at 40 on the page the operator was holding.
 * Each one carries its words in aria-label and title, so nothing here
 * depends on the picture being understood. */
.row-actions-icons {
  gap: var(--space-1);
}

/* 44 WHERE A THUMB HAS TO HIT IT, and „a thumb“ is a pointer question rather
 * than a width one. Gated on width, this was lost the moment the operator's own
 * phone was turned on its side — 844 px in landscape — and on every tablet.
 * `pointer: coarse` is true in the hand and false at the desk, which is the
 * thing actually being asked.
 *
 * THE ROW WRAPS AT 320 px AND THAT IS THE TRADE. Five 44-px buttons and four
 * gaps need 236 px; the card has 224 at the narrowest screen still sold. Two
 * rows of targets a thumb can hit beat one row it misses, and the alternative —
 * closing the gaps — would put the delete button hard against the edit one. */
@media (pointer: coarse) {
  .row-actions-icons .icon-button {
    width: 44px;
    height: 44px;
  }
}

/* AN ICON BUTTON LOOKS LIKE A BUTTON BEFORE IT IS TOUCHED.
 *
 * "the removal (trash can) icon button has no outline unless hovered" (the
 * operator, 2026-09-12). The border was `transparent` at rest and only
 * appeared on hover — so a control that is already only a picture was, until
 * the pointer found it, indistinguishable from the drawing beside it. On a
 * touch screen there is no hover at all, which made the resting state the only
 * state a phone ever showed. */
.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 40px;
  height: 40px;
  padding: 0;
  border: 1px solid var(--hair);
  border-radius: 8px;
  /* Its ink at rest is `--ink-2` and its ink in a state is `--ink` — the step
     it has always made. The ground and the border join it now, from the same
     four tokens every other control uses (PKG-G18 §3). */
  --btn-hover: color-mix(in srgb, var(--surface) 92%, var(--ink));
  --btn-active: color-mix(in srgb, var(--surface) 84%, var(--ink));
  --btn-line: var(--ink-2);
  --btn-ink: var(--ink);
  background: var(--surface);
  color: var(--ink-2);
  cursor: pointer;
}

.icon-button:disabled {
  opacity: 0.35;
  cursor: default;
}

/* The danger one carries its colour at rest too, for the same reason: a
   delete that looks like every other button until touched is a delete
   somebody presses by accident. */
.icon-button-danger {
  color: var(--red);
  border-color: color-mix(in srgb, var(--red) 40%, var(--hair));
  --btn-line: var(--red);
  --btn-ink: var(--red);
}

/* The grip says "take hold of this" before anything is read. */
[data-grip] {
  cursor: grab;
}

/* THROUGH HOVER AND PRESS AS WELL, which is what the two tokens are for.
   `.icon-button:active:not(:disabled)` is 0,3,0 and this rule is 0,2,0, so
   the shared state rules outranked it: the accent cue that says „this row is
   lifted“ was repainted `--ink` at the very keystroke that established it,
   because Space holds `:active` down. Rather than out-specify the system, the
   grip tells it what its line and label are while lifted. */
[data-grip][aria-pressed="true"] {
  cursor: grabbing;
  color: var(--accent);
  border-color: var(--accent);
  --btn-line: var(--accent);
  --btn-ink: var(--accent);
}

/* THE ROW BEING MOVED, by pointer or by keyboard. The same state either way:
 * somebody dragging with the space bar should see what somebody dragging with
 * a mouse sees. */
.row[data-lifted] {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  opacity: 0.85;
}

@media (prefers-reduced-motion: no-preference) {
  .icon-button {
    /* `background-color` is in the list because PKG-G18 §3 put the ground in
       the state rules: without it the ground snapped while the glyph and the
       border eased, and one control moved at two speeds. */
    transition: color 120ms ease-out, border-color 120ms ease-out,
      background-color 120ms ease-out;
  }
}

/* ---- the stepped flow ------------------------------------------------ */

/* "there is no progress bar of steps" (the operator, 2026-09-11).
 *
 * A numbered list, and a list is what it is: four things in an order, one of
 * them now. `aria-current="step"` carries that to anybody listening, so the
 * colour is not the only thing saying where you are — which is the rule about
 * never conveying meaning by colour alone.
 *
 * It wraps rather than scrolls: at 320 px four steps do not fit on a line, and
 * a progress indicator you have to scroll to read is not one. */
.steps {
  list-style: none;
  margin: 0 0 var(--space-4);
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-4);
}

.step {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 13px;
  color: var(--ink-2);
}

.step-number {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 50%;
  border: 1px solid var(--hair);
  font-family: var(--mono);
  font-size: 12px;
  flex: none;
}

.step[data-state="now"] {
  color: var(--ink);
  font-weight: 600;
}

.step[data-state="now"] .step-number {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--on-accent);
}

/* A step already answered keeps its number rather than swapping it for a tick:
 * the number is what the person counted, and a tick where a 2 was is one more
 * thing to work out. */
.step[data-state="done"] .step-number {
  border-color: var(--accent);
  color: var(--accent);
}

/* ---- the review list ------------------------------------------------- */

/* What is about to be made, before it is made. Two columns where there is
 * room and two lines where there is not. */
.review {
  margin: 0;
  display: grid;
  gap: var(--space-2) var(--space-4);
}

.review dt {
  font-size: 13px;
  color: var(--ink-2);
}

.review dd {
  margin: 0;
  font-size: 15px;
}

/* THE FIVE ACTIONS ON ONE LINE, FROM 1100 PX UP.
 *
 * "Events should be one row at a time not 2 side by side, that way the invite
 * link doesn't get truncated and the buttons don't have to wrap." (the
 * operator, 2026-09-12.) The card is one per row now, and this is the second
 * half of that sentence said out loud rather than hoped for: a sixth action,
 * or a label that grows in translation, fails the clipping guard instead of
 * quietly making the row two lines high again.
 *
 * 1100 AND NOT 640, which is where the card grid's own block starts: five
 * buttons do not fit on one line in a 640-px window, and nowrap there would
 * be a promise the width cannot keep. 1100 is the operator's own window — the
 * width G7 shipped broken because nobody measured it. */
@media (min-width: 1100px) {
  .rows-cards .row-actions {
    flex-wrap: nowrap;
  }
}

@media (min-width: 480px) {
  .review {
    grid-template-columns: minmax(9rem, auto) 1fr;
  }

  .review dt {
    grid-column: 1;
  }

  .review dd {
    grid-column: 2;
  }
}

/* ---- an event's own picture ------------------------------------------ */

/* "I'd also like to have the option to set an (optional) logo and banner for
 * events (uploadable)" (the operator, 2026-09-11).
 *
 * A reserved shape rather than whatever the file happens to be: `aspect-ratio`
 * with `object-fit: cover` means the space is the same before and after the
 * bytes arrive, so the page does not jump when it loads. That is the layout
 * rule about reserving space for media, and it matters most here because this
 * is the first thing on a guest's page. */
/* NO MARGIN OF ITS OWN. „There is a lot of dead space on the submission page
 * between the group banner and/or event image and the time/place and header.“
 * (the operator, 2026-09-13.)
 *
 * `.page` is a flex column with one step of gap between its children, and the
 * comment on that rule says why: „a margin per block is how the numbers
 * stopped agreeing the first time“. Each banner then added 16 px of its own on
 * top of that 24, and a page with a club banner AND an event picture spent
 * 80 px on two gaps before the title. */
.event-banner {
  display: block;
  width: 100%;
  aspect-ratio: 3 / 1;
  object-fit: cover;
  border-radius: 10px;
  background: var(--band);
}

/* The organiser's own preview, small enough to sit in a form. */
.event-picture {
  display: block;
  width: 100%;
  max-width: 420px;
  aspect-ratio: 3 / 1;
  object-fit: cover;
  border-radius: 8px;
  background: var(--band);
}

/* ---- a club's own card on the dashboard ------------------------------- */

/* "The event overview should then be grouped in cards of the groups the user
 * has access to." (the operator, 2026-09-11.)
 *
 * ONLY WHEN THERE IS MORE THAN ONE. Somebody in a single club never sees any
 * of this: one heading over the only thing there is would be furniture, and
 * the operator's own page has to look exactly as it did before groups. The
 * page decides that; these rules only say what it looks like when it happens.
 */
/* ONE OUTLINE PER CLUB.
 *
 * "I'd like to have an outline per group to cleanly distinguish them from one
 * another." (the operator, 2026-09-12.) The sections were separated by air
 * alone, and air does not read as a boundary when the thing above it is
 * already a card and the thing below it is an empty state: the operator's
 * screenshot shows one club's lists, then a gap, then a name, then another
 * gap, with nothing saying which belongs to which.
 *
 * A hairline and a corner rather than a filled panel, because the lists inside
 * are already cards on a band — a second filled surface would be a box in a
 * box. The heading sits on the line's own edge. */
.group-section {
  display: grid;
  gap: var(--space-5);
  margin-block: var(--space-5);
  padding: var(--space-4) var(--space-4) var(--space-5);
  border: 1px solid var(--hair);
  border-radius: 12px;
}

.group-section:first-of-type {
  margin-block-start: var(--space-4);
}

/* THE SECTION SPACES ITS OWN CHILDREN, so the heading has no margin of its
 * own. „There should be more of a visual separator between the group banner
 * and the first event.“ (the operator, 2026-09-13.)
 *
 * The banner lost its bottom margin when the guest's page was fixed — there
 * the page column already gives a step between its children, and the margin on
 * top of it was the double gap the operator saw. This section is a plain block
 * and gave nothing, so removing that margin left the banner sitting on the
 * first card.
 *
 * One step of gap on the section, rather than a margin put back on the banner:
 * that is the rule `.page` states in as many words — „a margin per block is
 * how the numbers stopped agreeing the first time“ — and it spaces the heading
 * and the empty state too. */
.group-section > .panel-head-group {
  margin-block-end: 0;
}

/* The avatar sits ON the baseline of the name rather than above it: one
 * lockup, the rule G-62 settled for the header's own mark. */
.panel-head-group {
  gap: var(--space-3);
  font-size: 17px;
  color: var(--ink);
  margin-block-end: var(--space-3);
}

.panel-head-group .avatar {
  flex: 0 0 auto;
}

/* A reserved shape, for the reason the event's banner reserves one: the space
 * is the same before and after the bytes arrive, so a card that is three
 * sections down does not jump when a picture higher up finishes loading. */
/* And the club's, for the same reason — see `.event-banner` above. */
.group-banner {
  display: block;
  width: 100%;
  aspect-ratio: 4 / 1;
  object-fit: cover;
  border-radius: 10px;
  background: var(--band);
}

/* The group admin's own previews, small enough to sit in a form. The avatar is
 * square because it is drawn round everywhere it is used, and a 3:1 crop of a
 * face is not a face. */
.group-picture {
  display: block;
  border-radius: 8px;
  object-fit: cover;
  background: var(--band);
}

.group-picture-avatar {
  width: 96px;
  height: 96px;
  border-radius: 50%;
}

.group-picture-banner {
  width: 100%;
  max-width: 420px;
  aspect-ratio: 4 / 1;
}

/* The line on a public page that says which club's list this is: the avatar on
 * the baseline of the words, one lockup, as everywhere else. */
.event-host {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-3) 0 0;
  font-size: 14px;
  color: var(--ink-2);
}

.event-host .avatar {
  flex: 0 0 auto;
}

/* THE CLUB'S LINE, WHEN IT IS ALSO THE WAY TO THE CLUB'S PAGE.
 *
 * An anchor, so `display: flex` has to be said again — `.event-host` sets it
 * on a `p` and an `a` is inline by default, which would put the disc on the
 * baseline and the sentence beside it at the wrong height. No underline at
 * rest: the disc and the words are one target and a rule under half of it
 * reads as a mistake. */
a.event-host-link {
  text-decoration: none;
  color: var(--ink-2);
  border-radius: 6px;
}

a.event-host-link:hover span,
a.event-host-link:focus-visible span {
  text-decoration: underline;
}

/* ---- who did it, and when ------------------------------------------- */

/* "When a group has multiple non-admin organisers, working on the same event
 * has zero transparency" … "Also add 'Created by [avatar] [name] at [time]' to
 * groups and events." (the operator, 2026-09-13.)
 *
 * Quiet, small and last: this is a footnote about the thing above it, not part
 * of it. Same shape as the club's line on the guest's page — disc first, then
 * the sentence — because they say the same kind of thing. */
.provenance {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: var(--space-2) 0 0;
  font-size: 13px;
  color: var(--ink-2);
}

/* `flex-start` on the cross axis for the same reason the guest's header needed
 * it: nothing in this line is taller than a line of text today, and a disc
 * that grows should hang from the top rather than stretch — and once the
 * sentence wraps to two lines on a phone, a centred disc would float beside
 * the gap between them. */
.provenance-corner {
  align-items: flex-start;
}

/* "As for events, place it bottom right" (the operator, 2026-09-14) — AT THE
 * DESK, which is where that instruction was given and the only width it works
 * at.
 *
 * „The 'Created by x at x' line at the bottom of an event has its avatar
 * dislocated at the opposite edge than the text, and the timestamp is placed
 * in a new line." (the operator, 2026-09-19.) That is these three declarations
 * on a 390 px screen: `width: fit-content` shrank the row to the column it had,
 * the sentence wrapped inside it, and `text-align: end` then pushed the last
 * line — the timestamp — to the right while the disc stayed at the left. One
 * footnote, reading as two things at opposite edges.
 *
 * Phone-first, so the corner is what is added at 640 rather than what is
 * undone below it. `auto` on the start edge rather than `justify-content`,
 * so the disc and the sentence stay together and the PAIR moves — with
 * `justify-content: flex-end` on a row of two they would have been pushed
 * apart at any width wide enough to allow it. */
@media (min-width: 640px) {
  .provenance-corner {
    margin-inline-start: auto;
    width: fit-content;
    text-align: end;
  }
}

/* The disc is small here. It names somebody in a footnote, not in a list of
 * people, and at 28 px it was the loudest thing on the card. */
.provenance .avatar {
  flex: 0 0 auto;
  width: 20px;
  height: 20px;
  font-size: 9px;
}

/* ---- the way back --------------------------------------------------- */

/* "While inside an event, no tab is highlighted. And there are no back
 * buttons." (the operator, 2026-09-13.)
 *
 * Above the header and quiet: this is a path, not a heading. The row wraps
 * rather than scrolls, because the middle step is an event title somebody
 * typed and there is no length it cannot be. */
/* NO MARGIN OF ITS OWN. `.page` is a flex column with a 24 px gap, and flex
 * items do not collapse margins — so a bottom margin here ADDED to that gap
 * and the trail sat 24 px under the nav and 36 px above the header it labels,
 * reading as part of the navigation band rather than as the path to the title.
 * The same rule is written out at `.group-section`: a margin per block is how
 * the numbers stopped agreeing the first time. */
.crumbs {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 2px var(--space-1);
  margin: 0;
  font-size: 14px;
  color: var(--ink-2);
}

.crumb {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  /* 44 px of height for the links, which is the rule for anything you tap —
   * and the same height on the plain steps so the row does not jog. */
  min-height: 44px;
  padding: 2px 6px;
  border-radius: 6px;
  border: 1px solid transparent;
}

a.crumb {
  color: var(--ink);
  text-decoration: none;
}

a.crumb:hover,
a.crumb:focus-visible {
  border-color: var(--input-line);
}

/* The parent, which is what "back" means: one level up, not all the way out. */
.crumb-back {
  font-weight: 500;
}

.crumb-arrow {
  flex: none;
}

.crumb-sep {
  color: var(--ink-2);
  user-select: none;
}

.crumb[aria-current="page"] {
  color: var(--ink-2);
}

/* ---- the price field: its placeholder and its sign -------------------- */

/* "The auto formatting for the price isn't properly implemented, no
 * placeholder light grey digits in the background to fill as is the usual
 * established setup. Also no € in the corner." (the operator, 2026-09-12.)
 *
 * THE GREY DIGITS ARE THE INPUT'S OWN PLACEHOLDER, not a background image and
 * not a value. That matters: a placeholder disappears the moment anything is
 * typed and is never submitted, so nobody can mistake it for a price somebody
 * set — which a pre-filled `0,00` would be, and would quietly turn every
 * Angebot somebody forgot to price into a free one.
 *
 * The colour is the muted ink rather than a lighter grey of its own: this text
 * is decoration and must not compete with a real value, but it still has to be
 * legible enough to read as "0,00" rather than as a smudge. */
.field-price .price-input {
  position: relative;
  display: block;
}

.field-price input {
  /* Room for the sign, so a long price never runs under it. */
  padding-inline-end: 30px;
}

.field-price input::placeholder {
  color: var(--ink-2);
  opacity: 0.55;
}

/* THE SIGN SITS IN THE FIELD, aria-hidden, because it is the unit of the box
 * rather than a word anybody needs read to them — the label already says
 * "Preis". Absolutely positioned so it does not move when the value grows, and
 * pointer-events: none so clicking it still focuses the input under it. */
.field-price .price-currency {
  position: absolute;
  inset-block: 0;
  inset-inline-end: 10px;
  display: flex;
  align-items: center;
  color: var(--ink-2);
  font-size: 14px;
  pointer-events: none;
}

