/* ── moirée UI TOOLKIT — DRAFT, NOT WIRED UP ─────────────────────────────
   A drop-in component set, not a redesign.

   Everything here is ADDITIVE and namespaced `ui-`. No existing selector is
   touched, so a single component can be adopted on a single page without
   changing anything else. `.btn-primary` keeps working exactly as it does.

   It reuses moirée's existing tokens only — same palette, same radii, same
   fonts, same hairline aesthetic. What it borrows from CivicTheme 1.13.0 is
   the *discipline*, not the look:
     · sizes land on an 8px grid instead of ad-hoc 11px/22px padding
     · every interactive element has a real focus-visible ring (today: none)
     · states are colour changes, not `opacity: 0.88` (which dims the label too)
     · errors are semantic coral, not brand purple
     · one badge idiom instead of three (10px/10px/11px, 4px/4px/12px radius)
   ───────────────────────────────────────────────────────────────────────── */

.ui {
  /* Sizing rhythm — 8px grid, the one thing the current CSS has no version of */
  --ui-1: 4px;
  --ui-2: 8px;
  --ui-3: 12px;
  --ui-4: 16px;
  --ui-6: 24px;
  --ui-8: 32px;

  /* Semantic state colours, mapped onto tokens that already exist */

  /* Font roles. CivicTheme defines exactly two font kinds and assigns EVERY
     role — headings, text and labels alike — to the primary one; monospace
     appears twice in 439 KB of its compiled CSS, i.e. for code only.
     A label is therefore not a different typeface, it is the same typeface
     at a different size and weight. --ui-font-chrome is the single switch
     for every label, button, badge, tab and error message in this kit;
     mono is reserved for data: IDs, timestamps, figures, code.

     style.css now owns the canonical value (Option A — Source Serif at
     --font-chrome-weight), so this is a fallback for the case where the kit is
     used without it rather than an override of it. */
  --ui-font-chrome: var(--font-chrome, var(--font-body));
  --ui-font-chrome-weight: var(--font-chrome-weight, 600);
  --ui-focus: var(--primary-text);
  --ui-danger: var(--coral-text);
  --ui-danger-bg: var(--coral-light);
  --ui-ok: var(--teal);

  /* Elevation — CivicTheme ships exactly four levels and no more, which is
     what keeps depth meaningful. style.css currently has 8 unrelated
     box-shadow declarations and no scale. Values are CivicTheme's, in the
     same 8px particle steps. */
  --ui-e1: 0 2px 8px rgb(0 0 0 / 25%);
  --ui-e2: 0 4px 16px rgb(0 0 0 / 20%);
  --ui-e3: 0 8px 24px rgb(0 0 0 / 20%);
  --ui-e4: 0 16px 64px rgb(0 0 0 / 20%);

  /* Select indicator — two icons like CivicTheme's light/dark pair. */
  --ui-select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='7' viewBox='0 0 12 7' fill='none'%3E%3Cpath d='M1 1L6 6L11 1' stroke='%23b4b2a9' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --ui-select-chevron-strong: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='7' viewBox='0 0 12 7' fill='none'%3E%3Cpath d='M1 1L6 6L11 1' stroke='%23f0ede6' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
}

[data-theme='light'] .ui,
.ui[data-theme='light'] {
  --ui-select-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='7' viewBox='0 0 12 7' fill='none'%3E%3Cpath d='M1 1L6 6L11 1' stroke='%235f5e5a' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --ui-select-chevron-strong: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='7' viewBox='0 0 12 7' fill='none'%3E%3Cpath d='M1 1L6 6L11 1' stroke='%231a1a18' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  --ui-e1: 0 2px 8px rgb(0 0 0 / 8%);
  --ui-e2: 0 4px 16px rgb(0 0 0 / 8%);
  --ui-e3: 0 8px 24px rgb(0 0 0 / 10%);
  --ui-e4: 0 16px 64px rgb(0 0 0 / 12%);
}

/* ═══════════════════════════════════════════════════════════════════════
   BUTTONS — .ui-btn
   Current: padding 11px 22px, hover = opacity .88, no focus ring, no sizes,
   no disabled state. The opacity hover fades the *text* as well, which is
   why the primary button feels like it goes greyer rather than brighter.
   ═══════════════════════════════════════════════════════════════════════ */

.ui-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--ui-2);
  font-family: var(--ui-font-chrome);
  font-size: 14px;
  font-weight: var(--ui-font-chrome-weight);
  line-height: 20px;
  padding: var(--ui-3) var(--ui-6);        /* 12/24 — on grid, same visual mass */
  border: 1px solid transparent;
  border-radius: var(--radius-btn);
  cursor: pointer;
  text-decoration: none;
  white-space: nowrap;
  transition: background-color 0.15s ease, border-color 0.15s ease, color 0.15s ease;
}

/* Hover shifts toward the text colour — which lightens in dark mode and
   darkens in light mode, so one rule is correct in both themes. */
.ui-btn--primary {
  background: var(--primary);
  border-color: var(--primary);
  color: #fff;
}

.ui-btn--primary:hover {
  background: color-mix(in oklab, var(--primary) 86%, var(--text));
  border-color: color-mix(in oklab, var(--primary) 86%, var(--text));
}

.ui-btn--primary:active { background: color-mix(in oklab, var(--primary) 74%, var(--text)); }

.ui-btn--secondary {
  background: transparent;
  border-color: var(--border2);
  color: var(--text);
}

.ui-btn--secondary:hover {
  background: var(--bg2);
  border-color: var(--primary-text);
  color: var(--text);
}

/* The quiet text link — the second half of the "one filled button, one
   underlined link" pair, so a hero no longer needs two equal pills. */
.ui-btn--quiet {
  background: transparent;
  border-color: transparent;
  color: var(--text2);
  padding-inline: var(--ui-1);
  text-decoration: underline;
  text-underline-offset: 4px;
}

.ui-btn--quiet:hover { color: var(--text); }

.ui-btn--danger {
  background: transparent;
  border-color: color-mix(in oklab, var(--ui-danger) 50%, transparent);
  color: var(--ui-danger);
}

.ui-btn--danger:hover { background: var(--ui-danger-bg); border-color: var(--ui-danger); }

/* Sizes */
.ui-btn--sm { padding: var(--ui-2) var(--ui-4); font-size: 13px; line-height: 18px; }
.ui-btn--lg { padding: var(--ui-4) var(--ui-8); font-size: 15px; line-height: 22px; }
.ui-btn--block { display: flex; width: 100%; }

/* States the current buttons don't have at all */
.ui-btn:focus-visible {
  outline: 2px solid var(--ui-focus);
  outline-offset: 2px;
}

.ui-btn:disabled,
.ui-btn[aria-disabled='true'] {
  opacity: 0.45;
  cursor: not-allowed;
  pointer-events: none;
}

/* Loading — spinner replaces the label without the button changing width */
.ui-btn[data-loading] { position: relative; color: transparent !important; pointer-events: none; }

.ui-btn[data-loading]::after {
  content: '';
  position: absolute;
  width: 14px;
  height: 14px;
  border: 2px solid currentcolor;
  border-top-color: transparent;
  border-radius: 50%;
  color: #fff;
  animation: ui-spin 0.6s linear infinite;
}

.ui-btn--secondary[data-loading]::after,
.ui-btn--quiet[data-loading]::after { color: var(--text); }

@keyframes ui-spin { to { transform: rotate(360deg); } }

@media (prefers-reduced-motion: reduce) {
  .ui-btn { transition: none; }
  .ui-btn[data-loading]::after { animation-duration: 2s; }
}

.ui-btn-group { display: flex; flex-wrap: wrap; gap: var(--ui-3); align-items: center; }

/* The `.tk-*` rules that styled the toolkit demo page's own chrome (banner,
   compare panes, section headers) were stripped when this file was copied
   into the site: they are the presentation of ui-toolkit.html, not components
   of moirée, and they cannot match anything in the site's markup. They stay
   in docs/design/ui-toolkit.css in the draft worktree. */

