/* GENERATED FILE — every edit here is thrown away.
   The source is app/static/stil/*.css; this file is what
   `python manage.py build-css` makes out of them, and the Dockerfile
   makes it again at every build. Change a part, not this. */
/* ==========================================================================
   mycity — one stylesheet, one token system.

   Read GE1 to GE9 in /home/cotta/ai/mycity/docs/design/GESTALTUNG.md before
   changing a value here. In short:

   ST1. Every colour, space, radius, shadow and type size is a token. A raw
        hex value below the token blocks is a mistake, not a shortcut.
   ST2. Dark is the default and lives on bare :root (H8). Light is a second
        CHOSEN set under :root[data-theme="light"], and the same set answers
        the machine's own preference under :root:not([data-theme]) — the
        server writes the attribute only when somebody actually chose
        (app/theme.py), so "no attribute" really does mean "ask the machine".
   ST3. The six category hues carry all the colour on this site. The interface
        accent is brass and is spent on four things only: the focus ring, the
        "Heute" marker, the brand mark and the current-page underline.
   ST4. No inline style anywhere — the Content-Security-Policy has no
        unsafe-inline and the browser drops a style attribute in silence. A
        per-card hue is therefore a CLASS (.k-konzert sets --kat), never a
        style attribute.
   ST5. 360 px is the first layout. Nothing may make the page scroll sideways;
        a chip rail scrolls inside itself and says so with .rail.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. The two faces, self-hosted (H4, ST4 of the brief). No outside host.

   Two families, four files: each family ships its latin and its latin-ext cut.
   latin-ext is not decoration on a Vienna calendar — Dvořák, Bartók, Szabó and
   Łódź all appear in real programme titles, and without it the browser falls
   back mid-word.
   -------------------------------------------------------------------------- */

@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 100 900;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url("schrift/archivo-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
    U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Archivo";
  font-style: normal;
  font-weight: 100 900;
  font-stretch: 62% 125%;
  font-display: swap;
  src: url("schrift/archivo-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
    U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
    U+A720-A7FF;
}

@font-face {
  font-family: "Instrument Sans";
  font-style: normal;
  font-weight: 400 700;
  font-stretch: 100%;
  font-display: swap;
  src: url("schrift/instrument-sans-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
    U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122,
    U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

@font-face {
  font-family: "Instrument Sans";
  font-style: normal;
  font-weight: 400 700;
  font-stretch: 100%;
  font-display: swap;
  src: url("schrift/instrument-sans-latin-ext.woff2") format("woff2");
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
    U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
    U+A720-A7FF;
}

/* --------------------------------------------------------------------------
   2. Tokens — dark, the default (H8)

   --bg MUST stay #0e1116 and the light --bg MUST stay #f4f5f7: those two
   values are also written in app/theme.py as THEME_COLOURS, which is what
   paints the address bar on a phone. A mismatch between the two files shows
   up as a visible seam above the page. Whoever changes one changes both in
   the same commit.
   -------------------------------------------------------------------------- */

:root {
  /* ground and surfaces */
  --bg: #0e1116;
  --bg-deep: #080a0e;
  --surface: #151a22;
  --surface-lift: #1d242e;
  --surface-sink: #11161d;
  --line: #262f3b;
  --line-strong: #3b4655;

  /* ink

     --ink-faint was #6e7a89 and did not carry text. Measured against the two
     grounds it actually sits on, with the WCAG 2.1 relative-luminance sum:
     4.33 : 1 on --bg and 4.16 : 1 on --surface-sink, which a row takes while
     it is open or under the pointer. Both are under 4.5 : 1, and this token
     carries the Datum and the Ende cell of the table at 12 and 13 px. It is
     lifted along its own hue until the worse of the two grounds clears the
     bar: 5.46 : 1 on --bg, 5.24 : 1 on --surface-sink, 5.04 : 1 on --surface.
     It stays plainly quieter than --ink-soft, which reads 8.88 : 1. */
  --ink: #edf1f6;
  --ink-soft: #a7b3c1;
  --ink-faint: #7f8b9a;
  --ink-invert: #0e1116;

  /* the interface accent: brass. Four jobs, no fifth. */
  --accent: #e8b84b;
  --accent-ink: #17120a;
  --accent-wash: rgba(232, 184, 75, 0.13);

  /* states */
  --good: #5ec98a;
  --warn: #e8b84b;
  --bad: #ef6b6b;

  /* the six families (B2). Colour is scarce: a sub-genre is a word. */
  --cat-konzert: #f2603f;
  --cat-club: #a887f5;
  --cat-buehne: #ee6fa8;
  --cat-kunst: #34c2bd;
  --cat-film: #5e9bf5;
  --cat-stadtleben: #58c281;
  --cat-none: #8792a1;
  /* Only the dashed proposal chip of SG10 ever wears this. */
  --cat-vorschlag: #c9dd4a;
  /* What is legible ON a category hue. The hues are light in the dark theme
     and dark in the light one, so this token flips with the theme. */
  --kat-ink: #0e1116;
  /* The hue in hand. Every .k-* class below sets it; nothing else may. */
  --kat: var(--cat-none);

  /* space — a 4 px step, named by size and not by where it is used */
  --s1: 0.25rem;
  --s2: 0.5rem;
  --s3: 0.75rem;
  --s4: 1rem;
  --s5: 1.5rem;
  --s6: 2rem;
  --s7: 3rem;
  --s8: 4.5rem;

  /* radius */
  --r1: 6px;
  --r2: 10px;
  --r3: 18px;
  --r-pille: 999px;

  /* shadow — three steps, all cool, none of them a glow */
  --sh1: 0 1px 2px rgba(0, 0, 0, 0.35);
  --sh2: 0 6px 18px -6px rgba(0, 0, 0, 0.55);
  --sh3: 0 18px 44px -14px rgba(0, 0, 0, 0.7);

  /* type */
  --font-display: "Archivo", "Helvetica Neue", Arial, sans-serif;
  --font-text: "Instrument Sans", "Segoe UI", system-ui, sans-serif;
  --t-xs: 0.75rem;
  --t-s: 0.8125rem;
  --t-m: 0.9375rem;
  --t-l: 1.0625rem;
  --t-xl: clamp(1.15rem, 1.02rem + 0.6vw, 1.4rem);
  --t-2xl: clamp(1.4rem, 1.15rem + 1.2vw, 1.95rem);
  --t-3xl: clamp(1.9rem, 1.3rem + 2.8vw, 3.1rem);

  /* Archivo's width axis, used as a system rather than as an effect:
     titles are narrow, small labels are wide. */
  --w-eng: 82%;
  --w-normal: 100%;
  --w-weit: 116%;

  /* structure */
  --huelle: 76rem;
  --kopf: 3.5rem;
  --rail: 1px;
  --zeit-spalte: 3.5rem;

  /* The table's own tracks (section 25). Every one of them is a fixed track
     on purpose: the heading row and the rows carry the same grid, and a
     content-sized track (`auto`, `min-content`) would make every row a
     different width from the heading above it.

     EACH ONE IS THE WIDER OF TWO MEASURED THINGS — the widest cell it has to
     hold, and its own heading word plus the sort arrow. The numbers are the
     advance widths out of the two font files themselves, which is how a
     narrow screen is checked here (M29, rule 48); the whole reckoning stands
     at the head of section 25.

     `--zeit-spalte` above is NOT one of them any more. It is 3.5 rem, which
     holds `19:30` and nothing else, and the word BEGINN over it is 82.9 px:
     the heading painted 27 px across the title. The dense row keeps that
     token unchanged; the table has `--sp-beginn` of its own.

     EVERY ONE OF THEM CARRIES 0.5 REM (--s2) OF CUSHION SINCE 2026-08-10.
     The operator asked for padding on every column, so every grid child of
     `.tz-raster` has `padding-inline-end: var(--s2)` and every track below
     grew by exactly that much — while the column gap gave the same 8 px back
     (`--s4` became `--s2` from 46 em, `--s3` became `--s1` below it). The
     content each track holds is therefore unchanged to the pixel, and so is
     the width the title is left with. The measurements in the comments below
     and in section 25 are all CONTENT widths: track minus 8. */
  --sp-datum: 6rem;
  /* 5.5 rem of content and not 5.25, and the widest thing this track holds is
     the heading and not a clock time. Measured out of archivo-latin.woff2 at
     exactly what the rule sets — 12 px, weight 700, wdth 116, uppercase,
     letter-spacing .09em — BEGINN is 62.88 px, the collapsed space before the
     sort arrow 4.33, the arrow 7.08, and `.tz-kopf-feld` adds 8 px of padding
     and 2 px of border: 84.29 px in 84 px of content. The field is
     `white-space: nowrap`, so it does not wrap and does not shrink — those
     0.29 px painted straight into the column beside it. At 88 px of content
     there are 3.71 px to spare, and the title's track still clears 161 px at
     its narrowest, which is 992 px with all eight columns up. */
  --sp-beginn: 6rem;
  --sp-ende: 4.75rem;
  --sp-preis: 5.25rem;
  --sp-punkte: 7rem;
  /* The Ort track is the only one whose content decides it rather than its
     heading, so it is the one that gives way where the screen is tighter:
     152 px of content while it is the last of five columns, 160 from 62 em,
     where section 25 widens it again. */
  --sp-ort: 10rem;
  --sp-sprache: 4.25rem;
  --tempo: 140ms;
  --kurve: cubic-bezier(0.2, 0.7, 0.3, 1);

  color-scheme: dark;
}

/* --------------------------------------------------------------------------
   3. Tokens — light, chosen or asked for

   Only the tokens that change are repeated. Everything structural above is
   read from :root either way, so the two themes cannot drift in spacing.
   -------------------------------------------------------------------------- */

:root[data-theme="light"] {
  --bg: #f4f5f7;
  --bg-deep: #e6e9ee;
  --surface: #ffffff;
  --surface-lift: #eef0f4;
  --surface-sink: #e9ecf1;
  --line: #dbdfe6;
  --line-strong: #b9c0cb;

  --ink: #11161d;
  --ink-soft: #4b5563;
  /* #77818f read 3.62 : 1 on --bg and 3.33 : 1 on --surface-sink, which is
     the paler of the two themes' failures. Darkened along the same hue until
     both clear: 5.10 : 1 and 4.70 : 1. What gets lighter in the dark theme
     gets darker here, so nothing fades out. */
  --ink-faint: #5f6977;
  --ink-invert: #ffffff;

  --accent: #8a6412;
  --accent-ink: #ffffff;
  --accent-wash: rgba(138, 100, 18, 0.1);

  --good: #1f7a45;
  --warn: #8a6412;
  --bad: #b3271f;

  --cat-konzert: #c23c1c;
  --cat-club: #6a45c8;
  --cat-buehne: #b1306c;
  --cat-kunst: #0a7a75;
  --cat-film: #1f5fc4;
  --cat-stadtleben: #1f7a45;
  --cat-none: #5a6570;
  --cat-vorschlag: #5f7a12;
  --kat-ink: #ffffff;

  --sh1: 0 1px 2px rgba(17, 22, 29, 0.08);
  --sh2: 0 6px 18px -6px rgba(17, 22, 29, 0.16);
  --sh3: 0 18px 44px -14px rgba(17, 22, 29, 0.22);

  color-scheme: light;
}

/* Nobody has chosen, and the machine asks for light. app/theme.py writes no
   data-theme attribute at all in that case, so :not([data-theme]) is exact. */
@media (prefers-color-scheme: light) {
  :root:not([data-theme]) {
    --bg: #f4f5f7;
    --bg-deep: #e6e9ee;
    --surface: #ffffff;
    --surface-lift: #eef0f4;
    --surface-sink: #e9ecf1;
    --line: #dbdfe6;
    --line-strong: #b9c0cb;

    --ink: #11161d;
    --ink-soft: #4b5563;
    /* The same lift as the chosen light theme above, for the same reason. */
    --ink-faint: #5f6977;
    --ink-invert: #ffffff;

    --accent: #8a6412;
    --accent-ink: #ffffff;
    --accent-wash: rgba(138, 100, 18, 0.1);

    --good: #1f7a45;
    --warn: #8a6412;
    --bad: #b3271f;

    --cat-konzert: #c23c1c;
    --cat-club: #6a45c8;
    --cat-buehne: #b1306c;
    --cat-kunst: #0a7a75;
    --cat-film: #1f5fc4;
    --cat-stadtleben: #1f7a45;
    --cat-none: #5a6570;
    --cat-vorschlag: #5f7a12;
    --kat-ink: #ffffff;

    --sh1: 0 1px 2px rgba(17, 22, 29, 0.08);
    --sh2: 0 6px 18px -6px rgba(17, 22, 29, 0.16);
    --sh3: 0 18px 44px -14px rgba(17, 22, 29, 0.22);

    color-scheme: light;
  }
}

/* The hue in hand, set by a class because a style attribute is dropped by the
   Content-Security-Policy (ST4). One class per family, and one for an event
   that carries no family at all — which is not a seventh category. */
.k-konzert { --kat: var(--cat-konzert); }
.k-club { --kat: var(--cat-club); }
.k-buehne { --kat: var(--cat-buehne); }
.k-kunst { --kat: var(--cat-kunst); }
.k-film { --kat: var(--cat-film); }
.k-stadtleben { --kat: var(--cat-stadtleben); }
.k-none { --kat: var(--cat-none); }
.k-vorschlag { --kat: var(--cat-vorschlag); }

/* --------------------------------------------------------------------------
   4. Reset, and the three things that keep a narrow screen honest
   -------------------------------------------------------------------------- */

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

/* NB1: a long unbroken word — an event title, a venue, a source address —
   is the commonest way a 360 px page gets a sideways scrollbar. */
html {
  -webkit-text-size-adjust: 100%;
  overflow-wrap: break-word;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font-family: var(--font-text);
  font-size: var(--t-m);
  line-height: 1.55;
  font-synthesis-weight: none;
  -webkit-font-smoothing: antialiased;
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
}

/* NB2: a grid or flex child defaults to min-width:auto, which refuses to
   shrink below its content. Every one of them is told otherwise.

   `.tz-fuss > *` is here for a trap worth naming: from 46 em `.tz-fuss` is
   `display: contents`, so Preis and the family dots inside it become grid
   children of `.tz-raster` in their own right — but a selector reads the
   document tree, not the box tree, so `.tz-raster > *` never reaches them.
   Without this line they kept min-width:auto and the price ran across the
   Ort column beside it. */
main > *,
.gitter > *,
.zeile > *,
.tz-raster > *,
.tz-fuss > *,
.kopf-innen > *,
.karte > *,
.stadtwahl-karte > * {
  min-width: 0;
}

/* I2. The title gives its own cushion back, and it is the one cell that may.
   Every other column can carry content wider than its track — a time, a
   heading, a nowrap pill — and the cushion is what keeps that off its
   neighbour. The title wraps and breaks (NA1), so it can never reach the next
   column at all, and 8 px there is 8 px of the narrowest track on the page:
   153 px at 992 px against 161. */
.tz-titel {
  padding-inline-end: 0;
}

img,
svg,
video {
  display: block;
  max-width: 100%;
  height: auto;
}

h1,
h2,
h3,
h4 {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.01em;
}

p {
  margin: 0 0 var(--s3);
}

p:last-child {
  margin-bottom: 0;
}

a {
  color: inherit;
  text-decoration-color: color-mix(in oklab, currentColor 40%, transparent);
  text-underline-offset: 0.18em;
}

a:hover {
  text-decoration-color: currentColor;
}

ul,
ol {
  margin: 0;
  padding: 0;
  list-style: none;
}

button,
input,
select,
textarea {
  font: inherit;
  color: inherit;
}

/* 16 px on a text field, or iOS zooms the whole page in on focus and the
   reader is then looking at a page 1.3 times too wide. */
input,
select,
textarea {
  font-size: max(1rem, var(--t-m));
}

:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--r1);
}

::selection {
  background: var(--accent);
  color: var(--accent-ink);
}

.nur-vorlesen {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.springen {
  position: absolute;
  left: var(--s3);
  top: -4rem;
  z-index: 60;
  padding: var(--s2) var(--s4);
  background: var(--accent);
  color: var(--accent-ink);
  border-radius: var(--r1);
  font-weight: 600;
  text-decoration: none;
  transition: top var(--tempo) var(--kurve);
}

.springen:focus {
  top: var(--s2);
}

.huelle {
  width: 100%;
  max-width: var(--huelle);
  margin-inline: auto;
  padding-inline: var(--s4);
}

@media (min-width: 46em) {
  .huelle {
    padding-inline: var(--s5);
  }
}

/* --------------------------------------------------------------------------
   5. The header

   One sticky row on a phone: the mark, the search, the two controls. The
   wordmark and the day navigation come back as the screen allows.
   -------------------------------------------------------------------------- */

.kopf {
  position: sticky;
  top: 0;
  z-index: 40;
  background: color-mix(in oklab, var(--bg) 88%, transparent);
  backdrop-filter: blur(14px) saturate(1.3);
  border-bottom: 1px solid var(--line);
}

.kopf-innen {
  display: flex;
  align-items: center;
  gap: var(--s2);
  min-height: var(--kopf);
  padding-block: var(--s2);
}

.marke {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  text-decoration: none;
  flex: 0 0 auto;
}

.marke-bild {
  width: 28px;
  height: 28px;
  border-radius: 7px;
}

.marke-name {
  display: none;
  font-family: var(--font-display);
  font-weight: 800;
  font-stretch: var(--w-eng);
  font-size: var(--t-l);
  letter-spacing: -0.02em;
}

/* ST5. THE CITY NAME OPENS THE OTHER CITIES, on the operator's word of
   2026-08-10. It is a <summary>, so two things have to go: the browser's own
   triangle, in both spellings, or the header carries two markers. The caret
   beside the name is ours and turns over when the panel opens.

   It stands beside the brand rather than inside it, because a link inside a
   link is not a thing: the logo goes to this city's front page, the name
   opens the list. */
.marke-stadt {
  display: none;
  align-items: center;
  gap: var(--s1);
  font-family: var(--font-display);
  font-weight: 500;
  font-stretch: var(--w-weit);
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  white-space: nowrap;
  cursor: pointer;
  list-style: none;
  color: var(--ink-faint);
  border-left: 1px solid var(--line-strong);
  padding-left: var(--s2);
  margin-left: var(--s1);
}

.marke-stadt::-webkit-details-marker {
  display: none;
}

.marke-stadt:hover {
  color: var(--ink-soft);
}

/* One city switched on: the name still stands there and says where you are,
   but it is not a control and must not look like one. */
.marke-stadt-still {
  cursor: default;
}

.stadtwahl {
  display: none;
  position: relative;
  flex: 0 0 auto;
}

.stadtwahl-pfeil {
  font-size: 0.75em;
  line-height: 1;
  transition: transform var(--tempo) var(--kurve);
}

.stadtwahl[open] .stadtwahl-pfeil {
  transform: rotate(180deg);
}

/* Hung from the LEFT edge of the name, because the name sits at the left edge
   of the page — the clock and the calendar hang from their right edge for the
   same reason, and both rules are the same rule: a panel opens towards the
   middle of the window and never over its edge. At 480 px the name starts
   about 44 px in, so a 12 rem panel runs from 44 px to 236 px. */
.stadtwahl-tafel {
  position: absolute;
  z-index: 40;
  inset-inline-start: 0;
  top: calc(100% + var(--s2));
  min-width: 12rem;
  max-width: min(16rem, calc(100vw - var(--s6)));
  padding: var(--s1);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r2);
  box-shadow: var(--sh3);
}

.stadtwahl-liste {
  list-style: none;
  margin: 0;
  padding: 0;
}

/* 2.75 rem is a finger, not a mouse pointer: this control appears from 30 em,
   which is a phone held sideways as well as a desktop. */
.stadtwahl-zeile {
  display: flex;
  align-items: center;
  min-height: 2.75rem;
  padding: var(--s1) var(--s3);
  border-radius: var(--r-klein);
  color: var(--ink);
  text-decoration: none;
  font-family: var(--font-display);
  font-size: var(--t-s);
}

.stadtwahl-zeile:hover {
  background: var(--surface-sink);
}

.stadtwahl-zeile[aria-current="true"] {
  color: var(--accent);
  font-weight: 700;
}

.suche {
  display: flex;
  align-items: center;
  flex: 1 1 auto;
  min-width: 0;
  gap: var(--s2);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r-pille);
  padding: 0 var(--s2) 0 var(--s3);
}

