/* 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-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 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-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;
    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-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;
  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-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-soft: #3d2410;
  --on-accent: #1a0f04;
}

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

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

[data-accent="beere"] {
  --accent: #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-soft: #dcfce7;
    --on-accent: #ffffff;
  }

  :root:not([data-theme="dark"])[data-accent="glut"],
  :root:not([data-theme="dark"]) [data-accent="glut"] {
    --accent: #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-soft: #fef3c7;
    --on-accent: #ffffff;
  }

  :root:not([data-theme="dark"])[data-accent="see"],
  :root:not([data-theme="dark"]) [data-accent="see"] {
    --accent: #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-soft: #fce7f3;
    --on-accent: #ffffff;
  }
}

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

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

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

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

:root[data-theme="light"][data-accent="beere"],
:root[data-theme="light"] [data-accent="beere"] {
  --accent: #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;
}

/* 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 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;
}
.tag {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  padding: 3px 7px;
  border-radius: 4px;
  background: var(--band);
  color: var(--ink-2);
  border: 1px solid var(--hair);
}

.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;
}

.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, AS ONE OBJECT.
 *
 * A disc with initials, the name, the role, and the way out — in that
 * order and in one box, so it reads as a card and not as three more items
 * at the end of the navigation. The whole card is on the spacing scale;
 * before this the address had no margin of its own at all and touched the
 * link before it. */
.account-card {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-3);
  border: 1px solid var(--hair);
  border-radius: 10px;
  background: var(--band);
  align-self: flex-start;
  max-width: 100%;
}

/* 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. */
.account-who {
  display: flex;
  flex-direction: column;
  min-width: 0;
}

.account-name {
  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 way out sits at the card's right edge with air of its own. */
.account-card form {
  margin-left: auto;
  padding-left: var(--space-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;
}

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

/* 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 bar ---------------------------------------------- */

/* Two things, not one list: where you can go, and who you are. Stacked on
 * a phone, side by side from the first breakpoint. */
.admin-bar {
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  padding: var(--space-4) 0;
  border-bottom: 1px solid var(--hair);
}

.nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  padding: 0;
}

/* SEVEN LINKS, AND THE NUMBER IS MEASURED AT EVERY WIDTH, NOT AT ONE.
 *
 * "Konto still gets wrapped." (the operator, 2026-09-12.) Stacking the bar's
 * two halves below 1100 px gave the links the whole content column and was not
 * enough: at 12 px of padding and a 12 px gap the seven labels come to 708 px,
 * so the row fits only from about 758 px of viewport upward.
 *
 * 8 px of padding and an 8 px gap bring the row to 628 px, which fits from
 * about 678 px up — better, and still not the whole of the desktop range the
 * site claims: 640 px is where this stylesheet says "wider than a phone", and
 * between 640 and 678 the last link still dropped onto a line of its own. The
 * band below is what closes that, so the bar is one line everywhere it is a
 * row at all. Swept in the browser in 20 px steps from 620 to 1140.
 *
 * Only the space between each word and the edge of its own hit area got
 * smaller. The targets stay 44 px tall and they touch, which is what a row of
 * links is. */
.nav a {
  font-size: 14px;
  font-weight: 500;
  color: var(--ink);
  text-decoration: none;
  padding: 10px 8px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  border-radius: 6px;
  border: 1px solid transparent;
}

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

.nav a[aria-current="page"] {
  background: var(--accent-soft);
  color: var(--accent);
}

.nav .who {
  margin-left: auto;
  align-self: center;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--ink-2);
}

.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 TOP OF THE PAGE THE ACTION LANDED ON.
 *
 * The same box as any other notice, because it is one: „Liste geschlossen“ is
 * the same kind of sentence as „Einstellungen gespeichert“, and a second shape
 * for it would be a second thing to learn. What it adds is a place — first
 * child of the page column, which is where the browser's jump to the top puts
 * the reader — and, where motion is welcome, a short arrival, so a page that
 * otherwise looks unchanged has one thing on it that moved.
 *
 * AFTER `.notice`, not before it. Written above, its `margin-top: 0` lost to
 * `.notice`'s `margin: 14px 0 0` at equal specificity and the flash sat 14 px
 * down the page doing nothing — the same ordering mistake as the nav band in
 * G-91, in the same stylesheet, four hundred lines apart.
 *
 * AND A WAY OUT, which this rule exists to place. „Give notifications a
 * dismiss button.“ (the operator, 2026-09-13.) This paragraph used to argue
 * the other way — the message is gone at the next navigation by construction,
 * so a close button only hurries that along — and that reasoning was
 * overruled by the one thing it had not measured: the notice sits above
 * everything and pushes the thing the reader came back to do further down the
 * page. A comment arguing against the code beneath it is the next reader's
 * wrong answer, so it says what is true now. */
.notice-flash {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
  margin-top: 0;
}