/* ═══════════════════════════════════════════════════════════════════════
   BADGES — .ui-badge
   Current: three idioms — .case-type (10px, radius 4), .study-badge (10px,
   radius 4, teal), .meta-pill (11px, radius 12). Three sizes, two radii,
   two type scales. This is one component with variants.
   ═══════════════════════════════════════════════════════════════════════ */

.ui-badge {
  display: inline-flex;
  align-items: center;
  gap: var(--ui-1);
  font-family: var(--ui-font-chrome);
  font-size: 11px;
  line-height: 16px;
  font-weight: var(--ui-font-chrome-weight);
  padding: 3px var(--ui-2);
  border-radius: var(--radius-badge);
  border: 1px solid transparent;
  text-decoration: none;
  white-space: nowrap;
}

/* Category — tinted from one colour, so a new experience type needs one
   custom property rather than a new CSS rule per type. */
.ui-badge--type {
  color: var(--ui-badge-color, var(--text2));
  background: color-mix(in oklab, var(--ui-badge-color, var(--text2)) 12%, transparent);
}

.ui-badge--study { color: var(--teal); background: var(--teal-light); }
.ui-badge--study:hover { border-color: var(--teal); }

/* The meta variant is the one that carries free-form values — an industry
   name, a career level, a translation notice — so it is the one that must be
   allowed to wrap. Keeping it nowrap made a case card's min-content 309px at
   a 320px viewport and pushed the whole page sideways: WCAG 1.4.10. Type and
   study badges stay nowrap; they are short and breaking them mid-word would
   read as a rendering fault. */
.ui-badge--meta { color: var(--text3); border-color: var(--border2); white-space: normal; }

.ui-badge--count { color: var(--text2); background: var(--bg2); font-variant-numeric: tabular-nums; }

a.ui-badge:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }

/* ═══════════════════════════════════════════════════════════════════════
   FORM FIELDS — .ui-field
   Current: label 12px in --text3 (the lowest-contrast text token, used for
   the thing you must read first); controls bordered at 0.5px, which is
   below a device pixel on many screens; no focus ring; and .field-error
   plus .invalid both use --primary, so an error is rendered in the brand
   colour and reads as decoration.
   ═══════════════════════════════════════════════════════════════════════ */

.ui-field { margin-bottom: var(--ui-4); }

.ui-field__label {
  display: block;
  font-family: var(--ui-font-chrome);
  font-weight: var(--ui-font-chrome-weight);
  font-size: 12px;
  line-height: 16px;
  color: var(--text2);                     /* was --text3 */
  margin-bottom: var(--ui-2);
}

.ui-field--required .ui-field__label::after {
  content: ' *';
  color: var(--ui-danger);
}

.ui-field__help {
  font-size: 12px;
  line-height: 18px;
  color: var(--text3);
  margin-bottom: var(--ui-2);
}

:is(.ui-field__control, .ui-field) :is(
  select,
  input[type='text'],
  input[type='email'],
  input[type='tel'],
  input[type='number'],
  input[type='date'],
  input[type='search'],
  textarea
) {
  display: block;
  width: 100%;
  font-family: var(--font-ui);
  font-size: 16px;                         /* 16px avoids iOS zoom-on-focus */
  line-height: 24px;
  color: var(--text);
  background-color: var(--bg);
  border: 1px solid var(--border2);        /* was 0.5px */
  border-radius: var(--radius-btn);
  padding: 10px var(--ui-3);
  min-height: 44px;                        /* touch target */
  transition: border-color 0.15s ease;
}

/* Dropdown chevron — CivicTheme's select atom.
   The native arrow is whatever the OS draws, at whatever distance from the
   edge it likes; on macOS that reads as a floating glyph with an arbitrary
   gap. CivicTheme kills the native appearance and paints the control's own
   indicator: an icon block flush to the inner edge (offset by exactly the
   border width), with a divider hairline, and enough right padding that the
   value can never slide under it.
   Two background layers keep the chevron from being stretched: the divider
   is a gradient, the chevron keeps its own aspect ratio. */
:is(.ui-field__control, .ui-field) select {
  appearance: none;
  background-image: var(--ui-select-chevron), linear-gradient(var(--border2), var(--border2));
  background-repeat: no-repeat, no-repeat;
  background-position: right 15px center, right 42px center;
  background-size: 12px 7px, 1px 56%;
  padding-right: 58px;
}

:is(.ui-field__control, .ui-field) select:hover { background-image: var(--ui-select-chevron-strong), linear-gradient(var(--text3), var(--text3)); }

:is(.ui-field__control, .ui-field) select:disabled { background-image: none; }

:is(.ui-field__control, .ui-field) :is(select, input, textarea):hover { border-color: var(--text3); }

:is(.ui-field__control, .ui-field) :is(select, input, textarea):focus-visible {
  outline: 2px solid var(--ui-focus);
  outline-offset: 1px;
  border-color: var(--primary);
}

:is(.ui-field__control, .ui-field) :is(select, input, textarea):disabled {
  opacity: 0.5;
  cursor: not-allowed;
  background-color: var(--bg2);
}

/* Error state — semantic, and announced rather than only coloured */

/* A descendant selector already covers both shapes: the control may sit inside
   a .ui-field__control wrapper or directly inside the field. The `&` that was
   here needs a scoping root and is invalid at the top level — stylelint caught
   it, eslint's line is the one that gets grepped, and it was missed once. */
.ui-field--invalid :is(select, input, textarea) { border-color: var(--ui-danger); }

/* ...and the line above never painted anything, which is the border bug the
   handoff carried open. Three reasons stacked, each real:

   1. It is specificity 0,1,1. The base control rule at the top of this section
      is `:is(.ui-field__control, .ui-field) :is(select, input[type='text'], …)`
      — 0,2,1, because `:is()` takes the weight of its most specific argument
      and `input[type='text']` is 0,1,1. The toolkit's own error state lost to
      the toolkit's own resting state, on every field, always.
   2. `.ui-field--invalid` is set by form-errors.js via
      `field.closest('.form-group')`. submit.html has 18 `.field-group`
      wrappers and no `.form-group`, so the class is never added there at all.
   3. style.css defends the same thing with `.field-group …​.invalid`, also
      0,2,1 — a tie with the base rule, decided by stylesheet order, and
      style.css is linked before this file. It lost too.

   Raising specificity in style.css therefore could not work: on a tie it is
   still the earlier sheet, and on the pages measured the wrapper was
   `.form-group`, which that selector does not match.

   The rule below keys off `.invalid`, which form-errors.js sets on the control
   itself and unconditionally, and carries 0,2,1 in the later sheet — so it
   beats the resting border by source order the way the hover and focus rules
   above already do. */
:is(.ui-field__control, .ui-field) :is(select, input, textarea).invalid {
  border-color: var(--ui-danger);
}

/* Listed twice on purpose. An error message is injected into whatever markup
   the form happens to sit in, and a single class is specificity 0,1,0 — which
   `.content-section p { color: var(--text2) }` in style.css beats at 0,1,1.
   The result was an error rendered in body grey: role="alert" announced it,
   aria-describedby resolved to it, and it did not look like an error at all.
   The `.ui` variant is 0,2,0 and survives that; the bare one keeps the class
   working outside a `.ui` root. style.css defends `.field-error` the same way
   with `.field-group .field-error`, which is why the submission form inside
   its field groups was coral while the contact forms were not. */
.ui-field__error,
.ui .ui-field__error {
  display: flex;
  align-items: flex-start;
  gap: var(--ui-1);
  font-family: var(--ui-font-chrome);
  font-weight: var(--ui-font-chrome-weight);
  font-size: 12px;
  line-height: 18px;
  color: var(--ui-danger);
  margin-top: var(--ui-2);
}

.ui-field__error::before { content: '!'; font-weight: 700; }

/* Checkbox / radio — real targets, brand accent, keyboard visible */
.ui-choice-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
  gap: var(--ui-2);
}