.suche:focus-within {
  border-color: var(--line-strong);
  box-shadow: 0 0 0 3px var(--accent-wash);
}

.suche-feld {
  flex: 1 1 auto;
  min-width: 0;
  background: none;
  border: 0;
  padding: var(--s2) 0;
  outline: none;
}

.suche-feld::placeholder {
  color: var(--ink-faint);
}

.suche-knopf {
  flex: 0 0 auto;
  background: none;
  border: 0;
  padding: var(--s1) var(--s2);
  color: var(--ink-soft);
  cursor: pointer;
  border-radius: var(--r-pille);
}

.suche-knopf:hover {
  color: var(--ink);
}

/* `margin-left: auto` for the one page that draws no search field: the city
   chooser (L1a). Everywhere else `.suche` is `flex: 1 1 auto` and has already
   eaten the free space, so there is none left for this margin to take and the
   line looks exactly as it did. Without it, the theme and account buttons sit
   against the logo on `/mycity/` with the rest of the row empty. */
.kopf-werkzeug {
  display: flex;
  align-items: center;
  gap: var(--s1);
  flex: 0 0 auto;
  margin-left: auto;
}

.kopf-werkzeug form {
  margin: 0;
  display: flex;
}

/* The day navigation: a rail of its own, under the sticky row on a phone. */
.zeitnav {
  border-bottom: 1px solid var(--line);
  background: var(--bg);
}

.zeitnav .rail {
  padding-block: var(--s2);
}

.zeitnav a {
  text-decoration: none;
  white-space: nowrap;
  font-family: var(--font-display);
  font-weight: 600;
  font-stretch: var(--w-weit);
  font-size: var(--t-s);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-soft);
  padding: var(--s2) 0;
  border-bottom: 2px solid transparent;
}

.zeitnav a:hover {
  color: var(--ink);
}

.zeitnav a[aria-current] {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

/* ST5. The city moved down from 62 em to 30 em with the drop-down: it stopped
   being a label and became the way to the other cities, and a control nobody
   can see is a control nobody uses — the same argument that took the filters
   out of their folded box in 0.8.1. Under 30 em it is still not drawn: at
   360 px the header is the logo, the search field and two buttons, and a
   third control there would leave the search field about 70 px wide. The way
   to the other cities on a phone is the line in the footer, which stands at
   every width. */
@media (min-width: 30em) {
  .marke-name {
    display: inline;
  }

  .marke-stadt {
    display: inline-flex;
  }

  .stadtwahl {
    display: block;
  }
}

@media (min-width: 62em) {
  .kopf-innen {
    gap: var(--s4);
  }

  .suche {
    max-width: 24rem;
    margin-left: auto;
  }
}

/* --------------------------------------------------------------------------
   6. Rails — the one place anything scrolls sideways, and it says so

   NB3: a row of chips whose combined width beats 360 px is the second
   commonest way a page starts scrolling sideways. It is contained here:
   the rail scrolls, the page never does.
   -------------------------------------------------------------------------- */

.rail {
  display: flex;
  align-items: center;
  gap: var(--s2);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: thin;
  scroll-snap-type: x proximity;
  -webkit-overflow-scrolling: touch;
  padding-block: var(--s1);
  /* the last chip must not sit flush against the edge while scrolling */
  scroll-padding-inline: var(--s4);
}

.rail > * {
  flex: 0 0 auto;
  scroll-snap-align: start;
}

.rail::-webkit-scrollbar {
  height: 4px;
}

.rail::-webkit-scrollbar-thumb {
  background: var(--line-strong);
  border-radius: var(--r-pille);
}

/* --------------------------------------------------------------------------
   7. Buttons and form controls
   -------------------------------------------------------------------------- */

.knopf {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--s2);
  min-height: 2.25rem;
  padding: var(--s2) var(--s4);
  border-radius: var(--r-pille);
  border: 1px solid var(--line-strong);
  background: var(--surface);
  color: var(--ink);
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--t-s);
  font-stretch: var(--w-normal);
  cursor: pointer;
  text-decoration: none;
  transition: background var(--tempo) var(--kurve),
    border-color var(--tempo) var(--kurve), color var(--tempo) var(--kurve);
}

.knopf:hover {
  background: var(--surface-lift);
  border-color: var(--ink-faint);
}

.knopf-stark {
  background: var(--accent);
  border-color: var(--accent);
  color: var(--accent-ink);
}

.knopf-stark:hover {
  background: color-mix(in oklab, var(--accent) 88%, var(--ink));
  border-color: color-mix(in oklab, var(--accent) 88%, var(--ink));
  color: var(--accent-ink);
}

.knopf-still {
  background: none;
  border-color: transparent;
  color: var(--ink-soft);
  padding-inline: var(--s3);
}

.knopf-still:hover {
  background: var(--surface-lift);
  color: var(--ink);
}

.knopf-klein {
  min-height: 1.875rem;
  padding: var(--s1) var(--s3);
  font-size: var(--t-xs);
}

.knopf-rund {
  width: 2.25rem;
  min-height: 2.25rem;
  padding: 0;
}

.knopf[aria-pressed="true"] {
  background: var(--accent-wash);
  border-color: var(--accent);
  color: var(--ink);
}

/* --------------------------------------------------------------------------
   8. Chips — the filter strip, the card badges, the proposal
   -------------------------------------------------------------------------- */

.chip {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  min-height: 2rem;
  padding: var(--s1) var(--s3);
  border-radius: var(--r-pille);
  border: 1px solid color-mix(in oklab, var(--kat) 34%, var(--line));
  background: color-mix(in oklab, var(--kat) 12%, var(--surface));
  color: color-mix(in oklab, var(--kat) 78%, var(--ink));
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--t-s);
  font-stretch: var(--w-normal);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background var(--tempo) var(--kurve),
    border-color var(--tempo) var(--kurve), color var(--tempo) var(--kurve);
}

.chip:hover {
  background: color-mix(in oklab, var(--kat) 22%, var(--surface));
  border-color: var(--kat);
}

.chip[aria-pressed="true"],
.chip[aria-current="true"] {
  background: var(--kat);
  border-color: var(--kat);
  color: var(--kat-ink);
}

.chip-punkt {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: var(--kat);
  flex: 0 0 auto;
}

.chip[aria-pressed="true"] .chip-punkt,
.chip[aria-current="true"] .chip-punkt {
  background: var(--kat-ink);
}

/* A badge on a card. On a page with no filter it is not a control: no border,
   no hover, no press state. */
.chip-marke {
  min-height: 1.5rem;
  padding: 0 var(--s2);
  font-size: var(--t-xs);
  font-stretch: var(--w-weit);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  background: color-mix(in oklab, var(--kat) 16%, transparent);
  border-color: transparent;
  cursor: default;
}

/* MK1. On a list the same badge IS a control — pressing it switches that
   family on and off. It keeps the badge's size and hue and takes back the
   two things `.chip-marke` gives up, so that a pill somebody can press does
   not look exactly like one they cannot. The pressed state is the general
   `.chip[aria-current="true"]` rule above and is not repeated here; only the
   border needs saying again, because this selector would otherwise win. */
.chip-marke.chip-schalter {
  border-color: color-mix(in oklab, var(--kat) 45%, transparent);
  cursor: pointer;
}

/* TB4. The same pill again as a <button>, which is what it has to be inside a
   <summary> (TA6). A button says it is on with `aria-pressed` and a link says
   it with `aria-current` — two attributes for one state, because they are two
   different controls and ARIA has one word for each. The fill is already the
   general `.chip[aria-pressed="true"]` rule above; only the border needs
   saying twice, exactly as it did for the link. */
.chip-marke.chip-schalter[aria-current="true"],
.chip-marke.chip-schalter[aria-pressed="true"] {
  border-color: var(--kat);
}

/* A chip whose key is not a category wears no hue at all — time, price and
   district are questions, not families. */
.chip-schlicht {
  --kat: var(--ink-faint);
  color: var(--ink-soft);
  background: var(--surface);
}

.chip-schlicht[aria-pressed="true"] {
  background: var(--ink);
  border-color: var(--ink);
  color: var(--bg);
}

/* V3, K5. A filter the search field lifted out of what somebody typed. It is
   the ordinary chip in its pressed state — nothing here invents a look — plus
   the cross that says it can be taken back. */
.chip-kreuz {
  margin-inline-start: var(--s2);
  font-size: var(--t-xs);
  opacity: 0.75;
}

.chip-weg:hover .chip-kreuz {
  opacity: 1;
}

