Zum Inhalt
← Hero & Section-Layouts

Team-/Über-mich-Layout mit Porträt-Interaktion

Köpfe einer Salzburger Kanzlei als einheitliche Rasterporträts mit Filter nach Rechtsgebiet: Beim Hover deckt ein clip-path von unten das private Bild samt Satz auf, dazu Namensliste und Statement mit Inline-Porträts.

Teams ab etwa sechs Personen, bei denen Besucher die passende Ansprechperson suchen: Kanzlei, Praxis, Architekturbüro, Handwerksbetrieb. Filter nach Fachgebiet oder Standort, Porträt-Tausch zeigt die Person hinter der Funktion.

Personen erscheinen als einheitlich bearbeitete Porträtreihe (gleicher Ausschnitt, gleiches Licht, Schwarzweiß oder Duoton) oder als große typografische Namensliste. Hover oder Fokus auf ein Porträt tauscht das ernste Bild gegen eine lockere Variante. In der Namensliste zeigt der Hover auf einen Namen das passende Porträt in einer festen Spalte. Über-uns-Seiten kombinieren ein großes Statement in Satzform mit kleinen Porträts zwischen den Wörtern und Fakten wie Ort, Gründung, Erfahrung oder Sprachen. Zu jeder Person gehören Funktion, Fachgebiet und direkter Kontakt als Text.

Menschen machen ein Unternehmen greifbar. Das ernste Bild steht für Kompetenz, das lockere zeigt Persönlichkeit. Weil der Wechsel erst auf Interesse hin passiert, bleibt die Seite seriös und wirkt trotzdem nahbar.

Wann einsetzen

Studios, Agenturen, Kanzleien, Beratungen, Praxen und Freelancer-Über-mich-Seiten. Teams bis etwa 20 Personen.

Wann nicht

Ohne einheitliches Fotomaterial, sonst wirkt die Reihe zusammengewürfelt. Bei sehr großen Teams (dann Filter und Liste). Wenn nicht alle Personen einem lockeren Bild zustimmen.

Varianten

  • Raster mit Filter nach Fachgebiet, Porträt-Tausch ernst/locker
  • Namensliste mit Porträtspalte oder Hover-Porträt
  • Statement-Satz mit Inline-Porträts
  • Sticky-Team-Sektion
  • Geschichtete Porträts, Video- oder WebGL-Porträts

Umsetzung

Porträt-Tausch: zwei gestapelte Bilder, das lockere oben mit clip-path: inset(100% 0 0 0). Auf :hover und :focus-visible auf inset(0) animieren (power3.out, 300 bis 500 ms). Der Auslöser ist ein button mit aria-pressed, damit Tastatur und Touch denselben Zustand nutzen. Namensliste wie projektindex-hover-vorschau: eine Porträtspalte, das neue Bild deckt das alte per clip-path zu. Inline-Porträts: <span class="pille"> mit height 1em, border-radius 0.5em und width 0, beim Einblenden auf etwa 1.5em aufklappen (gestaffelt). Alle Fotos vorab gleich bearbeiten: Ausschnitt, Licht, Farbe.

Robust umsetzen

Telefon. Porträts direkt sichtbar, kein Hover. Tausch per Tippen oder automatisch beim Durchscrollen. Namenslisten zeigen ein Daumenporträt in jeder Zeile.

Ohne Bewegung. Kein Bildtausch, nur das Standardporträt. Zusatztexte dürfen ohne Bewegung erscheinen.

Barrierefrei. Das ernste Porträt hat alt mit dem Namen, das lockere ist dekorativ (alt leer, aria-hidden). Namen, Funktion und Kontakt stehen als Text, nie im Bild. Der Tausch-Knopf ist per Tastatur erreichbar und meldet seinen Zustand mit aria-pressed. Telefon und E-Mail als echte Links.

Code

<!DOCTYPE html>
<!--
  Team-/Über-mich-Layout mit Porträt-Interaktion
  Fundus · Hero & Section-Layouts · https://cecodesign.de/lab/fundus/hero-layouts/team-ueber-mich-layout/
  Eigenständige Datei. Bilder liegen hier unter bilder/ – eigene einsetzen.