.ui-choice {
  display: flex;
  align-items: center;
  gap: var(--ui-2);
  padding: 10px var(--ui-3);
  border: 1px solid var(--border);
  border-radius: var(--radius-card);
  font-size: 14px;
  color: var(--text2);
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease;
}

.ui-choice:hover { border-color: var(--border2); background: var(--bg2); }

.ui-choice input { accent-color: var(--primary); width: 18px; height: 18px; flex: none; }

.ui-choice:has(input:checked) {
  border-color: var(--primary);
  background: var(--primary-light);
  color: var(--text);
}

.ui-choice:has(input:focus-visible) { outline: 2px solid var(--ui-focus); outline-offset: 2px; }

.ui-field-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--ui-4); }

@media (width <= 600px) { .ui-field-row { grid-template-columns: 1fr; } }

/* ═══════════════════════════════════════════════════════════════════════
   CARD — .ui-card
   Same shell as .case-card (bg2, hairline, 8px radius) with the states it
   is missing: a hover that reads, and a focus ring when the card's own
   trigger is focused — today a keyboard user gets no visible target.
   ═══════════════════════════════════════════════════════════════════════ */

.ui-card {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--ui-3);
  background: var(--bg2);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--ui-4);
  transition: border-color 0.15s ease, background 0.15s ease;
}

.ui-card:hover { border-color: var(--border2); }
.ui-card:focus-within { outline: 2px solid var(--ui-focus); outline-offset: 2px; }

/* Category as a hairline rather than another filled badge. */
.ui-card--accent { border-left: 2px solid var(--ui-badge-color, var(--border2)); }

.ui-card__head { display: flex; align-items: center; justify-content: space-between; gap: var(--ui-3); }

.ui-card__quote {
  font-family: var(--font-headline);
  font-size: 17px;
  line-height: 1.45;
  color: var(--text);
  margin: 0;
  text-wrap: pretty;
}

.ui-card__meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--ui-1) var(--ui-2);
  font-family: var(--ui-font-chrome);
  font-size: 11px;
  line-height: 16px;
  color: var(--text3);
  margin: 0;
}

/* ═══════════════════════════════════════════════════════════════════════
   INLINE NOTICE — .ui-notice
   There is no shared component for "this went wrong" / "this worked" today;
   each page invents one. Same four semantic roles as CivicTheme's message.
   ═══════════════════════════════════════════════════════════════════════ */

.ui-notice {
  display: flex;
  gap: var(--ui-3);
  padding: var(--ui-3) var(--ui-4);
  border-radius: var(--radius-card);
  border-left: 3px solid var(--ui-notice-color, var(--border2));
  background: var(--ui-notice-bg, var(--bg2));
  font-size: 14px;
  line-height: 21px;
  color: var(--text2);
}

.ui-notice strong { color: var(--text); }
.ui-notice--info    { --ui-notice-color: var(--blue);  --ui-notice-bg: var(--blue-light); }
.ui-notice--success { --ui-notice-color: var(--teal);  --ui-notice-bg: var(--teal-light); }
.ui-notice--warning { --ui-notice-color: var(--amber); --ui-notice-bg: var(--amber-light); }
.ui-notice--error   { --ui-notice-color: var(--coral); --ui-notice-bg: var(--coral-light); }

/* ═══════════════════════════════════════════════════════════════════════
   PAGE FURNITURE — `tk-` prefix. Belongs to that sheet only, not the kit.

   Nothing follows. The thirteen `.tk-*` rules that styled the toolkit demo
   page's own chrome — banner, compare panes, section headings — were stripped
   when this file was copied into the site, because they are the presentation
   of ui-toolkit.html rather than components of moirée, and they cannot match
   anything in the site's markup. They stay in the draft worktree's copy at
   docs/design/ui-toolkit.css.

   The strip left one rule behind: a `@media (width <= 800px)` block narrowing
   `.tk-compare` and `.tk-section`, two classes that no longer existed here.
   Removed 2026-08-16 — see docs/ui-toolkit-reconciliation.md.
   ═══════════════════════════════════════════════════════════════════════ */

/* ═══════════════════════════════════════════════════════════════════════
   TEIL 2 — components moirée has no CSS for at all today
   Verified by grep against style.css: no pagination, no accordion, no
   table rules, no table-of-contents, no back-to-top. These are gaps, not
   replacements, so there is no "before" to compare against.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── CHIPS — CivicTheme's chip atom (kinds: default / input / link) ──────
   The database filter bar is three <select>s and a result count. Once two
   or three filters are set, nothing on screen says what is currently
   filtering the list, and there is no way to drop one without hunting for
   the right select. Chips make the active filter set visible and each one
   individually removable. */

.ui-chip {
  display: inline-flex;
  align-items: center;
  gap: var(--ui-2);
  font-family: var(--ui-font-chrome);
  font-weight: var(--ui-font-chrome-weight);
  font-size: 12px;
  line-height: 18px;
  padding: 5px 10px;
  border: 1px solid var(--border2);
  border-radius: 999px;
  background: transparent;
  color: var(--text2);
  cursor: pointer;
  transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}

.ui-chip:hover { border-color: var(--primary-text); color: var(--text); }

.ui-chip[aria-pressed='true'],
.ui-chip--selected {
  background: var(--primary-light);
  border-color: var(--primary);
  color: var(--text);
}

.ui-chip:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }

/* Dismiss affordance — the × is part of the chip, not a sibling button */
.ui-chip__dismiss {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: color-mix(in oklab, var(--text) 12%, transparent);
  color: var(--text2);
  font-size: 11px;
  line-height: 1;
}

.ui-chip:hover .ui-chip__dismiss { background: color-mix(in oklab, var(--text) 22%, transparent); color: var(--text); }

.ui-chip-bar { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-2); }

.ui-chip-bar__label { font-family: var(--ui-font-chrome); font-size: 11px; color: var(--text3); }

/* ── ACCORDION — CivicTheme's collapsible, applied to <details> ──────────
   <details> is already used on 15+ pages (methodology 18, accessibility 22,
   datenschutz 19) with no styling of its own, so it renders as a bare
   browser disclosure triangle in the middle of a designed page. */

.ui-accordion { border-top: var(--border-width) solid var(--border); }

.ui-accordion details { border-bottom: var(--border-width) solid var(--border); }

.ui-accordion summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ui-3);
  padding: var(--ui-4) 0;
  cursor: pointer;
  list-style: none;
  font-family: var(--font-headline);
  font-size: 16px;
  color: var(--text);
  transition: color 0.15s ease;
}

.ui-accordion summary::-webkit-details-marker { display: none; }
.ui-accordion summary:hover { color: var(--primary-text); }
.ui-accordion summary:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }

/* Chevron drawn in CSS — no icon font, no inline SVG per instance */
.ui-accordion summary::after {
  content: '';
  flex: none;
  width: 8px;
  height: 8px;
  border-right: 1.5px solid var(--text3);
  border-bottom: 1.5px solid var(--text3);
  transform: rotate(45deg);
  transition: transform 0.2s ease;
}

.ui-accordion details[open] summary::after { transform: rotate(-135deg); }

.ui-accordion__body {
  padding: 0 0 var(--ui-4);
  font-size: 14px;
  line-height: 1.7;
  color: var(--text2);
  max-width: 72ch;
}

@media (prefers-reduced-motion: reduce) {
  .ui-accordion summary::after { transition: none; }
}

/* ── TABLE OF CONTENTS — CivicTheme's table-of-contents + scrollspy ──────
   methodology.html has 11 <h2> sections and no in-page navigation. */

.ui-toc {
  border-left: 2px solid var(--border);
  padding-left: var(--ui-4);
}

.ui-toc__title {
  font-family: var(--ui-font-chrome);
  font-weight: var(--ui-font-chrome-weight);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1.5px;
  color: var(--text3);
  margin-bottom: var(--ui-3);
}

.ui-toc__links { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: var(--ui-2); }