/* SG10, the one dashed thing on the site: a family proposed, never taken. */
.vorschlag {
  --kat: var(--cat-vorschlag);
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  padding: var(--s1) var(--s2) var(--s1) var(--s3);
  border-radius: var(--r-pille);
  border: 1px dashed var(--kat);
  background: color-mix(in oklab, var(--kat) 10%, transparent);
  color: color-mix(in oklab, var(--kat) 72%, var(--ink));
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--t-s);
  white-space: nowrap;
}

.vorschlag form {
  display: inline-flex;
  margin: 0;
}

.vorschlag-knopf {
  width: 1.6rem;
  height: 1.6rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid color-mix(in oklab, var(--kat) 40%, transparent);
  background: none;
  color: inherit;
  cursor: pointer;
  line-height: 1;
}

.vorschlag-knopf:hover {
  background: var(--kat);
  color: var(--kat-ink);
  border-color: var(--kat);
}

/* --------------------------------------------------------------------------
   9. The filter strip

   Everything on it stands in the open. The <details> panel that used to hold
   the rest of it went with 0.8.2 and its rules followed; nothing here draws a
   sheet, a summary or a count any more. What is left is two columns, and the
   wall between them is `.filter-oben`.
   -------------------------------------------------------------------------- */

.filter {
  position: relative;
  padding-block: var(--s3);
  border-bottom: 1px solid var(--line);
}

/* FI19. THE RIGHT COLUMN: every filter that is not a family, right-aligned,
   each group whole.

   THE TWO GAPS DO TWO DIFFERENT JOBS and the operator asked for both to grow
   on 2026-08-10 — "some more space between the languages and the open air, and
   also some more space between the languages row pills and the bezirke":

   - `--s6` sideways is what stands between two groups sharing a row, so the
     last language chip and the Open Air chip cannot read as one run of chips.
   - `--s4` downwards is what stands between two rows of groups, so the
     languages and the districts are two blocks and not a paragraph.

   Against `--s1` inside a group, both say the same thing: what is close
   together belongs together. */
.filter-rechts {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: flex-end;
  gap: var(--s4) var(--s6);
  min-width: 0;
}

/* One group of the right column: the languages, Open Air, the districts, the
   genres. It is right-aligned in itself as well, so a group wide enough to
   wrap keeps its second line under the right edge and not adrift on the left
   (the whole of the operator's complaint of 2026-08-10). */
.filter-immer {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-end;
  gap: var(--s1);
  min-width: 0;
}

/* FI18. The way out of everything: a group of the right column like any
   other, and only drawn when there is something to clear. */
.filter-schalter {
  gap: var(--s2);
}

.filter-allesweg {
  flex: 0 0 auto;
}

/* The genres in a row's fold-out — a word, never a colour. Colour on this
   site means a family and nothing else. */
.tz-genres {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1);
  margin: 0 0 var(--s2);
}

/* FI19. TWO COLUMNS, AND AN INVISIBLE WALL BETWEEN THEM.

   The operator's words, 2026-08-10: "NUR kategorien links aligned. ALLE
   anderen rechts aligned. Kategorien | andere ... die beiden pillen bereiche
   sind wie durch eine unsichtbare wand getrennt, verlaufen in eigenen
   columns."

   So this is a grid and not a flex row, and that is the whole point: a flex
   row with `space-between` puts the seam wherever the content happens to end,
   which moves from page to page and is what made the strip read as a heap.
   Two equal tracks put the seam in the same place on every page, the families
   start at the left edge of the first, and everything else ends at the right
   edge of the second. The wall is the gap — no hairline: the operator asked
   for an invisible one, and a rule there is a third thing on a strip that is
   already busy.

   `align-items: start` matters as much: with `center` the families floated
   half a line down as soon as the right column wrapped, and nothing on the
   strip lined up with anything.

   Under 46 em there is no wall, because there is no room for two columns:
   one track, the families above with their sideways rail (NA3), the rest
   under them and still right-aligned, so the two areas stay two areas. */
.filter-oben {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  align-items: start;
  gap: var(--s3) var(--s5);
}

/* The left column, and the only thing on this strip that is left-aligned. Two
   rows stacked: the six families, and under them the genres of whichever
   family is on — which is where the operator wants them and nowhere else.

   `min-width: 0` is what lets the column shrink instead of widening the page
   (NA2); below 46 em `.rail` inside it then scrolls sideways, as it always
   did. `max-width: 100%` on the children is the other half of the same rule:
   without it the genre row, which wraps rather than scrolls, would push the
   column wider than its track. */
.filter-links {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--s2);
  min-width: 0;
}

.filter-links > * {
  max-width: 100%;
  min-width: 0;
}

/* FI17. The genres, under their families and in their families' colours. They
   wrap — they are not a rail, and NA3 allows this site exactly two sideways
   scrollers, of which `.rail` above is already one. */
.filter-unter {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-start;
  gap: var(--s1);
}

@media (min-width: 46em) {
  .filter-oben {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }

  /* In a column of its own the rail has nowhere to scroll to and no reason
     to. The arithmetic: 76 rem of hull gives each track about 596 px and the
     six families measure about 549 px, so they sit on one line at the full
     width and wrap to two as the window narrows. A scroller half the page
     wide would hide chips nobody would think to look for. */
  .filter-links > .rail {
    overflow-x: visible;
    flex-wrap: wrap;
  }
}

/* FI13. What the search field understood as a filter. A group of the right
   column like any other, with a word in front of its chips, so it needs a
   little more air between them than a bare group of chips does. */
.filter-suche {
  gap: var(--s2);
}

/* The line's own top margin belongs to a group of chips it stands under, not
   to a row it stands in. */
.filter-suche .filter-hinweis {
  margin-top: 0;
}

.filter-wahl {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}

/* One quiet line under a group of chips, where the chips alone would be
   misread: the genres are empty until a category is chosen, and pressing a
   language also shows everything that needs none. */
.filter-hinweis {
  margin-top: var(--s2);
  font-size: var(--t-xs);
  color: var(--ink-faint);
}

.filter-fuss {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  align-items: center;
  justify-content: space-between;
  border-top: 1px solid var(--line);
  padding-top: var(--s3);
}

/* --------------------------------------------------------------------------
   10. The day header, and the evening rail it belongs to

   The signature of this site: an evening is a timetable. A hairline runs
   down the left of every day, the time sits in its own column against it,
   and a dot in the event's own family hue marks the hour.
   -------------------------------------------------------------------------- */

.tag {
  margin-bottom: var(--s6);
}

.tagkopf {
  position: sticky;
  top: var(--kopf);
  z-index: 20;
  display: flex;
  align-items: baseline;
  gap: var(--s3);
  padding: var(--s3) 0 var(--s2);
  background: linear-gradient(
    to bottom,
    var(--bg) 62%,
    color-mix(in oklab, var(--bg) 0%, transparent)
  );
}

.tagkopf::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: var(--line);
}

.tagkopf[data-fest="ja"] {
  background: color-mix(in oklab, var(--bg) 94%, transparent);
  backdrop-filter: blur(10px);
}

.tagkopf-marke {
  font-family: var(--font-display);
  font-weight: 800;
  font-stretch: var(--w-weit);
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.16em;
  color: var(--accent);
  flex: 0 0 auto;
}

.tagkopf-datum {
  font-family: var(--font-display);
  font-weight: 700;
  font-stretch: var(--w-eng);
  font-size: var(--t-2xl);
  letter-spacing: -0.02em;
  min-width: 0;
}

.tagkopf-zahl {
  margin-left: auto;
  flex: 0 0 auto;
  font-size: var(--t-xs);
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
  white-space: nowrap;
}

/* --------------------------------------------------------------------------
   11. The card
   -------------------------------------------------------------------------- */

.gitter {
  display: grid;
  gap: var(--s4);
  /* The min() guard is what stops a 17 rem track overflowing a 360 px page:
     below 17 rem the track simply becomes the full width. */
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
}

.karte {
  position: relative;
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--kat);
  border-radius: var(--r2);
  overflow: hidden;
  transition: transform var(--tempo) var(--kurve),
    border-color var(--tempo) var(--kurve), box-shadow var(--tempo) var(--kurve);
}

.karte:hover {
  border-color: var(--line-strong);
  border-left-color: var(--kat);
  box-shadow: var(--sh2);
  transform: translateY(-2px);
}

.karte:focus-within {
  border-color: var(--line-strong);
  box-shadow: var(--sh2);
}

/* The picture, and the generated ground behind it. The ground is always
   there: when the picture 404s, mycity.js takes the <img> away and the ground
   is already underneath — never a broken-image glyph (PI5). */
.karte-bild {
  position: relative;
  aspect-ratio: 3 / 2;
  overflow: hidden;
  background-color: color-mix(in oklab, var(--kat) 14%, var(--surface-lift));
  /* so the initials on the generated ground below can be sized against the
     card's own width rather than the screen's */
  container-type: inline-size;
}

.karte-bild img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* PI1: the category hue at 14 % over --surface-lift, the venue's initials in
   Archivo at 18 %. Nothing else. */
.karte-grund {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-family: var(--font-display);
  font-weight: 800;
  font-stretch: var(--w-eng);
  font-size: clamp(3rem, 22cqw, 5.5rem);
  letter-spacing: -0.04em;
  color: var(--kat);
  opacity: 0.18;
  user-select: none;
}

.karte-gutschrift {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s2);
  padding: var(--s2) var(--s4) 0;
  font-size: var(--t-xs);
  color: var(--ink-faint);
}

.karte-gutschrift a {
  color: var(--ink-faint);
}

.karte-koerper {
  display: flex;
  flex-direction: column;
  gap: var(--s2);
  padding: var(--s4);
  flex: 1 1 auto;
}

.karte-wann {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  font-family: var(--font-display);
  font-weight: 600;
  font-stretch: var(--w-weit);
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

.karte-uhr {
  color: var(--ink);
  font-weight: 700;
  letter-spacing: 0.06em;
}

.karte-titel {
  font-size: var(--t-xl);
  font-stretch: var(--w-eng);
  font-weight: 700;
  letter-spacing: -0.015em;
}

.karte-titel a {
  text-decoration: none;
}

/* The whole card is the target, and the title is still the only thing a
   screen reader announces as the link. */
.karte-titel a::after {
  content: "";
  position: absolute;
  inset: 0;
}

.karte-untertitel {
  font-size: var(--t-s);
  color: var(--ink-soft);
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.karte-ort {
  font-size: var(--t-s);
  color: var(--ink-soft);
}

.karte-ort a {
  position: relative;
  z-index: 1;
}

.karte-bezirk {
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

.karte-fuss {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-top: auto;
  padding-top: var(--s2);
}

.preis {
  font-family: var(--font-display);
  font-weight: 700;
  font-stretch: var(--w-normal);
  font-size: var(--t-s);
  font-variant-numeric: tabular-nums;
  color: var(--ink);
}

.preis-gratis {
  color: var(--good);
  text-transform: uppercase;
  font-stretch: var(--w-weit);
  font-size: var(--t-xs);
  letter-spacing: 0.1em;
}

.preis-offen {
  color: var(--ink-faint);
  font-weight: 500;
}

.karten-stand {
  font-size: var(--t-xs);
  font-weight: 600;
  padding: 0.1rem var(--s2);
  border-radius: var(--r1);
  border: 1px solid currentColor;
  white-space: nowrap;
}

.stand-verfuegbar { color: var(--good); }
.stand-wenige { color: var(--warn); }
.stand-ausverkauft { color: var(--bad); }
.stand-abendkassa { color: var(--ink-soft); }

.abgesagt {
  color: var(--bad);
  font-weight: 700;
  text-transform: uppercase;
  font-size: var(--t-xs);
  letter-spacing: 0.1em;
}

.karte-abgesagt .karte-titel {
  text-decoration: line-through;
  text-decoration-color: var(--bad);
}

/* The save control sits above the card-wide link. */
.merken {
  position: relative;
  z-index: 1;
  margin-left: auto;
}

.merken form {
  margin: 0;
}

.merken-knopf[aria-pressed="true"] {
  color: var(--cat-buehne);
  border-color: currentColor;
  background: color-mix(in oklab, var(--cat-buehne) 12%, transparent);
}

/* Both states are in the markup and the stylesheet shows one. That is what
   keeps every German word in app/strings/de.py and leaves mycity.js with one
   attribute to flip and no text to invent. */
.merken-knopf .herz-an,
.merken-knopf .merken-an {
  display: none;
}

.merken-knopf[aria-pressed="true"] .herz-aus,
.merken-knopf[aria-pressed="true"] .merken-aus {
  display: none;
}

.merken-knopf[aria-pressed="true"] .herz-an,
.merken-knopf[aria-pressed="true"] .merken-an {
  display: inline;
}

.merken-knopf svg {
  display: block;
}

.marken {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1);
}

/* --- the card on a phone, below 30 em ------------------------------------

   WHERE THE HEIGHT WENT, measured at 360 px. The page has 328 px of content
   and the card's own borders leave 324 px inside it, so the 3:2 picture box
   was 216 px tall — 48 % of a 449 px card, and the single largest thing on
   it by a wide margin. A 640 px screen less the 3.5rem sticky header is
   584 px of list, which held one card and a quarter of the next.

   The same picture beside the text instead of above it is 88 px, and the
   card is 199 px: two whole cards and most of a third. Nothing is dropped
   and nothing is shrunk — the title keeps --t-xl and is still the biggest
   thing on the card, the venue, the chips, the price and the save control
   all stay. The save button GROWS, to the 44 px a finger needs, because it
   is the one thing on a card that is tapped rather than read.

   The rest of the saving is the three paragraph bottom margins inside the
   body, which were 12 px each on top of a gap that already separated them.
   -------------------------------------------------------------------------- */

@media (max-width: 29.99em) {
  .gitter {
    gap: var(--s3);
  }

  .karte {
    display: grid;
    grid-template-columns: 5.5rem minmax(0, 1fr);
    column-gap: var(--s3);
    padding: var(--s3);
  }

  /* `align-self: start` is what keeps it a square. A grid item stretches to
     its row by default and a stretched item ignores its aspect-ratio, so
     without this line the picture becomes an 88 px strip the full height of
     the card and every landscape photograph is cropped to a sliver. */
  .karte > .karte-bild {
    aspect-ratio: 1;
    align-self: start;
    border-radius: var(--r1);
  }

  /* The clamp floor was written for a 324 px box, where 22cqw wins anyway.
     In an 88 px box the floor is doing all the work and 3rem all but fills
     it, so the ground reads as a logo rather than as a ground (PI1). */
  .karte > .karte-bild .karte-grund {
    font-size: clamp(1.75rem, 38cqw, 2.5rem);
  }

  .karte > .karte-gutschrift {
    grid-column: 1 / -1;
    padding: var(--s2) 0 0;
  }

  .karte-koerper {
    padding: 0;
    gap: var(--s1);
  }

  .karte-koerper p {
    margin-bottom: 0;
  }

  .karte-fuss .merken-knopf {
    min-height: 2.75rem;
  }

  /* The loading state is the shape of the answer and not a spinner, so it
     takes the same shape here — otherwise the page jumps the moment the real
     cards land, which is the one thing that state exists to prevent. */
  .laedt {
    gap: var(--s3);
  }

  .laedt-karte {
    display: grid;
    grid-template-columns: 5.5rem minmax(0, 1fr);
    column-gap: var(--s3);
    padding: var(--s3);
  }

  .laedt-bild {
    aspect-ratio: 1;
    align-self: start;
    border-radius: var(--r1);
  }

  .laedt-koerper {
    padding: 0;
  }
}

/* The language line under the chips on an event page. One quiet sentence: it
   is a fact about the evening, not a control. */
.veranstaltung-sprache {
  margin-top: var(--s2);
  font-size: var(--t-s);
  color: var(--ink-soft);
}

/* --------------------------------------------------------------------------
   12. The dense row — the same event, read as a timetable
   -------------------------------------------------------------------------- */

.zeilen {
  border-left: var(--rail) solid var(--line);
  padding-left: var(--s4);
  margin-left: var(--s2);
  display: flex;
  flex-direction: column;
}

.zeile {
  position: relative;
  display: grid;
  grid-template-columns: var(--zeit-spalte) minmax(0, 1fr);
  column-gap: var(--s3);
  row-gap: var(--s1);
  padding: var(--s3) 0;
  border-bottom: 1px solid var(--line);
  transition: background var(--tempo) var(--kurve);
}

.zeile:hover {
  background: var(--surface-sink);
}

.zeile:last-child {
  border-bottom: 0;
}

/* the dot on the rail, in the event's own family hue */
.zeile::before {
  content: "";
  position: absolute;
  left: calc(-1 * var(--s4) - var(--rail) - 3px);
  top: calc(var(--s3) + 0.45rem);
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--kat);
  box-shadow: 0 0 0 3px var(--bg);
}

.zeile-zeit {
  grid-column: 1;
  grid-row: 1;
  font-family: var(--font-display);
  font-weight: 700;
  font-stretch: var(--w-eng);
  font-size: var(--t-l);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  color: var(--ink);
}

.zeile-zeit-offen {
  font-size: var(--t-xs);
  font-stretch: var(--w-weit);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-faint);
}

