Zum Inhalt
← Mikroanimationen

Kopieren-Feedback

Kopieren bestätigt sich direkt am Knopf: Häkchen, Tooltip oder rollender Text, nach 2 s zurück.

Ein Klick auf „Kopieren“ legt E-Mail-Adresse, Code oder Link mit navigator.clipboard.writeText in die Zwischenablage. Die Bestätigung erscheint genau dort, wo geklickt wurde: Das Icon wird zum Häkchen, das Label zu „Kopiert“. Nach etwa 2 s kehrt der Ausgangszustand zurück. Scheitert das Kopieren, sieht man einen Hinweis.

Kopieren ist eine unsichtbare Aktion. Die Bestätigung am Ort macht sie sichtbar, ohne Toast und ohne den Blick vom Inhalt zu lösen.

Wann einsetzen

E-Mail-Adresse im Kontaktbereich (statt nur mailto), Code-Snippets, Teilen-Links, Gutscheincodes.

Wann nicht

Wenn Kopieren nicht die Hauptabsicht ist. Dann ist ein normaler Link besser.

Varianten

  • Icon-Tausch: Kopier-Icon verschwimmt und schrumpft, Häkchen zeichnet sich per pathLength ein
  • Tooltip „Kopiert!“ steigt über dem Knopf auf und verblasst
  • Text-Roll: der Wert selbst rollt zu „Kopiert“
  • Cursor-Label wechselt (siehe kontextueller-cursor)

Umsetzung

await navigator.clipboard.writeText(text); zeile.dataset.zustand = 'kopiert'; live.textContent = 'E-Mail-Adresse kopiert'; clearTimeout(t); t = setTimeout(() => zeile.dataset.zustand = 'bereit', 2000). Den Timeout bei erneutem Klick löschen. CSS: .icon { transition: opacity .2s, filter .2s, scale .2s } [data-zustand=kopiert] .icon-kopie { opacity: 0; filter: blur(4px); scale: .6 }. Häkchen mit pathLength="1" am Pfad: stroke-dasharray: 1; stroke-dashoffset: 1; im Zustand kopiert stroke-dashoffset: 0 mit 300 ms und 100 ms Verzögerung. Rückfall für alte Browser: execCommand('copy'), sonst Text markieren und Hinweis zeigen.

Robust umsetzen

Telefon. Identisch. Der Knopf braucht mindestens 44 px Zielfläche und darf nur als Icon erscheinen, wenn der Name für Screenreader bleibt.

Ohne Bewegung. Nur Überblenden ohne Blur, Scale, Aufsteigen oder Rollen. Das Häkchen erscheint fertig gezeichnet.

Barrierefrei. Statusmeldung per aria-live="polite". Der Button-Name bleibt gleich („E-Mail-Adresse kopieren“), nur die sichtbare Bestätigung wechselt. Im Fehlerfall den Text markieren und den Hinweis vorlesen lassen.

Code

<!DOCTYPE html>
<!--
  Kopieren-Feedback
  Fundus · Mikroanimationen · https://cecodesign.de/lab/fundus/mikroanimationen/kopieren-feedback/
  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>Kopieren-Feedback · 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: 'Special Gothic'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/special-gothic:vf@latest/latin-standard-normal.woff2') format('woff2-variations'); font-weight: 400 700; font-stretch: 75% 125%; font-display: swap; }
@font-face { font-family: 'Atkinson Hyperlegible Next'; src: url('https://cdn.jsdelivr.net/fontsource/fonts/atkinson-hyperlegible-next:vf@latest/latin-wght-normal.woff2') format('woff2-variations'); font-weight: 200 800; 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; }

/* Tischlerei Holm, Lüneburg · Rezept „Handwerk: Werkstatt und Klartext“:
   Materialflächen (Zement, Holz, Salbei), dunkle Schrift, ein Signalton nur für Anfrage und Anruf. */