.ui-toc__link {
  font-size: 13px;
  line-height: 1.5;
  color: var(--text2);
  text-decoration: none;
  border-left: 2px solid transparent;
  margin-left: calc(var(--ui-4) * -1 - 2px);
  padding-left: var(--ui-4);
}

.ui-toc__link:hover { color: var(--text); }

/* scrollspy — the section you are actually reading */
.ui-toc__link[aria-current='true'] { color: var(--text); border-left-color: var(--primary); }

@media (width >= 1000px) {
  .ui-toc--sticky { position: sticky; top: 80px; }
}

/* ── TABLE — CivicTheme's table atom ─────────────────────────────────────
   style.css has no table rules at all, so the tables in accessibility.html
   render fully unstyled. Wrap in .ui-table-wrap so wide tables scroll
   themselves instead of pushing the page sideways on mobile. */

.ui-table-wrap { overflow-x: auto; border: var(--border-width) solid var(--border); border-radius: var(--radius-card); }

.ui-table { width: 100%; border-collapse: collapse; font-size: 14px; }

.ui-table th,
.ui-table td { text-align: left; padding: var(--ui-3) var(--ui-4); border-bottom: var(--border-width) solid var(--border); }

.ui-table thead th {
  font-family: var(--ui-font-chrome);
  font-weight: var(--ui-font-chrome-weight);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--text3);
  background: var(--bg2);
  white-space: nowrap;
}

.ui-table tbody tr:last-child td { border-bottom: 0; }
.ui-table tbody tr:hover td { background: var(--bg2); }
.ui-table td.ui-table__num { font-family: var(--font-mono); font-variant-numeric: tabular-nums; text-align: right; }

/* ── PAGINATION — CivicTheme's pagination molecule ───────────────────────
   The case database has no pagination component today. */

.ui-pagination { display: flex; align-items: center; gap: var(--ui-2); flex-wrap: wrap; }

.ui-pagination__item {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 36px;
  height: 36px;
  padding: 0 var(--ui-2);
  font-family: var(--font-mono);
  font-size: 13px;
  color: var(--text2);
  text-decoration: none;
  border: 1px solid transparent;
  border-radius: var(--radius-btn);
  transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
}

.ui-pagination__item:hover { background: var(--bg2); color: var(--text); }
.ui-pagination__item:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }

.ui-pagination__item[aria-current='page'] {
  background: var(--primary-light);
  border-color: var(--primary);
  color: var(--text);
}

.ui-pagination__item[aria-disabled='true'] { opacity: 0.4; pointer-events: none; }
.ui-pagination__ellipsis { color: var(--text3); padding: 0 var(--ui-1); }

/* ── BACK TO TOP — CivicTheme's back-to-top ──────────────────────────────
   Relevant for methodology / datenschutz / accessibility, which are long. */

.ui-to-top {
  display: inline-flex;
  align-items: center;
  gap: var(--ui-2);
  font-family: var(--ui-font-chrome);
  font-size: 12px;
  color: var(--text2);
  text-decoration: none;
  padding: var(--ui-2) var(--ui-3);
  border: 1px solid var(--border2);
  border-radius: 999px;
  background: var(--bg);
  transition: border-color 0.15s ease, color 0.15s ease;
}

.ui-to-top:hover { border-color: var(--primary-text); color: var(--text); }
.ui-to-top:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }
.ui-to-top::before { content: '↑'; }

/* ═══════════════════════════════════════════════════════════════════════
   TEIL 3 — one kit for the public site AND the admin
   Measured against admin/index.html: 2 724 lines of CSS in a single inline
   \3c style> block, no linked stylesheet at all, 324 classes, 29 re-declared
   custom properties and 39 unique hardcoded hex colours. Inside it there
   are TWO parallel button systems (btn-* and cloud-btn--*, 16 classes) and
   19 badge/status classes — for what is one component with a variant.
   CivicTheme's answer is structural: it ships its token layer as a separate
   file from its component layer, precisely so several surfaces can share
   one source of truth.
   ═══════════════════════════════════════════════════════════════════════ */

/* Status — replaces b-approved / b-pending / b-rejected / badge-verdict-*
   with one component whose colour comes from a variable. */
.ui-badge--status { background: color-mix(in oklab, var(--ui-status) 14%, transparent); color: var(--ui-status); }
.ui-badge--status::before { content: ''; width: 6px; height: 6px; border-radius: 50%; background: currentcolor; }

.ui-status-approved { --ui-status: var(--teal); }
.ui-status-pending  { --ui-status: var(--amber); }
.ui-status-rejected { --ui-status: var(--coral-text); }
.ui-status-flagged  { --ui-status: var(--primary-text); }

/* Elevation utilities — four levels, CivicTheme's values. */
.ui-e1 { box-shadow: var(--ui-e1); }
.ui-e2 { box-shadow: var(--ui-e2); }
.ui-e3 { box-shadow: var(--ui-e3); }
.ui-e4 { box-shadow: var(--ui-e4); }

/* Panel — the admin's card, using the same shell as the public card so the
   two surfaces stop diverging. */
.ui-panel {
  background: var(--bg2);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-card);
  overflow: hidden;
}

.ui-panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--ui-3);
  padding: var(--ui-3) var(--ui-4);
  border-bottom: var(--border-width) solid var(--border);
  background: var(--bg);
}

.ui-panel__title { font-family: var(--ui-font-chrome); font-size: 11px; text-transform: uppercase; letter-spacing: 1.5px; color: var(--text3); }
.ui-panel__body { padding: var(--ui-4); }

/* A moderation row — the admin's densest, most-used unit. */
.ui-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: var(--ui-3);
  align-items: start;
  padding: var(--ui-4);
  border-bottom: var(--border-width) solid var(--border);
}

.ui-row:last-child { border-bottom: 0; }
.ui-row:hover { background: var(--bg2); }
.ui-row__quote { font-family: var(--font-headline); font-size: 15px; line-height: 1.5; color: var(--text); margin: 0 0 var(--ui-2); }
.ui-row__meta { display: flex; flex-wrap: wrap; gap: var(--ui-2); font-family: var(--ui-font-chrome); font-size: 11px; color: var(--text3); }
.ui-row__actions { display: flex; gap: var(--ui-2); flex-wrap: wrap; justify-content: flex-end; }

@media (width <= 700px) { .ui-row { grid-template-columns: 1fr; } .ui-row__actions { justify-content: flex-start; } }

/* ═══════════════════════════════════════════════════════════════════════
   TEIL 4 — Bewegung & Interaktion
   What actually makes an interface feel current is mostly restraint plus a
   few platform features. CivicTheme's own motion is deliberately small:
   --ct-button-animation-duration is 0.25s and the flyout is 0.5s; there is
   no decorative animation anywhere in it. The items below are the ones with
   a real effect on moirée.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── 1. Anchor targets under the sticky nav — a genuine bug ──────────────
   <nav> is position:sticky; top:0, but scroll-margin-top is set on exactly
   one selector (.type-definition). Every other in-page link — the eleven
   methodology sections, footnotes, the table of contents above — scrolls
   its heading underneath the nav bar. :where() keeps specificity at 0 so
   any existing rule still wins. */
:where(.ui [id]) { scroll-margin-top: 96px; }

@media (width <= 600px) {
  :where(.ui [id]) { scroll-margin-top: 72px; }
}

/* ── 2. Where did I land? — :target highlight ────────────────────────────
   After following a link the page just stops somewhere. A brief highlight
   on the actual target says which paragraph you were sent to. */
.ui-target:target {
  animation: ui-target-flash 1.6s ease-out;
  border-radius: var(--radius-btn);
}

@keyframes ui-target-flash {
  0%, 25% { background: var(--primary-light); box-shadow: 0 0 0 6px var(--primary-light); }
  100%    { background: transparent; box-shadow: 0 0 0 6px transparent; }
}

/* ── 3. Accordion that actually opens ────────────────────────────────────
   <details> normally snaps open. interpolate-size lets height animate to
   `auto`; browsers without it simply snap, exactly as today. */