.zeile-haupt {
  grid-column: 2;
  grid-row: 1;
  min-width: 0;
}

.zeile-titel {
  font-family: var(--font-display);
  font-weight: 600;
  font-stretch: var(--w-eng);
  font-size: var(--t-l);
  letter-spacing: -0.01em;
}

.zeile-titel a {
  text-decoration: none;
}

.zeile-titel a::after {
  content: "";
  position: absolute;
  inset: 0;
}

.zeile-ort {
  font-size: var(--t-s);
  color: var(--ink-soft);
}

.zeile-fuss {
  grid-column: 2;
  grid-row: 2;
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--s2);
}

@media (min-width: 46em) {
  .zeile {
    grid-template-columns: 4.5rem minmax(0, 1fr) auto;
    align-items: center;
    row-gap: 0;
  }

  .zeile-fuss {
    grid-column: 3;
    grid-row: 1;
    justify-content: flex-end;
  }
}

/* --------------------------------------------------------------------------
   13. The reason line and its menu (OV1)
   -------------------------------------------------------------------------- */

.grund {
  position: relative;
  z-index: 1;
  margin-top: var(--s2);
}

.grund > summary {
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  cursor: pointer;
  font-size: var(--t-xs);
  color: var(--ink-soft);
  border-bottom: 1px dotted var(--line-strong);
  padding-bottom: 1px;
}

.grund > summary::-webkit-details-marker {
  display: none;
}

.grund > summary:hover {
  color: var(--ink);
  border-bottom-color: var(--ink-faint);
}

.grund-strich {
  width: 1.25rem;
  height: 2px;
  border-radius: var(--r-pille);
  background: var(--kat);
  flex: 0 0 auto;
}

.grund-menue {
  position: absolute;
  left: 0;
  top: calc(100% + var(--s2));
  z-index: 30;
  min-width: 14rem;
  max-width: min(20rem, calc(100vw - var(--s6)));
  padding: var(--s2);
  display: grid;
  gap: var(--s1);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r2);
  box-shadow: var(--sh3);
}

.grund-menue form {
  margin: 0;
}

.grund-menue button,
.grund-menue a {
  display: block;
  width: 100%;
  text-align: left;
  background: none;
  border: 0;
  border-radius: var(--r1);
  padding: var(--s2) var(--s3);
  font-size: var(--t-s);
  color: var(--ink);
  cursor: pointer;
  text-decoration: none;
}

.grund-menue button:hover,
.grund-menue a:hover {
  background: var(--surface-lift);
}

.grund-menue .weg {
  color: var(--bad);
}

/* The card leaves in 140 ms and the undo bar takes over (OV1). */
.karte[data-weg="ja"],
.zeile[data-weg="ja"],
.tz-falt[data-weg="ja"] {
  opacity: 0;
  transform: scale(0.97);
  max-height: 0;
  padding-block: 0;
  margin-block: 0;
  border-width: 0;
  overflow: hidden;
  transition: opacity var(--tempo) var(--kurve),
    transform var(--tempo) var(--kurve), max-height var(--tempo) var(--kurve),
    padding var(--tempo) var(--kurve), margin var(--tempo) var(--kurve);
}

/* --------------------------------------------------------------------------
   14. The undo bar
   -------------------------------------------------------------------------- */

.rueck {
  position: fixed;
  z-index: 50;
  left: var(--s3);
  right: var(--s3);
  bottom: calc(var(--s3) + env(safe-area-inset-bottom, 0px));
  max-width: 28rem;
  margin-inline: auto;
  display: flex;
  align-items: center;
  gap: var(--s3);
  padding: var(--s3) var(--s3) var(--s3) var(--s4);
  background: var(--surface-lift);
  border: 1px solid var(--line-strong);
  border-radius: var(--r3);
  box-shadow: var(--sh3);
}

.rueck[hidden] {
  display: none;
}

.rueck-text {
  flex: 1 1 auto;
  min-width: 0;
  font-size: var(--t-s);
}

.rueck-uhr {
  position: absolute;
  left: var(--s4);
  right: var(--s4);
  bottom: 0.4rem;
  height: 2px;
  border-radius: var(--r-pille);
  background: var(--line);
  overflow: hidden;
}

.rueck-uhr span {
  display: block;
  height: 100%;
  width: 100%;
  background: var(--accent);
  transform-origin: left center;
  animation: rueck-ablauf 8s linear forwards;
}

@keyframes rueck-ablauf {
  from { transform: scaleX(1); }
  to { transform: scaleX(0); }
}

/* --------------------------------------------------------------------------
   14b. The city chooser at /mycity/ (L1E2)
   --------------------------------------------------------------------------

   Three cards and no width of its own: the list is `.gitter`, whose track is
   minmax(min(100%, 17rem), 1fr), so at 360 and 390 px it is one full-width
   column and nothing can overflow. The card is the same surface, line and
   radius as an event card, without the category stripe an event card carries
   on its left edge — a city has no family. */

.stadtwahl-karte {
  position: relative;
  display: grid;
  gap: var(--s2);
  padding: var(--s5);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  transition: border-color var(--tempo) var(--kurve);
}

.stadtwahl-karte:hover,
.stadtwahl-karte:focus-within {
  border-color: var(--line-strong);
}

.stadtwahl-name {
  font-size: var(--t-2xl);
  font-stretch: var(--w-eng);
  letter-spacing: -0.02em;
}

/* SA2: the whole card is the one link, so the count below it is not a second
   tab stop. */
.stadtwahl-name a {
  text-decoration: none;
}

.stadtwahl-name a::after {
  content: "";
  position: absolute;
  inset: 0;
}

.stadtwahl-was {
  color: var(--ink-soft);
}

.stadtwahl-zahl {
  font-family: var(--font-display);
  font-size: var(--t-xs);
  font-stretch: var(--w-weit);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-faint);
}

/* --------------------------------------------------------------------------
   15. Empty, loading and error
   -------------------------------------------------------------------------- */

.leer {
  display: grid;
  gap: var(--s3);
  justify-items: start;
  padding: var(--s7) 0;
  max-width: 34rem;
}

.leer-zeichen {
  display: flex;
  gap: var(--s1);
}

.leer-zeichen i {
  display: block;
  width: 1.75rem;
  height: 3px;
  border-radius: var(--r-pille);
  background: var(--line-strong);
}

.leer-zeichen i:nth-child(1) { width: 2.5rem; background: var(--cat-konzert); opacity: 0.5; }
.leer-zeichen i:nth-child(2) { width: 1.25rem; background: var(--cat-film); opacity: 0.5; }
.leer-zeichen i:nth-child(3) { width: 2rem; background: var(--cat-kunst); opacity: 0.5; }

.leer-titel {
  font-size: var(--t-2xl);
  font-stretch: var(--w-eng);
}

.leer-text {
  color: var(--ink-soft);
}

.leer-fehler .leer-zeichen i {
  background: var(--bad);
  opacity: 0.6;
}

/* The loading state is the shape of the answer, not a spinner: the reader
   already knows what is coming and the page does not jump when it lands. */
.laedt {
  display: grid;
  gap: var(--s4);
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 17rem), 1fr));
}

.laedt-karte {
  border: 1px solid var(--line);
  border-radius: var(--r2);
  overflow: hidden;
  background: var(--surface);
}

.laedt-bild {
  aspect-ratio: 3 / 2;
  background: var(--surface-lift);
}

.laedt-koerper {
  padding: var(--s4);
  display: grid;
  gap: var(--s2);
}

.laedt-balken {
  height: 0.75rem;
  border-radius: var(--r1);
  background: var(--surface-lift);
}

.laedt-balken.kurz { width: 45%; }
.laedt-balken.lang { width: 85%; }

.laedt-karte,
.laedt-bild,
.laedt-balken {
  animation: pulsen 1.4s ease-in-out infinite;
}

@keyframes pulsen {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.55; }
}

/* While a filter swap is in flight the answer on screen is the old one. It is
   dimmed rather than thrown away: an empty box under a pressed chip reads as
   "nothing found" for the third of a second before the real answer lands. */
[data-ergebnis][aria-busy="true"] {
  cursor: progress;
  opacity: 0.5;
  transition: opacity var(--tempo) var(--kurve);
}

/* Flask's own flashed messages. Views put German in them from
   app/strings/de.py, never a sentence written on the spot. */
.meldungen {
  display: grid;
  gap: var(--s2);
  margin-bottom: var(--s4);
}

.meldung {
  padding: var(--s3) var(--s4);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--r2);
  background: var(--surface);
  font-size: var(--t-s);
}

/* --------------------------------------------------------------------------
   16. The storage notice (NW1 to NW7)

   A normal block at the foot of the page. Not a modal, nothing over the
   content, nothing that moves.
   -------------------------------------------------------------------------- */

.hinweis {
  margin: var(--s7) 0 var(--s5);
  padding: var(--s5);
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--accent);
  border-radius: var(--r2);
  max-width: 44rem;
}

.hinweis h2 {
  font-size: var(--t-l);
  font-stretch: var(--w-eng);
  margin-bottom: var(--s3);
}

.hinweis ul {
  display: grid;
  gap: var(--s2);
  margin-bottom: var(--s3);
}

.hinweis li {
  position: relative;
  padding-left: var(--s4);
  font-size: var(--t-s);
  color: var(--ink-soft);
}

.hinweis li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.55em;
  width: 8px;
  height: 2px;
  border-radius: var(--r-pille);
  background: var(--line-strong);
}

.hinweis-abschluss {
  font-size: var(--t-s);
  margin-bottom: var(--s4);
}

.hinweis-fuss {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s4);
}

.hinweis-fuss form {
  margin: 0;
}

/* --------------------------------------------------------------------------
   17. Main and footer
   -------------------------------------------------------------------------- */

.inhalt {
  flex: 1 1 auto;
  padding-block: var(--s5) var(--s7);
}

.seitentitel {
  font-size: var(--t-3xl);
  font-stretch: var(--w-eng);
  font-weight: 800;
  letter-spacing: -0.03em;
  margin-bottom: var(--s2);
}

.seiten-unter {
  color: var(--ink-soft);
  margin-bottom: var(--s5);
  max-width: 44rem;
}

.fuss {
  border-top: 1px solid var(--line);
  background: var(--bg-deep);
  padding-block: var(--s5);
  font-size: var(--t-s);
  color: var(--ink-soft);
}

.fuss-innen {
  display: grid;
  gap: var(--s4);
}

.fuss-nav {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2) var(--s4);
}

.fuss-nav a {
  color: var(--ink-soft);
  text-decoration: none;
}

.fuss-nav a:hover {
  color: var(--ink);
  text-decoration: underline;
}

.fuss-marke {
  display: flex;
  align-items: baseline;
  gap: var(--s2);
  font-family: var(--font-display);
  font-weight: 700;
  font-stretch: var(--w-eng);
  color: var(--ink);
}

.fuss-ausgabe {
  font-family: var(--font-display);
  font-weight: 500;
  font-stretch: var(--w-weit);
  font-size: var(--t-xs);
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

@media (min-width: 46em) {
  .fuss-innen {
    grid-template-columns: auto minmax(0, 1fr);
    align-items: center;
  }

  .fuss-nav {
    justify-content: flex-end;
  }
}

/* --------------------------------------------------------------------------
   18. Paging — one page at a time on the server, and from there the browser
       goes on by itself (EL1 to EL4)

   The brief said pages and never infinite scroll (B3, B6). The operator asked
   for the opposite on 2026-08-09 and his word wins; the reversal is written
   out in section 7 of docs/design/GESTALTUNG.md. What changed is only what
   the browser does — the server still answers one page at a time, every page
   is still an address, and the control below is still real, still rendered
   and still the whole answer for a reader with no script.
   -------------------------------------------------------------------------- */

.blaettern {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s3);
  margin-top: var(--s6);
  padding-top: var(--s4);
  border-top: 1px solid var(--line);
}

.blaettern-stand {
  font-size: var(--t-s);
  color: var(--ink-faint);
  font-variant-numeric: tabular-nums;
}

