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

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

body {
  margin: 0;
  background: var(--surface-05);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: var(--text-base);
  font-weight: var(--wght-light);
  line-height: var(--leading-normal);
  /* Titles, descriptions and prices are typed by venues, and a pasted ticket
     URL or a hashtag with no spaces is long enough to push the layout sideways
     at 375px. Break the word rather than the page. */
  overflow-wrap: anywhere;
}

a { color: inherit; }

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

/* Keep anchor targets below the sticky header. */
:target {
  scroll-margin-top: var(--header-h);
}

/* The top step was dead space while the header scrolled away with the page:
   it ended in padding, the filter row carried its own margin, and this only
   pushed the first event further down. A pinned header changed that — it has
   a border now, content slides under it, and a title starting 8px below a
   hairline reads as having fallen against it. */
.main {
  max-width: var(--measure);
  margin: 0 auto;
  padding: var(--space-6) var(--space-5) var(--space-8);
}

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

/* The site-top wrapper holds the header. It is sticky at every width so
   the brand and nav stay visible while scrolling. */

/* --header-h gives downstream sticky elements their top offset, and it has to
   be the real height at every width: .site-top is sticky from 0px up, so a 0
   here pins the bucket title underneath it and to a reader it simply vanishes.
   The 0px default in tokens.css is the fallback for a page that renders none of
   this chrome, not a width.

   Measured against the live site 2026-08-13: 69px at 375px, 73px at 1280px. The
   two rem values below are those, and the second is why they are two. */
:root { --header-h: 4.3rem; }
@media (min-width: 40rem) {
  :root { --header-h: 4.5rem; }
}

.site-top {
  position: sticky;
  top: 0;
  z-index: 4;
  background: var(--surface-05);
  border-bottom: 1px solid var(--line);
}

/* Centred, not baseline-aligned. Baseline put the wordmark's feet on the nav
   text's feet, which is right for two runs of type on one line — but the nav
   items are 44px tap targets with a marker box in front of them, so what the
   eye reads as their line is the middle of that box and not the letters in it.
   Baseline hung the whole row low against a header that is now pinned and has
   a border under it to be measured against. */
.site-header {
  display: flex;
  align-items: center;
  gap: var(--space-5);
  max-width: var(--measure);
  margin: 0 auto;
  padding: var(--space-4) var(--space-5) var(--space-2);
}

.brand { display: flex; align-items: baseline; gap: var(--space-2); text-decoration: none; }
.brand-mark {
  font-size: var(--text-3xs);
  letter-spacing: var(--track-mark);
  text-transform: uppercase;
  color: var(--text-faint);
}
.brand-name {
  font-family: var(--font-display);
  font-weight: 600;
  font-size: var(--text-wordmark);
  letter-spacing: -0.03em;
}

.site-nav { display: flex; gap: var(--space-4); margin-left: auto; font-size: var(--text-xs); }
.site-nav a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap);
  color: var(--text-muted);
  text-decoration: none;
}
.site-nav a:hover { color: var(--text); }
/* One or the other, never both. Above 30rem the word carries the entry and a
   glyph beside it would be decoration; below, the glyph replaces it. Both are
   always in the markup so the switch is CSS and needs no script. */
/* fill:none here as well as in the markup. A glyph that arrives filled would
   claim a state it does not have — a filled heart in this product means "you
   follow this" — and one rule is cheaper than trusting five path strings. */
.site-nav .nav-glyph { display: none; flex: none; fill: none; stroke: currentColor; }

/* Every item reserves the marker, not just the current one. Putting 6px plus a
   gap on the active item alone made it wider than its neighbours, so the whole
   row re-laid out on each navigation and the words visibly jumped. The box is
   always there and only its fill changes, which is a repaint and moves
   nothing. */
.site-nav a::before {
  content: "";
  order: -1;
  width: var(--marker);
  height: var(--marker);
  flex: none;
  background: transparent;
}
.site-nav a[aria-current="page"] { color: var(--text); }
.site-nav a[aria-current="page"]::before { background: var(--accent); }

/* The account slot is the one entry that keeps its glyph at every width.
   Explore, Feed and Saved are content views and their words earn the space;
   this one is not a view, it is where you live, and the person glyph top right
   is the single convention nobody has to learn. It also takes the longest
   string out of a row that was already running out of width at 375px.

   No reserved marker box here: in a square sized for one glyph it would push
   that glyph off centre, so the current page takes the accent itself — the
   same trade the glyph-only layout below makes. */
.site-nav .nav-account { width: var(--tap); justify-content: center; gap: 0; }
.site-nav .nav-account .nav-label { display: none; }
.site-nav .nav-account .nav-glyph { display: block; }
.site-nav .nav-account::before { content: none; }
.site-nav .nav-account[aria-current="page"] { color: var(--accent); }

/* The account menu. <summary> carries the same square as the plain slot, minus
   the disclosure triangle the browser draws by default. */
.site-nav .account > summary {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap);
  color: var(--text-muted);
  cursor: pointer;
  list-style: none;
}
.site-nav .account > summary::-webkit-details-marker { display: none; }
.site-nav .account[open] > summary { color: var(--text); }

/* Narrow, right-aligned, hanging off the icon itself, and built from the ramp
   like everything else: --surface-15 is the raised step, the one chips and
   secondary buttons already stand on.

   The hover step is mixed toward --text rather than picked from the ramp,
   because the light theme's steps above the base sit too close together to
   read as two. Mixing follows the theme automatically — lighter on dark,
   darker on light — which is the direction fills move anyway, and it adds no
   value the palette does not already contain.

   The hairline between items is the same divider the register uses. */
.account { position: relative; }
.account-panel {
  position: absolute;
  top: calc(100% + var(--space-2));
  right: 0;
  z-index: 3;
  min-width: 11rem;
  display: grid;
  background: var(--surface-15);
}
.site-nav .account-panel a {
  display: flex;
  align-items: center;
  min-height: var(--tap);
  padding-inline: var(--space-4);
  color: var(--text-muted);
}
.site-nav .account-panel a::before { content: none; }
.site-nav .account-panel a + a { border-top: 1px solid var(--line); }
@media (hover: hover) {
  .site-nav .account-panel a:hover {
    color: var(--text);
    background: color-mix(in srgb, var(--surface-15) 92%, var(--text));
  }
}

/* border-style, not the `border: 0` shorthand: the guard test forbids the
   shorthand outright so that a `border: 1px solid …` cannot slip past review
   inside a rule that already looked harmless.

   No fill. In the header it was a filled block competing with the wordmark;
   in the footer, among four quiet links, it would have been the loudest thing
   on the page for a control set once and then forgotten. The glyph alone is
   the affordance, and the tap target stays 44px around it. */
/* A switch is not an action, so it does not look like one. The text buttons
   beside it do something; these change what you are looking at or which mode
   you are in, and they stay bare icons at tap size rather than competing with
   the actions for weight. */