@supports (interpolate-size: allow-keywords) {
  .ui { interpolate-size: allow-keywords; }

  .ui-accordion details::details-content {
    block-size: 0;
    overflow: hidden;
    transition: block-size 0.28s ease, content-visibility 0.28s allow-discrete;
  }

  .ui-accordion details[open]::details-content { block-size: auto; }
}

/* ── 4. Notices that arrive instead of appearing ─────────────────────────
   @starting-style gives an element an "off" state to animate FROM the
   first time it is rendered — no JS class-toggling dance. */
/* stylelint-disable-next-line no-duplicate-selectors -- the toolkit is grouped by concern (components, then theming, then print), so a selector deliberately reappears in a later group rather than being merged back into its component block. */
.ui-notice {
  transition: opacity 0.25s ease, transform 0.25s ease;
}

@starting-style {
  .ui-notice { opacity: 0; transform: translateY(-4px); }
}

/* ── 5. Focus that moves with you ────────────────────────────────────────
   The ring itself should not animate its colour in and out (that reads as
   lag), but the offset settling is what makes keyboard travel feel smooth. */
.ui-btn,
.ui-chip,
.ui-pagination__item,
.ui-to-top { transition-property: background-color, border-color, color, outline-offset; }

/* ── 6. Selection in the brand colour — one line, always noticed ─────────
   style.css styles no ::selection at all, so text selection falls back to
   the OS blue, which is the one colour nowhere else in the palette. */
.ui ::selection { background: var(--primary); color: #fff; }

/* ── 7. Carousel that snaps — .cases-carousel-wrap exists, snapping doesn't */
.ui-snap-row {
  display: flex;
  gap: var(--ui-4);
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  scroll-padding-inline: var(--ui-4);
  scrollbar-width: thin;
}

.ui-snap-row > * { scroll-snap-align: start; flex: 0 0 min(320px, 80%); }

/* ── 8. One reduced-motion guard for the whole kit ───────────────────────
   The current block covers three rules. This covers everything above. */
@media (prefers-reduced-motion: reduce) {
  .ui *,
  .ui *::before,
  .ui *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ═══════════════════════════════════════════════════════════════════════
   TEIL 5 — the section label
   .section-label is IBM Plex Mono, 11px, uppercase, letter-spacing 1.5px,
   in --text3. Monospace + all-caps + wide tracking is the single most
   common "developer template" idiom on the web — it is a large part of why
   a page reads as generated rather than edited. moirée already owns two
   serifs; the label is the one place where neither is used.
   Four candidates, all using type the project already ships.
   ═══════════════════════════════════════════════════════════════════════ */

.ui-eyebrow { display: block; margin-bottom: 20px; }

/* A — today */
.ui-eyebrow--mono {
  font-family: var(--font-mono);
  font-size: 11px;
  text-transform: uppercase;
  letter-spacing: 1.5px;
  color: var(--text3);
}

/* B — the body serif, quieter tracking. Editorial, ties to running text. */
.ui-eyebrow--serif {
  font-family: var(--font-body);
  font-size: 13px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 1px;
  color: var(--text2);
}

/* C — Playfair italic, sentence case. Magazine register; strongest link to
   the brand, because Playfair is already the voice of every headline. */
.ui-eyebrow--display {
  font-family: var(--font-headline);
  font-style: italic;
  font-size: 17px;
  letter-spacing: 0;
  color: var(--text2);
}

/* D — small caps. Typographically the "correct" answer for a label set in a
   serif: same colour and weight as text, distinguished by form alone. */
.ui-eyebrow--smallcaps {
  font-family: var(--font-body);
  font-size: 15px;
  font-variant-caps: all-small-caps;
  letter-spacing: 0.6px;
  color: var(--text2);
}

/* Optional hairline, any variant — anchors the label to the section it
   opens instead of floating above it. */
.ui-eyebrow--rule {
  display: flex;
  align-items: center;
  gap: 12px;
}

.ui-eyebrow--rule::after {
  content: '';
  flex: 1;
  height: var(--border-width);
  background: var(--border);
}

/* ═══════════════════════════════════════════════════════════════════════
   TEIL 6 — Mobile & Responsiveness
   Measured: style.css has 29 media queries across SEVEN different width
   values — 560, 600, 601, 640, 700, 760, 768 — and 21 of them are
   max-width. Five of those thresholds are within 200px of each other, so
   components rearrange one after another as the viewport narrows instead
   of together, which is what produces the "staircase" reflow.
   CivicTheme has one breakpoint map used everywhere through a mixin, and
   it is mobile-first (min-width), so the phone is the default case and
   gets the least CSS rather than the most overrides.
   ═══════════════════════════════════════════════════════════════════════ */

/* stylelint-disable-next-line no-duplicate-selectors -- the toolkit is grouped by concern (components, then theming, then print), so a selector deliberately reappears in a later group rather than being merged back into its component block. */
.ui {
  /* CivicTheme's breakpoint scale, verbatim. Deliberately few, deliberately
     far apart — that is what makes them usable everywhere. */
  --ui-bp-xs: 368px;
  --ui-bp-s: 576px;
  --ui-bp-m: 768px;
  --ui-bp-l: 992px;
  --ui-bp-xl: 1280px;
  --ui-bp-xxl: 1440px;
}

/* ── Container — padding steps WITH the breakpoint ───────────────────────
   A fixed 48px side padding costs a phone a fifth of its screen width. */
.ui-container {
  width: 100%;
  max-width: var(--max-width);
  margin-inline: auto;
  padding-inline: 20px;
}

@media (width >= 576px) { .ui-container { padding-inline: 32px; } }

@media (width >= 992px) { .ui-container { padding-inline: 48px; } }

/* ── Mobile-first stacks ─────────────────────────────────────────────────
   One class instead of a bespoke max-width query per component. Written
   min-width: the stacked phone layout is the default, the columns are the
   addition. */
.ui-cols { display: grid; gap: var(--ui-4); }

@media (width >= 576px)  { .ui-cols--s-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }

@media (width >= 768px)  { .ui-cols--m-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
                           .ui-cols--m-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }

@media (width >= 992px)  { .ui-cols--l-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
                           .ui-cols--l-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } }

/* Intrinsic version — no breakpoint at all. Often the better answer:
   the component decides from its own width, not the viewport's. */
.ui-cols--auto { grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); }

/* ── Visibility utilities — CivicTheme generates these per breakpoint ──── */
.ui-hide-s  { display: none; }

@media (width >= 576px) { .ui-hide-s { display: revert; } .ui-show-s { display: none; } }

.ui-hide-m  { display: none; }

@media (width >= 768px) { .ui-hide-m { display: revert; } .ui-show-m { display: none; } }

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

/* ── Touch — pointer, not viewport width ─────────────────────────────────
   Screen width does not tell you whether there is a mouse. A small laptop
   window is not a phone, and a tablet is wide AND touch. CivicTheme keeps
   hover styling out of the way on coarse pointers; style.css has exactly
   one (hover: none) query today. */
@media (hover: none) {
  .ui-btn:hover,
  .ui-chip:hover,
  .ui-card:hover { background-color: revert; border-color: revert; }
}

@media (pointer: coarse) {
  .ui-btn,
  .ui-pagination__item,
  .ui-chip { min-height: 44px; }

  .ui-row__actions .ui-btn { flex: 1 1 auto; }
}

/* ── The two mobile bugs that actually bite ──────────────────────────────
   1. Long unbreakable strings (case IDs, URLs, DOIs) forcing a sideways
      scroll on the whole page.
   2. A wide table doing the same — handled by .ui-table-wrap above. */
.ui-container,
.ui-card,
.ui-row { overflow-wrap: break-word; }

/* Same defence for the components that were already here. A grid item's
   automatic minimum is its min-content, so `1fr` alone lets one long word
   push a track wider than the grid — `minmax(0, 1fr)` is what actually lets
   it shrink, and `min-width: 0` is the flex equivalent. */
.ui :is(.case-card, .study-badges, .case-meta, .case-text) { min-width: 0; overflow-wrap: break-word; }