/* The empty mark behind the control (EL1 to EL4). It measures nothing while
   it is at rest — it exists to be seen coming, and a box with a height would
   push the control around for no reason.

   While a block is on its way `mycity.js` puts `aria-busy` on it, and this is
   the only thing that says so. It has to be drawn and not written: there is
   no key for `wird geladen` in app/strings/de.py, and a German word invented
   in a stylesheet would break M22 twice over — wrong file, and unreachable
   for `t()`. A reading aid needs no drawing anyway; it is told by `aria-busy`
   itself. No animation: the block is fetched 600 px before the mark is
   reached, so this is a mark that something is happening and not a thing
   anybody sits and watches. */
[data-nachlader][aria-busy]::after {
  content: "";
  display: block;
  width: 4rem;
  height: 2px;
  margin: var(--s4) auto 0;
  border-radius: var(--r1);
  background: var(--line-strong);
}

/* --------------------------------------------------------------------------
   19. Forms, tables and the small furniture the other pages need

   The card, the row and the filter strip are this project's own shapes. What
   follows is the plain stuff every remaining page wants — sign in, settings,
   my categories, why am I seeing this, and the whole admin area — so that no
   view has to invent a look of its own and no second stylesheet is ever
   started.
   -------------------------------------------------------------------------- */

.feld {
  display: grid;
  gap: var(--s1);
  margin-bottom: var(--s4);
}

.feld > label,
.formular-schild {
  font-family: var(--font-display);
  font-weight: 600;
  font-stretch: var(--w-weit);
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.1em;
  color: var(--ink-soft);
}

input[type="text"],
input[type="email"],
input[type="password"],
input[type="search"]:not(.suche-feld),
input[type="number"],
input[type="date"],
input[type="url"],
select,
textarea {
  width: 100%;
  max-width: 100%;
  padding: var(--s2) var(--s3);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r1);
  outline: none;
}

input:focus-visible,
select:focus-visible,
textarea:focus-visible {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-wash);
}

input[type="checkbox"],
input[type="radio"] {
  width: 1.1rem;
  height: 1.1rem;
  accent-color: var(--accent);
}

/* A page that is one column of text and one small form: sign in, sign out,
   settings, the privacy page, the sources page. */
.konto,
.textseite {
  max-width: 34rem;
}

.konto h1,
.textseite h1 {
  font-size: var(--t-3xl);
  font-stretch: var(--w-eng);
  letter-spacing: -0.03em;
  margin-bottom: var(--s4);
}

.konto-formular {
  margin-block: var(--s4);
}

.konto-frage,
.konto-hinweis {
  color: var(--ink-soft);
  font-size: var(--t-s);
}

.konto-fehler {
  padding: var(--s3) var(--s4);
  border: 1px solid var(--bad);
  border-radius: var(--r2);
  background: color-mix(in oklab, var(--bad) 10%, transparent);
  margin-bottom: var(--s4);
}

/* A weight, 0 to 100, as a plain bar with the number beside it (OV2). It
   wears the family's own hue, which is the only place on that page colour
   appears at all. */
.gewicht {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--s3);
}

.gewicht-balken {
  height: 8px;
  border-radius: var(--r-pille);
  background: var(--surface-lift);
  overflow: hidden;
}

.gewicht-balken > span {
  display: block;
  height: 100%;
  border-radius: var(--r-pille);
  background: var(--kat);
}

.gewicht-zahl {
  font-family: var(--font-display);
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  font-size: var(--t-s);
}

/* An admin table. It scrolls inside its own box rather than making the page
   scroll — the third way a narrow screen breaks, and the one a data table
   causes every time. */
.tabelle-huelle {
  overflow-x: auto;
  overscroll-behavior-x: contain;
  border: 1px solid var(--line);
  border-radius: var(--r2);
}

table.tabelle {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--t-s);
}

table.tabelle th,
table.tabelle td {
  padding: var(--s2) var(--s3);
  text-align: left;
  border-bottom: 1px solid var(--line);
  vertical-align: top;
  white-space: nowrap;
}

table.tabelle th {
  font-family: var(--font-display);
  font-weight: 700;
  font-stretch: var(--w-weit);
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--ink-faint);
  background: var(--surface-sink);
  position: sticky;
  top: 0;
}

table.tabelle td.breit {
  white-space: normal;
  min-width: 14rem;
}

table.tabelle tr:last-child td {
  border-bottom: 0;
}

/* A folding section, which is how every admin page is built (AD1). */
details.falt {
  border: 1px solid var(--line);
  border-radius: var(--r2);
  background: var(--surface);
  margin-bottom: var(--s3);
}

details.falt > summary {
  list-style: none;
  cursor: pointer;
  padding: var(--s3) var(--s4);
  font-family: var(--font-display);
  font-weight: 700;
  font-stretch: var(--w-eng);
  font-size: var(--t-l);
}

details.falt > summary::-webkit-details-marker {
  display: none;
}

details.falt[open] > summary {
  border-bottom: 1px solid var(--line);
}

details.falt > .falt-inhalt {
  padding: var(--s4);
}

/* --------------------------------------------------------------------------
   20. The event page and the venue page (UR9, UR10)

   These two are read differently from a list: one thing at a time, top to
   bottom, on a column narrow enough to read. So they get the reading measure
   rather than the full shell width, a hairline between sections instead of a
   card each, and the same time column the day rail uses — the times of a
   four-date run have to line up under one another or the eye cannot compare
   them.

   Every hue here is `--kat`, which the page sets once on `.veranstaltung`
   through its `k-<slug>` class. Nothing below invents a colour.
   -------------------------------------------------------------------------- */

.veranstaltung,
.ort-kopf,
.ort-programm {
  max-width: 46rem;
}

.veranstaltung > .karte-bild {
  border-radius: var(--r2);
  margin-bottom: var(--s2);
}

.veranstaltung > .karte-gutschrift {
  padding-inline: 0;
  margin-bottom: var(--s5);
}

.veranstaltung > section {
  border-top: 1px solid var(--line);
  padding-block: var(--s5);
}

.veranstaltung > section > h2,
.ort-programm > h2 {
  font-family: var(--font-display);
  font-size: var(--t-xs);
  font-stretch: var(--w-weit);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-faint);
  margin-bottom: var(--s3);
}

/* --- the dates ---------------------------------------------------------- */

.termine {
  list-style: none;
  display: grid;
  gap: var(--s1);
}

.termin {
  display: grid;
  /* The time keeps its own column so four dates read as a table. `auto` on
     the last one lets a cancellation mark sit beside the time without
     pushing the date out of line. `minmax(0, 1fr)` and not a bare `1fr`:
     a bare one carries an implicit `min-width: auto`, and one long date
     string would then widen the whole page (NA2). */
  grid-template-columns: minmax(0, 1fr) var(--zeit-spalte) auto;
  align-items: baseline;
  gap: var(--s2);
  padding-block: var(--s2);
  border-bottom: 1px solid var(--line);
}

.termin:last-child {
  border-bottom: 0;
}

.termin-uhr {
  font-variant-numeric: tabular-nums;
  color: var(--ink-soft);
  text-align: right;
}

/* A date already past stays on the page — somebody following a link from a
   mail wants to see that they missed it — but it stops competing for the eye. */
.termin-vorbei {
  color: var(--ink-faint);
}

.termin-vorbei .termin-uhr {
  color: var(--ink-faint);
}

.termin-marke {
  font-size: var(--t-xs);
  color: var(--ink-faint);
}

/* --- the venue block on the event page ---------------------------------- */

.ort-name {
  font-size: var(--t-l);
  font-weight: 600;
}

.ort-adresse {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s3);
  color: var(--ink-soft);
  margin-top: var(--s1);
}

.ort-zugang,
.ort-website {
  margin-top: var(--s2);
  font-size: var(--t-s);
  color: var(--ink-soft);
}

/* --- price, tickets, description, organiser ------------------------------ */

.veranstaltung-karten {
  margin-top: var(--s3);
}

.veranstaltung-text p {
  max-width: 38rem;
  margin-bottom: var(--s3);
}

.veranstaltung-text p:last-child {
  margin-bottom: 0;
}

/* --- where it came from -------------------------------------------------- */

/* EP8: the strongest control on the page points away from here, because that
   is what this site is for. */
.veranstaltung-hin {
  margin-bottom: var(--s4);
}

.quellen {
  list-style: none;
  display: grid;
  gap: var(--s3);
}

.quelle {
  display: grid;
  gap: var(--s1);
  font-size: var(--t-s);
  padding-left: var(--s3);
  border-left: 2px solid var(--kat);
}

.quelle-lizenz,
.quelle-nennung,
.quelle-stand {
  color: var(--ink-faint);
}

.quelle-stand {
  margin-top: var(--s3);
  font-size: var(--t-xs);
}

/* --- the venue page ------------------------------------------------------ */

.ort-kopf {
  padding-bottom: var(--s5);
  border-bottom: 1px solid var(--line);
  margin-bottom: var(--s5);
}

.ort-kopf .seiten-unter {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1) var(--s3);
  margin-bottom: var(--s2);
}

/* --- 360 and 390 px ------------------------------------------------------ */

/* A long unbroken event title — and Vienna programme titles are long — would
   otherwise widen the whole page. Three columns of dates do not fit either:
   under 26em the time drops onto its own line under the date. */
@media (max-width: 25.99em) {
  .termin {
    grid-template-columns: minmax(0, 1fr) auto;
  }

  .termin-uhr {
    text-align: left;
  }
}

/* --------------------------------------------------------------------------
   21. Motion, only where somebody asked for it
   -------------------------------------------------------------------------- */

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }

  .karte:hover {
    transform: none;
  }
}

/* --------------------------------------------------------------------------
   22. The calendar — two months, and the range picked out of them

   ON A PHONE THE TWO MONTHS STACK, and that is the whole narrow-screen answer
   here. Side by side on 328 px of content each month gets 164 px, which is
   23 px a day cell: under a thumb, under a legible digit, and unreadable. The
   only other way out would be a sideways scroller, and NA3 allows exactly two
   of those on this site (.rail and .tabelle-huelle) — a calendar is not going
   to be a third. So the second month sits under the first until there is
   46 em of room, which is the same breakpoint the filter panel already uses.

   Stacked, one month is seven columns of (328 - 6 x 4) / 7 = 43 px, and the
   cell is square with a 36 px floor. Nothing in this block is a fixed width.
   -------------------------------------------------------------------------- */

.kalender {
  max-width: 46rem;
  margin-bottom: var(--s5);
  padding-bottom: var(--s4);
  border-bottom: 1px solid var(--line);
}

.kalender-kopf {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--s2);
}

.kalender-marke {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 800;
  font-stretch: var(--w-weit);
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: var(--ink-faint);
}

.kalender-schritte {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s1);
}

.kalender-satz {
  margin-top: var(--s1);
  font-size: var(--t-s);
  color: var(--ink-soft);
}

.kalender-weg {
  margin-left: var(--s2);
  color: var(--ink-faint);
  white-space: nowrap;
}

.kalender-monate {
  display: grid;
  gap: var(--s5);
  margin-top: var(--s4);
}

.kalender-monate > *,
.kalender-gitter > * {
  min-width: 0;
}

.kalender-monat-name {
  font-family: var(--font-display);
  font-weight: 700;
  font-stretch: var(--w-eng);
  font-size: var(--t-l);
  margin-bottom: var(--s2);
}

.kalender-wochentage,
.kalender-gitter {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  gap: var(--s1);
}

.kalender-wochentage > span {
  text-align: center;
  font-family: var(--font-display);
  font-weight: 600;
  font-stretch: var(--w-weit);
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-faint);
  padding-bottom: var(--s1);
}

/* The first of the month goes in its own weekday's column. Seven classes, and
   they are what makes the grid need no empty cells at all — an empty cell is
   still something whoever is listening has to walk through. */
.kalender-start-1 { grid-column-start: 1; }
.kalender-start-2 { grid-column-start: 2; }
.kalender-start-3 { grid-column-start: 3; }
.kalender-start-4 { grid-column-start: 4; }
.kalender-start-5 { grid-column-start: 5; }
.kalender-start-6 { grid-column-start: 6; }
.kalender-start-7 { grid-column-start: 7; }

.kalender-tag {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  aspect-ratio: 1 / 1;
  min-height: 2.25rem;
  padding: var(--s1) 0;
  border-radius: var(--r1);
  border: 1px solid transparent;
  background: var(--surface);
  color: var(--ink);
  text-decoration: none;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--t-s);
  font-variant-numeric: tabular-nums;
  transition: background var(--tempo) var(--kurve),
    border-color var(--tempo) var(--kurve);
}

a.kalender-tag:hover {
  border-color: var(--ink-faint);
}

/* A day already gone is drawn and is not a control: the site answers what is
   on from today onwards, so there is nothing behind it to pick. */
.kalender-vorbei {
  background: none;
  color: var(--ink-faint);
}

.kalender-nummer {
  line-height: 1;
}

/* There is deliberately no mark under the number saying how full a day is.
   It was four steps of ink under every cell and it was removed on the
   operator's word: a grid of 61 days each wearing a little bar is 61 things
   to read before you can find a date. How full a day is lives in the day's
   own spoken label, where the real number is. */

/* Inside the chosen range, and the two days that are its edges. The pressed
   treatment is the plain chip's — ink on paper — and not the accent's: the
   brass has four jobs (TO1 to TO4) and a fifth is a bug. */
.kalender-drin {
  background: color-mix(in oklab, var(--ink) 12%, var(--surface));
}

.kalender-rand {
  background: var(--ink);
  color: var(--bg);
}

/* Today. This is the accent's second job (TO2), the same one it does on a day
   plate, and not a fifth one. */
.kalender-heute {
  border-color: var(--accent);
}

.kalender-form {
  display: flex;
  flex-wrap: wrap;
  align-items: end;
  gap: var(--s3);
  margin-top: var(--s4);
}

/* NA2: a flex child defaults to "never narrower than my content", and a
   native date field's content is a whole widget. Without this the two fields
   push the form wider than a phone. */
.kalender-form .feld {
  flex: 1 1 9rem;
  min-width: 0;
  margin-bottom: 0;
}

/* Only the standing grid of /mycity/kalender puts its months side by side.
   The panel in a heading holds one month at any width, so the selector names
   the section rather than the class — unscoped, this rule gave that one month
   half of a 320 px panel on every desktop screen. */