.theme-toggle,
.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  font: inherit;
  background: none;
  color: var(--text-muted);
  text-decoration: none;
  border-style: none;
  width: var(--tap);
  height: var(--tap);
  cursor: pointer;
}
.icon-button svg { fill: none; stroke: currentColor; stroke-width: 1.6; }
@media (hover: hover) {
  .theme-toggle:hover,
  .icon-button:hover { color: var(--text); }
}
.theme-toggle:active,
.icon-button:active { color: var(--text); }

/* Back-to-top: fixed at the bottom right, appears on scroll.
   The .icon-button class carries the shape, sizing and hover;
   this layer adds position, surface and the fade. */
.scroll-top {
  position: fixed;
  /* Fixed at 24px from the bottom edge put a 44px tap target inside the home
     indicator's strip, where iOS treats an upward swipe as its own gesture — so
     the button was both hard to hit and sitting under the one thing on screen
     that is not ours. The footers take the same inset; this is the same edge. */
  bottom: calc(var(--space-5) + env(safe-area-inset-bottom, 0px));
  right: var(--space-5);
  z-index: 10;
  background: var(--surface-15);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--dur-fast) var(--ease);
}
.scroll-top.is-visible {
  opacity: 1;
  pointer-events: auto;
}

/* feed --------------------------------------------------------------- */

.bucket { margin-block: var(--space-6) 0; }

.bucket-title {
  /* Stacked on a phone, because the two halves genuinely do not fit beside each
     other: at 375px the row is 327px wide and a range like "Fr 14.08. – So
     16.08." already claims 268 of it, leaving the word a column too narrow to
     hold "Wochenende". Widening the label only moves the break onto the date,
     which is the half nobody can guess from context. Both languages overflow the
     same way — "This weekend" is no shorter than the German. */
  z-index: 2;
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--space-1);
  /* These are our own words, not a venue's: body breaks anywhere so a pasted URL
     cannot push the page sideways, and inherited here that turns a heading into
     "Wochenen / de". A chrome string wraps between words or not at all. */
  overflow-wrap: normal;
  margin: 0;
  padding: var(--space-3) 0;
  background: var(--surface-05);
  border-bottom: 1px solid var(--line);
  font-size: var(--text-base);
  font-weight: var(--wght-light);
  color: var(--text-muted);
}
/* Pinned at every width as of 2026-08-13. This reverses the rule that used to
   live in the breakpoint below, and that rule's objection is kept rather than
   deleted because it was measured and is still true: a pinned heading on a
   375px screen costs a line of a viewport that shows two and a half cards.

   What decided it against that: 69px of header plus 93.5px of heading on an
   812px screen leaves 650px, which is three and a half cards, and knowing which
   day you are looking at while scrolling a date-bucketed list is what the
   heading is for. The maintainer asked for it on the device it costs the most.

   It stays two lines here. That is its own measured rule, a few lines up:
   "Wochenende" and the date do not share a line at this width. */
.bucket-title {
  position: sticky;
  top: var(--header-h);
}
@media (min-width: 40rem) {
  /* One line once there is room for one. */
  .bucket-title {
    flex-direction: row;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--space-4);
  }
}
.bucket-date {
  font-size: var(--text-2xl);
  font-weight: var(--wght-medium);
  letter-spacing: -0.02em;
  font-variant-numeric: tabular-nums;
  color: var(--text);
}

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

/* The hover surface runs to the viewport edge rather than stopping at the
   measure: an inset fill reads as a card, which is the shape this replaces. */
.tip {
  position: relative;
  display: grid;
  grid-template-columns: 1fr var(--img-w);
  gap: 0 var(--space-4);
  align-items: start;
  padding: var(--space-4) var(--space-5);
  margin-inline: calc(var(--space-5) * -1);
  border-bottom: 1px solid var(--line);
  background: transparent;
  transition: background var(--dur-surface) var(--ease);
}

.tip-when {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin: 0 0 var(--space-2);
  font-size: var(--text-sm);
  font-weight: var(--wght-medium);
  font-variant-numeric: tabular-nums;
  color: var(--text-muted);
  transition: color var(--dur-fast) var(--ease);
}

/* font-weight and font-variation-settings are both set: the first is what
   font matching uses, the second is what animates. */
.tip-title {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: var(--wght-medium);
  font-variation-settings: "wght" 500;
  line-height: var(--leading-tight);
  letter-spacing: var(--track-title);
  transition: font-variation-settings var(--dur-type) var(--ease);
}
.tip-title a { text-decoration: none; }

/* The whole row is the link, via a pseudo-element on the title stretched over
   the card. Wrapping the row in an <a> instead would nest the save button and
   the venue link inside it: invalid HTML, and a screen reader would announce
   the row as one link with controls buried in its name. Here the accessible
   name stays the title alone, and the two real controls are lifted above the
   overlay so they keep their own hit area.
   The cost is that text inside a row can no longer be selected by dragging —
   the overlay is on top of it. Accepted: these rows are navigation, and the
   event page below has the same text selectable. */
.tip-title a::after {
  content: "";
  position: absolute;
  inset: 0;
}
.tip-meta a,
.tip .share-link,
.tip .save { position: relative; z-index: 1; }

/* Beside the save button, not below it: both are things you do to this row,
   and the row's own link already covers everything else. Lifted above the
   title's overlay like the other two controls, or the card link would swallow
   it. */
/* Scoped to the card: on the event page the same form stands on its own line
   and must keep its block box. */
.tip .going-form { display: inline-flex; }
.share-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap);
  min-height: var(--tap);
  color: var(--text-muted);
}
.share-link svg { fill: none; stroke: currentColor; }
@media (hover: hover) {
  .share-link:hover { color: var(--text); }
}

.tip-meta {
  margin: var(--space-1) 0 0;
  font-size: var(--text-xs);
  font-weight: var(--wght-light);
  color: var(--text-muted);
}

.tip-cancelled .tip-title { text-decoration: line-through; color: var(--text-muted); }

/* Without a flyer there is no carrier at all and the text takes the full
   width. Keeping the column and merely emptying it traded a missing picture
   for a grey rectangle claiming to be one — and that is the normal case here,
   not the exception: the aggregation agent takes facts and deliberately not
   artwork, which is a legal boundary rather than an omission. */
.tip-flat {
  grid-template-columns: 1fr;
}

/* The frame stays wherever there *is* an image, and it is not decoration:
   flyers are often near-black or near-white, and one of the two disappears
   into the page without a neutral carrier behind it. */
.tip-figure {
  background: var(--surface-20);
  aspect-ratio: var(--img-ratio);
  overflow: hidden;
}
.tip-figure img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-image) var(--ease);
}

@media (hover: hover) {
  .tip:hover { background: var(--surface-10); }
  .tip:hover .tip-title { font-variation-settings: "wght" var(--wght-hover); }
  .tip:hover .tip-when { color: var(--text); }
  .tip:hover .tip-figure img { transform: scale(1.03); }
}

/* Touch has no hover, and the keyboard must not be worse off than the mouse:
   both states reproduce all four effects hover does, not just the surface
   and the title weight. */
.tip:active { background: var(--surface-10); }
.tip:active .tip-title { font-variation-settings: "wght" var(--wght-hover); }
.tip:active .tip-when { color: var(--text); }
.tip:active .tip-figure img { transform: scale(1.03); }