/* THE WAY OUT, IN THE CORNER OF THE MESSAGE.
 *
 * „Give notifications a dismiss button.“ (the operator, 2026-09-13.) Quiet,
 * because the sentence is what matters and this is only the way to stop
 * reading it — and a 44 px target all the same, since a small × in a corner is
 * the control people miss most often. */
.flash-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;
}

/* AND THE FOURTH TIME. `[hidden]` is a rule in the BROWSER's stylesheet, so
 * the `display` two lines up outranks it and the button was on the screen with
 * no script to make it do anything — which is the one thing it was written not
 * to be. Caught by the guard, not by the browser test: that test runs the
 * script every time, so it can never see this state. */
.flash-close[hidden] {
  display: none;
}

/* A BORDER AND NOT A WASH. This was `rgb(255 255 255 / 8%)` — the only
 * hardcoded white in four thousand lines, and invisible on the light theme's
 * pale notice, so the × had no hover state at all for half the site's
 * readers. */
.flash-close:hover,
.flash-close:focus-visible {
  border-color: var(--accent);
}

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

@keyframes flash-in {
  from {
    opacity: 0;
    transform: translateY(-4px);
  }

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

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

@media (min-width: 640px) {
  /* THE BAR'S TWO HALVES SHARE A ROW FROM 1100, NOT FROM 640.
   *
   * "The bar wraps Konto into a newline." (the operator, 2026-09-12.)
   *
   * A site admin has seven links, and their words plus their padding come to
   * about 810 px. Side by side from 640 the links had roughly 700 px of that
   * row, so the last one dropped alone onto a second line — not clipped, which
   * is why the clipping walk was green about it, just plainly broken to look
   * at. Below 1100 the bar stacks and the links get the whole content width;
   * the gap is measured at 1024, 1280 and 1536 now.
   *
   * The rule is kept in this block rather than moved wholesale so the phone
   * layout above it is untouched. */
  .nav {
    gap: var(--space-2);
  }
  .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 NARROWEST WIDTH AT WHICH THE BAR IS A ROW.
 *
 * At 640 px the content column is 600 px and the row above needs 628, so the
 * seven links have to give up another 28. 6 px of padding a side and a 4 px gap
 * bring them to 576 — inside 600 with room to spare, and it is only the air
 * around each word that shrinks. Above 768 the column is wide enough for the
 * roomier version, so this stops there rather than being the whole desktop's
 * look.
 *
 * AFTER the 640 block, not before it: that block sets the row's gap at the same
 * specificity, so written above it this one would lose on exactly the widths it
 * is for. */
@media (min-width: 640px) and (max-width: 767px) {
  .nav {
    gap: var(--space-2) 4px;
  }

  .nav a {
    padding: 10px 6px;
  }
}

/* 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;
}

@page sheet {
  margin: 0;
}

@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 NOT HERE, ON PURPOSE, AND IT IS NOT FREE. The one coloured
   * thing on either sheet is the dot in the mark (§9 of PKG-G21), coloured by
   * the accent the page resolves — which the event's icon set may choose, so
   * there is nowhere to read it back from once it is overwritten. It stays,
   * and the cost is that the OTHER readers of `--accent` stay theme-dependent
   * on paper: `.free`, `.tag-open` and the border on `.banner` print at about
   * 2.4:1 in the dark theme and about 7:1 in the light one. That is
   * pre-existing rather than new — the body ground was already white here —
   * but the earlier version of this comment claimed „nothing printed reads
   * `--accent` for text or for a rule“, and that was simply wrong: it is read
   * on every page this block reaches, which is all of them. Recorded for the
   * operator rather than fixed under a package about the two sheets. */
  :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 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. */
  /* `.notice-flash` 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,
  .admin-bar,
  .crumbs,
  .foot,
  .notice-flash,
  .row-actions,
  .no-print {
    display: none !important;
  }

  /* THE TWO SHEETS, AND THE ONLY TWO. See `@page` above for what the
   * marginless box is for; this is the whole list of what is a sheet rather
   * than a page. `.sheet` is styled in `print.css`, which the printable list
   * is the only page to load — the box it uses is named here because the box
   * is defined here and one place should say who is in it. */
  .qr,
  .sheet {
    page: sheet;
  }

  /* 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(--accent);
  }

  /* 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;
  }

  .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 organiser bar, once there is room for both halves ---------- */

/* 1100 rather than 640: the seven links and the account card need about
 * 1070 px between them before they stop fighting, and „Konto“ alone on a
 * second line is what the shortfall looked like. See the note in the 640
 * block. */
@media (min-width: 1100px) {
  .admin-bar {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    gap: var(--space-5);
  }
}

/* ---- 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);
}

/* 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);
}

/* "As for events, place it bottom right" (the operator). `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.
 *
 * `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. */
.provenance-corner {
  margin-inline-start: auto;
  align-items: flex-start;
  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;
}