@media (min-width: 46em) {
  .kalender .kalender-monate {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* --------------------------------------------------------------------------
   23. The last item of the day rail, pushed hard right

   `Meine Kategorien` is the reader's own settings and belongs at the end of
   the menu rather than between four stretches of time. `margin-inline-start:
   auto` eats whatever room is left over, so it sits against the right edge on
   a wide screen.

   On a rail whose items are wider than the rail there is no room left over,
   the automatic margin is nothing, and the item simply follows the others and
   scrolls into view — which is the honest answer on a phone and needs no
   media query. `.rail` is one of exactly two containers in this stylesheet
   allowed to scroll sideways (NA3), and this adds no third.
   -------------------------------------------------------------------------- */

.rail > .rechts-aussen {
  margin-inline-start: auto;
}

/* --------------------------------------------------------------------------
   24. The page heading, and the calendar at the right-hand end of it

   The heading is a row: the page's own name and the line under it on the
   left, the calendar button hard right. `align-items: start` is what keeps
   the button beside the FIRST line of a heading that wraps — at 360 px
   `Sa, 12. Aug. bis Mi, 16. Aug.` is three lines of --t-3xl, and a button
   centred against those three sits in the middle of nowhere.

   NA2 applies to the text half, which is the one holding a stranger's words:
   without `min-width: 0` a long family name refuses to shrink and pushes the
   button off the screen. The button half never shrinks — it is one glyph and
   one short word.

   ON A PHONE NOTHING ABOUT THIS CHANGES AND THAT IS DELIBERATE. The row is
   328 px, the button is about 105 px of it, and the heading keeps the rest
   and wraps inside it. The one thing that would break is the panel, and the
   panel is not in the flow at all — see below.
   -------------------------------------------------------------------------- */

/* `flex-wrap: wrap` is what lets the three pills take a row of their own
   under the heading on a phone. They do it by asking for `flex-basis: 100%`,
   which is a whole line by definition — never by hoping the browser runs out
   of room, which would be a different layout on every heading. */
.seitenkopf {
  display: flex;
  flex-wrap: wrap;
  align-items: start;
  justify-content: space-between;
  gap: var(--s3);
}

.seitenkopf-text {
  min-width: 0;
  flex: 1 1 auto;
}

/* The heading's own bottom margin belongs to the row, so the button is not
   dragged down by it and the gap under the whole block stays the one
   `.seiten-unter` already sets. */
.seitenkopf .seitentitel:last-child {
  margin-bottom: var(--s5);
}

/* --------------------------------------------------------------------------
   The calendar button and its panel

   The panel hangs from the right edge of the button and is absolutely
   positioned, so it is out of the flow and cannot widen the page whatever is
   in it. At 360 px the button's right edge is 344 px from the left of the
   window and the panel is at most 320 px wide, so its left edge lands at
   24 px — inside the window, with no rule for the width of the window.

   Inside it one month gets 296 px, which is seven columns of
   (296 - 6 x 4) / 7 = 38.8 px. The day cell's own 2.25 rem floor is 36 px, so
   the grid is square and legible at the narrowest width this site draws.

   Two months would not be: this is the same arithmetic as section 22, and it
   is why the panel shows one month and the standing grid of /mycity/kalender
   shows two.
   -------------------------------------------------------------------------- */

/* `position: relative` and NO z-index, deliberately. A z-index here would
   make this a stacking context of its own, and the panel's 30 would then be
   a rank inside it rather than against the page — the sticky day plate at 20
   is a cousin, not a child, and would paint straight over an open calendar.
   Left at `auto` there is no context and 30 beats 20 where it has to. */
.kopfkalender {
  position: relative;
  flex: 0 0 auto;
}

/* `white-space: normal` and not `nowrap`, and that changed when the button
   became the date pill. It used to carry the word Kalender, which is 8
   characters and can never overflow anything; it now carries a date, and the
   longest this site writes is `Di, 29. Dez. bis So, 3. Jän. 2027`. A nowrap
   flex item refuses to be narrower than its own text, so one long enough
   would push the page sideways at 360 px — NA1 and NA3 in one. Normal lets it
   break inside the pill instead, which is two lines of pill and never a
   scrollbar on the document. */
.kopfkalender-knopf {
  list-style: none;
  white-space: normal;
}

.kopfkalender-knopf::-webkit-details-marker {
  display: none;
}

/* A range is on. The panel that would say so is shut most of the time, so the
   button says it — the same accent-washed treatment a pressed control gets
   everywhere else on this site, and not a fifth job for the brass (TO1-TO4). */
.kopfkalender-an {
  background: var(--accent-wash);
  border-color: var(--accent);
  color: var(--ink);
}

.kopfkalender-tafel {
  position: absolute;
  inset-inline-end: 0;
  top: calc(100% + var(--s2));
  z-index: 30;
  width: min(20rem, calc(100vw - var(--s6)));
  max-height: min(80vh, 34rem);
  overflow-y: auto;
  padding: var(--s3);
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--r2);
  box-shadow: var(--sh3);
}

/* The grid inside the panel is section 22's. One month, so the gap between
   months never applies and the top margin is the panel's own rhythm. */
.kopfkalender-tafel .kalender-monate {
  margin-top: var(--s3);
  gap: var(--s4);
}

.kopfkalender-tafel .kalender-satz {
  margin: 0;
}

/* Two date fields side by side want 18 rem and the panel has 18.5 less its
   padding. They stack, and stacked they are the fastest way into a month
   that is a year out. */
.kopfkalender-tafel .kalender-form {
  margin-top: var(--s3);
  gap: var(--s2);
}

.kopfkalender-tafel .kalender-form .feld {
  flex: 1 1 100%;
}

/* --------------------------------------------------------------------------
   24b. The three pills above the list: Jetzt, a time, a day (V5)

   HOW WIDE IT IS, COUNTED. 360 px of screen is 328 px of content, 390 px is
   358 px. The items sit `--s2` (8 px) apart and a character is counted at
   0.6 em, which is an upper bound for Archivo at 600 rather than a
   measurement — the point of the number is that it cannot be beaten, not that
   it is exact.

     Jetzt          2 x --s4 + 2 border + 5 chars at --t-s          =  73 px
     the time pill  2 x --s2 + 2 + field 7.5rem + --s2 + button     = 230 px
     its two steps  2 x 2.75rem + --s2                              =  96 px
     the date pill  2 x --s4 + 2 + icon 15 + --s2 + `Heute`         =  96 px
     the same, at the longest word it can carry (39 chars)          = 361 px

   First line: 73 + 8 + 230 = 311, which fits in 328 and in 358. Adding the
   date column would want 311 + 8 + 96 = 415, which fits in neither, so it
   wraps to a second line at BOTH widths — the same shape at 360 as at 390,
   which is what section 4 of docs/design/GESTALTUNG.md asks of every block
   here.

   The second line does not always fit, and that is why the pill wraps inside
   itself. The longest word `filters.zeitraum_wort` can produce was measured
   over the whole two years the calendar reaches:
   `So, 10. Jän. 2027 bis Mo, 11. Jän. 2027`, 39 characters, about 361 px —
   both ends carry the year once both are in another one. It breaks over two
   lines inside the pill, which is then about 72 px tall, and nothing widens.

   Two rows and not a sideways scroller: NA3 allows exactly two containers to
   scroll sideways on this site and this is not one of them. The date pill's
   own word is the one thing that could still have widened the page, which is
   why `.kopfkalender-knopf` above is `white-space: normal`.

   It costs about 200 px of height on a phone — two rows of a 44 px pill over
   44 px of steps, 8 px apart. That is what the contract's "steps under each
   pill" costs, and it is paid once at the top rather than on every row.
   -------------------------------------------------------------------------- */

/* RIGHT-ALIGNED, on the operator's word of 2026-08-10 — the same wall the
   filter strip under it obeys. Jetzt, the clock and the day are controls and
   not a heading, and every control on this page now ends at the right edge;
   left-aligned they read as a second title under the first. */
.kopfleiste {
  display: flex;
  flex-wrap: wrap;
  align-items: start;
  justify-content: flex-end;
  gap: var(--s2);
  /* Its own line under the heading, by asking for one. */
  flex: 1 1 100%;
  margin-bottom: var(--s5);
}

/* NA2: the halves that hold a stranger's date range are the ones that need
   it, or a long word makes the column wider than its track. */
.kopfleiste > *,
.kopfsatz > * {
  min-width: 0;
  max-width: 100%;
}

/* A control with a chevron on each side. It was a column with two steps
   under it until 2026-08-09; the operator asked for `<` and `>` beside the
   field instead, which is also about 90 px shorter on a phone — the two rows
   of steps were the whole reason the bar cost 200 px of height. */
.kopfsatz {
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: var(--s2);
}

/* K2. 44 px is the smallest thing a thumb hits reliably, and `.knopf` on its
   own is 36. The pills and their steps are the one place on this site where
   that floor is stated rather than inherited: they are the controls somebody
   presses over and over while deciding what to do tonight. */
/* Every pill in the bar is an ordinary `.knopf` and nothing taller: the
   operator asked for the day, the clock and Jetzt to be the same size as the
   rest, and a 44 px row beside 36 px chips read as three odd ones out. */
.kopfpille {
  flex: 0 0 auto;
}

/* KF9. `Jetzt` is the one pill that is marked, because it is the one that
   answers the question the whole site asks — what is on now. Two marks, and
   the brass accent is neither of them: it is spent on exactly four things and
   never on a chip (GESTALTUNG 2.2).

   The second frame is a shadow and not an `outline`, so it cannot be wiped
   out by the focus ring, which IS an outline: the two then stand side by side
   the way they should. The first shadow is the page's own ground and is what
   makes the gap between the two lines. */
.kopf-jetzt {
  color: var(--ink);
  font-weight: 700;
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 3px var(--line-strong);
}

.kopf-jetzt:hover {
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 3px var(--ink-faint);
}

/* Pressed, the pill is already a solid block, so the gap has to be drawn in
   the pill's own colour rather than in the page's or a hairline of ground
   would cut through it. */
.kopf-jetzt[aria-current="true"] {
  box-shadow: 0 0 0 2px var(--bg), 0 0 0 3px var(--ink-soft);
}

/* KF10. The caret on the right of the two pills that fold out — the clock and
   the day. A <summary> announces its own state to a reading aid and nothing
   at all to an eye, which is the whole of what this is for. */
.kopfpille-pfeil {
  margin-inline-start: var(--s1);
  font-size: 0.75em;
  line-height: 1;
  color: var(--ink-faint);
  transition: transform var(--tempo) var(--kurve);
}

.kopfwahl[open] .kopfpille-pfeil,
.kopfkalender[open] .kopfpille-pfeil {
  transform: rotate(180deg);
}

/* The word between the clock and the caret. It may not be pushed out of shape
   by either of them: at 360 px `18:00 bis 23:00` is the longest thing this
   pill ever says. */
.kopfpille-wort {
  min-width: 0;
}

/* KF7. The clock is a list of hours behind a pill. */
.kopfwahl {
  position: relative;
}

.kopfwahl > summary {
  list-style: none;
}

.kopfwahl > summary::-webkit-details-marker {
  display: none;
}

/* It hangs from the RIGHT edge of its pill, the way the calendar panel beside
   it already does. Since the strip became right-aligned the pill can stand
   close to the right edge of the window, and a panel hanging from its left
   edge — four columns of 3.5 rem, about 260 px — would then run off the page.
   At 360 px the pill's right edge is 344 px from the left, so the panel runs
   from 84 px to 344 px and stays inside. */
.kopfwahl-tafel {
  position: absolute;
  z-index: 20;
  inset-inline-end: 0;
  margin-top: var(--s2);
  padding: var(--s3);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-tafel);
  background: var(--surface-lift);
  box-shadow: var(--schatten);
}

/* Four columns of six consecutive hours: 00–05, 06–11, 12–17, 18–23. Each
   cell is 3.5 rem, so the panel is about 248 px wide with its padding —
   inside 328 px of content at 360 px, with nothing to scroll. */
.kopfstunden {
  display: grid;
  grid-template-columns: repeat(4, 3.5rem);
  gap: var(--s1);
}

.kopfstunden-spalte {
  display: flex;
  flex-direction: column;
  gap: var(--s1);
}

.kopfstunde {
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 2.25rem;
  border-radius: var(--r-klein);
  color: var(--ink-soft);
  font-family: var(--font-display);
  font-size: var(--t-s);
  font-variant-numeric: tabular-nums;
  text-decoration: none;
}

.kopfstunde:hover,
.kopfstunde:focus-visible {
  color: var(--ink);
  background: var(--surface-sink);
}

.kopfstunde-an {
  color: var(--bg);
  background: var(--accent);
}

.kopfwahl-weg {
  margin-top: var(--s3);
}

.kopfschritt {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  min-height: 2.75rem;
  padding: 0;
  font-size: var(--t-l);
  line-height: 1;
}

/* A step that leads nowhere keeps its place. Drawn as nothing it would let
   the field jump 52 px sideways the moment somebody reaches today or 23:00,
   and a control that moves under the finger is a control that gets pressed
   twice. */
.kopfschritt-leer {
  display: inline-block;
  width: 2.75rem;
  min-height: 2.75rem;
  border: 0;
  background: none;
}

/* THE CHECK, AND IT IS A SWITCH. Grey it applies what stands in the field;
   green the time is really in force and pressing it takes the time away. Both
   states are 44 px, both are in the flow, and neither needs a script — green
   is an <a> to this page without `ab`, grey is the form's own submit. The
   green is `--good`, the token this stylesheet already carries for a state
   that is in order; no new colour is invented for it. */
.kopfhaken {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  min-height: 2.75rem;
  padding: 0;
  font-size: var(--t-m);
  line-height: 1;
  color: var(--ink-faint);
  border: 1px solid var(--line);
  border-radius: var(--r-pille);
  background: var(--surface-lift);
  cursor: pointer;
}

.kopfhaken:hover,
.kopfhaken:focus-visible {
  color: var(--ink);
  border-color: var(--line-strong);
}

.kopfhaken-an {
  color: var(--bg);
  border-color: var(--good);
  background: var(--good);
}

.kopfhaken-an:hover,
.kopfhaken-an:focus-visible {
  color: var(--bg);
  border-color: var(--good);
}

/* The time-of-day pills, directly under the day and the clock. Its own row,
   so it wraps on a phone without pushing the two controls above it apart. */
.kopfband {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--s2);
  flex: 1 1 100%;
}

/* The time field and its button are one pill: the border belongs to the pill,
   not to the two things inside it. It wraps like everything else here, so a
   browser whose own time widget is wider than the 7.5 rem below puts the
   button on a second line instead of widening the page. */