.tip:focus-within { background: var(--surface-10); }
.tip:focus-within .tip-title { font-variation-settings: "wght" var(--wght-hover); }
.tip:focus-within .tip-when { color: var(--text); }
.tip:focus-within .tip-figure img { transform: scale(1.03); }

.going-form { margin: var(--space-2) 0 0; }
.going {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap);
  padding: 0;
  font: inherit;
  font-size: var(--text-xs);
  font-weight: var(--wght-medium);
  font-variant-numeric: tabular-nums;
  background: none;
  border-style: none;
  color: var(--text-muted);
  cursor: pointer;
}
/* Following is the one state that does not take the square marker. Every
   other indicator means "this row is in some state"; this one means "you did
   something", and a heart says that without a legend. Outlined until you
   follow, accent-filled after — the fill is the whole signal. */
.follow,
.save {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap);
  padding: 0;
  font: inherit;
  font-size: var(--text-xs);
  font-weight: var(--wght-medium);
  background: none;
  border-style: none;
  color: var(--text-muted);
  cursor: pointer;
}
.follow .heart,
.save .mark-icon {
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linejoin: miter;
  transition: fill var(--dur-fast) var(--ease), stroke var(--dur-fast) var(--ease);
}
.follow[aria-pressed="true"],
.save[aria-pressed="true"] { color: var(--text); }
.follow[aria-pressed="true"] .heart,
.save[aria-pressed="true"] .mark-icon { fill: var(--accent); stroke: var(--accent); }

.going[aria-pressed="true"] { color: var(--text); }
.going[aria-pressed="true"]::before {
  content: "";
  width: var(--marker);
  height: var(--marker);
  background: var(--accent);
}

/* pages -------------------------------------------------------------- */

.page-title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-display);
  font-weight: var(--wght-medium);
  line-height: var(--leading-display);
  letter-spacing: var(--track-display);
}
.page-lede { margin: 0 0 var(--space-5); color: var(--text-muted); }

/* event detail ------------------------------------------------------- */

/* Up, not back. The arrow points at the venue this event belongs to, which is
   also its parent in the URL — so it works for someone who arrived from a
   shared link with no history behind them, and it needs no script. */
/* The row holds the way up and, for whoever may edit this date, the way into
   the panel — pushed to the far end so the two never read as a pair. */
.event-back {
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
}
.event-back a {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: var(--tap);
  font-size: var(--text-xs);
  color: var(--text-muted);
  text-decoration: none;
}
.event-back svg { flex: none; fill: none; stroke: currentColor; stroke-width: 1.6; }
@media (hover: hover) {
  .event-back a:hover { color: var(--text); }
}

.event { display: grid; gap: var(--space-4); }

.event-figure {
  width: min(100%, 24rem);
  background: var(--surface-20);
  aspect-ratio: var(--img-ratio);
  overflow: hidden;
}
/* contain, not cover: a detail page must not crop the flyer it exists to show. */
.event-figure img { display: block; width: 100%; height: 100%; object-fit: contain; }

/* The flyer is the trigger for its own lightbox — the small print on it is the
   only reason anyone enlarges an event image, so the image is the target. */
.event-figure-zoom { display: block; width: 100%; height: 100%; cursor: zoom-in; }

.event-title {
  margin: 0;
  font-size: var(--text-display);
  font-weight: var(--wght-medium);
  line-height: var(--leading-display);
  letter-spacing: var(--track-display);
}
.event-subtitle { margin: 0; color: var(--text-muted); font-size: var(--text-lg); }
.event-when {
  margin: 0;
  font-size: var(--text-lg);
  font-weight: var(--wght-medium);
  font-variant-numeric: tabular-nums;
}
.event-body p { margin: 0 0 var(--space-4); }

.event-facts { margin: 0; font-size: var(--text-xs); }
.event-facts div {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  padding: var(--space-3) 0;
  border-bottom: 1px solid var(--line);
}
.event-facts dt { color: var(--text-muted); }
.event-facts dd { margin: 0; }

/* Links, never embeds — a video is a line in this list, not an iframe. Rows
   rather than chips: a label is a sentence fragment ("Live-Set von letzter
   Woche"), and wrapping those as tags gives a ragged block nobody can scan.
   Full-width targets, one tap height each, because this is read on a phone. */
.event-links { margin-top: var(--space-6); }
.event-links-title {
  margin: 0 0 var(--space-2);
  font-size: var(--text-2xs);
  font-weight: 500;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-muted);
}
.link-list { list-style: none; margin: 0; padding: 0; }
.link-list li { border-bottom: 1px solid var(--line); }
.link-list a {
  display: flex;
  align-items: center;
  min-height: var(--tap);
  padding: var(--space-2);
  margin-inline: calc(var(--space-2) * -1);
  font-size: var(--text-xs);
  color: var(--text);
  text-decoration: none;
  overflow-wrap: anywhere;
  transition: background var(--dur-fast) var(--ease);
}
@media (hover: hover) {
  .link-list a:hover { background: var(--surface-15); }
}
.link-list a:active,
.link-list a:focus-visible { background: var(--surface-15); }

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

.share-open svg { margin-right: var(--space-2); fill: none; stroke: currentColor; }

/* overlays: share sheet and artwork lightbox -------------------------- */

/* One pattern used twice, rather than two. Both open on :target and close with
   a link back to the canonical URL — no script on either path, which is what
   the no-JS rule requires of the surface opened from an Instagram bio.

   display:none while closed does three jobs at once: it keeps the panel out of
   the tab order, keeps it out of the accessibility tree, and is what stops the
   lightbox's loading="lazy" image from being fetched at all until somebody
   opens it. A hidden image never enters the viewport, and a lazy image outside
   the viewport is never requested. */
.overlay {
  display: none;
  position: fixed;
  inset: 0;
  z-index: 20;
}
.overlay:target { display: block; }

/* With a script running, the two classes below decide instead of :target, and
   `is-closed` has to come last so it wins the tie on source order.
   history.replaceState drops the fragment from the address bar but does *not*
   re-evaluate :target — a document's target element is set by navigation, and
   the History API is not one. Closing therefore has to say so in the DOM, or
   the panel stays open forever with the URL already clean. */
.overlay.is-open { display: block; }
.overlay.is-closed { display: none; }

/* A close target that fills the screen. Out of the tab order and hidden from
   assistive tech on purpose: the labelled control beside the title is the
   keyboard route, and a second identical "close" stop would be noise. */
.overlay-scrim {
  position: absolute;
  inset: 0;
  background: var(--overlay);
}

/* Separated from the dimmed page by the surface ramp, not by an outline —
   there is no border shorthand in this product and a box drawn round a panel
   is exactly the card shape the ramp replaced. */
.overlay-panel {
  position: absolute;
  background: var(--surface-10);
}

.overlay-close {
  position: absolute;
  top: var(--space-2);
  right: var(--space-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--tap);
  height: var(--tap);
  color: var(--text-muted);
}
.overlay-close svg { fill: none; stroke: currentColor; }
@media (hover: hover) {
  .overlay-close:hover { color: var(--text); }
}