-->
<html lang="de">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Team-/Über-mich-Layout mit Porträt-Interaktion · Fundus-Demo</title>
<script>document.documentElement.classList.add('js');if(matchMedia('(prefers-reduced-motion: reduce)').matches)document.documentElement.classList.add('reduziert')</script>
<style>
@font-face { font-family: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-normal.woff2') format('woff2-variations'); font-weight: 200 800; font-style: normal; font-display: swap; }
@font-face { font-family: 'Newsreader'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/newsreader:vf@latest/latin-opsz-italic.woff2') format('woff2-variations'); font-weight: 200 800; font-style: italic; font-display: swap; }
@font-face { font-family: 'Schibsted Grotesk'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/schibsted-grotesk:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 400 900; font-display: swap; }
@font-face { font-family: 'Geist Mono'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/geist-mono:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 100 900; font-display: swap; }

/* Welt: Rainberg Rechtsanwälte, Salzburg (Rezept kanzlei-beratung-souveraen) */
:root {
  --grund: #f9f8f8; --flaeche: #efebeb; --linie: #d2cdc7;
  --marke: #1e1e50; --text: #212529; --meta: #6e5f52; --akzent: #00bee6;
  --kurve: cubic-bezier(0.25, 1, 0.5, 1);   /* power3.out, gemessene Kurve des Musters */
  --tausch: 400ms;                           /* Bildtausch (Messwert 300–500 ms) */
  --ui: 125ms;                               /* Farbe, Unterstreichung, Abblenden */
  --pille: 500ms;                            /* Inline-Porträt klappt auf */
  --sortieren: 500ms;                        /* Raster sortiert sich nach dem Filtern neu */
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { background: var(--grund); color: var(--text); font: 400 17px/1.6 'Schibsted Grotesk', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
.kontakt, .nr, .fakten dd { font-variant-numeric: tabular-nums; }
body { min-height: 100svh; padding: clamp(18px, 3.4vw, 48px) clamp(16px, 4vw, 56px); display: flex; flex-direction: column; gap: clamp(22px, 3.4vw, 44px); }
a { color: inherit; }
button { font: inherit; color: inherit; background: none; border: 0; cursor: pointer; }
:focus-visible { outline: 2px solid var(--akzent); outline-offset: 3px; }
.serif { font-family: 'Newsreader', Georgia, serif; font-weight: 400; letter-spacing: -.01em; }
.mono { font: 500 11px/1.3 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--meta); }

/* Kopf der Kanzlei */
.kopf { display: flex; align-items: baseline; justify-content: space-between; gap: 24px; padding-bottom: 16px; border-bottom: 1px solid var(--linie); }
.marke { display: flex; align-items: baseline; gap: 12px; text-decoration: none; color: var(--marke); }
.marke b { font: 500 27px/1 'Newsreader', Georgia, serif; letter-spacing: -.02em; }
.marke span { font-size: 13px; font-weight: 500; color: var(--meta); }
.nav { display: flex; gap: clamp(14px, 2vw, 28px); list-style: none; font-size: 15px; font-weight: 500; color: var(--marke); }
.nav a { text-decoration: none; background: linear-gradient(currentColor, currentColor) 0 100% / 0 1px no-repeat; transition: background-size var(--ui) var(--kurve); padding-bottom: 2px; }
.nav a:hover, .nav a[aria-current] { background-size: 100% 1px; }
.nav a[aria-current] { background-size: 100% 2px; }
.nav .klein { display: none; }

/* Abschnittskopf */
.sektion-kopf { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: 20px clamp(24px, 5vw, 80px); align-items: end; }
.auge { font-size: 13px; font-weight: 500; color: var(--marke); display: flex; gap: 10px; align-items: center; margin-bottom: 12px; }
.auge::before { content: ''; width: 22px; height: 1px; background: var(--marke); }
h2 { font-size: clamp(1.75rem, 1.2rem + 1.7vw, 2.75rem); line-height: 1.1; color: var(--marke); text-wrap: balance; }
.intro { font-size: 15px; color: var(--text); max-width: 46ch; text-wrap: pretty; }
.intro em { font-style: normal; color: var(--meta); }
.hinweis-touch { display: none; }

.ansicht { display: none; }
.ansicht.an { display: block; }

/* Porträt: zwei gestapelte Bilder, das lockere liegt oben und wird per clip-path von unten aufgedeckt */
.bild { position: relative; aspect-ratio: 4 / 5; background: var(--flaeche); overflow: hidden; }
.bild img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
.bild .locker { clip-path: inset(100% 0 0 0); transition: clip-path var(--tausch) var(--kurve); }
.bild .ernst { transition: scale var(--tausch) var(--kurve); }
.privat { position: absolute; left: 0; right: 0; bottom: 0; padding: 10px 12px 11px; background: var(--grund); color: var(--marke); font: italic 400 15px/1.3 'Newsreader', Georgia, serif; clip-path: inset(100% 0 0 0); transition: clip-path var(--tausch) var(--kurve); }
.privat small { display: block; font: 500 10px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--meta); margin-bottom: 5px; font-style: normal; }
.an > .bild .locker, .an > .bild .privat, .pille.an .locker { clip-path: inset(0 0 0 0); }
.an > .bild .ernst { scale: 1.04; }
.privat { transition-delay: 0ms; }
.an > .bild .privat { transition-delay: 80ms; }

/* Variante 1: Porträtreihe mit Tausch ernst → locker */
.reihe { list-style: none; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: clamp(14px, 2vw, 28px); }
.person { display: flex; flex-direction: column; gap: 14px; }
.person > button { display: block; text-align: left; width: 100%; }
.person h3 { font: 400 clamp(19px, 1.5vw, 24px)/1.15 'Newsreader', Georgia, serif; color: var(--marke); letter-spacing: -.01em; }
.person .funktion { font-size: 14px; font-weight: 500; margin-top: 4px; line-height: 1.4; }
.person .sprachen { font-size: 13px; color: var(--meta); line-height: 1.4; margin-top: 2px; }
.kontakt { display: flex; flex-wrap: wrap; gap: 4px 14px; font-size: 13px; margin-top: 8px; }
.kontakt a { text-decoration: none; color: var(--marke); background: linear-gradient(currentColor, currentColor) 0 100% / 100% 1px no-repeat; transition: color var(--ui) var(--kurve); }
.kontakt a:hover { color: #0b7f94; }

/* Filter über dem Raster (Rezept: Köpfe filterbar nach Fachgebiet) */
.filter { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; margin-top: clamp(20px, 2.6vw, 34px); padding-top: 14px; border-top: 1px solid var(--linie); }
.filter button { height: 34px; padding: 0 14px; border: 1px solid var(--linie); border-radius: 999px; font-size: 14px; font-weight: 500; color: var(--marke); transition: background-color var(--ui) var(--kurve), color var(--ui) var(--kurve), border-color var(--ui) var(--kurve); }
.filter button:hover { border-color: var(--marke); }
.filter button[aria-pressed="true"] { background: var(--marke); border-color: var(--marke); color: var(--grund); }
.filter button span { font-size: 12px; font-variant-numeric: tabular-nums; opacity: .72; margin-left: 4px; }
.filter .treffer { margin-left: auto; }
.reihe { margin-top: clamp(16px, 2vw, 26px); }
.person[hidden] { display: none; }
/* Umsortieren per View Transition: jede Person hat einen eigenen Namen (im Skript gesetzt) */
::view-transition-group(*) { animation-duration: var(--sortieren); animation-timing-function: var(--kurve); }
::view-transition-old(*), ::view-transition-new(*) { animation-duration: calc(var(--sortieren) * .5); }
::view-transition-new(*) { animation-delay: calc(var(--sortieren) * .3); animation-fill-mode: both; }
::view-transition-old(root), ::view-transition-new(root) { animation: none; }

/* Variante 2: Namensliste mit Porträtspalte */
.liste-raster { display: grid; grid-template-columns: minmax(0, 1fr) minmax(220px, 27%); gap: clamp(24px, 5vw, 80px); align-items: start; }
.namen { list-style: none; border-bottom: 1px solid var(--linie); }
.namen button { width: 100%; display: grid; grid-template-columns: 2.6em minmax(0, 1fr) auto; align-items: baseline; gap: 16px; padding: clamp(9px, 1.15vw, 15px) 0; border-top: 1px solid var(--linie); text-align: left; }
.namen .nr { font-size: 13px; color: var(--meta); }
.namen .name { font: 400 clamp(26px, 2.9vw, 42px)/1.05 'Newsreader', Georgia, serif; color: var(--marke); letter-spacing: -.015em; transition: opacity var(--ui) var(--kurve), translate var(--tausch) var(--kurve); }
.namen .fach { font-size: 14px; font-weight: 500; color: var(--text); text-align: right; transition: opacity var(--ui) var(--kurve); }
.namen .daumen { display: none; }
.namen.aktiv .name, .namen.aktiv .fach { opacity: .32; }
.namen.aktiv .an .name, .namen.aktiv .an .fach { opacity: 1; }
.namen.aktiv .an .name { translate: .18em 0; }
.karte { position: sticky; top: 24px; }
.karte .bild img { clip-path: inset(100% 0 0 0); transition: clip-path var(--tausch) var(--kurve); }
.karte .bild img.oben { clip-path: inset(0 0 0 0); }
.karte .bild img.unten { clip-path: inset(0 0 0 0); transition: none; }
.karte-text { padding-top: 14px; transition: opacity var(--ui) var(--kurve); }
.karte-text.wechsel { opacity: 0; }
.karte-text h3 { font: 400 22px/1.15 'Newsreader', Georgia, serif; color: var(--marke); }
.karte-text p { font-size: 14px; line-height: 1.45; }
.karte-text .sprachen { color: var(--meta); font-size: 13px; }

/* Variante 3: Statement mit eingebetteten Porträts */
.statement { font: 400 clamp(1.9rem, 0.9rem + 3.5vw, 4.25rem)/1.14 'Newsreader', Georgia, serif; color: var(--marke); letter-spacing: -.018em; max-width: 29ch; text-wrap: pretty; }
.pille { display: inline-block; position: relative; height: 1em; width: 0; vertical-align: -.14em; border-radius: .5em; overflow: hidden; background: var(--flaeche); margin: 0 .06em; transition: width var(--pille) var(--kurve); }
.pille.offen { width: 1.5em; }
.pille img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 34%; scale: 1.5; }
.pille .locker { clip-path: inset(100% 0 0 0); transition: clip-path var(--tausch) var(--kurve); }
.fakten { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 18px; margin-top: clamp(26px, 4vw, 54px); border-top: 1px solid var(--linie); padding-top: 16px; }
.fakten dt { font-size: 13px; font-weight: 500; color: var(--meta); }
.fakten dd { font: 400 clamp(22px, 2vw, 30px)/1.15 'Newsreader', Georgia, serif; color: var(--marke); margin-top: 4px; }

/* Demo-Bedienung (dezent) */
.steuer { margin-top: auto; display: flex; justify-content: space-between; align-items: center; gap: 12px 24px; flex-wrap: wrap; padding-top: 14px; border-top: 1px solid var(--linie); }
.umschalter { display: flex; gap: 4px; }
.umschalter button { font: 500 11px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--meta); padding: 8px 10px; border: 1px solid transparent; border-radius: 2px; transition: color var(--ui) var(--kurve), border-color var(--ui) var(--kurve); }
.umschalter button:hover { color: var(--marke); }
.umschalter button[aria-pressed="true"] { color: var(--marke); border-color: var(--linie); }
.werte { text-align: right; }

/* Vorführung: gezeichneter Zeiger */
.zeiger { position: fixed; left: 0; top: 0; z-index: 9; width: 18px; height: 18px; pointer-events: none; display: none; transition: transform 700ms var(--kurve); }
.vorfuehrung .zeiger { display: block; }

/* Vorschau 960 × 720: nur die Bühne */
.vorschau body { overflow: hidden; height: 100svh; }
.vorschau .werte { display: none; }

/* Touch: Porträts direkt sichtbar, Tausch per Tippen */
@media (hover: none) {
  .hinweis-maus { display: none; } .hinweis-touch { display: inline; }
  .liste-raster { grid-template-columns: 1fr; }
  .karte { display: none; }
  .namen button { grid-template-columns: 56px minmax(0, 1fr); gap: 4px 14px; }
  .namen .nr { display: none; }
  .namen .daumen { display: block; position: relative; overflow: hidden; grid-row: span 2; width: 56px; aspect-ratio: 4 / 5; background: var(--flaeche); align-self: center; }
  .namen .daumen img { position: absolute; inset: 0; width: 100%; height: 100%; }
  .namen .daumen .locker { clip-path: inset(100% 0 0 0); transition: clip-path var(--tausch) var(--kurve); }
  .namen .an .daumen .locker { clip-path: inset(0 0 0 0); }
  .namen .fach { text-align: left; grid-column: 2; }
  .namen.aktiv .name, .namen.aktiv .fach { opacity: 1; }
  .namen.aktiv .an .name { translate: none; }
}
@media (max-width: 900px) {
  .nav li:not(.klein) { display: none; }
  .nav .klein { display: block; }
  .sektion-kopf { grid-template-columns: 1fr; }
  .reihe { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .fakten { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 560px) {
  .marke span { display: none; }
  .werte { text-align: left; }
  .namen button { grid-template-columns: 56px minmax(0, 1fr); gap: 4px 14px; }
  .namen .nr { display: none; }
  .namen .fach { text-align: left; grid-column: 2; font-size: 13px; }
  .reihe { gap: 22px 14px; }
  .filter button { height: 36px; padding: 0 12px; font-size: 13px; }
  .filter .treffer { margin-left: 0; width: 100%; }
  .privat { font-size: 13px; padding: 8px 9px; }
}
/* Zugang: 320 px ohne Querscrollen (lange Berufsbezeichnungen trennen) */
@media (max-width: 360px) {
  .person .funktion, .person .name { hyphens: auto; overflow-wrap: anywhere; }
}
@media (forced-colors: active) { button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; } }

/* Reduzierte Bewegung: kein Bildtausch, nur das Standardporträt */
.reduziert * { transition-duration: .01ms !important; transition-delay: 0ms !important; animation-duration: .01ms !important; }
.reduziert .bild .locker, .reduziert .pille .locker, .reduziert .daumen .locker { display: none; }
.reduziert .an > .bild .ernst { scale: 1; }
.reduziert .pille { width: 1.5em; }
.reduziert .namen.aktiv .an .name { translate: none; }
.reduziert::view-transition-group(*), .reduziert::view-transition-old(*), .reduziert::view-transition-new(*) { animation: none; }
</style>
</head>
<body>
<header class="kopf">
  <a class="marke" href="#start"><b>Rainberg</b><span>Rechtsanwälte · Salzburg</span></a>
  <ul class="nav">
    <li><a href="#rechtsgebiete">Rechtsgebiete</a></li>
    <li><a href="#branchen">Branchen</a></li>
    <li><a href="#koepfe" aria-current="page">Köpfe</a></li>
    <li><a href="#insights">Insights</a></li>
    <li><a href="#kontakt">Kontakt</a></li>
    <li class="klein"><a href="#menue">Menü</a></li>
  </ul>
</header>

<main id="koepfe">
  <!-- Variante 1: Porträtreihe -->
  <section class="ansicht an" data-ansicht="raster" aria-labelledby="t-reihe">
    <div class="sektion-kopf">
      <div><p class="auge">Köpfe</p><h2 class="serif" id="t-reihe">Acht Köpfe. Ein Haus am Rainberg.</h2></div>
      <p class="intro">Seit 1998 beraten wir Salzburger Familienunternehmen und den Mittelstand. Jedes Mandat führt ein Partner persönlich. <em class="hinweis-maus">Fahren Sie über ein Porträt.</em><em class="hinweis-touch">Tippen Sie auf ein Porträt.</em></p>
    </div>
    <div class="filter" role="group" aria-label="Nach Rechtsgebiet filtern" data-filter></div>
    <ul class="reihe" data-reihe></ul>
  </section>

  <!-- Variante 2: Namensliste -->
  <section class="ansicht" data-ansicht="liste" aria-labelledby="t-liste">
    <div class="liste-raster">
      <div>
        <p class="auge">Köpfe · 6 Berufsträger</p>
        <h2 class="serif" id="t-liste" style="margin-bottom: clamp(18px, 2.4vw, 32px)">Wer Sie berät.</h2>
        <ul class="namen" data-namen></ul>
      </div>
      <aside class="karte" aria-live="polite">
        <div class="bild" data-karte-bild></div>
        <div class="karte-text" data-karte-text></div>
      </aside>
    </div>
  </section>

  <!-- Variante 3: Statement mit Inline-Porträts -->
  <section class="ansicht" data-ansicht="statement" aria-labelledby="t-statement">
    <p class="auge" id="t-statement">Über uns</p>
    <p class="statement" data-statement>Wir sind <span class="pille" data-p="0"></span> sechs Juristinnen und Juristen <span class="pille" data-p="1"></span> aus Salzburg, die Unternehmen durch Gründung, <span class="pille" data-p="2"></span> Wachstum und Übergabe <span class="pille" data-p="3"></span> begleiten. Persönlich, <span class="pille" data-p="4"></span> seit 1998.</p>
    <dl class="fakten">
      <div><dt>Gegründet</dt><dd>1998</dd></div>
      <div><dt>Berufsträger</dt><dd>6</dd></div>
      <div><dt>Arbeitssprachen</dt><dd>6</dd></div>
      <div><dt>Standort</dt><dd>Salzburg-Riedenburg</dd></div>
    </dl>
  </section>
</main>

<footer class="steuer">
  <div class="umschalter" role="group" aria-label="Variante">
    <button type="button" data-variante="raster" aria-pressed="true">Raster mit Filter</button>
    <button type="button" data-variante="liste" aria-pressed="false">Namensliste</button>
    <button type="button" data-variante="statement" aria-pressed="false">Statement</button>
  </div>
  <p class="mono werte" data-werte></p>
</footer>

<svg class="zeiger" data-zeiger viewBox="0 0 18 18" aria-hidden="true"><path d="M2 1.5 15.5 9.2l-6 1.3-2.9 5.6Z" fill="#1e1e50" stroke="#f9f8f8" stroke-width="1.2" stroke-linejoin="round"/></svg>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const $ = (s, r = document) => r.querySelector(s), $$ = (s, r = document) => [...r.querySelectorAll(s)];

  /* ---------- Menschen ---------- */
  const TEAM = [
    { name: 'Dr. Katharina Wallner', funktion: 'Partnerin', fach: 'Gesellschaftsrecht, M&A', sprachen: 'Deutsch, Englisch, Italienisch', tel: '+43 662 84 17 30-11', mail: 'k.wallner', privat: 'Skitouren am Untersberg, meist vor sieben.',
      gebiete: ['unternehmen'], p: { haut: .74, haar: 'dutt', ton: .22, jacke: .24, kragen: 'rund', brille: false, kopf: [0, 0] } },
    { name: 'Mag. Florian Aigner', funktion: 'Partner', fach: 'Arbeitsrecht', sprachen: 'Deutsch, Englisch', tel: '+43 662 84 17 30-12', mail: 'f.aigner', privat: 'Bratsche im Laienorchester, zweites Pult.',
      gebiete: ['arbeit'], p: { haut: .7, haar: 'scheitel', ton: .2, jacke: .2, kragen: 'krawatte', brille: true, kopf: [0, 0] } },
    { name: 'Mag. Selin Kaya', funktion: 'Partnerin', fach: 'Immobilien- und Baurecht', sprachen: 'Deutsch, Türkisch, Englisch', tel: '+43 662 84 17 30-14', mail: 's.kaya', privat: 'Backt sonntags Simit für die halbe Kanzlei.',
      gebiete: ['immobilien'], p: { haut: .6, haar: 'lang', ton: .12, jacke: .3, kragen: 'rund', brille: false, kopf: [0, 0] } },
    { name: 'Dr. Georg Pichler', funktion: 'Partner', fach: 'Erbrecht, Unternehmensnachfolge', sprachen: 'Deutsch, Englisch', tel: '+43 662 84 17 30-10', mail: 'g.pichler', privat: 'Wattet jeden Donnerstag in Mülln.',
      gebiete: ['privat', 'unternehmen'], p: { haut: .72, haar: 'grau', ton: .66, jacke: .26, kragen: 'offen', brille: true, bart: true, kopf: [0, 0] } },
    { name: 'Mag. Lena Hofer, LL.M.', funktion: 'Rechtsanwältin', fach: 'IT- und Datenschutzrecht', sprachen: 'Deutsch, Englisch, Französisch', tel: '+43 662 84 17 30-16', mail: 'l.hofer', privat: 'Fährt das ganze Jahr Rad, auch im Schneeregen.',
      gebiete: ['unternehmen', 'arbeit'], p: { haut: .76, haar: 'bob', ton: .45, jacke: .34, kragen: 'rund', brille: false, kopf: [0, 0] } },
    { name: 'Mag. Jakob Brandstätter', funktion: 'Rechtsanwalt', fach: 'Vergaberecht', sprachen: 'Deutsch, Englisch, Kroatisch', tel: '+43 662 84 17 30-15', mail: 'j.brandstaetter', privat: 'Imkert zwei Völker auf dem Kanzleidach.',
      gebiete: ['unternehmen', 'immobilien'], p: { haut: .68, haar: 'kurz', ton: .3, jacke: .22, kragen: 'offen', brille: false, bart: true, kopf: [0, 0] } },
    { name: 'Mag. Theresa Ruhdorfer', funktion: 'Rechtsanwaltsanwärterin', fach: 'Arbeitsrecht, Mietrecht', sprachen: 'Deutsch, Englisch, Spanisch', tel: '+43 662 84 17 30-21', mail: 't.ruhdorfer', privat: 'Singt Alt im Domchor, auch an Feiertagen.',
      gebiete: ['arbeit', 'immobilien'], p: { haut: .73, haar: 'lang', ton: .5, jacke: .3, kragen: 'rund', brille: true, kopf: [0, 0] } },
    { name: 'Mag. Elias Leitner', funktion: 'Rechtsanwaltsanwärter', fach: 'Familien- und Erbrecht', sprachen: 'Deutsch, Englisch, Bosnisch', tel: '+43 662 84 17 30-22', mail: 'e.leitner', privat: 'Läuft im Sommer jeden Gipfel rund um Salzburg.',
      gebiete: ['privat'], p: { haut: .64, haar: 'scheitel', ton: .16, jacke: .18, kragen: 'krawatte', brille: false, kopf: [0, 0] } },
  ];
  const BERUFSTRAEGER = 6;   // Liste und Statement zeigen die sechs Anwälte, das Raster alle acht
  const GEBIETE = [['alle', 'Alle'], ['unternehmen', 'Unternehmen'], ['arbeit', 'Arbeit'], ['immobilien', 'Immobilien & Bau'], ['privat', 'Privat & Nachfolge']];

  /* ---------- Porträts: Szene in Graustufen zeichnen, dann als Raster in Nachtblau drucken ---------- */
  const W = 480, H = 600, WINKEL = Math.PI / 4;
  const grau = v => { const c = Math.round(Math.max(0, Math.min(1, v)) * 255); return `rgb(${c},${c},${c})`; };

  function szene(p, locker) {
    const c = document.createElement('canvas'); c.width = W; c.height = H;
    const g = c.getContext('2d');
    // Grund: einheitliches Studiolicht von links, weicher Schatten an der Wand rechts
    let lg = g.createLinearGradient(0, 0, W, H); lg.addColorStop(0, grau(.97)); lg.addColorStop(1, grau(.89));
    g.fillStyle = lg; g.fillRect(0, 0, W, H);
    let rg = g.createRadialGradient(330, 300, 20, 330, 300, 260); rg.addColorStop(0, 'rgba(0,0,0,.2)'); rg.addColorStop(1, 'rgba(0,0,0,0)');
    g.fillStyle = rg; g.fillRect(0, 0, W, H);

    const kipp = locker ? (p.kipp || -0.07) : 0, hoch = locker ? -6 : 0;
    g.save(); g.translate(240, 300); g.scale(1.32, 1.32); g.translate(-240, -290);   // Kopf groß ins Format, Schultern angeschnitten
    // Körper
    g.save();
    g.translate(240, 600); g.rotate(kipp * .3); g.translate(-240, -654);   // Schulterlinie näher ans Kinn
    lg = g.createLinearGradient(40, 0, 440, 0); lg.addColorStop(0, grau(p.jacke + .1)); lg.addColorStop(1, grau(p.jacke - .08));
    g.fillStyle = lg;
    g.beginPath(); g.moveTo(30, 600); g.bezierCurveTo(40, 488, 110, 452, 190, 440); g.lineTo(290, 440); g.bezierCurveTo(370, 452, 440, 488, 450, 600); g.closePath(); g.fill();
    // Hals
    lg = g.createLinearGradient(0, 392, 0, 462); lg.addColorStop(0, grau(p.haut - .26)); lg.addColorStop(1, grau(p.haut - .06));
    g.fillStyle = lg; g.beginPath(); g.moveTo(203, 350); g.lineTo(277, 350); g.lineTo(284, 452); g.quadraticCurveTo(240, 470, 196, 452); g.closePath(); g.fill();
    // Kragen
    g.fillStyle = grau(.96);
    if (p.kragen === 'krawatte') {
      g.beginPath(); g.moveTo(196, 440); g.lineTo(240, 520); g.lineTo(284, 440); g.lineTo(262, 436); g.lineTo(240, 470); g.lineTo(218, 436); g.closePath(); g.fill();
      g.fillStyle = grau(.14); g.beginPath(); g.moveTo(232, 470); g.lineTo(248, 470); g.lineTo(256, 600); g.lineTo(224, 600); g.closePath(); g.fill();
      // Revers
      g.fillStyle = grau(p.jacke - .1);
      g.beginPath(); g.moveTo(190, 442); g.lineTo(232, 600); g.lineTo(150, 600); g.lineTo(168, 500); g.closePath(); g.fill();
      g.beginPath(); g.moveTo(290, 442); g.lineTo(248, 600); g.lineTo(330, 600); g.lineTo(312, 500); g.closePath(); g.fill();
    } else if (p.kragen === 'offen') {
      g.beginPath(); g.moveTo(192, 440); g.lineTo(240, 540); g.lineTo(288, 440); g.lineTo(268, 432); g.lineTo(240, 488); g.lineTo(212, 432); g.closePath(); g.fill();
      g.fillStyle = grau(p.haut - .12); g.beginPath(); g.moveTo(214, 440); g.lineTo(240, 488); g.lineTo(266, 440); g.closePath(); g.fill();
      g.fillStyle = grau(p.jacke - .1);
      g.beginPath(); g.moveTo(186, 444); g.lineTo(236, 600); g.lineTo(150, 600); g.lineTo(166, 500); g.closePath(); g.fill();
      g.beginPath(); g.moveTo(294, 444); g.lineTo(244, 600); g.lineTo(330, 600); g.lineTo(314, 500); g.closePath(); g.fill();
    } else {
      // Rundhals (Bluse unter Blazer)
      g.fillStyle = grau(.9); g.beginPath(); g.moveTo(186, 446); g.quadraticCurveTo(240, 500, 294, 446); g.lineTo(300, 600); g.lineTo(180, 600); g.closePath(); g.fill();
      g.fillStyle = grau(p.haut - .1); g.beginPath(); g.moveTo(204, 446); g.quadraticCurveTo(240, 484, 276, 446); g.closePath(); g.fill();
      g.fillStyle = grau(p.jacke - .1);
      g.beginPath(); g.moveTo(180, 448); g.lineTo(222, 600); g.lineTo(140, 600); g.lineTo(160, 500); g.closePath(); g.fill();
      g.beginPath(); g.moveTo(300, 448); g.lineTo(258, 600); g.lineTo(340, 600); g.lineTo(320, 500); g.closePath(); g.fill();
    }
    g.restore();

    // Kopf (beim lockeren Bild leicht geneigt und angehoben)
    g.save();
    g.translate(240, 380); g.rotate(kipp); g.translate(-240, -380 + hoch);
    const haarHinten = () => {
      g.fillStyle = grau(p.ton);
      if (p.haar === 'lang') { g.beginPath(); g.moveTo(160, 250); g.bezierCurveTo(150, 160, 330, 160, 320, 250); g.bezierCurveTo(332, 340, 346, 410, 330, 470); g.lineTo(150, 470); g.bezierCurveTo(134, 410, 148, 340, 160, 250); g.fill(); }
      if (p.haar === 'bob') { g.beginPath(); g.moveTo(158, 250); g.bezierCurveTo(150, 160, 330, 160, 322, 250); g.bezierCurveTo(328, 300, 336, 340, 326, 362); g.lineTo(154, 362); g.bezierCurveTo(144, 340, 152, 300, 158, 250); g.fill(); }
      if (p.haar === 'dutt') { g.beginPath(); g.arc(250, 168, 34, 0, Math.PI * 2); g.fill(); }
    };
    haarHinten();
    // Ohren
    g.fillStyle = grau(p.haut - .14);
    g.beginPath(); g.ellipse(168, 276, 11, 21, -.1, 0, Math.PI * 2); g.fill();
    g.beginPath(); g.ellipse(312, 276, 11, 21, .1, 0, Math.PI * 2); g.fill();
    // Gesicht mit Volumen
    rg = g.createRadialGradient(212, 238, 10, 236, 262, 118);
    rg.addColorStop(0, grau(p.haut + .16)); rg.addColorStop(.62, grau(p.haut)); rg.addColorStop(1, grau(p.haut - .2));
    g.fillStyle = rg;
    const gesicht = new Path2D('M240 172C296 172 314 218 312 262C310 312 286 352 240 360C194 352 170 312 168 262C166 218 184 172 240 172Z');
    g.fill(gesicht);
    // Seitenlicht: Schattenseite rechts, Wangenknochen, Kinn
    g.save(); g.clip(gesicht);
    lg = g.createLinearGradient(236, 0, 316, 0); lg.addColorStop(0, 'rgba(0,0,0,0)'); lg.addColorStop(1, 'rgba(0,0,0,.26)');
    g.fillStyle = lg; g.fillRect(160, 160, 160, 210);
    g.fillStyle = 'rgba(0,0,0,.07)';
    g.beginPath(); g.ellipse(286, 300, 18, 34, -.2, 0, Math.PI * 2); g.fill();
    g.beginPath(); g.ellipse(194, 300, 12, 28, .2, 0, Math.PI * 2); g.fill();
    g.beginPath(); g.ellipse(240, 340, 16, 5, 0, 0, Math.PI * 2); g.fill();
    g.restore();
    // Bart
    if (p.bart) {
      g.fillStyle = grau(p.ton - (p.haar === 'grau' ? .12 : 0));
      g.beginPath(); g.moveTo(172, 280); g.bezierCurveTo(176, 330, 204, 362, 240, 366); g.bezierCurveTo(276, 362, 304, 330, 308, 280); g.bezierCurveTo(300, 316, 280, 330, 262, 326); g.quadraticCurveTo(240, 318, 218, 326); g.bezierCurveTo(200, 330, 180, 316, 172, 280); g.fill();
    }
    // Haar vorn
    g.fillStyle = grau(p.ton);
    g.beginPath();
    if (p.haar === 'kurz') { g.moveTo(170, 250); g.bezierCurveTo(160, 176, 214, 156, 246, 160); g.bezierCurveTo(296, 162, 322, 196, 310, 252); g.bezierCurveTo(302, 212, 286, 200, 262, 202); g.bezierCurveTo(232, 206, 196, 206, 170, 250); }
    else if (p.haar === 'scheitel') { g.moveTo(168, 258); g.bezierCurveTo(156, 180, 206, 150, 252, 156); g.bezierCurveTo(304, 162, 326, 200, 312, 258); g.bezierCurveTo(306, 220, 292, 206, 270, 200); g.bezierCurveTo(232, 196, 212, 218, 200, 214); g.bezierCurveTo(186, 224, 176, 238, 168, 258); }
    else if (p.haar === 'grau') { g.moveTo(168, 262); g.bezierCurveTo(162, 222, 172, 196, 190, 190); g.bezierCurveTo(186, 210, 182, 230, 176, 266); g.closePath(); g.moveTo(312, 262); g.bezierCurveTo(318, 222, 308, 196, 290, 190); g.bezierCurveTo(294, 210, 298, 230, 304, 266); g.closePath(); g.moveTo(190, 196); g.bezierCurveTo(210, 170, 270, 170, 290, 196); g.bezierCurveTo(268, 186, 212, 186, 190, 196); }
    else if (p.haar === 'lang') { g.moveTo(166, 262); g.bezierCurveTo(160, 186, 214, 160, 246, 162); g.bezierCurveTo(298, 164, 324, 200, 314, 266); g.bezierCurveTo(300, 220, 276, 204, 252, 200); g.bezierCurveTo(232, 214, 200, 226, 180, 262); g.bezierCurveTo(178, 300, 176, 330, 182, 360); g.bezierCurveTo(166, 330, 164, 296, 166, 262); }
    else if (p.haar === 'bob') { g.moveTo(164, 270); g.bezierCurveTo(158, 186, 214, 160, 246, 162); g.bezierCurveTo(300, 164, 326, 204, 316, 270); g.bezierCurveTo(300, 236, 290, 214, 270, 208); g.bezierCurveTo(240, 224, 206, 226, 186, 214); g.bezierCurveTo(176, 232, 170, 250, 164, 270); }
    else if (p.haar === 'dutt') { g.moveTo(168, 258); g.bezierCurveTo(160, 184, 214, 162, 244, 164); g.bezierCurveTo(296, 166, 322, 200, 312, 258); g.bezierCurveTo(304, 218, 280, 200, 244, 196); g.bezierCurveTo(206, 196, 180, 220, 168, 258); }
    g.fill();
    // Glanz im Haar (Licht von links oben)
    if (p.haar !== 'grau') {
      g.save(); g.globalCompositeOperation = 'lighter';
      rg = g.createRadialGradient(205, 186, 4, 205, 186, 70); rg.addColorStop(0, 'rgba(255,255,255,.16)'); rg.addColorStop(1, 'rgba(255,255,255,0)');
      g.fillStyle = rg; g.fill(); g.restore();
    }

    // Gesichtszüge
    const dunkel = grau(.08);
    g.lineCap = 'round'; g.lineJoin = 'round';
    // Brauen: deutlicher Abstand zu den Augen, beim lockeren Bild gehoben
    g.strokeStyle = grau(Math.min(p.ton, .4) - .06); g.lineWidth = 5.5;
    const b = locker ? -6 : 0;
    g.beginPath(); g.moveTo(194, 236 + b); g.quadraticCurveTo(208, (locker ? 226 : 229) + b, 227, 233 + b); g.stroke();
    g.beginPath(); g.moveTo(253, 233 + b); g.quadraticCurveTo(272, (locker ? 226 : 229) + b, 286, 236 + b); g.stroke();
    // Augen
    if (locker) {
      g.strokeStyle = dunkel; g.lineWidth = 4.5;
      g.beginPath(); g.moveTo(199, 261); g.quadraticCurveTo(211, 251, 223, 260); g.stroke();
      g.beginPath(); g.moveTo(257, 260); g.quadraticCurveTo(269, 251, 281, 261); g.stroke();
      g.strokeStyle = grau(p.haut - .2); g.lineWidth = 2.5;
      g.beginPath(); g.moveTo(203, 268); g.quadraticCurveTo(211, 271, 219, 268); g.stroke();
      g.beginPath(); g.moveTo(261, 268); g.quadraticCurveTo(269, 271, 277, 268); g.stroke();
    } else {
      [[211, 1], [269, -1]].forEach(([x]) => {
        g.fillStyle = grau(.9);
        g.beginPath(); g.moveTo(x - 13, 260); g.quadraticCurveTo(x, 249, x + 13, 260); g.quadraticCurveTo(x, 267, x - 13, 260); g.fill();
        g.fillStyle = dunkel; g.beginPath(); g.arc(x + .5, 259, 5.2, 0, Math.PI * 2); g.fill();
        g.strokeStyle = grau(.14); g.lineWidth = 3;
        g.beginPath(); g.moveTo(x - 14, 260); g.quadraticCurveTo(x, 248, x + 14, 259); g.stroke();
      });
    }
    // Brille
    if (p.brille) {
      g.strokeStyle = grau(.08); g.lineWidth = 4;
      g.beginPath(); g.roundRect(188, 243, 47, 33, 9); g.stroke();
      g.beginPath(); g.roundRect(245, 243, 47, 33, 9); g.stroke();
      g.beginPath(); g.moveTo(235, 255); g.quadraticCurveTo(240, 251, 245, 255); g.stroke();
      g.beginPath(); g.moveTo(188, 252); g.lineTo(170, 256); g.moveTo(292, 252); g.lineTo(310, 256); g.stroke();
    }
    // Nase: Schatten rechts, Nasenflügel
    g.fillStyle = 'rgba(0,0,0,.2)';
    g.beginPath(); g.moveTo(241, 258); g.quadraticCurveTo(254, 284, 251, 297); g.quadraticCurveTo(246, 301, 240, 300); g.quadraticCurveTo(247, 288, 241, 258); g.fill();
    g.fillStyle = grau(p.haut - .34);
    g.beginPath(); g.ellipse(232, 297, 4, 2.2, .2, 0, Math.PI * 2); g.fill();
    g.beginPath(); g.ellipse(248, 297, 4, 2.2, -.2, 0, Math.PI * 2); g.fill();
    // Mund
    if (locker) {
      g.fillStyle = grau(.12);
      g.beginPath(); g.moveTo(213, 317); g.quadraticCurveTo(240, 350, 267, 317); g.quadraticCurveTo(240, 324, 213, 317); g.fill();
      g.fillStyle = grau(.97);
      g.beginPath(); g.moveTo(218, 319); g.quadraticCurveTo(240, 332, 262, 319); g.quadraticCurveTo(240, 325, 218, 319); g.fill();
      g.strokeStyle = grau(p.haut - .24); g.lineWidth = 2.5;
      g.beginPath(); g.moveTo(205, 310); g.quadraticCurveTo(208, 317, 212, 319); g.stroke();
      g.beginPath(); g.moveTo(275, 310); g.quadraticCurveTo(272, 317, 268, 319); g.stroke();
    } else {
      g.fillStyle = grau(p.haut - .26);
      g.beginPath(); g.moveTo(220, 320); g.quadraticCurveTo(232, 313, 240, 316); g.quadraticCurveTo(248, 313, 260, 320); g.quadraticCurveTo(240, 322, 220, 320); g.fill();
      g.fillStyle = grau(p.haut - .14);
      g.beginPath(); g.moveTo(222, 321); g.quadraticCurveTo(240, 332, 258, 321); g.quadraticCurveTo(240, 323, 222, 321); g.fill();
      g.strokeStyle = grau(.24); g.lineWidth = 2.4;
      g.beginPath(); g.moveTo(218, 320); g.quadraticCurveTo(240, 323, 262, 320); g.stroke();
    }
    g.restore();
    g.restore();
    return g.getImageData(0, 0, W, H).data;
  }

  // Rasterdruck: Punktgröße nach Dunkelheit, Raster um 45° gedreht. Gerendert wird in der echten
  // Anzeigegröße (× Pixeldichte), damit das Punktraster beim Skalieren kein Moiré bildet.
  function raster(daten, cssBreite) {
    const dpr = Math.min(devicePixelRatio || 1, 2);
    const bw = Math.max(40, Math.round(cssBreite * dpr)), f = bw / W;
    const c = document.createElement('canvas'); c.width = bw; c.height = Math.round(bw * H / W);
    const g = c.getContext('2d');
    g.fillStyle = '#efebeb'; g.fillRect(0, 0, c.width, c.height);
    g.fillStyle = '#1e1e50'; g.beginPath();
    const abstand = Math.max(1.8, Math.min(3.4, cssBreite / 95)) * dpr;   // Rasterweite in Gerätepixeln
    const zelle = abstand / f;                                            // dieselbe Weite in Szenen-Einheiten
    const cos = Math.cos(WINKEL), sin = Math.sin(WINKEL), R = Math.hypot(W, H);
    const hell = (x, y) => daten[((y | 0) * W + (x | 0)) * 4] / 255;
    const s = Math.max(1, zelle / 3);
    for (let u = -R; u < R; u += zelle) for (let v = -R; v < R; v += zelle) {
      const x = W / 2 + u * cos - v * sin, y = H / 2 + u * sin + v * cos;
      if (x < s || y < s || x >= W - s || y >= H - s) continue;
      const l = (hell(x, y) * 2 + hell(x - s, y) + hell(x + s, y) + hell(x, y - s) + hell(x, y + s)) / 6;
      const r = Math.min(abstand * .5 * Math.sqrt(1 - l) * 1.22, abstand * .74);
      if (r < .2 * dpr) continue;
      const rx = x * f, ry = y * f;
      g.moveTo(rx + r, ry); g.arc(rx, ry, r, 0, Math.PI * 2);
    }
    g.fill();
    return c.toDataURL('image/png');
  }

  const SZENEN = TEAM.map(t => ({ ernst: szene(t.p, false), locker: szene(t.p, true) }));
  const ablage = new Map();
  function bildFuer(i, art, cssBreite) {
    const stufe = Math.ceil(cssBreite / 24) * 24;          // in 24-px-Stufen zwischenspeichern
    const k = `${i}-${art}-${stufe}`;
    if (!ablage.has(k)) ablage.set(k, raster(SZENEN[i][art], stufe));
    return ablage.get(k);
  }
  // Alle sichtbaren Porträts in ihrer Anzeigegröße füllen
  function malen() {
    $$('img[data-i]').forEach(img => {
      const pille = img.closest('.pille');
      const breite = pille ? parseFloat(getComputedStyle(pille).fontSize) * 1.5 * 1.5 : img.clientWidth;
      if (!breite) return;
      const src = bildFuer(+img.dataset.i, img.dataset.art, breite);
      if (img.getAttribute('src') !== src) img.src = src;
    });
  }

  const bildPaar = (i, alt) => `<img class="ernst" data-i="${i}" data-art="ernst" alt="${alt}"><img class="locker" data-i="${i}" data-art="locker" alt="" aria-hidden="true">`;
  const kontakt = t => `<p class="kontakt"><a href="tel:${t.tel.replace(/[^+\d]/g, '')}">${t.tel}</a><a href="mailto:${t.mail}@rainberg-recht.at">E-Mail</a></p>`;

  /* ---------- Variante 1: Porträtreihe ---------- */
  const reihe = $('[data-reihe]');
  reihe.innerHTML = TEAM.map((t, i) => `
    <li class="person" style="view-transition-name: person-${i}">
      <button type="button" class="bild-knopf" aria-pressed="false" aria-label="Privates Foto von ${t.name} zeigen">
        <div class="bild">${bildPaar(i, 'Porträt ' + t.name)}<p class="privat" aria-hidden="true"><small>Privat</small>${t.privat}</p></div>
      </button>
      <div><h3>${t.name}</h3><p class="funktion">${t.funktion} · ${t.fach}</p><p class="sprachen">${t.sprachen}</p>${kontakt(t)}</div>
    </li>`).join('');
  const knoepfe = $$('.bild-knopf', reihe);
  const tausch = (k, an) => { k.classList.toggle('an', an); k.setAttribute('aria-pressed', an); };
  knoepfe.forEach(k => {
    k.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse') tausch(k, true); });
    k.addEventListener('pointerleave', e => { if (e.pointerType === 'mouse') tausch(k, false); });
    k.addEventListener('focus', () => { if (k.matches(':focus-visible')) tausch(k, true); });
    k.addEventListener('blur', () => tausch(k, false));
    k.addEventListener('click', e => { if (e.pointerType !== 'mouse') tausch(k, !k.classList.contains('an')); });
  });

  // Filter: Rechtsgebiet; das Raster sortiert sich per View Transition neu (ohne API: sofort)
  const personen = $$('.person', reihe), filterLeiste = $('[data-filter]');
  filterLeiste.innerHTML = GEBIETE.map(([id, name]) => `<button type="button" data-gebiet="${id}" aria-pressed="${id === 'alle'}">${name}<span>${id === 'alle' ? TEAM.length : TEAM.filter(t => t.gebiete.includes(id)).length}</span></button>`).join('')
    + '<p class="mono treffer" aria-live="polite" data-treffer></p>';
  const filterKnoepfe = $$('[data-gebiet]', filterLeiste), treffer = $('[data-treffer]');
  let gebiet = 'alle';
  function filtere(id, sofort) {
    gebiet = id;
    const anwenden = () => {
      filterKnoepfe.forEach(b => b.setAttribute('aria-pressed', String(b.dataset.gebiet === id)));
      let n = 0;
      personen.forEach((li, i) => { const zeigen = id === 'alle' || TEAM[i].gebiete.includes(id); li.hidden = !zeigen; if (zeigen) n++; });
      treffer.textContent = `${n} von ${TEAM.length} Personen`;
    };
    if (sofort || reduziert() || !document.startViewTransition || !$('[data-ansicht="raster"]').classList.contains('an')) { anwenden(); malen(); return; }
    document.startViewTransition(() => { anwenden(); malen(); });
  }
  filterKnoepfe.forEach(b => b.addEventListener('click', () => { stoppe(); filtere(b.dataset.gebiet); }));
  filtere('alle', true);

  /* ---------- Variante 2: Namensliste ---------- */
  const namen = $('[data-namen]'), karteBild = $('[data-karte-bild]'), karteText = $('[data-karte-text]');
  namen.innerHTML = TEAM.slice(0, BERUFSTRAEGER).map((t, i) => `
    <li><button type="button"><span class="nr">${String(i + 1).padStart(2, '0')}</span><span class="daumen" aria-hidden="true"><img class="ernst" data-i="${i}" data-art="ernst" alt=""><img class="locker" data-i="${i}" data-art="locker" alt=""></span><span class="name">${t.name.replace(/, LL\.M\.$/, '')}</span><span class="fach">${t.fach}</span></button></li>`).join('');
  karteBild.innerHTML = TEAM.slice(0, BERUFSTRAEGER).map((t, i) => `<img data-i="${i}" data-art="ernst" alt="Porträt ${t.name}">`).join('');
  const zeilen = $$('li', namen), kartenBilder = $$('img', karteBild);
  let gezeigt = -1, textUhr = null;
  function karte(i) {
    if (i === gezeigt) return;
    const alt = kartenBilder[gezeigt];
    kartenBilder.forEach(b => b.classList.remove('unten'));
    if (alt) { alt.classList.remove('oben'); alt.classList.add('unten'); }
    kartenBilder.forEach(b => { if (b !== alt && b.dataset.i != i) b.classList.remove('oben'); });
    const neu = kartenBilder[i];
    neu.style.zIndex = 2; if (alt) alt.style.zIndex = 1;
    kartenBilder.forEach(b => { if (b !== neu && b !== alt) b.style.zIndex = 0; });
    void neu.offsetWidth; neu.classList.add('oben');
    gezeigt = i;
    const t = TEAM[i];
    clearTimeout(textUhr);
    karteText.classList.add('wechsel');
    textUhr = setTimeout(() => {
      karteText.innerHTML = `<h3>${t.name}</h3><p>${t.funktion} · ${t.fach}</p><p class="sprachen">${t.sprachen}</p>${kontakt(t)}`;
      karteText.classList.remove('wechsel');
    }, reduziert() ? 0 : 125);
  }
  function zeile(i) {
    zeilen.forEach((z, j) => z.classList.toggle('an', j === i));
    namen.classList.toggle('aktiv', i >= 0);
    if (i >= 0) karte(i);
  }
  zeilen.forEach((z, i) => {
    const b = $('button', z);
    z.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse') zeile(i); });
    b.addEventListener('focus', () => { if (b.matches(':focus-visible')) zeile(i); });
    b.addEventListener('click', () => zeile(z.classList.contains('an') && matchMedia('(hover: none)').matches ? -1 : i));
  });
  namen.addEventListener('pointerleave', () => { zeilen.forEach(z => z.classList.remove('an')); namen.classList.remove('aktiv'); });

  /* ---------- Variante 3: Statement ---------- */
  const pillen = $$('.pille');
  const PILLE_REIHE = [0, 4, 2, 5, 1];
  pillen.forEach((p, k) => {
    const i = PILLE_REIHE[k];
    p.innerHTML = bildPaar(i, TEAM[i].name);
    p.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse') p.classList.add('an'); });
    p.addEventListener('pointerleave', () => p.classList.remove('an'));
  });
  let pillenUhren = [];
  function pillenAuf() {
    pillenUhren.forEach(clearTimeout); pillenUhren = [];
    pillen.forEach(p => { p.style.transition = 'none'; p.classList.remove('offen', 'an'); void p.offsetWidth; p.style.transition = ''; });
    if (reduziert()) { pillen.forEach(p => p.classList.add('offen')); return; }
    pillen.forEach((p, k) => pillenUhren.push(setTimeout(() => p.classList.add('offen'), 250 + k * 120)));
  }

  /* ---------- Varianten und Werte ---------- */
  const WERTE = {
    raster: 'Tausch clip-path inset(100% 0 0 0) → inset(0) · 400 ms · Filter: View Transition 500 ms · cubic-bezier(0.25, 1, 0.5, 1)',
    liste: 'Porträt clip-path 400 ms · Abblenden 125 ms auf 0.32 · power3.out',
    statement: 'Inline-Porträt 1em hoch · width 0 → 1.5em · 500 ms · stagger 120 ms',
  };
  // Variante aus der URL (?v=raster|liste|statement), unbekannt oder leer: erste Variante
  const VARIANTEN = ['raster', 'liste', 'statement'];
  const ausUrl = new URLSearchParams(location.search).get('v');
  let variante = VARIANTEN.includes(ausUrl) ? ausUrl : VARIANTEN[0];
  const umschalter = $$('[data-variante]');
  function zeigeVariante(v) {
    variante = v;
    $$('[data-ansicht]').forEach(s => s.classList.toggle('an', s.dataset.ansicht === v));
    umschalter.forEach(b => b.setAttribute('aria-pressed', b.dataset.variante === v));
    $('[data-werte]').textContent = WERTE[v];
    knoepfe.forEach(k => tausch(k, false));
    zeile(-1); if (gezeigt < 0) karte(0);
    malen();
    if (v === 'statement') pillenAuf();
  }
  umschalter.forEach(b => b.addEventListener('click', () => {
    stoppe(); zeigeVariante(b.dataset.variante);
    const u = new URL(location.href);
    u.searchParams.set('v', variante);   // andere Parameter (reduziert, vorschau) bleiben stehen
    history.replaceState(history.state, '', u);
  }));

  /* ---------- Vorführung: gedachter Zeiger ---------- */
  const zeiger = $('[data-zeiger]');
  let uhren = [], laeuft = false, schritt = 0;
  const warte = (f, ms) => uhren.push(setTimeout(f, ms));
  function zielen(el, fx = .5, fy = .5) {
    const r = el.getBoundingClientRect();
    zeiger.style.transform = `translate(${r.left + r.width * fx - 2}px, ${r.top + r.height * fy - 1}px)`;
  }
  const ABLAEUFE = {
    // Raster: Zeiger tauscht zwei Porträts, filtert nach „Arbeit“, tauscht eines, zurück auf „Alle“
    raster: () => { zeigeVariante('raster'); filtere('alle', true); malen(); zeiger.style.transform = `translate(${innerWidth * .5}px, ${innerHeight + 30}px)`;
      const knopf = id => filterKnoepfe.find(b => b.dataset.gebiet === id);
      [0, 1].forEach((k, n) => {
        warte(() => zielen(knoepfe[k], .55, .5), 500 + n * 1400);
        warte(() => tausch(knoepfe[k], true), 900 + n * 1400);
        warte(() => tausch(knoepfe[k], false), 1700 + n * 1400);
      });
      warte(() => zielen(knopf('arbeit'), .5, .5), 3300);
      warte(() => filtere('arbeit'), 3800);
      warte(() => { const k = knoepfe.find(x => !x.closest('.person').hidden && x !== knoepfe[1]); zielen(k, .55, .5); warte(() => tausch(k, true), 400); warte(() => tausch(k, false), 1300); }, 4800);
      warte(() => zielen(knopf('alle'), .5, .5), 6600);
      warte(() => filtere('alle'), 7100);
      return 8600; },
    // Namensliste: Zeiger fährt die Namen ab
    liste: () => { zeigeVariante('liste');
      zeilen.forEach((z, i) => {
        warte(() => zielen($('.name', z), .35, .5), 400 + i * 900);
        warte(() => zeile(i), 700 + i * 900);
      });
      warte(() => { zeiger.style.transform = `translate(${innerWidth * .52}px, ${innerHeight + 30}px)`; namen.dispatchEvent(new Event('pointerleave')); }, 6200);
      return 6900; },
    // Statement: Porträts klappen auf, zwei werden getauscht
    statement: () => { zeigeVariante('statement');
      warte(() => zielen(pillen[2], .5, .55), 1500);
      warte(() => pillen[2].classList.add('an'), 1900);
      warte(() => pillen[2].classList.remove('an'), 3100);
      warte(() => zielen(pillen[3], .5, .55), 3300);
      warte(() => pillen[3].classList.add('an'), 3700);
      warte(() => { pillen[3].classList.remove('an'); zeiger.style.transform = `translate(${innerWidth * .6}px, ${innerHeight + 30}px)`; }, 4900);
      return 5800; },
  };
  function takt() {
    if (!laeuft) return;
    const dauer = ABLAEUFE[variante]();
    schritt++;
    warte(takt, dauer);
  }
  function stoppe() { uhren.forEach(clearTimeout); uhren = []; laeuft = false; html.classList.remove('vorfuehrung'); }

  function neustart() { stoppe(); schritt = 0; zeigeVariante(variante); }
  function vorfuehren() { stoppe(); html.classList.add('vorfuehrung'); laeuft = true; schritt = 0; takt(); }
  function pause() { uhren.forEach(clearTimeout); uhren = []; laeuft = false; }
  function weiter() { if (!laeuft && html.classList.contains('vorfuehrung')) { laeuft = true; takt(); } }

  zeigeVariante(variante);
  let groesse = null;
  addEventListener('resize', () => { clearTimeout(groesse); groesse = setTimeout(malen, 200); });
  document.fonts && document.fonts.ready.then(malen);

})();
</script>
</body>
</html>