.kopfzeit {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  min-height: 2.75rem;
  padding: var(--s1) var(--s2);
  border: 1px solid var(--line-strong);
  border-radius: var(--r-pille);
  background: var(--surface);
}

/* 7.5 rem is 120 px, which holds `19:30` at the 16 px every input on this
   site carries (or iOS zooms the page in on focus) plus whatever picker the
   browser draws beside it. It is a fixed width and it is well under the
   320 px NA3 forbids. */
.kopfzeit-feld {
  width: 5.5rem;
  padding: var(--s1) var(--s2);
  border: 0;
  background: none;
  color: var(--ink);
}

/* THE PANEL HANGS FROM THE PILL'S LEFT EDGE HERE, AND THAT IS NOT A DETAIL.
   Section 24 above anchors it to the right, because the calendar button used
   to stand at the right-hand end of the heading and a panel hung from its
   left would have run off the right of the window. The date pill is the same
   control in a different place: while the bar has a row of its own it starts
   at the left margin, so a 320 px panel hung from a 96 px pill's right edge
   would begin 224 px off the left of the window and most of it would be
   unreachable. Anchored left it runs from 16 px to 336 px inside a 360 px
   window, and to 336 inside 390 — inside both, with nothing to scroll. */
.kopfsatz .kopfkalender-tafel {
  inset-inline-start: 0;
  inset-inline-end: auto;
}

/* From here up there is room for the bar beside the heading, which is where
   the calendar button stood before it became the date pill — and the panel
   goes back to hanging from the right, for the reason section 24 gives. */
@media (min-width: 62em) {
  .kopfleiste {
    flex: 0 0 auto;
    margin-bottom: 0;
  }

  .kopfsatz .kopfkalender-tafel {
    inset-inline-start: auto;
    inset-inline-end: 0;
  }
}

/* --------------------------------------------------------------------------
   25. The table — one row is one occurrence

   IT IS NOT A <table>, AND THAT IS THE WHOLE POINT. A row is a <details>:
   only that way does it open with no script at all, and only that way does it
   fold together at 360 px instead of scrolling sideways. NA3 lets exactly two
   containers scroll sideways — `.rail` and `.tabelle-huelle` — and eight
   columns in a <table> would need a third.

   THE GRID IS WRITTEN ONCE. The heading row and every row carry `.tz-raster`,
   so the columns cannot drift apart. No track is content-sized (`auto`,
   `min-content`): a row would then be a different width from the heading
   above it.

   THE GRID DOES NOT SIT ON THE <summary>. `display: grid` there takes
   `list-item` away, and in WebKit it can stop the <details> being a
   disclosure at all — then the row never opens. It sits on the
   <span class="tz-raster"> inside it. There is no browser in this toolchain
   (M29, rule 48), so the safe shape wins over the short one.

   COUNTED, NOT GUESSED. At 360 px the page has 328 px of content; the row
   inside is 328 − 3 (the family edge) − 24 (twice --s3) = 301 px. The four
   breakpoints and what each one adds are written at the top of
   `app/templates/_tabelle.html`, and so is the width the title is left with
   at each of them.

   EVERY TRACK IS THE WIDER OF TWO MEASURED THINGS — its widest cell, and its
   own heading word plus the sort arrow. There is no browser here (M29), so
   the widths are the advance sums out of `app/static/schrift/` themselves,
   with the font instanced at the weight and width axis each rule sets, GPOS
   kerning applied, and letter-spacing charged once per character the way a
   browser charges it. The heading figure below is the whole box: the word,
   the separator, the arrow and the 10 px of padding and border the field
   adds. Only a track that cannot wrap can actually overlap — `html` carries
   `overflow-wrap: break-word` (NB1), so a wrapping cell only ever grows
   taller. The two that cannot wrap are the heading fields and Datum.

     Read as CONTENT widths: since 2026-08-10 every grid child carries 8 px of
     cushion (`padding-inline-end: var(--s2)`) and every track is 8 px wider
     than the figure below to pay for it.

     column     content  widest cell               heading box   fits
     Datum         88    83.18  `Mo, 10. März`     77.69         yes, nowrap
     Beginn        88    69.34  `UHRZEIT`, wrapped 84.29         yes
     Ende          68    29.76  `23:59`            66.02         yes
     Titel        1fr    wraps                     66.54         yes
     Preis         76    wraps and breaks          69.42         yes
     Kategorie    104    98     `Stadtleben`, cut  56.11 `Kat.`  yes
     Ort      152/160    wraps and breaks          55.12         yes
     Sprache       60    18.12  `DE`               58.88 `Spr.`  yes

   THE ONE THAT DID NOT FIT, AND WHAT IT COST. On 2026-08-10 the Kategorie
   heading was written out — `KATEGORIE` rather than `Kat.` — because the rows
   below it had just started writing the family out too. A heading field is
   `white-space: nowrap`: `KATEGORIEN` whole is 127 px, `KATEGORIE` is one
   letter shorter at about 116, and the content of that column is 104. It
   painted about 12 px into Ort, and that is the overlap the operator reported
   the same evening. The rows keep the written-out pill; the heading is `Kat.`
   again with the whole word in its <abbr title>. The other seven were counted
   against their tracks at the same time and every one of them clears — by
   1.12 px for `Spr.` in 60 and by 96 px for Ort.

   The separator between the word and the arrow is not one number, and the
   reason is worth naming: five of the eight heading fields are blockified
   grid items, where the two are held apart by the collapsed space in the
   markup (4.33 px at this size and letter-spacing); Titel, Preis and
   Kategorien carry a cell class that makes them flex, and there the
   `gap: var(--s1)` below does the job instead (4 px). `display` is
   deliberately never set on `.tz-kopf .tz-kopf-feld` itself — it would beat
   the `display: none` on the cell classes and the heading would show three
   columns the rows do not have.

   WHAT WAS OVERLAPPING, AND WHY. Three separate faults, all of them real:

   OL1. The heading fields are `white-space: nowrap`, and four of them were
        wider than the track they head — BEGINN 82.9 in 56, ENDE 64.6 in 52,
        KATEGORIEN 127 in 44, SPRACHE 97.8 in 40. `min-width: 0` lets the box
        shrink; it does nothing about text that will not wrap, so the word
        simply painted across the next column. The two that no track could
        ever hold are shortened — `Kat.`, `Spr.`, each an <abbr> carrying the
        whole word — and the rest of the tracks were widened to fit.

   OL2. Datum is `white-space: nowrap` too. Its longest value is not a guess:
        `datum_kurz` can build exactly 2604 strings out of the seven day words
        and the twelve month words in `app/strings/de.py`, and the widest of
        them is `Mo, 10. März` at 83.18 px. It stood in a 5 rem (80 px) track,
        so it painted 3.2 px into Beginn at every width and 27.2 px into the
        title under 30 em, where that track was `--zeit-spalte`. The month
        word is `Sep.` and never `Sept.`, which is why nothing here is
        measured off a string somebody remembered.

   OL4. BEGINN with its sort arrow is 84.29 px and its track was 84. Found by
        re-measuring after OL1 to OL3 were fixed; the token block in section 2
        carries the arithmetic. That one is the reason a heading may not be
        sized by eye at all: 0.29 px is invisible in a screenshot and is still
        an overlap.

   OL3. From 46 em `.tz-fuss` is `display: contents`, which made Preis and
        the dots grid children that `.tz-raster > *` never reached, so they
        kept min-width:auto — see NB2 in section 4. The ticket state rode in
        that same cell at up to 154 px of unbreakable text in a 96 px track;
        it stands in the opened part now.

   NO ZEBRA STRIPES. The family edge on the left already says where a row
   begins, and a stripe on every second row would be a seventh colour running
   against the six families. An open row takes `--surface-sink` instead, the
   same as hovering.

   NOT ONE NEW COLOUR: --kat, --line, --ink, --ink-soft, --ink-faint,
   --surface-lift, --surface-sink, --bg, --accent and --accent-wash all exist
   already. Ort now carries --ink and Sprache --ink-soft, because both are
   data and were reading as the quiet chrome around it; --ink-faint itself was
   lifted in section 2, where the two contrast readings stand.
   -------------------------------------------------------------------------- */

.tz {
  display: flex;
  flex-direction: column;
}

/* --- the grid in its base shape: 360 and 390 px --------------------------- */

/* The one track here holds Datum above Beginn, so it is `--sp-datum` and not
   `--zeit-spalte`: `Mo, 10. März` does not wrap and is 83.18 px.

   THE AIR BETWEEN TWO COLUMNS IS 12 PX BELOW 46 EM AND 16 FROM THERE, and
   since 2026-08-10 it is paid in two parts rather than one: 8 px of cushion
   inside the cell on the left plus 4 px of gap below 46 em, 8 plus 8 from
   there. It used to be 12 and 16 of pure gap. The sum is the same, which is
   the point — the operator asked for padding on every column and nothing was
   allowed to move for it.

   Counted at 360 px: the row inside is 301 px, the one fixed track is 96 with
   88 of content in it, the gap is 4, so the title's track is 201 — exactly
   what it was when the track was 88 and the gap 12. Measured over the 2030
   titles in the database, 201 px puts 56.9 % of them on one line and 1.5 %
   past three. Its text box is 193 inside its own cushion. */
.tz-raster {
  display: grid;
  grid-template-columns: var(--sp-datum) minmax(0, 1fr);
  column-gap: var(--s1);
  row-gap: var(--s1);
  align-items: start;
}

/* THE CUSHION ITSELF, and it reaches both kinds of grid child for the reason
   NB2 in section 4 gives: from 46 em `.tz-fuss` is `display: contents`, so
   Preis and Kategorie become grid children of `.tz-raster` in their own
   right — but a selector reads the document tree and `.tz-raster > *` never
   reaches them.

   It is `padding-inline-end` and not `padding-inline`: a column starts at its
   own left edge, and padding on that side would move every value 8 px to the
   right of the heading over it and 8 px away from the family edge the row is
   already lined up against.

   The heading fields carry it too — see `.tz-kopf .tz-kopf-feld` below, which
   adds it to its own chrome padding rather than replacing it. They are the
   one thing in this grid that can really overlap, so exempting them would be
   exempting the case. */
.tz-raster > *,
.tz-fuss > * {
  padding-inline-end: var(--s2);
}

.tz-datum { grid-column: 1; grid-row: 1; }
.tz-beginn { grid-column: 1; grid-row: 2; }
.tz-titel { grid-column: 2; grid-row: 1 / span 2; }
.tz-fuss { grid-column: 2; grid-row: 3; }

/* Under 30 em these three do not exist. Nothing is lost: every column that
   falls away stands in the opened part. */
.tz-ende,
.tz-ort,
.tz-sprache {
  display: none;
}

/* --- the heading row ------------------------------------------------------ */

/* Under 30 em Datum and Beginn share one track, and there is no honest
   heading for a shared track. Sorting happens in the filter sheet there —
   which is where it happens at every other width too. */
.tz-kopf {
  display: none;
}

/* TWO CLASSES, AND THAT IS DELIBERATE. A heading carries the same cell class
   as the rows so the track assignment is the same one — and the cell class
   also sets size and colour. With one class `.tz-beginn` would win and the
   word Beginn would come out in row size. About `display` there is
   deliberately nothing here: the cell classes alone decide it, or the heading
   would show a column the rows do not have. */
/* The horizontal padding is --s1 and not --s2: it is chrome, it is charged
   to every track twice over, and from 46 em the column gap gives the pressed
   wash all the room it needs to stand clear of the next heading.

   The second line is the cushion of `.tz-raster > *` above, which this
   shorthand would otherwise wipe out — two classes beat one, and the
   shorthand sets all four sides. Written as chrome plus cushion so it is
   obvious that the heading grew by exactly the 8 px its track grew by, and
   that every heading box therefore keeps the slack it had: BEGINN 84.29 + 8
   in 96, `Spr.` 58.88 + 8 in 68, `Kat.` 56.11 + 8 in 112. */
.tz-kopf .tz-kopf-feld {
  align-items: baseline;
  gap: var(--s1);
  padding: var(--s1);
  padding-inline-end: calc(var(--s1) + var(--s2));
  border: 1px solid transparent;
  border-radius: var(--r1);
  font-family: var(--font-display);
  font-weight: 700;
  font-stretch: var(--w-weit);
  font-size: var(--t-xs);
  text-transform: uppercase;
  letter-spacing: 0.09em;
  color: var(--ink-faint);
  text-decoration: none;
  white-space: nowrap;
}

.tz-kopf .tz-kopf-feld:hover {
  color: var(--ink);
}

/* The treatment every pressed thing on this site gets — no fifth job for the
   brass (TO1 to TO4). */
.tz-kopf .tz-kopf-an {
  background: var(--accent-wash);
  border-color: var(--accent);
  color: var(--ink);
}

.tz-kopf-pfeil {
  font-size: var(--t-xs);
  line-height: 1;
}

/* Two headings are abbreviations, because no honest track holds KATEGORIEN
   (127 px over three 7 px dots) or SPRACHE (97.8 px over `EN`). The whole
   word rides in the <abbr title>, which is where a reading aid finds it and
   where a pointer shows it. The dotted underline browsers draw under an
   <abbr> would read as a second kind of link in a row of links. */
.tz-kopf-feld abbr {
  text-decoration: none;
}

/* --- one row -------------------------------------------------------------- */

.tz-falt {
  border-bottom: 1px solid var(--line);
  border-inline-start: 3px solid var(--kat);
  transition: background var(--tempo) var(--kurve);
}

.tz-falt:hover,
.tz-falt[open] {
  background: var(--surface-sink);
}

/* The <summary> keeps its own display type (see the head of this section).
   Only the marker goes: the triangle is drawn in the markup instead, so it
   stands where it is wanted, at the right-hand end of the title. */
.tz-summary {
  list-style: none;
  cursor: pointer;
  padding: var(--s3);
  user-select: none;
}

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

.tz-summary:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}

/* --- the cells ------------------------------------------------------------ */

.tz-datum {
  font-family: var(--font-display);
  font-weight: 600;
  font-stretch: var(--w-weit);
  font-size: var(--t-xs);
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
  white-space: nowrap;
}

.tz-beginn {
  font-family: var(--font-display);
  font-weight: 700;
  font-stretch: var(--w-eng);
  font-size: var(--t-l);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
  color: var(--ink);
}

.tz-ende {
  font-family: var(--font-display);
  font-weight: 600;
  font-stretch: var(--w-eng);
  font-size: var(--t-s);
  font-variant-numeric: tabular-nums;
  color: var(--ink-faint);
}