/* A bottom sheet on a phone: this is opened one-handed, and the entries belong
   within reach of a thumb rather than centred on a screen. Above 40rem there is
   no thumb to reach with and a full-width bar would be a strip of links across
   a desktop window, so it becomes a centred panel. */
.sheet-panel {
  left: 0;
  right: 0;
  bottom: 0;
  max-height: 88vh;
  overflow-y: auto;
  padding: var(--space-5) var(--space-5) calc(var(--space-5) + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--line-strong);
}
@media (min-width: 40rem) {
  .sheet-panel {
    left: 50%;
    right: auto;
    top: 50%;
    bottom: auto;
    width: min(100% - var(--space-7), var(--measure-form));
    transform: translate(-50%, -50%);
  }
}

/* Room for the close control, which is absolutely positioned over this line. */
.sheet-title {
  margin: 0 0 var(--space-4);
  padding-right: var(--tap);
  font-size: var(--text-lg);
  font-weight: var(--wght-medium);
  line-height: var(--leading-tight);
  letter-spacing: var(--track-title);
}
.sheet-heading {
  margin: var(--space-5) 0 var(--space-2);
  font-size: var(--text-2xs);
  font-weight: var(--wght-medium);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-muted);
}

/* Same shape as .link-list on the page behind it: full-width rows, one tap
   height each, because every one of these is a destination on a phone. */
.sheet-list { list-style: none; margin: 0; padding: 0; }
.sheet-list li { border-bottom: 1px solid var(--line); }
.sheet-list a {
  display: flex;
  align-items: center;
  min-height: var(--tap);
  padding: var(--space-2);
  margin-inline: calc(var(--space-2) * -1);
  font-size: var(--text-sm);
  color: var(--text);
  text-decoration: none;
  overflow-wrap: anywhere;
  transition: background var(--dur-fast) var(--ease);
}
@media (hover: hover) {
  .sheet-list a:hover { background: var(--surface-15); }
}
.sheet-list a:active,
.sheet-list a:focus-visible { background: var(--surface-15); }

.sheet-copy { display: grid; gap: var(--space-2); margin-top: var(--space-5); }
.sheet-copy-label {
  font-size: var(--text-2xs);
  font-weight: var(--wght-medium);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-muted);
}
/* The button is absent until share.js adds it — there is no clipboard without
   a script, and a dead button is worse than none. */
.sheet-copy-row { display: flex; gap: var(--space-2); align-items: stretch; }
.sheet-copy-row input { flex: 1 1 auto; min-width: 0; }
.sheet-copy-row .button { flex: none; padding-inline: var(--space-4); }

.sheet .follow-form { margin-top: var(--space-5); }

/* The flyer, as large as the screen allows. The panel is the neutral frame the
   theme rules ask for: event artwork is often near-black or near-white, and one
   of the two disappears against an unframed backdrop in either theme. */
.lightbox-panel {
  inset: var(--space-4);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-4);
  background: var(--surface-00);
}
/* Fills the panel and is contained inside it, rather than sized from its own
   intrinsic dimensions. `width: auto` looks like the more natural rule and is
   a deadlock: this image is lazy, an unloaded image with auto dimensions lays
   out at 0x0, and a 0x0 element never intersects the viewport — so it would
   never load, and so it would never gain the dimensions that would have given
   it a size. object-fit does the letterboxing a portrait flyer needs here. */
.lightbox-panel img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

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

.form { display: grid; gap: var(--space-5); max-width: var(--measure-form); }
.field { display: grid; gap: var(--space-2); }
/* A checkbox and its sentence sit on one line; the label is the tap target,
   so it takes the full height rather than the checkbox alone — and so both
   halves say so with the cursor. The box alone did, the sentence beside it did
   not, which made the larger of the two targets look like text you cannot
   click. Under `hover: hover`, because a cursor is a mouse thing and the rule
   is that hover states never fire on touch. */
@media (hover: hover) {
  .field-check,
  .field-check label,
  .field-check input { cursor: pointer; }
}
.field-check {
  grid-template-columns: auto 1fr;
  align-items: center;
  gap: var(--space-3);
}
.field-check label {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  text-transform: none;
  letter-spacing: normal;
  font-size: var(--text-xs);
  color: var(--text-muted);
}
.field-check input { width: 1.125rem; height: 1.125rem; accent-color: var(--accent); }

.field label {
  font-size: var(--text-2xs);
  letter-spacing: var(--track-label);
  text-transform: uppercase;
  color: var(--text-faint);
}

/* appearance:none rather than border-radius:0 — it is what actually removes
   iOS Safari's rounded inset on text fields, and it keeps the radius guard
   honest. */
input[type="email"],
input[type="text"],
input[type="password"],
input[type="url"],
input[type="search"],
textarea,
select {
  width: 100%;
  padding: var(--space-3) var(--space-4);
  font: inherit;
  font-weight: var(--wght-light);
  color: var(--text);
  background: var(--surface-00);
  appearance: none;
  border-style: none;
}

input:focus-visible,
textarea:focus-visible,
select:focus-visible {
  box-shadow: inset 2px 0 0 0 var(--accent);
  outline-offset: 0;
}

.code-input {
  font-size: var(--text-2xl);
  font-weight: var(--wght-medium);
  letter-spacing: 0.42em;
  text-align: center;
  font-variant-numeric: tabular-nums;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: var(--tap);
  padding: var(--space-3) var(--space-5);
  font: inherit;
  font-weight: var(--wght-medium);
  text-decoration: none;
  background: var(--invert-surface);
  color: var(--invert-text);
  border-style: none;
  cursor: pointer;
}
.button-quiet { background: var(--surface-15); color: var(--text); }
.button-plain { background: none; color: var(--text-muted); padding-inline: 0; }

.notice {
  padding: var(--space-4);
  background: var(--surface-00);
  font-size: var(--text-xs);
  color: var(--text-muted);
}
.notice-error { padding-inline: 0; background: none; color: var(--danger); }
/* A notice that introduces a form belongs to it: it lines up with the field
   and the button below rather than running out to the full text measure. */
.notice-form { max-width: var(--measure-form); }

.honeypot { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

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

/* The settings tabs are addresses, not a widget: four sub-paths of /settings,
   each with its own form. The bar is a hairline with the active tab's accent
   bar sitting on it — the same repaint-never-remeasure rule as the site nav
   marker: every tab reserves the bar and only its fill changes. */
.settings-tabs {
  display: flex;
  gap: var(--space-4);
  margin-bottom: var(--space-5);
  border-bottom: 1px solid var(--line);
}
.settings-tab {
  position: relative;
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  color: var(--text-muted);
  text-decoration: none;
}
.settings-tab:hover { color: var(--text); }
.settings-tab::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -1px;
  height: 2px;
  background: transparent;
}
.settings-tab[aria-current="page"] { color: var(--text); }
.settings-tab[aria-current="page"]::after { background: var(--accent); }

/* register rows ------------------------------------------------------ */