/* A formula cannot be broken across lines, so it gets to scroll on its own
   rather than taking the page with it. KaTeX renders into .katex-display;
   MathML falls back to <math>. */
.ui :is(.katex-display, math, .ui-scroll-x) {
  max-width: 100%;
  overflow: auto hidden;
}

/* A button never gets to push the page sideways. `white-space: nowrap` is
   right for the short labels this kit was drawn against, and wrong for
   German — "Beitrittserklärung absenden & herunterladen" is 344px on one
   line and the mitwirken form sits in a 280px column at 320px, so the label
   took the whole document with it. Below the tablet breakpoint the label
   wraps instead; above it, nothing changes. */
/* stylelint-disable-next-line no-duplicate-selectors -- the reflow guards are
   grouped together here on purpose rather than scattered back into each
   component's own block; keeping them in one place is what makes them
   reviewable against the 1.4.10 audit. */
.ui-btn { max-width: 100%; }

@media (width <= 700px) {
  .ui-btn { white-space: normal; }
}

/* Long German compounds are the other unbreakable string. `hyphens` needs a
   lang attribute to know where the breaks go, and every page sets one. */
.ui :is(p, li, dd, dt, h1, h2, h3, h4, blockquote, figcaption, .content-section) {
  overflow-wrap: break-word;
  hyphens: auto;
}

/* ── Off-canvas navigation — CivicTheme's mobile-navigation organism ─────
   Its flyout runs 0.5s with a direction transform. moirée's mobile menu is
   a CSS checkbox toggle; the pattern below keeps that (no JS) but adds the
   transform, the scrim and the reduced-motion case. */
.ui-flyout {
  position: fixed;
  inset: 0 0 0 auto;
  width: min(320px, 85vw);
  background: var(--bg2);
  border-left: var(--border-width) solid var(--border);
  box-shadow: var(--ui-e4);
  transform: translateX(100%);
  transition: transform 0.35s cubic-bezier(0.32, 0.72, 0, 1);
  z-index: 200;
  padding: var(--ui-6) var(--ui-4);
}

.ui-flyout[data-open] { transform: translateX(0); }

.ui-flyout__scrim {
  position: fixed;
  inset: 0;
  background: rgb(0 0 0 / 45%);
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.35s ease;
  z-index: 199;
}

.ui-flyout__scrim[data-open] { opacity: 1; pointer-events: auto; }

/* ═══════════════════════════════════════════════════════════════════════
   TEIL 7 — Fließtext, Verweise, Anhänge
   CivicTheme's basic-content component sets every element that can appear
   inside editorial text ONCE — headings, lists, quotes, code, rules,
   tables, links — so a long document is typeset rather than assembled.
   .content-section is moirée's equivalent and covers 8 rules.

   Measured on the long documents (datenschutz, accessibility, methodology):
     · no max-width on prose at all → ~120 characters per line inside the
       1080px wrap. The readability optimum is 45–75; convention is ~66.
     · .content-section p uses --font-ui, so the documentation pages are
       NOT set in the brand serif that the rest of the site uses.
     · .content-section h2 is text-align: center — centred headings above
       left-aligned body text, in documents 11–22 sections long.
     · a:hover is opacity: 0.8, the same fade the buttons had.
   ═══════════════════════════════════════════════════════════════════════ */

.ui-prose {
  --ui-measure: 68ch;

  font-family: var(--font-body);
  font-size: 17px;
  line-height: 1.75;
  color: var(--text2);
  max-width: var(--ui-measure);
  text-wrap: pretty;
}

/* Headings: left, brand serif, and more space ABOVE than below — a heading
   belongs to what follows it, not to what precedes it. */
.ui-prose h2,
.ui-prose h3,
.ui-prose h4 {
  font-family: var(--font-headline);
  color: var(--text);
  text-align: left;
  text-wrap: balance;
  margin: 2em 0 0.5em;
}

.ui-prose h2 { font-size: 26px; line-height: 1.25; font-weight: 700; }
.ui-prose h3 { font-size: 20px; line-height: 1.3; font-weight: 700; }
.ui-prose h4 { font-size: 17px; line-height: 1.4; font-weight: 700; }
.ui-prose > :first-child { margin-top: 0; }

.ui-prose p { margin: 0 0 1em; }

/* The opening paragraph carries the summary — one size up, lighter colour */
.ui-prose .ui-prose__lead { font-size: 20px; line-height: 1.6; color: var(--text); margin-bottom: 1.25em; }

.ui-prose ul,
.ui-prose ol { margin: 0 0 1em; padding-left: 1.4em; }
.ui-prose li { margin-bottom: 0.4em; }
.ui-prose li::marker { color: var(--text3); }

.ui-prose blockquote {
  margin: 1.5em 0;
  padding-left: 1.2em;
  border-left: 2px solid var(--primary-text);
  font-family: var(--font-headline);
  font-size: 19px;
  line-height: 1.5;
  color: var(--text);
}

.ui-prose blockquote cite { display: block; margin-top: 0.6em; font-family: var(--ui-font-chrome); font-size: 12px; font-style: normal; color: var(--text3); }

.ui-prose code {
  font-family: var(--font-mono);
  font-size: 0.88em;
  background: var(--bg2);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-badge);
  padding: 0.1em 0.35em;
}

.ui-prose pre {
  font-family: var(--font-mono);
  font-size: 13px;
  line-height: 1.6;
  background: var(--bg2);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-card);
  padding: var(--ui-4);
  overflow-x: auto;
  margin: 1.5em 0;
  max-width: 100%;
}

.ui-prose pre code { background: none; border: 0; padding: 0; }

.ui-prose hr { border: 0; border-top: var(--border-width) solid var(--border); margin: 2.5em 0; }

.ui-prose a { color: var(--primary-text); text-decoration: underline; text-underline-offset: 2px; transition: color 0.15s ease; }
.ui-prose a:hover { color: var(--text); text-decoration-thickness: 2px; }
.ui-prose a:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; border-radius: 2px; }

/* Footnote / definition list — used by methodology and the codebook */
.ui-prose dl { margin: 0 0 1em; }
.ui-prose dt { font-family: var(--font-mono); font-size: 13px; color: var(--text); margin-top: 1em; }
.ui-prose dd { margin: 0.25em 0 0; padding-left: 1.2em; border-left: var(--border-width) solid var(--border); }

/* Tables and figures may exceed the measure — they are not running text. */
.ui-prose .ui-table-wrap,
.ui-prose figure { max-width: none; width: 100%; }

/* ── FIGURE — CivicTheme's figure molecule ───────────────────────────────
   The whole site contains exactly one <figure> and one <figcaption>. */
.ui-figure { margin: 2em 0; }
.ui-figure img { display: block; width: 100%; height: auto; border-radius: var(--radius-card); }

.ui-figure figcaption {
  margin-top: var(--ui-2);
  font-family: var(--ui-font-chrome);
  font-size: 12px;
  line-height: 1.6;
  color: var(--text3);
  max-width: var(--ui-measure);
}

/* ── CONTENT-LINK — external links, marked ───────────────────────────────
   72 links use target="_blank"; three of them have no rel="noopener", and
   the stylesheet has no rule matching "external" at all, so none of them
   is marked. WCAG guidance is that opening a new window should be
   announced — this does it in CSS, and the .ui-visually-hidden span does
   it for screen readers. */
.ui-link-external::after {
  content: '';
  display: inline-block;
  width: 0.7em;
  height: 0.7em;
  margin-left: 0.3em;
  vertical-align: baseline;
  background-color: currentcolor;
  mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M7 17L17 7M17 7H8M17 7v9'/%3E%3C/svg%3E") no-repeat center / contain;
}

/* ── DATETIME — CivicTheme's datetime base component ─────────────────────
   The site uses <time> exactly zero times. Every case date, study year and
   newsletter date is plain text: not machine-readable, not exposed to
   assistive tech as a date, and not usable by search engines. */
