diff --git a/assets/legal.mjs b/assets/legal.mjs
new file mode 100644
index 0000000..9acd1b6
--- /dev/null
+++ b/assets/legal.mjs
@@ -0,0 +1,83 @@
+const dialogs = new Map(
+ [...document.querySelectorAll('.legal-dialog')].map(dialog => [dialog.id, dialog]),
+);
+let returnFocus;
+
+function syncDialog() {
+ const next = dialogs.get(location.hash.slice(1));
+ let closed = false;
+
+ for (const dialog of dialogs.values()) {
+ if (dialog.open && dialog !== next) {
+ dialog.close();
+ closed = true;
+ }
+ }
+
+ if (next && !next.open) {
+ returnFocus ??= document.querySelector(`[data-legal-open][aria-controls="${next.id}"]`);
+ next.showModal();
+ next.querySelector('.legal-copy').scrollTop = 0;
+ next.querySelector('h1').focus({ preventScroll: true });
+ } else if (!next && closed) {
+ returnFocus?.focus({ preventScroll: true });
+ returnFocus = undefined;
+ }
+}
+
+function dismissDialog() {
+ history.replaceState(null, '', location.pathname + location.search);
+ syncDialog();
+}
+
+for (const link of document.querySelectorAll('[data-legal-open]')) {
+ link.addEventListener('click', event => {
+ if (event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
+ event.preventDefault();
+ returnFocus = link;
+ history.pushState(null, '', link.hash);
+ syncDialog();
+ });
+}
+
+for (const dialog of dialogs.values()) {
+ dialog.querySelector('[data-legal-close]').addEventListener('click', event => {
+ event.preventDefault();
+ dismissDialog();
+ });
+ dialog.addEventListener('cancel', event => {
+ event.preventDefault();
+ dismissDialog();
+ });
+ dialog.addEventListener('keydown', event => {
+ if (event.key !== 'Tab' || event.altKey || event.ctrlKey || event.metaKey) return;
+ const controls = [...dialog.querySelectorAll('a[href], button:not([disabled]), [tabindex="0"]')];
+ const first = controls[0];
+ const last = controls.at(-1);
+ const active = document.activeElement;
+ if (!controls.includes(active) || (event.shiftKey && active === first)
+ || (!event.shiftKey && active === last)) {
+ event.preventDefault();
+ (event.shiftKey ? last : first)?.focus();
+ }
+ });
+
+ // Close only when both ends of a click are on the backdrop, not a text-selection drag.
+ let startedOutside = false;
+ const outside = event => {
+ const rect = dialog.getBoundingClientRect();
+ return event.clientX < rect.left || event.clientX > rect.right
+ || event.clientY < rect.top || event.clientY > rect.bottom;
+ };
+ dialog.addEventListener('pointerdown', event => {
+ startedOutside = event.target === dialog && outside(event);
+ });
+ dialog.addEventListener('click', event => {
+ if (startedOutside && event.target === dialog && outside(event)) dismissDialog();
+ startedOutside = false;
+ });
+}
+
+window.addEventListener('hashchange', syncDialog);
+window.addEventListener('popstate', syncDialog);
+syncDialog();
diff --git a/assets/styles.css b/assets/styles.css
index 5dd145e..c66acf4 100644
--- a/assets/styles.css
+++ b/assets/styles.css
@@ -22,6 +22,10 @@
--color-sand: #e5cd9c;
--color-announcement: #f2e6cc;
--color-focus: #ffc300;
+ --color-accent: #ef3a57;
+ --color-dialog: #721a3d;
+ --color-overlay: rgb(24 8 16 / 72%);
+ --color-shadow: rgb(24 8 16 / 45%);
/* One design unit is one point on the supplied 1200 × 900 layout. */
--unit: 0.0833333333vw;
--font-body: Roboto, system-ui, sans-serif;
@@ -284,6 +288,166 @@ address {
transform: none;
}
+.site-footer {
+ margin-block-start: 28px;
+ border-block-start: 1px solid color-mix(in srgb, var(--color-sand) 35%, transparent);
+ padding-block-start: 8px;
+ color: var(--color-sand);
+ font-size: 14px;
+}
+
+.site-footer nav {
+ display: flex;
+ justify-content: center;
+ gap: 24px;
+}
+
+.site-footer a {
+ display: inline-flex;
+ align-items: center;
+ min-block-size: 44px;
+}
+
+html:has(.legal-dialog[open]),
+html:has(.legal-dialog:target) {
+ overflow: hidden;
+}
+
+.legal-dialog {
+ position: fixed;
+ inset: 0;
+ inline-size: min(48rem, calc(100% - 32px));
+ max-inline-size: none;
+ max-block-size: calc(100svh - 32px);
+ margin: auto;
+ border: 1px solid color-mix(in srgb, var(--color-sand) 45%, transparent);
+ border-block-start: 4px solid var(--color-accent);
+ border-radius: 12px;
+ padding: 0;
+ overflow: hidden;
+ background: var(--color-dialog);
+ color: var(--color-cream);
+ box-shadow: 0 24px 80px var(--color-shadow);
+}
+
+.legal-dialog::backdrop {
+ background: var(--color-overlay);
+ backdrop-filter: blur(5px);
+}
+
+/* Fragment links still open the complete text if JavaScript is unavailable. */
+.legal-dialog:target:not([open]) {
+ display: block;
+ z-index: 3;
+ box-shadow: 0 0 0 100vmax var(--color-overlay);
+}
+
+.legal-shell {
+ display: flex;
+ flex-direction: column;
+ max-block-size: min(54rem, calc(100svh - 38px));
+}
+
+.legal-header {
+ display: flex;
+ flex: none;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ border-block-end: 1px solid color-mix(in srgb, var(--color-sand) 25%, transparent);
+ padding: 12px clamp(18px, 4vw, 36px);
+}
+
+.legal-header img {
+ inline-size: 48px;
+ block-size: 38px;
+}
+
+.legal-close {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 12px;
+ min-block-size: 44px;
+ border-radius: 6px;
+ padding-inline: 12px;
+ color: var(--color-sand);
+ font-size: 14px;
+}
+
+.legal-close:hover {
+ background: color-mix(in srgb, var(--color-cream) 8%, transparent);
+ text-decoration: none;
+}
+
+.legal-close span {
+ font-size: 28px;
+ line-height: 1;
+}
+
+.legal-copy {
+ min-block-size: 0;
+ padding: clamp(20px, 4vw, 36px);
+ overflow: auto;
+ overscroll-behavior: contain;
+ text-align: start;
+ overflow-wrap: anywhere;
+ font-size: 16px;
+ font-style: normal;
+ line-height: 1.65;
+ scrollbar-color: var(--color-sand) var(--color-dialog);
+}
+
+.legal-copy h1 {
+ margin: 0 0 24px;
+ color: var(--color-cream);
+ font-size: clamp(26px, 4vw, 34px);
+ font-style: italic;
+ line-height: 1.2;
+}
+
+.legal-copy h2 {
+ margin: 32px 0 16px;
+ color: var(--color-sand);
+ font-size: 22px;
+ line-height: 1.35;
+}
+
+.legal-copy h3 {
+ margin: 24px 0 12px;
+ color: var(--color-sand);
+ font-size: 18px;
+ line-height: 1.4;
+}
+
+.legal-copy h4 {
+ margin: 20px 0 10px;
+ font-size: 16px;
+ line-height: 1.5;
+}
+
+.legal-copy p,
+.legal-copy ul {
+ margin-block: 0 16px;
+}
+
+.legal-copy ul {
+ padding-inline-start: 24px;
+}
+
+.legal-copy li + li {
+ margin-block-start: 8px;
+}
+
+.legal-copy a {
+ color: var(--color-sand);
+ text-decoration: underline;
+}
+
+.legal-copy > :last-child {
+ margin-block-end: 0;
+}
+
@media (width > 1024px) {
body::after {
position: fixed;
diff --git a/design/legal/datenschutzerklaerung_buchhandlung_am_markt_de.pdf b/design/legal/datenschutzerklaerung_buchhandlung_am_markt_de.pdf
new file mode 100644
index 0000000..7efe0fe
Binary files /dev/null and b/design/legal/datenschutzerklaerung_buchhandlung_am_markt_de.pdf differ
diff --git a/design/legal/impressum_buchhandlung_am_markt_de.pdf b/design/legal/impressum_buchhandlung_am_markt_de.pdf
new file mode 100644
index 0000000..a15676d
Binary files /dev/null and b/design/legal/impressum_buchhandlung_am_markt_de.pdf differ
diff --git a/docs/implementation.md b/docs/implementation.md
index c03c412..ec89c59 100644
--- a/docs/implementation.md
+++ b/docs/implementation.md
@@ -6,6 +6,12 @@ CSS lives in `assets/styles.css`; all colors are CSS custom properties. Its link
At 1024 px and below, both decorative images are ordinary `body` backgrounds. The tape SVG applies the original shadow settings to each strip, including their overlap. Its 270 pt view box leaves room for the shadow without moving or scaling the artwork. The desktop pseudo-elements and isolated stacking context are disabled, leaving mobile touch scrolling to the document without separate decorative layers. At 600 px and below, the whole Kontodaten section is centered, including the account holder, bank name and each complete IBAN/BIC row.
+## Legal content
+
+The footer opens native dialogs for Impressum and Datenschutz. All legal HTML lives in `index.html`; `assets/legal.mjs` handles opening, closing, focus, and fragment navigation. The burgundy reading panels scroll independently while the page stays in place. The close link, Escape, backdrop click and browser Back close the popup; fragment links also expose the content without JavaScript. CSS and the legal module have content-hash URL parameters that must be refreshed when their files change.
+
+The supplied, unchanged PDFs are retained under `design/legal/`. The legal content was copied directly from their `imprint_html_de` and `privacy_html_de` form fields, preserving the supplied headings, paragraphs, lists, punctuation and entities. The PDF export's table of contents, integration instructions and duplicate text rendering are not website copy. Only title attributes for dialog accessibility were added. Run `python3 scripts/verify-legal-content.py` with the development dependency `pypdf` to compare the complete embedded HTML against both original fields. It ignores only indentation, line endings and the two title attributes. Do not revise the legal wording without replacement content from the owner.
+
## Preview and deployment
Requires Node 20.11 or newer. There are no npm dependencies and no third-party page requests.
diff --git a/docs/verification.md b/docs/verification.md
index 6091727..1941305 100644
--- a/docs/verification.md
+++ b/docs/verification.md
@@ -44,3 +44,12 @@ Mobile shadow and heading follow-up on 2026-09-07:
- Added each strip's shadow to the separate mobile tape SVG. At 390 px, initial screenshot differences are confined to the tape and shadow area (the top 146 px); content positions are unchanged. The 1200 px desktop screenshot remains pixel-identical.
- At 390 px, the Kontodaten heading's text center is 194.99 px against the viewport center of 195 px. Desktop keeps its original alignment. No horizontal overflow is present.
- The complete bank section is now centered on narrow mobile layouts: account holder, bank name, and each IBAN/BIC label-value row. Text centers match the viewport center at 320, 390 and 600 px, with no horizontal overflow. Checks at 601 and 1200 px retain the original alignment.
+
+Legal dialogs on 2026-09-10:
+
+- Retained both supplied PDFs byte for byte under `design/legal/`. `scripts/verify-legal-content.py` confirms that the complete embedded Impressum (712 characters) and Datenschutz (19,921 characters) HTML match their PDF form fields, allowing only line endings, indentation, and accessible title attributes.
+- Built with `npm run build`; module syntax and `git diff --check` pass. No runtime dependencies or third-party resource requests were added.
+- Inspected both dialogs at 1200, 390 and 320 px, including the final paragraphs. The complete text scrolls to the bottom, the close control remains visible, and neither the document nor reading area overflows horizontally. The popup also fits an 844 × 390 landscape viewport.
+- Close links, Escape, backdrop clicks, browser Back and direct fragment URLs work. Tab and Shift+Tab remain within each modal; closing restores focus to its footer link and preserves the underlying scroll position.
+- Emulated mobile touch input scrolls the Datenschutz reading area while the page remains stationary; after closing, normal page touch scrolling resumes. No JavaScript errors or external requests occurred.
+- With JavaScript disabled, both footer fragment links expose the full legal text through the CSS fallback; the close link returns to the footer.
diff --git a/index.html b/index.html
index 5e2cf2b..04ce64b 100644
--- a/index.html
+++ b/index.html
@@ -35,8 +35,9 @@
-
+
+