/* The same shape app.css gives the workspace, on the server-rendered side.
   The settings page reached for these classes while loading none of the file
   that defined them, so the security-key list rendered as bullet points with a
   button after each one.
 *
 * No `display` on .card: it is worn together with .row here, and a display
 * declared later in this file would otherwise decide the layout by source
 * order rather than by intent. */
.card-list { list-style: none; margin: 0; padding: 0; }

.card {
  padding: var(--space-4) var(--space-5);
  margin-inline: calc(var(--space-5) * -1);
  border-bottom: 1px solid var(--line);
  background: transparent;
}

.card-meta {
  margin: var(--space-1) 0 0;
  font-size: var(--text-xs);
  font-weight: var(--wght-light);
  color: var(--text-muted);
}

/* Pushes the row's last control to the far end — the remove button beside the
   key it removes. */
.row-end { margin-left: auto; }

.pub-error { padding: var(--space-4); background: var(--surface-00); color: var(--danger); }

@media (max-width: 30rem) {
  .card { padding-inline: var(--space-4); margin-inline: calc(var(--space-4) * -1); }
}

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

/* The bottom inset, and why the last row needs it. The viewport meta carries
   `viewport-fit=cover`, so the document extends behind the home indicator rather
   than stopping above it — which is the whole point on a page opened from an
   Instagram bio, and it means the last row in the document is the one that pays
   for it. The language switcher and the theme toggle were sitting in that strip.

   Same form as .sheet-panel, which already had this: the fallback keeps the
   declaration meaningful on every browser that has no such inset to report. */
.site-footer {
  max-width: var(--measure);
  margin: 0 auto;
  padding: var(--space-6) var(--space-5) calc(var(--space-6) + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--line);
  color: var(--text-faint);
  font-size: var(--text-xs);
}
.site-footer {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
}
.site-footer nav { display: flex; gap: var(--space-4); flex-wrap: wrap; }
.site-footer a { color: var(--text-muted); text-decoration: none; }
.footer-note { margin: var(--space-3) 0 0; }

/* A calendar page drops the site header and nav, but never the legal links:
   it is the page venues put in their bio, so § 5 DDG applies to it. Quiet
   enough not to compete with the venue's own design, present all the same. */
.page-footer {
  max-width: var(--measure);
  margin: var(--space-7) auto 0;
  /* The same inset as .site-footer, and this is the surface it matters most on:
     a calendar page is the thing venues put in their bio, so it is opened on a
     phone by definition and this footer is the last row of it. */
  padding: var(--space-5) var(--space-5) calc(var(--space-5) + env(safe-area-inset-bottom, 0px));
  border-top: 1px solid var(--line);
  color: var(--text-faint);
  font-size: var(--text-xs);
}
.page-footer nav { display: flex; gap: var(--space-4); flex-wrap: wrap; }
.page-footer a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  color: var(--text-muted);
  text-decoration: none;
}
.page-footer .footer-note { margin: var(--space-2) 0 0; }

/* 375px is the surface this product is actually opened on, and the header is
   what stands between somebody and the first event. Brand and nav share one
   line where they fit and wrap to two where they do not — the toggle used to
   force a third, which is why it now lives in the footer instead.
   "Wochenende" must not break mid-word. */
@media (max-width: 30rem) {
  .site-header {
    flex-wrap: wrap;
    /* One step in from the content, not the content's own inset: the header
       is the row that has to fit, and 16px of side padding is what buys the
       nav the width to stay beside the brand. */
    gap: var(--space-2) var(--space-3);
    padding: var(--space-4) var(--space-4) var(--space-2);
  }
  /* Wide enough to sit beside the brand, but still allowed to wrap. At 375px
     the row is Explore · Feed · Gemerkt · ▦ · ◯ against a ~86px wordmark, and
     it fits with room to spare — on a 320px screen it does not, and there the
     wrap is the right answer rather than a horizontal scrollbar.
     `margin-left: 0` is deliberately absent: pushing the nav right is what
     puts it beside the brand instead of under it. */
  .site-nav {
    flex: 0 1 auto;
    gap: var(--space-2);
    font-size: var(--text-xs);
    flex-wrap: wrap;
  }
  /* Glyphs only. Five words do not fit beside a wordmark at 375px, and every
     one of these glyphs is already met elsewhere in the product: the search of
     the filter row, the heart of the follow button, the bookmark of the save
     button. The word stays in the markup and in aria-label, so the link keeps
     its accessible name and the browser still shows it on long-press. */
  .nav-label { display: none; }
  .site-nav .nav-glyph { display: block; }
  /* Direct children only — the row's own links, never the account menu's. The
     entries inside .account-panel are words in a panel that opens below the
     row, and a 2rem box minus their padding left them a 0px content box: with
     body's overflow-wrap: anywhere inherited, "Moderation" wrapped one letter
     per line. Same leak the ::before marker already needed a corrective for
     further up; a child combinator states what the rule always meant. */
  .site-nav > a {
    justify-content: center;
    width: 2rem;
    gap: 0;
  }

  /* The square marker would be a second glyph beside the first, in a box
     sized for one. Dropped on every item, not just the current one: the
     reserved box would otherwise push each glyph 6px off centre. The current
     page takes the accent itself. */
  .site-nav a::before { content: none; }
  .site-nav a[aria-current="page"] { color: var(--accent); }

  /* Less than the desktop step, because vertical room is the scarce thing at
     375px — but not the 4px it was. That number came from a header that
     scrolled away with the page; against a pinned one with a border under it,
     a title starting 4px below the hairline reads as having fallen against
     it. */
  .main { padding-top: var(--space-4); }
}

/* A bare inline link is about 21px tall, well under the 44px target, so the
   footer links get the height back. The nav links carry it in their own rule
   further up, where they also carry the glyph layout. */
.site-footer a {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
}

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

/* tags ----------------------------------------------------------------- */

/* Small, quiet, and square. STYLE.md's first rule is that there is no radius
   token to reach for, so a tag reads as a tag through size and weight rather
   than through a rounded outline: one step below body text, muted until
   touched, and never filled with the accent — the accent is a marker in this
   system and never carries text. */
.tag-list {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  list-style: none;
  margin: var(--space-4) 0 0;
  padding: 0;
}
.tag {
  display: inline-flex;
  align-items: center;
  padding: 0.125rem var(--space-2);
  background: var(--surface-15);
  color: var(--text-muted);
  font-size: var(--text-2xs);
  line-height: 1.6;
  text-decoration: none;
  white-space: nowrap;
}
/* A facet reads as ours and a tag as the venue's, which is the one thing a
   reader needs to tell apart in a row that otherwise looks uniform. The
   difference is weight and ink, not a second shape: two chip shapes in one row
   would suggest two kinds of link, and both go to the same filter. */
.tag-facet {
  color: var(--text);
  font-weight: var(--wght-medium);
}

@media (hover: hover) {
  .tag:hover { background: var(--surface-20); color: var(--text); }
}
.tag:active,
.tag:focus-visible { background: var(--surface-20); color: var(--text); }

/* The row is small, so the targets are too. Coarse pointers get the height
   back — the same exception the calendar chips take, and for the same reason:
   on touch this is the only way in. */