/* A time the source never gave. Small and quiet, so it does not disturb the
   column of real clock times beside it. */
.tz-offen {
  font-size: var(--t-xs);
  font-stretch: var(--w-weit);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ink-faint);
}

.tz-titel {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
  min-width: 0;
}

.tz-titel-text {
  flex: 1 1 100%;
  min-width: 0;
  font-family: var(--font-display);
  font-weight: 600;
  font-stretch: var(--w-eng);
  font-size: var(--t-m);
  letter-spacing: -0.01em;
  color: var(--ink);
}

/* Under 30 em the heart drops below the title: 44 px of finger in a 233 px
   cell is a fifth of it. `order` moves only what is seen — the tab order
   stays as the markup wrote it, and there is one control here anyway. */
.tz-herz {
  order: 2;
  min-height: 2.75rem;
}

/* TA8. THE FOLD ARROW IS GONE, on the operator's word of 2026-08-10 — "in the
   tables, you can avoid the extension arrow". A row is a <summary>: the whole
   line opens it, the pointer over it is already a hand, and a browser tells a
   reading aid by itself whether it stands open or shut. The triangle said all
   of that a second time in every one of the rows on a page. */

/* Both words of the save control stay in the markup, so every German word
   still lives in app/strings/de.py — in a row they are there for the reading
   aid alone, because there is no room beside the title. The one that is not
   the current state is `display: none` from section 11 and stays out of the
   tree altogether; this only hides the one that is. */
.tz-herz .merken-aus,
.tz-herz .merken-an {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.tz-fuss {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--s2);
}

.tz-preis {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--s2);
  font-variant-numeric: tabular-nums;
}

.tz-punkte {
  display: inline-flex;
  align-items: center;
  gap: var(--s1);
}

/* TA11. IN THE TABLE A FAMILY IS WRITTEN LIKE A FILTER CHIP, on the operator's
   word of 2026-08-10: "the all pills should be written exactly as in the
   filtering section, e.g. Kunst instead of KUNST".

   `.chip-marke` is the badge on a card, and there the small caps with their
   letter-spacing are right: it is a mark beside a picture, not a word somebody
   reads. In a table it stands in a column with a heading over it, beside times
   and prices and house names — and everything else on that page writes
   `Kunst`. Two spellings of one word on one screen is the reader's problem and
   never the stylesheet's preference.

   Only the writing changes, not the badge's softer fill: the fill is what
   still tells a badge from the pressable chips in the strip above. The height
   goes to 2 rem with it, which is the strip's own — a 13 px word in a 24 px
   pill reads as an accident, and it happens to be a finger's worth taller too.

   Narrower, not wider: `Stadtleben` in small caps with 0.08 em of tracking was
   about 98 px, in ordinary writing it is about 74 px, and the track is 104. */
.tz .chip-marke {
  min-height: 2rem;
  font-size: var(--t-s);
  text-transform: none;
  letter-spacing: normal;
}

/* TA8. The family, written out. `.chip-marke` gives it its shape; what is
   added here is the one thing a fixed 6.5 rem track needs — a pill that is
   too wide for it truncates instead of pushing the whole grid sideways (NA2).
   `Stadtleben` is the longest of the six and measures about 98 px against the
   104 px of the track, so the ellipsis is a net and not the normal case. */
.tz-marke {
  display: block;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  /* A <button> centres its label and a <span> does not, and since 2026-08-10
     this cell is one or the other depending on whether the page is a filtered
     list (TB6). Written down once, both read the same. */
  text-align: start;
}

/* IT KEEPS THE PILL'S OWN 1.5 REM AND DOES NOT TAKE THE 2.75 OF A FINGER, and
   that is a decision rather than an oversight. `.chip-marke.chip-schalter` is
   already exactly this control on a card, at exactly this size, at 360 px —
   one size for one control. Raising this one alone would make the pill in a
   row a different thing from the pill on a card, and it would add about 20 px
   to every row on a phone, where the pill stands in a line of its own under
   the title. The bare house name in the Ort column is the other way round: it
   has no chip shape to be recognised by, so it takes the full 2.75 rem —
   `.tz-ort-knopf` below. */

/* A house name is never cut with an ellipsis. The longest in the database is
   82 characters — `Wiener Würstelstand, U4 Spittelau, Ausgang
   Josef-Holaubek-Platz (zur alten WU hin)` — and 1614 of the 7434 future
   occupied rows are past 25, so `Treffpunkt vor der Dominikanerk…` helps
   nobody. It wraps, and the row grows.

   --ink and not --ink-soft. Where a house name is the meta line under a card
   title, soft is right; here it is one of eight columns somebody reads across,
   and 8.88 : 1 against 16.67 : 1 for the title beside it read as a column
   that had been switched off. The size and the text face keep the two apart. */
.tz-ort {
  font-size: var(--t-s);
  color: var(--ink);
}

/* TA10. On a filtered list the house name is the switch that shows only that
   house. A <button> inherits nothing, so everything a browser would put on it
   is taken back off again — it has to go on reading as the column's text and
   not as a control in a row of eight columns, or the table turns into a row of
   grey boxes.

   Four things it keeps and each is deliberate:

   - `display: block` and not flex, so the first line sits where a span's first
     line sat. The grid is `align-items: baseline` from 30 em and every other
     cell lines up on that first line; a centred label inside a 2.75 rem box
     would drop this column's baseline about 14 px below the rest of the row.
   - `min-height: 2.75rem` — a finger and not a mouse pointer, the same floor
     `.tz-herz` takes in the same row. It only ever grows a row that has no
     other control in it, because the save control already sets that floor for
     anybody signed in.
   - Nothing that cuts the name (SP7): the whole 82-character worst case wraps
     here exactly as it did as text, and `width: 100%` is what gives it the
     whole track to wrap in — and the whole track to be pressed in.
   - The focus ring is the site's own `:focus-visible` and is not restated. */
.tz-ort-knopf {
  display: block;
  width: 100%;
  min-height: 2.75rem;
  padding: 0;
  border: 0;
  background: none;
  color: inherit;
  font: inherit;
  text-align: start;
  cursor: pointer;
}

/* Pressable, and it says so without a border: the accent is what every other
   pressed thing on this site wears (TO1 to TO4). */
.tz-ort-knopf:hover {
  color: var(--accent);
  text-decoration: underline;
}

.tz-ort-knopf[aria-pressed="true"] {
  color: var(--accent);
  font-weight: 700;
}

/* TB5. The same switch in the opened part, where the Ort column is not drawn
   at all under 46 em. Down there it is a chip, because it stands beside an
   address and a link and has to say that it is a control. */
.tz-ort-schalter {
  margin-inline-start: var(--s2);
  vertical-align: middle;
}

/* --ink-soft and not --ink-faint: two upright letters at 12 px are the
   smallest thing in the row, so this is the one cell that can least afford
   the quietest ink. 8.88 : 1 on --bg, where it read 4.33 : 1 before. */
.tz-sprache {
  font-size: var(--t-xs);
  font-stretch: var(--w-weit);
  letter-spacing: 0.06em;
  color: var(--ink-soft);
}

.tz-sprache abbr {
  text-decoration: none;
}

/* --- the opened part ------------------------------------------------------ */

/* Flex and not a grid, and for one reason: 762 of 2538 occurrences have no
   picture at all. A two-column grid would leave a 12 rem hole beside their
   text at every width where the picture would have been, and no selector
   here can tell an absent picture from a present one — `hidden` form and all,
   `:only-child` does not answer it either. Wrapping flex simply gives the
   text the whole line when nothing stands beside it. */
.tz-inhalt {
  display: flex;
  flex-wrap: wrap;
  align-items: start;
  gap: var(--s4);
  padding: 0 var(--s3) var(--s4);
}

.tz-bild {
  position: relative;
  flex: 0 0 12rem;
  aspect-ratio: 3 / 2;
  align-self: start;
  max-width: 12rem;
  overflow: hidden;
  border-radius: var(--r1);
  background-color: color-mix(in oklab, var(--kat) 14%, var(--surface-lift));
}

.tz-bild img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.tz-text {
  flex: 1 1 18rem;
  min-width: 0;
  display: grid;
  gap: var(--s2);
}

.tz-untertitel {
  font-size: var(--t-m);
  color: var(--ink-soft);
}

.tz-beschreibung {
  font-size: var(--t-m);
  color: var(--ink);
}

.tz-vorstellung,
.tz-ort-lang,
.tz-preis-lang,
.tz-termine {
  font-size: var(--t-s);
  color: var(--ink-soft);
}

.tz-vorstellung span + span::before,
.tz-ort-lang span::before {
  content: " · ";
}

.tz-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
}

/* --- from 30 em: the date gets a track of its own, and the heading comes --- */

@media (min-width: 30em) {
  .tz-raster {
    grid-template-columns: var(--sp-datum) var(--sp-beginn) minmax(0, 1fr);
    align-items: baseline;
  }

  .tz-datum { grid-column: 1; grid-row: 1; }
  .tz-beginn { grid-column: 2; grid-row: 1; }
  .tz-titel { grid-column: 3; grid-row: 1; }
  .tz-fuss { grid-column: 3; grid-row: 2; }

  /* The transparent edge is not decoration: every row carries a 3 px family
     edge and the heading does not, so without it the heading's grid started
     3 px to the left of every grid under it and no column ever stood over
     its own values. */
  .tz-kopf {
    display: grid;
    position: sticky;
    top: var(--kopf);
    z-index: 20;
    padding: var(--s2) var(--s3);
    border-inline-start: 3px solid transparent;
    background: var(--bg);
    border-bottom: 1px solid var(--line);
  }

  /* In a row Preis and Punkte stand in the second line until 46 em. There is
     no heading for a second line, so those two fields stay out of the heading
     until then. Ende, Ort and Sprache are settled by the cell classes
     themselves, for the heading and the rows alike. */
  .tz-kopf .tz-preis,
  .tz-kopf .tz-punkte {
    display: none;
  }

  .tz-titel-text {
    flex: 1 1 auto;
    font-size: var(--t-l);
  }

  .tz-herz {
    order: 0;
  }

  /* The opened text lines up under the title, so the indent is the summary's
     own left padding plus the two tracks and the two gaps in front of it.
     The --s3 was missing before and the block sat 12 px to the left of the
     title it belongs to. The gap is --s1 here since the cushion arrived, and
     the tracks are 8 px wider each: 12 + 96 + 96 + 2×4 = 220, the same number
     it was when it read 12 + 88 + 88 + 2×12. */
  .tz-inhalt {
    padding-inline: calc(var(--s3) + var(--sp-datum) + var(--sp-beginn)
                         + 2 * var(--s1)) var(--s4);
  }
}

/* --- from 46 em: Preis and Ort get tracks of their own -------------------- */

@media (min-width: 46em) {
  /* The wider air arrives here and not sooner: 16 px between two columns
     rather than 12, and since 2026-08-10 it is 8 px of cushion inside the
     cell plus 8 px of gap. Counted at 736 px: the row inside is 661, the four
     fixed tracks are 96 + 96 + 84 + 160 = 436 and the four gaps 32, so the
     title's track is 661 − 436 − 32 = 193 px — the very number it had when
     the tracks were 88 + 88 + 76 + 152 = 404 and the gaps 64. 193 px is about
     28 characters of a real title a line, which is why the Ort track gives up
     24 px until 62 em rather than the title giving up more. Below 46 em the
     air is 12 px in the same two parts: 8 of cushion and 4 of gap. */
  .tz-raster {
    grid-template-columns:
      var(--sp-datum) var(--sp-beginn) minmax(0, 1fr)
      var(--sp-preis) var(--sp-ort);
    column-gap: var(--s2);
    row-gap: 0;
  }

  /* 12 + 96 + 96 + 2×8 = 220, the same as 12 + 88 + 88 + 2×16 was. */
  .tz-inhalt {
    padding-inline: calc(var(--s3) + var(--sp-datum) + var(--sp-beginn)
                         + 2 * var(--s2)) var(--s4);
  }

  /* From here the grid places itself: `display: contents` dissolves the box
     and Preis and Punkte become grid children in the order the markup wrote
     them. */
  .tz-fuss {
    display: contents;
  }

  .tz-datum,
  .tz-beginn,
  .tz-titel,
  .tz-preis,
  .tz-ort {
    grid-column: auto;
    grid-row: auto;
  }

  .tz-ort {
    display: block;
  }

  .tz-kopf .tz-preis {
    display: inline-flex;
  }

  .tz-punkte {
    display: none;
  }
}

/* --- from 62 em: all eight ------------------------------------------------ */

@media (min-width: 62em) {
  /* Ort takes a rem back here. Counted at 992 px: the row inside is 917, the
     seven fixed tracks are 96 + 96 + 76 + 84 + 112 + 168 + 68 = 700 and the
     seven gaps 56, so the title's track is 917 − 700 − 56 = 161 px — the
     narrowest it gets anywhere, 153 of it text inside its own cushion.

     THE CUSHION COST IT NOTHING. Before it the same seven tracks were
     88 + 88 + 68 + 76 + 104 + 160 + 60 = 644 and the gaps 112: 917 − 644 −
     112 = 161 as well. Seven tracks 8 px wider against seven gaps 8 px
     narrower is the whole of it, and it comes out at zero.

     161 and not the 189 this comment used to claim: that number was the one
     before TA8 put a written-out pill in the Kategorie column on 2026-08-10.
     That took the track from 3.75 rem to 6.5 rem of content and Ort gave one
     rem back, so 28 px came out of the title and this line was not pulled
     into line with it. At 189 px, 53.5 % of the 2030 titles in the database
     were on one line and 97.9 % within three; narrower they wrap one line
     further, which is what a wrapping column is for.

     From about 1216 px the page is standing in its own 76 rem hull and stops
     growing: `box-sizing: border-box` puts `.huelle`'s 24 px of padding
     INSIDE those 76 rem, so the content is 1216 − 48 = 1168, the row inside
     is 1141, and the title's track is 1141 − 700 − 56 = 385 px. That is the
     width this breakpoint actually spends most of its life at. */
  .tz-raster {
    --sp-ort: 10.5rem;

    grid-template-columns:
      var(--sp-datum) var(--sp-beginn) var(--sp-ende) minmax(0, 1fr)
      var(--sp-preis) var(--sp-punkte) var(--sp-ort) var(--sp-sprache);
  }

  .tz-ende,
  .tz-sprache {
    display: block;
  }

  .tz-punkte,
  .tz-kopf .tz-punkte {
    display: inline-flex;
  }
}