.ui-time { font-family: var(--font-mono); font-size: 0.9em; color: var(--text3); font-variant-numeric: tabular-nums; }

/* ── ATTACHMENT — CivicTheme's attachment molecule ───────────────────────
   Real downloads exist (Vereinsstatuten, Beitrittserklärung, the data
   package, Zenodo exports) but there is no component: they are plain
   links, so format and size are invisible before clicking. */
.ui-attachment {
  display: flex;
  align-items: center;
  gap: var(--ui-3);
  padding: var(--ui-3) var(--ui-4);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-card);
  background: var(--bg2);
  text-decoration: none;
  color: var(--text);
  transition: border-color 0.15s ease, background 0.15s ease;
}

.ui-attachment:hover { border-color: var(--primary-text); }
.ui-attachment:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: 2px; }

.ui-attachment__type {
  flex: none;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: var(--radius-btn);
  background: var(--primary-light);
  color: var(--primary-text);
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.5px;
}

.ui-attachment__name { font-size: 14px; line-height: 1.4; }
.ui-attachment__meta { font-family: var(--font-mono); font-size: 11px; color: var(--text3); margin-top: 2px; }

/* ── PUBLICATION CARD — CivicTheme's publication-card ────────────────────
   studies.html renders a .study-list; a citation has a fixed shape
   (authors · year · title · venue · DOI) that a component can guarantee. */
.ui-pub {
  display: grid;
  gap: var(--ui-1);
  padding: var(--ui-4) 0;
  border-bottom: var(--border-width) solid var(--border);
}

.ui-pub__title { font-family: var(--font-headline); font-size: 17px; line-height: 1.4; color: var(--text); margin: 0; }
.ui-pub__authors { font-size: 14px; color: var(--text2); margin: 0; }
.ui-pub__meta { display: flex; flex-wrap: wrap; gap: var(--ui-2); align-items: center; font-family: var(--ui-font-chrome); font-size: 11px; color: var(--text3); margin-top: var(--ui-1); }

/* ── TABS — role="tab" already exists in the markup, with no CSS ───────── */
.ui-tabs { display: flex; gap: var(--ui-1); border-bottom: var(--border-width) solid var(--border); overflow-x: auto; }

.ui-tab {
  flex: none;
  padding: var(--ui-3) var(--ui-4);
  font-family: var(--ui-font-chrome);
  font-weight: var(--ui-font-chrome-weight);
  font-size: 13px;
  color: var(--text3);
  background: none;
  border: 0;
  border-bottom: 2px solid transparent;
  cursor: pointer;
  transition: color 0.15s ease, border-color 0.15s ease;
}

.ui-tab:hover { color: var(--text2); }
.ui-tab[aria-selected='true'] { color: var(--text); border-bottom-color: var(--primary); }
.ui-tab:focus-visible { outline: 2px solid var(--ui-focus); outline-offset: -2px; }

/* ── BREADCRUMB ─────────────────────────────────────────────────────────── */
.ui-breadcrumb { display: flex; flex-wrap: wrap; align-items: center; gap: var(--ui-2); font-family: var(--ui-font-chrome); font-size: 11px; color: var(--text3); }
.ui-breadcrumb a { color: var(--text3); text-decoration: none; }
.ui-breadcrumb a:hover { color: var(--text); text-decoration: underline; }
.ui-breadcrumb li + li::before { content: '/'; margin-right: var(--ui-2); color: var(--border2); }
.ui-breadcrumb ol { display: flex; flex-wrap: wrap; gap: var(--ui-2); list-style: none; margin: 0; padding: 0; }
.ui-breadcrumb li { display: flex; align-items: center; }

/* ── ICON — one sprite instead of 76 inline SVGs ─────────────────────────
   Across the 20 pages there are 76 inline <svg> elements, roughly 3–4 per
   page, and they are the SAME icons repeated: the sun/moon theme toggle
   and the hamburger. Each copy is bytes in every HTML file and a separate
   thing to change. CivicTheme references icons by name from one sprite. */