@media (pointer: coarse) {
  .tag { min-height: var(--tap); padding-inline: var(--space-3); }
  .tag-list { gap: var(--space-2); }
}

/* The two controls that change how a page is presented rather than where it
   goes. Grouped, and set apart from the legal links: among four destinations
   the language switcher read as a fifth. */
.footer-settings {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}
.lang-switch {
  display: inline-flex;
  align-items: center;
  min-height: var(--tap);
  padding-inline: var(--space-2);
  color: var(--text-muted);
  text-decoration: none;
  font-size: var(--text-xs);
}
@media (hover: hover) {
  .lang-switch:hover { color: var(--text); }
}
.lang-switch:active { color: var(--text); }
/* The word above 30rem, the code below it. Both are in the markup and one is
   hidden, never removed — the same rule as .nav-label in the header, and the same
   reason: display:none takes text out of the accessibility tree, and the link's
   own aria-label is what carries the full sentence either way. */
.lang-code { display: none; }
@media (max-width: 30rem) {
  .lang-full { display: none; }
  .lang-code { display: inline; }
}

/* A calendar page has no theme toggle, so the switcher sits alone at the end
   of the row rather than in the middle of nothing. */
.page-footer .footer-settings { margin-left: auto; }

/* dev sign-in ---------------------------------------------------------- */

/* Local only. Deliberately plain and slightly apart from the real form: it is
   scaffolding, and scaffolding that looks like product is how it ends up
   somewhere it should not be. */
.dev-login { margin-top: var(--space-7); }
.dev-login-list { display: grid; gap: var(--space-2); margin-top: var(--space-3); }
.dev-login-list .button {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  text-align: left;
}
.dev-login-role {
  font-size: var(--text-2xs);
  color: var(--text-faint);
  text-transform: uppercase;
  letter-spacing: var(--track-label);
}

/* Where the house is. Sits at lede weight rather than as a labelled field: it
   is one of the two facts a visitor came for and it reads as a subtitle to the
   name, not as a row in a record. */