:root {
  --grund: #efedea; --zement: #d3cdc2; --holz: #c3b4a5; --holz-dunkel: #a8957f; --salbei: #bdc1a9;
  --text: #1a1a1a; --leise: rgba(26, 26, 26, .64); --linie: rgba(26, 26, 26, .16); --linie-fest: rgba(26, 26, 26, .82);
  --signal: #fb6100;             /* nur „Angebot anfragen“ und „Anrufen“ */
  --ok: #3b5a2c; --fehler: #a8391b;
  --kurve: cubic-bezier(0.16, 1, 0.3, 1);   /* expo.out, für alle Bewegungen */
  --tausch: 200ms;    /* Icon- und Label-Tausch */
  --haken: 300ms;     /* Häkchen zeichnet sich ein … */
  --haken-vz: 100ms;  /* … nach dieser Verzögerung */
  --blase: 300ms;     /* Tooltip steigt 8 px auf */
  --rolle: 500ms;     /* Text rollt eine Zeile weiter */
  --druck: 150ms;     /* Knopf gibt beim Klick nach */
}
* { box-sizing: border-box; margin: 0; }
html { background: var(--grund); color: var(--text); font-family: 'Atkinson Hyperlegible Next', system-ui, sans-serif; -webkit-font-smoothing: antialiased; }
body { min-height: 100svh; padding: clamp(16px, 3vw, 40px); display: grid; grid-template-rows: auto 1fr auto; gap: clamp(22px, 3vw, 36px); }
a { color: inherit; }
.mono { font: 500 11px/1.35 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); }
.gewerk { font: 500 13px/1.2 'Special Gothic', 'Arial Narrow', sans-serif; font-stretch: 75%; letter-spacing: .04em; text-transform: uppercase; font-variant-numeric: tabular-nums; }
.sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* Kopf: Haarlinien-Raster, Telefon und Anfrage immer sichtbar */
.kopf { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; border: 1px solid var(--linie-fest); max-width: 1180px; width: 100%; margin: 0 auto; }
.kopf > * { display: flex; align-items: center; min-height: 50px; padding: 0 16px; }
.kopf > * + * { border-left: 1px solid var(--linie-fest); }
.marke { gap: 10px; text-decoration: none; }
.marke svg { width: 22px; height: 22px; flex: none; }
.marke b { font: 700 19px/1 'Special Gothic', sans-serif; font-stretch: 118%; letter-spacing: .02em; }
.marke span { color: var(--leise); }
.nav { gap: clamp(14px, 2vw, 26px); list-style: none; }
.nav li { color: var(--leise); }
.tel { gap: 8px; text-decoration: none; font-variant-numeric: tabular-nums; }
.tel svg { width: 16px; height: 16px; color: var(--signal); flex: none; }
.anfrage { background: var(--signal); color: var(--text); text-decoration: none; font-weight: 700; font-size: 15px; transition: background-color var(--druck) var(--kurve); }
@media (hover: hover) { .anfrage:hover { background: #ff7a26; } .tel:hover { background: rgba(26, 26, 26, .05); } }

/* Bühne: Kontaktbereich neben der Werkzeichnung */
.buehne { display: grid; grid-template-columns: minmax(0, .66fr) minmax(0, 1fr); gap: clamp(28px, 5vw, 80px); align-items: center; max-width: 1180px; width: 100%; margin: 0 auto; }

/* Werkzeichnung: Ansicht Sideboard, Detail Kante, Schriftfeld */
.zeichnung { position: relative; aspect-ratio: 4 / 5; max-height: min(66vh, 620px); width: 100%; justify-self: end; background: var(--salbei); display: grid; grid-template-rows: auto minmax(0, 1.25fr) minmax(0, 1fr) auto; overflow: hidden; }
.zeichnung svg { display: block; width: 100%; height: 100%; min-height: 0; }
.zeichnung .blatt { display: flex; justify-content: space-between; gap: 12px; padding: 14px 16px 0; color: rgba(26, 26, 26, .72); }
.ansicht { padding: 4px 6px 0; }
.detail { padding: 0 6px; }
.schriftfeld { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); border-top: 1px solid var(--linie-fest); }
.schriftfeld div { padding: 8px 12px 9px; min-width: 0; }
.schriftfeld div:nth-child(n+4) { border-top: 1px solid rgba(26, 26, 26, .3); }
.schriftfeld div:not(:nth-child(3n+1)) { border-left: 1px solid rgba(26, 26, 26, .3); }
.schriftfeld dt { font: 500 10.5px/1.2 'Special Gothic', sans-serif; font-stretch: 75%; letter-spacing: .06em; text-transform: uppercase; color: rgba(26, 26, 26, .72); }
.schriftfeld dd { margin-top: 3px; font-size: 13px; line-height: 1.25; overflow-wrap: anywhere; }

.info { display: grid; gap: clamp(16px, 2.2vw, 26px); max-width: 600px; }
.info > .gewerk { color: var(--leise); }
h1 { font: 700 clamp(2.25rem, 1.5rem + 3vw, 3.5rem)/1.05 'Special Gothic', sans-serif; font-stretch: 100%; letter-spacing: -.01em; text-wrap: balance; }
.text { font-size: 17px; line-height: 1.55; color: var(--leise); max-width: 44ch; text-wrap: pretty; }

/* Kontaktzeilen */
.zeilen { list-style: none; padding: 0; border-bottom: 1px solid var(--linie); }
.zeile { display: grid; grid-template-columns: minmax(0, 1fr) auto; grid-template-areas: "label knopf" "wert knopf"; align-items: center; gap: 6px 18px; padding: clamp(11px, 1.3vw, 15px) 0; border-top: 1px solid var(--linie); }
.zeile > .gewerk { grid-area: label; color: var(--leise); }
.zeile .wert { grid-area: wert; }
.zeile .knopf { grid-area: knopf; }
.wert { min-width: 0; }
.wert small { display: block; margin-top: 5px; font-size: 14px; line-height: 1.4; color: var(--leise); }
.wert small:empty { display: none; }

/* Rollfenster: zweite Zeile („Kopiert“) liegt unter dem Wert und rückt in Variante „Text-Roll“ nach oben */
.fenster { display: block; line-height: 1.25em; height: 1lh; overflow: hidden; }   /* lh: Maske wächst mit erhöhtem Zeilenabstand (1.4.12) */
.spur { display: grid; transition: translate var(--rolle) var(--kurve); }
.spur > span { height: 1lh; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.spur .ok { color: var(--ok); display: inline-flex; align-items: center; gap: .3em; }
.spur .ok svg { width: .8em; height: .8em; flex: none; }
.spur .ok svg path { stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset 0s linear var(--rolle); }
.e-mail { font: 600 clamp(20px, 2.3vw, 28px)/1.25 'Special Gothic', sans-serif; letter-spacing: -.005em; }
.code { display: inline-block; padding: 7px 11px 6px; background: var(--zement); border-left: 3px solid var(--holz-dunkel); font: 600 17px/1 'Special Gothic', sans-serif; font-stretch: 82%; letter-spacing: .08em; font-variant-numeric: tabular-nums; max-width: 100%; }
.code .fenster { height: 1lh; }
.adresse { font-size: 18px; font-weight: 600; letter-spacing: -.005em; }

/* Der Kopier-Knopf */
.knopf {
  position: relative; display: inline-flex; align-items: center; gap: 10px; height: 44px; padding: 0 16px 0 13px;
  border: 1px solid rgba(26, 26, 26, .3); border-radius: 3px; background: transparent; color: var(--text);
  font: 600 14px/1 'Atkinson Hyperlegible Next', system-ui, sans-serif; cursor: pointer; -webkit-tap-highlight-color: transparent; touch-action: manipulation;
  transition: background-color var(--druck) var(--kurve), border-color var(--druck) var(--kurve), scale var(--druck) var(--kurve);
}
.knopf:focus-visible, .kopf a:focus-visible { outline: 2px solid var(--text); outline-offset: 3px; }
@media (hover: hover) { .knopf:hover { background: rgba(26, 26, 26, .06); border-color: rgba(26, 26, 26, .55); } }
.knopf:active, .knopf.gedrueckt { scale: .94; }
.icons { position: relative; width: 18px; height: 18px; flex: none; }
.icons svg { position: absolute; inset: 0; width: 18px; height: 18px; transition: opacity var(--tausch) var(--kurve), filter var(--tausch) var(--kurve), scale var(--tausch) var(--kurve); }
.beschrift { display: grid; }
.beschrift > span { grid-area: 1 / 1; transition: opacity var(--tausch) var(--kurve), filter var(--tausch) var(--kurve), translate var(--tausch) var(--kurve); }
.ico-haken, .b-kopiert { opacity: 0; filter: blur(4px); }
.ico-haken { scale: .6; }
.b-kopiert { translate: 0 4px; }
.ico-haken path { stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset 0s linear var(--tausch); }

/* Variante „Icon-Tausch“: Kopier-Icon verschwimmt und schrumpft, Häkchen zeichnet sich per pathLength ein */
[data-variante="icon"] .zeile[data-zustand="kopiert"] .knopf { border-color: rgba(59, 90, 44, .7); background: rgba(189, 193, 169, .35); }
[data-variante="icon"] .zeile[data-zustand="kopiert"] .ico-kopie { opacity: 0; filter: blur(4px); scale: .6; }
[data-variante="icon"] .zeile[data-zustand="kopiert"] .ico-haken { opacity: 1; filter: blur(0); scale: 1; color: var(--ok); }
[data-variante="icon"] .zeile[data-zustand="kopiert"] .ico-haken path { stroke-dashoffset: 0; transition: stroke-dashoffset var(--haken) var(--kurve) var(--haken-vz); }
[data-variante="icon"] .zeile[data-zustand="kopiert"] .b-kopieren { opacity: 0; filter: blur(4px); translate: 0 -4px; }
[data-variante="icon"] .zeile[data-zustand="kopiert"] .b-kopiert { opacity: 1; filter: blur(0); translate: 0 0; color: var(--ok); }

/* Variante „Tooltip“: Blase „Kopiert!“ steigt über dem Knopf auf und verblasst */
.blase {
  position: absolute; left: 50%; bottom: calc(100% + 10px); translate: -50% 8px; opacity: 0; pointer-events: none;
  padding: 7px 10px 6px; border-radius: 3px; background: var(--ok); color: var(--grund); white-space: nowrap;
  font: 600 12px/1 'Special Gothic', sans-serif; font-stretch: 75%; letter-spacing: .08em; text-transform: uppercase;
  transition: opacity var(--tausch) var(--kurve), translate var(--tausch) var(--kurve);
}
.blase::after { content: ''; position: absolute; left: 50%; top: 100%; translate: -50% 0; border: 5px solid transparent; border-top-color: var(--ok); }
[data-variante="blase"] .zeile[data-zustand="kopiert"] .blase { opacity: 1; translate: -50% 0; transition-duration: var(--blase); }
[data-variante="blase"] .zeile[data-zustand="fehler"] .blase { opacity: 1; translate: -50% 0; background: var(--fehler); }
[data-variante="blase"] .zeile[data-zustand="fehler"] .blase::after { border-top-color: var(--fehler); }

/* Variante „Text-Roll“: der Wert selbst rollt zu „Kopiert“ */
[data-variante="rolle"] .zeile[data-zustand="kopiert"] .spur { translate: 0 -1lh; }
[data-variante="rolle"] .zeile[data-zustand="kopiert"] .spur .ok svg path { stroke-dashoffset: 0; transition: stroke-dashoffset var(--haken) var(--kurve) var(--haken-vz); }

/* Fehlerfall (Zwischenablage gesperrt): Hinweis unter dem Wert, Text ist markiert */
.hinweis { display: none; color: var(--fehler) !important; }
.zeile[data-zustand="fehler"] .hinweis { display: block; }
.zeile[data-zustand="fehler"] .knopf { border-color: rgba(168, 57, 27, .7); }

/* Fuß: Umschalter und eingesetzte Werte (Werkzeug der Demo, bewusst in Geist Mono) */
.fuss { display: flex; justify-content: space-between; align-items: center; gap: 14px 28px; flex-wrap: wrap; max-width: 1180px; width: 100%; margin: 0 auto; padding-top: 14px; border-top: 1px solid var(--linie); }
.schalter { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.gruppe { display: flex; align-items: center; gap: 10px; }
.umschalter { display: flex; gap: 2px; padding: 3px; border: 1px solid rgba(26, 26, 26, .22); border-radius: 4px; }
.umschalter button { white-space: nowrap; font: 500 10.5px/1 'Geist Mono', ui-monospace, monospace; letter-spacing: .14em; text-transform: uppercase; color: var(--leise); background: none; border: 0; border-radius: 2px; padding: 8px 12px; cursor: pointer; transition: background-color var(--druck) var(--kurve), color var(--druck) var(--kurve); }
.umschalter button[aria-pressed="true"] { background: var(--text); color: var(--grund); }
.umschalter button:focus-visible { outline: 2px solid var(--text); outline-offset: 2px; }
.werte { max-width: 60ch; text-align: right; }

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

/* Niedrige Fenster (auch Vorschau 960 × 720): Kontaktspalte verdichten, Umschalter bleibt sichtbar */
@media (max-height: 800px) and (min-width: 861px) {
  body { gap: 20px; padding-block: 22px; }
  .info { gap: 14px; }
  h1 { font-size: clamp(2.25rem, 1rem + 2.6vw, 3rem); }
  .text { font-size: 16px; line-height: 1.5; }
  .zeile { padding: 10px 0; }
  .wert small { margin-top: 3px; font-size: 13.5px; }
}

/* Vorschau: nur Bühne und Umschalter */
.vorschau body { overflow: hidden; height: 100svh; }
.vorschau .werte, .vorschau .info > .gewerk, .vorschau .wert small:not(.hinweis) { display: none; }

/* Reduzierte Bewegung: nur Überblenden, kein Blur, kein Scale, kein Steigen, kein Rollen, Häkchen sofort ganz */
.reduziert .icons svg, .reduziert .beschrift > span, .reduziert .blase { transition: opacity .15s linear !important; filter: none !important; scale: 1 !important; }
.reduziert .beschrift > span { translate: none !important; }
.reduziert .blase { translate: -50% 0 !important; }
.reduziert .ico-haken path, .reduziert .spur .ok svg path { stroke-dashoffset: 0 !important; transition: none !important; }
.reduziert .knopf, .reduziert .zeiger, .reduziert .anfrage { transition-duration: .01ms !important; }
.reduziert .knopf.gedrueckt, .reduziert .knopf:active { scale: 1; }
.reduziert .spur { translate: none !important; transition: none; display: grid; }
.reduziert .spur > span { grid-area: 1 / 1; transition: opacity .15s linear; }
.reduziert .spur .ok { opacity: 0; }
.reduziert [data-variante="rolle"] .zeile[data-zustand="kopiert"] .spur .ok { opacity: 1; }
.reduziert [data-variante="rolle"] .zeile[data-zustand="kopiert"] .spur .roh { opacity: 0; }

/* Schmal und Touch: gleiches Verhalten, Knopf nur als Icon mit 44 px Zielfläche */
@media (max-width: 1060px) { .nav { display: none !important; } .kopf { grid-template-columns: auto 1fr auto auto; } .kopf > .platz { display: flex; } }
@media (min-width: 1061px) { .kopf > .platz { display: none; } }
@media (max-width: 860px) {
  .buehne { grid-template-columns: 1fr; gap: 26px; }
  .zeichnung { aspect-ratio: auto; max-height: none; justify-self: stretch; grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); grid-template-rows: auto auto auto; }
  .zeichnung .blatt { grid-column: 1 / -1; }
  .ansicht { grid-column: 1; height: clamp(150px, 40vw, 260px); }
  .detail { grid-column: 2; height: clamp(150px, 40vw, 260px); }
  .schriftfeld { grid-column: 1 / -1; }
  .schriftfeld div:nth-child(n+4) { display: none; }
  .fuss { justify-content: flex-start; }
  .werte { text-align: left; }
}
@media (max-width: 560px) {
  .kopf > * { padding: 0 12px; min-height: 48px; }
  .marke span, .tel span, .kopf > .platz { display: none !important; }
  .kopf { grid-template-columns: minmax(0, 1fr) auto auto; }
  .tel { width: 48px; justify-content: center; padding: 0 !important; }
  .anfrage { font-size: 14px; }
  .zeichnung .blatt span:last-child, .zeichnung .detail { display: none; }
  .zeichnung .ansicht { grid-column: 1 / -1; height: auto; aspect-ratio: 400 / 250; }
  .schriftfeld div:nth-child(3) { display: none; }
  .schriftfeld { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .zeile { gap: 6px 14px; }
  .adresse { font-size: 17px; }
  .code { font-size: 15.5px; letter-spacing: .05em; }
  .gruppe { display: grid; gap: 8px; }
  .umschalter button { padding: 8px 10px; }
}
@media (hover: none), (max-width: 560px) {
  .knopf { width: 44px; padding: 0; justify-content: center; }
  .beschrift { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
}
/* 320 px: E-Mail und Adresse ganz ohne Auslassungspunkte */
@media (max-width: 360px) { .e-mail { font-size: 18.5px; } .adresse { font-size: 16px; } }
@media (forced-colors: active) {
  button, [role=button], [role=tab], [role=switch] { border: 1px solid ButtonText !important; }
}
</style>
</head>
<body>
<header class="kopf">
  <a class="marke" href="#kontakt" aria-label="Tischlerei Holm, Lüneburg">
    <svg viewBox="0 0 24 24" aria-hidden="true"><rect x="2" y="2" width="5.5" height="20" fill="#1a1a1a"/><rect x="16.5" y="2" width="5.5" height="20" fill="#1a1a1a"/><rect x="7.5" y="9.5" width="9" height="5" fill="#a8957f"/></svg>
    <b>HOLM</b><span class="gewerk">Tischlerei · Lüneburg</span>
  </a>
  <ul class="nav gewerk"><li>Möbel nach Maß</li><li>Innenausbau</li><li>Treppen</li><li>Referenzen</li></ul>
  <span class="platz" aria-hidden="true"></span>
  <a class="tel" href="tel:+494066969412"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" aria-hidden="true"><path d="M3.2 1.8h2.6l1.3 3.3-1.6 1a8 8 0 0 0 4.4 4.4l1-1.6 3.3 1.3v2.6a1.5 1.5 0 0 1-1.6 1.5A12.2 12.2 0 0 1 1.7 3.4a1.5 1.5 0 0 1 1.5-1.6Z"/></svg><span class="gewerk">040 669 69 412</span><b class="sr">Anrufen</b></a>
  <a class="anfrage" href="#kontakt">Angebot anfragen</a>
</header>

<main class="buehne" id="kontakt" data-buehne data-variante="icon">
  <figure class="zeichnung" aria-label="Werkzeichnung eines Sideboards aus Multiplex Birke mit Fronten in Eiche, Ansicht und Detail der Kante">
    <figcaption class="blatt gewerk"><span>Werkzeichnung · Sideboard SB-18</span><span>Ansicht von vorn</span></figcaption>

    <svg class="ansicht" viewBox="0 0 400 250" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
      <g fill="none" stroke="#1a1a1a" stroke-width="1.1" font-family="'Special Gothic', sans-serif" font-size="11" font-stretch="75%" letter-spacing=".5">
        <!-- Maßkette oben: Gesamtbreite -->
        <path d="M50 40v14M350 40v14M50 47h300" stroke-width=".8"/>
        <path d="M46 51l8-8M346 51l8-8" stroke-width="1.1"/>
        <text x="200" y="41" text-anchor="middle" fill="#1a1a1a" stroke="none">1800</text>
        <!-- Maßkette rechts: Höhe -->
        <path d="M362 66h14M362 196h14M369 66v130" stroke-width=".8"/>
        <path d="M365 70l8-8M365 200l8-8" stroke-width="1.1"/>
        <text x="0" y="0" transform="translate(386 131) rotate(-90)" text-anchor="middle" fill="#1a1a1a" stroke="none">760</text>
        <!-- Deckplatte, Korpus, Füße -->
        <rect x="46" y="66" width="308" height="7" fill="#d3cdc2"/>
        <rect x="50" y="73" width="300" height="103" fill="#efedea"/>
        <path d="M62 176h12l-3 20h-6ZM326 176h12l-2 20h-7Z" fill="#1a1a1a"/>
        <!-- Fronten in Eiche mit Griffleiste -->
        <g fill="#c3b4a5">
          <rect x="54" y="77" width="95" height="46"/>
          <rect x="54" y="126" width="95" height="46"/>
          <rect x="152" y="77" width="96" height="95"/>
          <rect x="251" y="77" width="95" height="95"/>
        </g>
        <g stroke="none" fill="#1a1a1a">
          <rect x="54" y="77" width="95" height="3"/>
          <rect x="54" y="126" width="95" height="3"/>
          <rect x="152" y="77" width="96" height="3"/>
          <rect x="251" y="77" width="95" height="3"/>
        </g>
        <!-- Öffnungsrichtung der Türen (Strichpunkt wie im Werkplan) -->
        <path d="M246 80 154 124.5 246 169M253 80l91 44.5-91 44.5" stroke-width=".6" stroke-dasharray="6 2 1 2"/>
        <!-- Teilmaße unten -->
        <path d="M50 212v10M150 212v10M250 212v10M350 212v10M50 217h300" stroke-width=".8"/>
        <path d="M46 221l8-8M146 221l8-8M246 221l8-8M346 221l8-8" stroke-width="1.1"/>
        <g fill="#1a1a1a" stroke="none" text-anchor="middle"><text x="100" y="236">600</text><text x="200" y="236">600</text><text x="300" y="236">600</text></g>
        <!-- Detailmarke A an der Deckplatte -->
        <circle cx="50" cy="70" r="11" stroke-width=".8" stroke-dasharray="3 2"/>
        <path d="M41 77 28 98" stroke-width=".8"/>
        <circle cx="24" cy="106" r="8" fill="#1a1a1a" stroke="none"/>
        <text x="24" y="110" text-anchor="middle" fill="#bdc1a9" stroke="none" font-weight="700">A</text>
      </g>
    </svg>

    <svg class="detail" viewBox="0 0 400 190" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
      <defs><clipPath id="lupe"><circle cx="112" cy="95" r="76"/></clipPath></defs>
      <circle cx="112" cy="95" r="76" fill="#efedea"/>
      <g clip-path="url(#lupe)">
        <!-- Kante Multiplex Birke: 9 Lagen, Faserrichtung wechselnd -->
        <g transform="translate(20 46)">
          <rect width="150" height="10.4" fill="#d9cdbd"/><rect y="10.4" width="150" height="10.4" fill="#c3b4a5"/>
          <rect y="20.8" width="150" height="10.4" fill="#d9cdbd"/><rect y="31.2" width="150" height="10.4" fill="#c3b4a5"/>
          <rect y="41.6" width="150" height="10.4" fill="#d9cdbd"/><rect y="52" width="150" height="10.4" fill="#c3b4a5"/>
          <rect y="62.4" width="150" height="10.4" fill="#d9cdbd"/><rect y="72.8" width="150" height="10.4" fill="#c3b4a5"/>
          <rect y="83.2" width="150" height="10.4" fill="#d9cdbd"/>
          <!-- Fase 2 mm an der Oberkante -->
          <path d="M141 0H151V9Z" fill="#efedea"/>
          <path d="M0 0H141L150 9V93.6H0" fill="none" stroke="#1a1a1a" stroke-width="1.4" stroke-linejoin="round"/>
        </g>
        <!-- Front Eiche mit Schattenfuge -->
        <rect x="20" y="150" width="130" height="40" fill="#c3b4a5"/>
        <path d="M20 150h130" stroke="#1a1a1a" stroke-width="1.4"/>
      </g>
      <circle cx="112" cy="95" r="76" fill="none" stroke="#1a1a1a" stroke-width="1.1"/>
      <g font-family="'Special Gothic', sans-serif" font-stretch="75%" fill="#1a1a1a">
        <circle cx="214" cy="34" r="8"/><text x="214" y="38" text-anchor="middle" font-size="11" font-weight="700" fill="#bdc1a9">A</text>
        <text x="230" y="38" font-size="13" font-weight="700" letter-spacing=".5">DETAIL KANTE · 1:2</text>
        <text x="214" y="66" font-size="12.5">Multiplex Birke, 19 mm</text>
        <text x="214" y="83" font-size="12.5">9 Lagen, Kante sichtbar</text>
        <text x="214" y="100" font-size="12.5">Fase 2 mm, Hartwachsöl</text>
        <text x="214" y="164" font-size="12.5">Front Eiche, Fuge 3 mm</text>
        <path d="M206 62h-36M206 160h-62" stroke="#1a1a1a" stroke-width=".8" fill="none"/>
      </g>
    </svg>

    <dl class="schriftfeld">
      <div><dt>Projekt</dt><dd>Wohnzimmer</dd></div>
      <div><dt>Kunde</dt><dd>Fam. Brandt</dd></div>
      <div><dt>Maßstab</dt><dd>1 : 20</dd></div>
      <div><dt>Gezeichnet</dt><dd>J. Holm</dd></div>
      <div><dt>Datum</dt><dd>14.10.2026</dd></div>
      <div><dt>Blatt</dt><dd>1 von 3</dd></div>
    </dl>
  </figure>

  <div class="info">
    <p class="gewerk">Kontakt &amp; Anfrage</p>
    <h1>Sagen Sie uns, was gebaut werden soll.</h1>
    <p class="text">Einbauschrank unter der Schräge, Küche aus Eiche oder eine neue Treppe: Wir kommen zum Aufmaß und schicken ein Festpreis-Angebot. Antwort innerhalb von zwei Werktagen.</p>

    <ul class="zeilen">
      <li class="zeile" data-zeile data-zustand="bereit">
        <span class="gewerk">E-Mail für Anfragen</span>
        <span class="wert"><span class="e-mail fenster"><span class="spur"><span class="roh" data-text>anfrage@tischlerei-holm.de</span><span class="ok" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path pathLength="1" d="m2.5 8.5 3.5 3.5 7.5-8"/></svg>Kopiert</span></span></span><small>Gern mit Fotos und groben Maßen</small><small class="hinweis" data-hinweis></small></span>
        <button class="knopf" type="button" data-kopieren data-name="E-Mail-Adresse">
          <span class="icons" aria-hidden="true">
            <svg class="ico-kopie" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><rect x="6" y="6" width="9.5" height="9.5" rx="1"/><path d="M12 6V4.5a2 2 0 0 0-2-2H4.5a2 2 0 0 0-2 2V10a2 2 0 0 0 2 2H6"/></svg>
            <svg class="ico-haken" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path pathLength="1" d="m3.5 9.5 3.6 3.6L14.5 5"/></svg>
          </span>
          <span class="sr">E-Mail-Adresse </span><span class="beschrift"><span class="b-kopieren">kopieren</span><span class="b-kopiert" aria-hidden="true">Kopiert</span></span>
          <span class="blase" aria-hidden="true">Kopiert!</span>
        </button>
      </li>
      <li class="zeile" data-zeile data-zustand="bereit">
        <span class="gewerk">Bankverbindung · IBAN</span>
        <span class="wert"><span class="code"><span class="fenster"><span class="spur"><span class="roh" data-text data-voll="DE47240599170032881506">DE47 2405 9917 0032 8815 06</span><span class="ok" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path pathLength="1" d="m2.5 8.5 3.5 3.5 7.5-8"/></svg>IBAN KOPIERT</span></span></span></span><small>Tischlerei Holm GmbH · Angebotsnummer als Verwendungszweck</small><small class="hinweis" data-hinweis></small></span>
        <button class="knopf" type="button" data-kopieren data-name="IBAN">
          <span class="icons" aria-hidden="true">
            <svg class="ico-kopie" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><rect x="6" y="6" width="9.5" height="9.5" rx="1"/><path d="M12 6V4.5a2 2 0 0 0-2-2H4.5a2 2 0 0 0-2 2V10a2 2 0 0 0 2 2H6"/></svg>
            <svg class="ico-haken" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path pathLength="1" d="m3.5 9.5 3.6 3.6L14.5 5"/></svg>
          </span>
          <span class="sr">IBAN </span><span class="beschrift"><span class="b-kopieren">kopieren</span><span class="b-kopiert" aria-hidden="true">Kopiert</span></span>
          <span class="blase" aria-hidden="true">Kopiert!</span>
        </button>
      </li>
      <li class="zeile" data-zeile data-zustand="bereit">
        <span class="gewerk">Werkstatt &amp; Ausstellung</span>
        <span class="wert"><span class="adresse fenster"><span class="spur"><span class="roh" data-text data-voll="Tischlerei Holm, Am Werkhof 7, 21339 Lüneburg">Am Werkhof 7, 21339 Lüneburg</span><span class="ok" aria-hidden="true"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path pathLength="1" d="m2.5 8.5 3.5 3.5 7.5-8"/></svg>Adresse kopiert</span></span></span><small>Einfahrt Tor 2 · Mo–Fr 7–16 Uhr, Sa nach Absprache</small><small class="hinweis" data-hinweis></small></span>
        <button class="knopf" type="button" data-kopieren data-name="Adresse">
          <span class="icons" aria-hidden="true">
            <svg class="ico-kopie" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"><rect x="6" y="6" width="9.5" height="9.5" rx="1"/><path d="M12 6V4.5a2 2 0 0 0-2-2H4.5a2 2 0 0 0-2 2V10a2 2 0 0 0 2 2H6"/></svg>
            <svg class="ico-haken" viewBox="0 0 18 18" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path pathLength="1" d="m3.5 9.5 3.6 3.6L14.5 5"/></svg>
          </span>
          <span class="sr">Adresse </span><span class="beschrift"><span class="b-kopieren">kopieren</span><span class="b-kopiert" aria-hidden="true">Kopiert</span></span>
          <span class="blase" aria-hidden="true">Kopiert!</span>
        </button>
      </li>
    </ul>
    <p class="sr" aria-live="polite" data-live></p>
  </div>
</main>

<footer class="fuss">
  <div class="schalter">
    <div class="gruppe"><span class="mono">Variante</span>
      <div class="umschalter" role="group" aria-label="Variante" data-varianten>
        <button type="button" data-v="icon" aria-pressed="true">Icon-Tausch</button>
        <button type="button" data-v="blase" aria-pressed="false">Tooltip</button>
        <button type="button" data-v="rolle" aria-pressed="false">Text-Roll</button>
      </div>
    </div>
    <div class="gruppe"><span class="mono">Haltezeit</span>
      <div class="umschalter" role="group" aria-label="Haltezeit" data-haltezeiten>
        <button type="button" data-h="2000" aria-pressed="true">2 s</button>
        <button type="button" data-h="1000" aria-pressed="false">1 s</button>
      </div>
    </div>
  </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="#1a1a1a" stroke="#efedea" stroke-width="1.2" stroke-linejoin="round"/></svg>

<script>
(() => {
  const html = document.documentElement;
  const reduziert = () => html.classList.contains('reduziert');
  const buehne = document.querySelector('[data-buehne]');
  const zeilen = [...document.querySelectorAll('[data-zeile]')];
  const live = document.querySelector('[data-live]');
  const werteEl = document.querySelector('[data-werte]');
  const zeiger = document.querySelector('[data-zeiger]');

  let variante = 'icon', haltezeit = 2000;   // Rückkehr nach 2000 ms (Tailwind, Cursor, Collins) bzw. 1000 ms (Raycast)
  const uhren = new Map();

  const WERTE = {
    icon:  'Tausch 200 ms · Blur 4 px · Scale 0,6 · Häkchen 300 ms +100 ms',
    blase: 'Tooltip steigt 8 px in 300 ms · verblasst 200 ms',
    rolle: 'Rolle 1 Zeile in 500 ms · Häkchen 300 ms +100 ms',
  };
  function werteZeigen() {
    werteEl.textContent = (reduziert() ? 'Reduziert: Überblenden 150 ms linear' : WERTE[variante] + ' · expo.out') + ' · Haltezeit ' + haltezeit + ' ms';
  }

  // Zwischenablage: Clipboard-API, sonst execCommand-Rückfall
  async function schreiben(text) {
    try { await navigator.clipboard.writeText(text); return true; }
    catch (e) {
      const ta = Object.assign(document.createElement('textarea'), { value: text });
      ta.setAttribute('readonly', ''); ta.style.cssText = 'position:fixed;opacity:0;pointer-events:none';
      document.body.append(ta); ta.select();
      let ok = false; try { ok = document.execCommand('copy'); } catch (_) {}
      ta.remove(); return ok;
    }
  }

  function melden(text) { live.textContent = ''; requestAnimationFrame(() => { live.textContent = text; }); }

  function zustand(zeile, z) {
    zeile.dataset.zustand = z;
    clearTimeout(uhren.get(zeile));   // erneuter Klick setzt die Haltezeit zurück
    if (z !== 'bereit') uhren.set(zeile, setTimeout(() => { zeile.dataset.zustand = 'bereit'; }, z === 'fehler' ? 4000 : haltezeit));
  }

  function erfolg(zeile) {
    const knopf = zeile.querySelector('[data-kopieren]');
    zustand(zeile, 'kopiert');
    melden(knopf.dataset.name + ' in die Zwischenablage kopiert');
  }

  function fehler(zeile) {
    const text = zeile.querySelector('[data-text]');
    zeile.querySelector('[data-hinweis]').textContent = 'Kopieren blockiert. Text ist markiert, bitte mit Strg/⌘ + C kopieren.';
    zeile.querySelector('.blase').textContent = 'Blockiert';
    const r = document.createRange(); r.selectNodeContents(text);
    const s = getSelection(); s.removeAllRanges(); s.addRange(r);
    zustand(zeile, 'fehler');
    melden('Kopieren nicht möglich. Text ist markiert.');
  }

  function drueck(knopf) {
    knopf.classList.add('gedrueckt');
    setTimeout(() => knopf.classList.remove('gedrueckt'), 150);
  }

  zeilen.forEach(zeile => {
    const knopf = zeile.querySelector('[data-kopieren]');
    const t = zeile.querySelector('[data-text]');
    knopf.addEventListener('click', async () => {
      if (html.classList.contains('vorfuehrung')) neustart();
      zeile.querySelector('.blase').textContent = 'Kopiert!';
      (await schreiben(t.dataset.voll || t.textContent.trim())) ? erfolg(zeile) : fehler(zeile);
    });
  });

  function setzeVariante(v) {
    variante = v; buehne.dataset.variante = v;
    document.querySelectorAll('[data-v]').forEach(b => b.setAttribute('aria-pressed', b.dataset.v === v));
    zeilen.forEach(z => zustand(z, 'bereit'));
    werteZeigen();
  }
  function setzeHaltezeit(h) {
    haltezeit = h;
    document.querySelectorAll('[data-h]').forEach(b => b.setAttribute('aria-pressed', +b.dataset.h === h));
    werteZeigen();
  }
  document.querySelectorAll('[data-v]').forEach(b => b.addEventListener('click', e => { if (e.isTrusted && html.classList.contains('vorfuehrung')) neustart(); setzeVariante(b.dataset.v); }));
  document.querySelectorAll('[data-h]').forEach(b => b.addEventListener('click', e => { if (e.isTrusted && html.classList.contains('vorfuehrung')) neustart(); setzeHaltezeit(+b.dataset.h); }));
  new MutationObserver(werteZeigen).observe(html, { attributes: true, attributeFilter: ['class'] });
  werteZeigen();

  // Vorführung: gedachter Zeiger wählt die Variante und klickt Kopieren (ohne echte Zwischenablage)
  const ABLAUF = [['icon', 0], ['icon', 1], ['blase', 0], ['blase', 2], ['rolle', 0], ['rolle', 1]];
  let schritt = 0, timer = [];
  const spaeter = (fn, ms) => timer.push(setTimeout(fn, ms));
  function zeigerAuf(el, dx = .5, dy = .55) {
    const r = el.getBoundingClientRect();
    zeiger.style.transform = `translate(${r.left + r.width * dx - 2}px, ${r.top + r.height * dy - 1}px)`;
  }
  function tippen(el) { zeiger.classList.add('klick'); setTimeout(() => zeiger.classList.remove('klick'), 150); drueck(el); }
  function spielSchritt() {
    const [v, i] = ABLAUF[schritt % ABLAUF.length];
    const knopf = zeilen[i].querySelector('[data-kopieren]');
    let t = 0;
    if (v !== variante) {
      const vb = document.querySelector(`[data-v="${v}"]`);
      zeigerAuf(vb); t += 750;
      spaeter(() => { tippen(vb); setzeVariante(v); }, t); t += 450;
    }
    spaeter(() => zeigerAuf(knopf, .3, .6), t); t += 800;
    spaeter(() => { tippen(knopf); erfolg(zeilen[i]); }, t); t += haltezeit + 650;
    spaeter(() => { schritt++; spielSchritt(); }, t);
  }
  function pause() { timer.forEach(clearTimeout); timer = []; }
  function vorfuehren() {
    pause();
    html.classList.add('vorfuehrung');
    zeiger.style.transform = `translate(${innerWidth * .72}px, ${innerHeight * .9}px)`;
    spielSchritt();
  }
  function weiter() { if (!timer.length && html.classList.contains('vorfuehrung')) spielSchritt(); }
  function neustart() {
    pause(); html.classList.remove('vorfuehrung'); schritt = 0;
    zeilen.forEach(z => zustand(z, 'bereit'));
  }

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