.ui-icon { width: 1em; height: 1em; flex: none; fill: none; stroke: currentcolor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.ui-icon--lg { width: 1.5em; height: 1.5em; }

/* ═══════════════════════════════════════════════════════════════════════
   TEIL 8 — how CivicTheme ENFORCES consistency
   Not "it has tokens" — the machinery that makes divergence hard:

   1 003 `!default` declarations (32 base + 971 component). Every value is
     overridable without forking, which is why consumers do not fork.
   415 light tokens · 415 dark tokens · 100 % paired, zero orphans — because
     ct-component-theme() loops `@each $theme in light, dark` and generates
     both. A component that works in one theme only cannot be written.
   87 of 94 components ship a .stories.js — 93 % of the system is rendered
     and reviewable, which is what makes drift visible.
   constants.json (96 KB) ships in dist with SCSS_VARIABLES, ICONS, LOGOS —
     one machine-readable source so CSS and JS cannot disagree.
   A deprecation mixin that warns with version AND replacement, instead of
     silently changing behaviour.

   For comparison, in style.css: 31 tokens on :root, 17 redefined for light,
   and SIX colour tokens with no light value at all — --primary, --teal,
   --coral, --blue, --purple. The filled button is the same purple on a
   near-black and on a near-white ground.
   ═══════════════════════════════════════════════════════════════════════ */

/* The missing light values, as CivicTheme would pair them. */
/* stylelint-disable-next-line no-duplicate-selectors -- the toolkit is grouped by concern (components, then theming, then print), so a selector deliberately reappears in a later group rather than being merged back into its component block. */
.ui { --ui-primary-solid: var(--primary); }

/* stylelint-disable-next-line no-duplicate-selectors -- the toolkit is grouped by concern (components, then theming, then print), so a selector deliberately reappears in a later group rather than being merged back into its component block. */
[data-theme='light'] .ui,
.ui[data-theme='light'] { --ui-primary-solid: #6B4FA0; }

/* stylelint-disable-next-line no-duplicate-selectors -- the toolkit is grouped by concern (components, then theming, then print), so a selector deliberately reappears in a later group rather than being merged back into its component block. */
.ui-btn--primary { background: var(--ui-primary-solid); border-color: var(--ui-primary-solid); }

/* ═══════════════════════════════════════════════════════════════════════
   TEIL 9 — why a page reads as "generated", and what CivicTheme has for it

   The tells, honestly: a violet accent on near-black is the signature
   AI-product palette of the moment; every block boxed in its own bordered
   card; three perfectly equal columns; uniformly even spacing; mono caps
   labels; and one accent colour doing every job. moirée has most of these.

   CivicTheme's structural answers are per-section theming, background
   variants, real page banners, and a three-colour brand — none of which
   need a redesign.
   ═══════════════════════════════════════════════════════════════════════ */

/* ── 1. PER-SECTION THEMING — CivicTheme's single most characteristic move
   ct-theme-light / ct-theme-dark are applied per SECTION, not per page. A
   CivicTheme page alternates light and dark bands down its length. moirée
   is one uniform dark from header to footer, which is the main reason it
   reads flat. These classes re-declare the tokens locally, so a band can
   be inverted no matter what the page theme is. */
.ui-theme-dark {
  --bg: #1a1a18;
  --bg2: #222220;
  --text: #f0ede6;
  --text2: #b4b2a9;
  --text3: #999891;
  --border: rgb(255 255 255 / 12%);
  --border2: rgb(255 255 255 / 22%);
  --primary-text: #B99EE0;
  --primary-light: #1e1535;
  --ui-primary-solid: #7A5AB5;

  background: var(--bg);
  color: var(--text);
}

.ui-theme-light {
  --bg: #edece8;
  --bg2: #e5e4e0;
  --text: #1a1a18;
  --text2: #5f5e5a;
  --text3: #636260;
  --border: rgb(0 0 0 / 12%);
  --border2: rgb(0 0 0 / 22%);
  --primary-text: #6B4FA0;
  --primary-light: #F0EAFB;
  --ui-primary-solid: #6B4FA0;
  --teal: #0F6E56;
  --coral: #C04A20;

  background: var(--bg);
  color: var(--text);
}

/* ── 2. BACKGROUND VARIANTS — sections differ by GROUND, not by border ────
   CivicTheme has a background base component for exactly this. When every
   block is a bordered box, nothing has hierarchy — "card soup". A band
   that simply changes its ground needs no border at all. */
.ui-band { padding-block: 64px; }
.ui-band--tint { background: var(--bg2); }
.ui-band--brand { background: var(--primary-light); }
.ui-band--edge { border-block: var(--border-width) solid var(--border); }

@media (width >= 768px) { .ui-band { padding-block: 96px; } }

/* Full-bleed inside a centred wrap, without breaking the layout */
.ui-band--bleed { margin-inline: calc(50% - 50vw); padding-inline: calc(50vw - 50%); }

/* ── 3. PAGE BANNER — CivicTheme's banner organism ───────────────────────
   Subpages currently start cold: navigation, then straight into text. A
   banner gives each page an identity and a place for the breadcrumb. */
.ui-page-banner { padding: 48px 0 40px; border-bottom: var(--border-width) solid var(--border); }
.ui-page-banner__title { font-family: var(--font-headline); font-size: 40px; line-height: 1.1; letter-spacing: -0.8px; color: var(--text); margin: var(--ui-3) 0 var(--ui-3); text-wrap: balance; }
.ui-page-banner__summary { font-family: var(--font-body); font-size: 18px; line-height: 1.6; color: var(--text2); max-width: 60ch; margin: 0; }

@media (width < 768px) { .ui-page-banner__title { font-size: 30px; } }

/* ── 4. THREE BRAND COLOURS, NOT ONE ─────────────────────────────────────
   CivicTheme separates brand1/brand2/brand3 from `interaction`. Purple in
   moirée currently marks links, buttons, active states, numbers, categories
   AND errors. A colour that means everything means nothing. */
.ui-accent-1 { color: var(--primary-text); }
.ui-accent-2 { color: var(--teal); }
.ui-accent-3 { color: var(--amber); }

/* ── 5. TYPOGRAPHIC DETAIL — the part that reads as "edited" ─────────────
   Small, and almost never present on generated pages. */
.ui-quote-hang {
  /* Opening quote mark hangs into the margin so the first line of text
     stays optically flush with the lines below it. */
  hanging-punctuation: first last;
}

@supports not (hanging-punctuation: first) {
  .ui-quote-hang { text-indent: -0.42em; }
}

.ui-nums-tabular { font-variant-numeric: tabular-nums; }   /* tables, counters */
.ui-nums-oldstyle { font-variant-numeric: oldstyle-nums; } /* running prose */

/* Optical sizing and real tracking on large display type. */
.ui-display {
  font-family: var(--font-headline);
  font-size: clamp(34px, 1.5rem + 3.4vw, 60px);
  line-height: 1.05;
  letter-spacing: -1.2px;
  font-optical-sizing: auto;
  text-wrap: balance;
  color: var(--text);
  margin: 0;
}

/* ═══════════════════════════════════════════════════════════════════════
   TEIL 10 — Light/Dark zusammenführen
   Two concrete faults, both invisible until you look for them:

   1. prefers-color-scheme is consulted NOWHERE — 0 occurrences in
      style.css, and fouc-theme.js only reads localStorage. The site is
      therefore dark for every first-time visitor regardless of their OS
      setting. Meanwhile the two <meta name="theme-color"> tags DO respond
      to prefers-color-scheme, so on a light OS the browser chrome goes
      light while the page stays dark.

   2. `color-scheme: light dark` is declared once on :root and never
      changed per theme. That property is what tells the browser how to
      paint NATIVE widgets — select popups, checkboxes, scrollbars, date
      pickers. Left static, those follow the OS instead of the site, so a
      dark-OS user in light mode gets a dark dropdown popup inside a light
      page. It is part of why the select looked wrong.
   ═══════════════════════════════════════════════════════════════════════ */

/* stylelint-disable-next-line no-duplicate-selectors -- the toolkit is grouped by concern (components, then theming, then print), so a selector deliberately reappears in a later group rather than being merged back into its component block. */
.ui { color-scheme: dark; }

[data-theme='light'] .ui,
.ui[data-theme='light'],
.ui-theme-light { color-scheme: light; }

/* stylelint-disable-next-line no-duplicate-selectors -- the toolkit is grouped by concern (components, then theming, then print), so a selector deliberately reappears in a later group rather than being merged back into its component block. */
.ui-theme-dark { color-scheme: dark; }

/* The five remaining orphan tokens, given the light values they never had.
   CivicTheme cannot produce an orphan because ct-component-theme() emits
   both themes; here they have to be written out. */
/* stylelint-disable-next-line no-duplicate-selectors -- the toolkit is grouped by concern (components, then theming, then print), so a selector deliberately reappears in a later group rather than being merged back into its component block. */
[data-theme='light'] .ui,
.ui[data-theme='light'],
.ui-theme-light {
  --teal: #0F6E56;
  --coral: #C04A20;
  --blue: #1F6FB2;
  --purple: #4A3FA0;
}

/* ═══════════════════════════════════════════════════════════════════════
   TEIL 11 — "auf jedem Browser und Betriebssystem gleich"
   Verified with fontTools against assets/fonts/*.woff2. Six files ship:
     playfair-display  400, 700   — 700 is a real Bold subfamily
     source-serif-4    300, 400   — NO 600, NO 700
     ibm-plex-mono     400, 500
   Every file reports italicAngle 0.0 and none declares an Italic
   subfamily. No file contains the `smcp` (small caps) or `onum`
   (oldstyle numerals) OpenType feature; only Source Serif has `tnum`.

   Consequences, all of them live today:
     · 17 <em>/<i> plus 2 explicit font-style: italic have no italic face
       to use, so every browser SKEWS the roman itself. The skew differs
       between engines — including <em>Sichtbar gemacht.</em> in the hero,
       the most prominent type on the homepage.
     · 103 <strong>/<b> and 16 × font-weight: 600 in Source Serif have no
       bold face, so every browser DILATES the glyphs itself. Also
       engine-specific.
   Synthesis is precisely what "looks the same everywhere" cannot survive.

   NOTE: font-synthesis is my recommendation, not CivicTheme's — it loads
   from Google Fonts and sets neither font-display nor font-synthesis. What
   IS CivicTheme's is the idea of a declarative font manifest: $ct-fonts
   lists every family with its exact weights, styles and files, and
   ct-font() raises a build error if you reference one that is not there.
   ═══════════════════════════════════════════════════════════════════════ */

/* Opt-in: stop the browser inventing faces that were never shipped.
   Applied, missing weights and italics render as their nearest REAL face —
   visibly plainer, but identical on every engine. That is the trade. */
.ui-font-strict { font-synthesis: none; }

/* The two utilities below are INERT with the current files and are kept
   only so the gap is documented rather than silently wrong:
   `all-small-caps` has no smcp table to use, so browsers scale capitals
   themselves; `oldstyle-nums` has no onum table at all. */
/* stylelint-disable-next-line no-duplicate-selectors -- the toolkit is grouped by concern (components, then theming, then print), so a selector deliberately reappears in a later group rather than being merged back into its component block. */
.ui-eyebrow--smallcaps { /* requires a font with smcp — see Teil 11 */ }
/* stylelint-disable-next-line no-duplicate-selectors -- the toolkit is grouped by concern (components, then theming, then print), so a selector deliberately reappears in a later group rather than being merged back into its component block. */
.ui-nums-oldstyle { /* requires a font with onum — currently does nothing */ }

/* tnum exists in Source Serif only; mono is monospaced anyway, so numeral
   alignment should come from the mono face, not from font-variant. */
/* stylelint-disable-next-line no-duplicate-selectors -- the toolkit is grouped by concern (components, then theming, then print), so a selector deliberately reappears in a later group rather than being merged back into its component block. */
.ui-nums-tabular { font-variant-numeric: tabular-nums; font-family: var(--font-mono); }