.venue-address {
  margin: 0 0 var(--space-5);
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* A venue whose programme tipsBOT maintains. Deliberately quiet rather than a
   warning banner: the venue has done nothing wrong, and the line is an invitation
   to take the page over, not an alert. */
.venue-bot {
  margin: 0.75rem 0 0;
  padding: 0.6rem 0.75rem;
  border-left: 2px solid var(--line);
  color: var(--text-muted);
  font-size: var(--text-sm);
  line-height: var(--leading-normal);
}

/* The accent never carries text — it is a marker in this system, and its
   curated tones are certified at 3:1 for a 6px square, not at 4.5:1 for a
   sentence. As a link colour it measured 3.73:1 on dark and 3.60:1 on light,
   under the floor in both. --text against the --text-muted line around it, plus
   the underline, is what makes it a link without spending the accent on it. */
.venue-bot a {
  color: var(--text);
  text-decoration: underline;
  white-space: nowrap;
}

/* The one switch in the product, and the one rounded thing in it.
 *
 * STYLE.md rule 1 says no border-radius anywhere, and this is a named
 * exception rather than a crack in it: a switch is the single control whose
 * affordance *is* its shape — a square one reads as two buttons that happen to
 * touch, which is exactly what it replaced and exactly what was wrong with it.
 * The exception is scoped to selectors containing `switch`, and the design
 * guard enforces that scope rather than dropping the rule.
 *
 * Small and quiet on purpose: 2.25rem of track, the same height as every other
 * control in a filter row, with the state carried by the knob's position and
 * one step of surface rather than by colour.
 */
.switch {
  display: inline-flex;
  flex: 0 0 auto;
  position: relative;
  background: var(--surface-00);
  border-radius: 1.125rem;
  /* No bottom hairline: the track's own outline is the boundary, and a rule
     under a rounded shape reads as a mistake. */
}
/* --text-muted, not --text-faint. The faint step is certified on --surface-05
   and on nothing else, and this label sits on the switch track, which is
   --surface-00: 4.05:1 on light, under the AA floor. The rule was already
   written down — "on a chip, a sunken field, the image carrier or a hovered row
   it falls below AA in one theme or the other" — and this is the sunken field. */
.switch-half {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-height: 2.25rem;
  padding: 0 var(--space-3);
  font-size: var(--text-2xs);
  color: var(--text-muted);
  cursor: pointer;
  white-space: nowrap;
  border-radius: 1.125rem;
  position: relative;
  z-index: 1;
  transition: color 120ms ease;
}
/* `.field label` (0,1,1) beats `.switch-half` (0,1,0), so the resting half was
   rendering at --text-faint on the track — 4.05:1 in light, which is exactly
   the failure the comment above says this rule exists to avoid. The intent was
   written down; the selector never won. This one matches on specificity. */
.field .switch-half { color: var(--text-muted); }

/* Off-screen rather than display:none — a hidden input is not focusable, and
   the keyboard path to a switch has to stay a real one. */
.switch-half input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* Neither side chosen is a real state and the resting one: nothing is filtered
   and the control says so by being evenly quiet.

   Inverted, not one step up the surface ramp. The ramp carries this fine in
   dark, where --surface-00 and --surface-20 are six values apart; in light they
   are three (#d9d9d9 against #d6d6d6) and the chosen half was indistinguishable
   from the other one. Inversion is the vocabulary the product already has for
   "chosen" — it is what a primary button wears — and STYLE.md reserves the
   accent for indicators, so this is the step that exists rather than a colour
   introduced for it. */
.switch-half[data-active] {
  color: var(--invert-text);
  background: var(--invert-surface);
}
/* Both halves lift to --text now that the resting step is --text-muted; hovering
   to muted from muted was a state that changed nothing. The active half stays
   told apart by its --surface-20 fill, not by its label colour. */
@media (hover: hover) {
  .switch-half:hover { color: var(--text); }
}
.switch-half:focus-within {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 1.125rem;
}
@media (prefers-reduced-motion: reduce) {
  .switch-half { transition: none; }
}

/* cycle switch — one question, three positions, one control ------------- */

/* Rounded by the same named exception as the pill switch, and the guard scopes
   that exception to selectors saying `switch` — which is why these are
   `switch-cycle` and not `cycle`.
 *
 * A link, so the cycle needs no script: its href is the address of the next
 * position and the label shows the current one. Sized like every other control
 * on the row, and narrower than the three segments it replaces, which is the
 * point.
 */
.switch-cycle {
  display: inline-flex;
  flex: 0 0 auto;
  position: relative;
  align-items: center;
  gap: var(--space-2);
  min-height: 2.25rem;
  padding: 0 var(--space-3) 0 var(--space-2);
  background: var(--surface-00);
  border-radius: 1.125rem;
  font-size: var(--text-2xs);
  color: var(--text-muted);
  text-decoration: none;
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--dur-fast) ease;
}

/* Three slots, so the knob has somewhere to be that is not one of two ends.
   2.25rem of track minus a 0.75rem knob leaves 1.5rem of travel, which divides
   into two steps of 0.75rem — the positions below are that arithmetic, not
   numbers picked to look right. */
.switch-cycle-track {
  display: inline-block;
  position: relative;
  width: 2.5rem;
  height: 1rem;
  flex: none;
  background: var(--surface-20);
  border-radius: 0.5rem;
}
.switch-cycle-knob {
  position: absolute;
  top: 0.125rem;
  left: 0.125rem;
  width: 0.75rem;
  height: 0.75rem;
  background: var(--text-muted);
  border-radius: 0.375rem;
  transition: transform var(--dur-fast) var(--ease), background var(--dur-fast) ease;
}
/* All three words occupy the same grid cell, so the widest one sets the width
   and the pill does not resize as it cycles — a control that changes size under
   the pointer moves everything to its right on every click.
   visibility, not display:none: the hidden words still have to take part in the
   layout, which is the whole mechanism. */
.switch-cycle-label { display: grid; }
.switch-cycle-word {
  grid-area: 1 / 1;
  visibility: hidden;
}
.switch-cycle-word[data-on] { visibility: visible; }

.switch-cycle-track[data-at="1"] .switch-cycle-knob { transform: translateX(0.75rem); }
.switch-cycle-track[data-at="2"] .switch-cycle-knob { transform: translateX(1.5rem); }

/* Inverted rather than coloured. The free switch is binary and its `on` earns a
   green; indoor and outdoor are two answers to a question with no better and no
   worse, so a colour here would claim one of them is the good one. The label
   names the state and the knob repeats it as a position — no hue involved. */
.switch-cycle[data-active] { color: var(--text); }
.switch-cycle[data-active] .switch-cycle-knob { background: var(--invert-surface); }

@media (hover: hover) {
  .switch-cycle:hover { color: var(--text); }
}
@media (prefers-reduced-motion: reduce) {
  .switch-cycle,
  .switch-cycle-knob { transition: none; }
}

/* pill switch — the one genuinely binary filter ------------------------ */

/* Rounded by the same named exception as .switch: a two-position control is
   the one thing whose affordance is its shape, and the design guard scopes the
   exception to selectors saying `switch`.
 *
 * Two signals, and the colour is only one of them: the knob moves and the track
 * turns --accent-positive. The knob is the one that does the work for somebody
 * who cannot tell the green from the grey — a position is a shape, not a hue.
 * The label named the state in words for a while and was dropped: it made the
 * one control on the row that says three things, next to neighbours that say
 * two.
 */
.pill-switch {
  display: inline-flex;
  flex: 0 0 auto;
  /* The off-screen input is absolute, so the label has to be what it resolves
     against — otherwise it lands against whatever ancestor happens to be
     positioned and can push the page's scroll width out. */
  position: relative;
  align-items: center;
  gap: var(--space-2);
  min-height: 2.25rem;
  padding: 0 var(--space-3) 0 var(--space-2);
  background: var(--surface-00);
  border-radius: 1.125rem;
  font-size: var(--text-2xs);
  color: var(--text-muted);
  cursor: pointer;
  white-space: nowrap;
  transition: color var(--dur-fast) ease;
}
.pill-switch input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.pill-switch-track {
  display: inline-block;
  position: relative;
  width: 1.75rem;
  height: 1rem;
  flex: 0 0 auto;
  background: var(--surface-20);
  border-radius: 0.5rem;
  transition: background var(--dur-fast) ease;
}
.pill-switch-knob {
  position: absolute;
  top: 0.125rem;
  left: 0.125rem;
  width: 0.75rem;
  height: 0.75rem;
  background: var(--text-muted);
  border-radius: 0.375rem;
  transition: transform var(--dur-fast) var(--ease), background var(--dur-fast) ease;
}
.pill-switch[data-active] { color: var(--text); }
.pill-switch[data-active] .pill-switch-track { background: var(--accent-positive); }
.pill-switch[data-active] .pill-switch-knob {
  background: var(--invert-text);
  transform: translateX(0.75rem);
}
@media (hover: hover) {
  .pill-switch:hover { color: var(--text); }
}
/* On the label, not on the track: the input is off-screen, so :focus-visible
   on it has nothing to draw around. */
.pill-switch:focus-within {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: 1.125rem;
}
@media (prefers-reduced-motion: reduce) {
  .pill-switch,
  .pill-switch-track,
  .pill-switch-knob { transition: none; }
}

/* byline — who put this on ------------------------------------------- */

/* The one round thing besides a switch, and the exception is scoped in the
   design guard rather than dropped. An avatar is a portrait: square, it reads
   as a thumbnail of the event, which is the other image on the same card and
   exactly the confusion this must not create. */
.byline { display: flex; align-items: center; gap: var(--space-2); }

/* No underline, like every other structural link here — the title, the nav,
   the footer. An underline would also run under the avatar, because both sit
   inside one link so that the thumbnail is a target too, and a line under a
   portrait reads as a rendering fault rather than as an affordance.

   What replaces it: the name brightens to --text on hover, which is the
   pattern the nav already uses, and the focus ring is the global one. */
.byline-link {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-width: 0;
  text-decoration: none;
}

@media (hover: hover) {
  a.byline-link:hover .byline-name { color: var(--text); }
}

.byline-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.avatar {
  flex: 0 0 auto;
  width: 1.25rem;
  height: 1.25rem;
  border-radius: 50%;
  object-fit: cover;
  /* A frame, because flyers and portraits are often near-black or near-white
     and one of the two disappears into the page without it. */
  background: var(--surface-10);
}

/* The editor's preview. Same shape as everywhere else — the venue only gets to
   choose round, square or hidden on its own calendar page, and this is not
   that page. */
/* An institution. The one exception to the round avatar, and it is not a
   choice anybody makes — the shape follows the account type, which is why it
   can carry meaning at 20px where a label could not fit. */
.avatar-square { border-radius: 0; }

.avatar-lg {
  width: 3rem;
  height: 3rem;
}

/* The institution's mark in the workspace: bigger than the form's avatar-lg
   preview because it is identity beside a display-size title, and the same
   size in the overview cards and the workspace header. */
.avatar-xl {
  width: 4rem;
  height: 4rem;
}

/* The one on an account page: as tall as the badge and the title together, so
   it reads as the account's face rather than as a thumbnail beside it.
 *
 * Computed from --acct-title rather than restated, so changing the title size
 * moves the picture with it. 0.98 is --leading-display, which is the one thing
 * here read off another rule.
 *
 * Not `align-self: stretch`, which is the derivation this wants and does not
 * survive contact: with aspect-ratio the width follows the height, the height
 * follows the text block, and the text block follows the width. Measured
 * rather than assumed — the browser resolves that circle at the image's
 * intrinsic 480px and drags the title to 175px with it. */
.avatar-head {
  width: calc(var(--acct-title) * 0.98 + var(--acct-meta-block));
  height: calc(var(--acct-title) * 0.98 + var(--acct-meta-block));
}

/* The monogram has to grow with its box, or a 6.5rem square holds a letter
   sized for a 20px one — the version of this that only shows on the accounts
   with no picture, which is most of them. */
.avatar-head.avatar-mono { font-size: clamp(1.5rem, 3vw, 2.25rem); }

/* Same problem one size down: the panel's 3rem institution mark (overview
   cards, workspace header, upload preview) must not hold a 0.75rem letter. */
.avatar-lg.avatar-mono { font-size: var(--text-lg); }

/* And one size up for the 4rem mark. */
.avatar-xl.avatar-mono { font-size: var(--text-2xl); }

/* The monogram is the normal case on accounts without a picture, and the
   fallback beside a picked one until `[hidden]` kicks in. Never the accent:
   an avatar is identity, and the accent is
   reserved for the six indicators STYLE.md allows it in. */
.avatar-mono {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--text-2xs);
  font-weight: var(--wght-medium);
  color: var(--text-muted);
  letter-spacing: normal;
}

/* `display: inline-flex` above outranks the hidden attribute's UA rule, so a
   monogram with a picture chosen would sit beside it forever. Same pattern
   as the other hidden-toggled components. */
.avatar-mono[hidden] { display: none; }

/* The archive. Dimmed by the muted token, never by dropping opacity: these
   rows are still links somebody taps, and a faded chip is a failed tap target
   before it is a design choice. What says "past" is the heading above and the
   date on the card — the tone only agrees with them. */
.tip-list-past .tip-title a {
  color: var(--text-muted);
}
.tip-list-past .tip-title a:hover,
.tip-list-past .tip-title a:focus-visible {
  color: var(--text);
}

.pager {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-5);
}

/* What the auto-loader says out loud. Announced, never drawn: the rows
   arriving are the visible feedback for anybody who can see them, and a line
   of chrome under the list would be the reader's reward for scrolling. */
.feed-status {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* ------------------------------------------------------- /intern/datenfluss

   Page furniture only. The board itself lives in flow.css, which that page
   loads and no other does. */
.flow-head { margin-bottom: var(--space-6); }

.flow-hidden {
  margin: var(--space-4) 0 0;
  padding: var(--space-3) var(--space-4);
  border-left: 2px solid var(--line);
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.flow-notes {
  display: grid;
  gap: var(--space-6);
}

@media (min-width: 48rem) {
  .flow-notes { grid-template-columns: 1fr 1fr; }
}

.flow-note {
  padding: var(--space-5);
  border-top: 1px solid var(--line-strong);
}

.flow-note-title {
  margin: 0 0 var(--space-3);
  font-size: var(--text-sm);
  font-weight: var(--wght-medium);
}

.flow-note p {
  margin: 0 0 var(--space-3);
  color: var(--text-muted);
  font-size: var(--text-xs);
  line-height: var(--leading-normal);
}

.flow-note p:last-child { margin-bottom: 0; }

/* The ladder is the one line in these boxes that is a statement rather than a
   gloss on one, so it keeps full contrast. */
.flow-note .flow-ladder { color: var(--text); }

/* The badge above an account name.
 *
 * Deliberately quiet, and the quiet is load-bearing in two directions. Loud,
 * "Institution" reads as a rosette we hand out, when it only means somebody at
 * the house confirmed they run it. Loud, "BOT" reads as a warning about a
 * venue that has done nothing wrong. And a prominent badge beside a claim link
 * is an invitation — the page should be claimable by the one person it belongs
 * to, not interesting to everybody else. */
/* The line under the name: the badge, then whatever the page has to add — the
   address for an institution, nothing for a person. It sat above the title
   until 2026-08-08, where it read as a kicker announcing the page, when what
   it says is a fact *about* the account and so belongs with the address rather
   than above the name. The title is now the first thing on the page.

   Baseline-aligned, so the badge sits on the address's line rather than
   floating over it; wrapping, because a street address and a badge do not fit
   side by side at 375px. */
.acct-meta {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: var(--space-2) var(--space-3);
  margin: var(--space-2) 0 0;
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.acct-badge {
  display: inline-block;
  padding: 2px 7px;
  border-width: 1px;
  border-style: solid;
  border-color: var(--line-strong);
  color: var(--text-muted);
  font-size: var(--text-3xs);
  font-weight: var(--wght-medium);
  letter-spacing: var(--track-mark);
  text-transform: uppercase;
}

/* The bot badge sits a step further back than the verified one: it describes
   who is filling the page in, not what the account is. */
.acct-badge-quiet {
  border-color: var(--line);
  color: var(--text-faint);
}

/* A line the page is showing to an administrator and to nobody else. The
   marker is not decoration: without it the next admin reads the rank beside a
   person's name, assumes visitors see it too, and starts working around a leak
   that was closed. */
/* No box. A dashed frame is what this wanted to be and the system forbids it
   outright — rightly, since a second border style is a second vocabulary for
   "this is different". The tone and the caps do the same job with nothing
   added to the page. */
.admin-only {
  margin-left: var(--space-2);
  color: var(--text-faint);
  font-size: var(--text-3xs);
  text-transform: uppercase;
  letter-spacing: var(--track-mark);
}

/* The picture, the badge and the name. One arrangement, three account pages.
   Aligned on the top rather than centred: an institution's block runs to three
   lines with the address, a person's to one, and centring would float the
   picture down the longer one. */
.acct-id {
  display: flex;
  align-items: flex-start;
  gap: var(--space-4);
  margin-bottom: var(--space-4);

  /* One size, two readers: the title below and the picture's height. Written
     here rather than twice, because the picture is meant to match the title
     and the badge exactly — two clamps that have to track each other are the
     shape of every drift bug in this repo.

     Smaller than --text-display, which the event detail page keeps. That title
     stands alone across the full measure; this one is in a lockup with a
     picture and a badge, competing for width on the same line. */
  --acct-title: clamp(2.25rem, 6vw, 3.25rem);
  /* The line under the title — the badge and the address — plus the gap above
     it. The one number here that restates something rather than deriving it.
     It kept its value when the badge moved from above the title to below it,
     because what it measures is the same line either way: the picture is as
     tall as the name and the row beside it. */
  --acct-meta-block: 2.1rem;
}

/* min-width: 0 so a long unbroken name wraps instead of pushing the picture
   off the left edge — the same reason body sets overflow-wrap. */
.acct-id-text { min-width: 0; }

.acct-id .page-title {
  margin: 0;
  font-size: var(--acct-title);
  /* body sets overflow-wrap: anywhere for publisher text, which is right for a
     pasted URL in a description and wrong for a name in 44px type — it broke
     "Filmwerkstatt Düsseldorf" as "Filmwerkstat / t Düsseldorf". break-word
     only splits a word that genuinely cannot fit on a line of its own. */
  overflow-wrap: break-word;
}

/* Side by side needs room for both. At 375px a 4.75rem picture plus its gap
   takes a third of the screen, and what is left cannot hold a two-word
   institution name at display size — so below 30rem the picture goes above the
   name and the text gets the full width. */
@media (max-width: 29.999rem) {
  .acct-id {
    display: block;
  }
  /* No display override: the monogram is an inline-flex box and that is what
     centres the letter in it. The text block below is a block element, so the
     picture already has the line to itself. */
  .acct-id .avatar-head { margin-bottom: var(--space-3); }
}
