diff --git a/Dockerfile.karma b/Dockerfile.karma
deleted file mode 100644
index 64fef20d69b..00000000000
--- a/Dockerfile.karma
+++ /dev/null
@@ -1,12 +0,0 @@
-# Karma/headless-Chrome test runner image for reproducing the dropdown bugs.
-# Node 20 (matches the project) + Chromium for ChromeHeadlessCI.
-FROM node:20-bookworm
-
-RUN apt-get update \
- && apt-get install -y --no-install-recommends chromium \
- && rm -rf /var/lib/apt/lists/*
-
-ENV CHROME_BIN=/usr/bin/chromium
-ENV CHROMIUM_FLAGS="--no-sandbox --disable-gpu"
-
-WORKDIR /app
diff --git a/demo/.gitignore b/demo/.gitignore
deleted file mode 100644
index c2658d7d1b3..00000000000
--- a/demo/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-node_modules/
diff --git a/demo/gif.mjs b/demo/gif.mjs
deleted file mode 100644
index c3c608d5031..00000000000
--- a/demo/gif.mjs
+++ /dev/null
@@ -1,69 +0,0 @@
-// Builds animated GIFs (before.gif / after.gif) of the dropdown behaviour,
-// so they can be embedded inline in a GitHub PR comment. No ffmpeg required.
-import { chromium } from 'playwright';
-import { pathToFileURL, fileURLToPath } from 'node:url';
-import { dirname, join } from 'node:path';
-import { writeFileSync } from 'node:fs';
-import { PNG } from 'pngjs';
-import gifenc from 'gifenc';
-
-const { GIFEncoder, quantize, applyPalette } = gifenc;
-
-const __dirname = dirname(fileURLToPath(import.meta.url));
-const pageUrl = (mode) => pathToFileURL(join(__dirname, 'index.html')).href + '?mode=' + mode;
-const size = { width: 820, height: 600 };
-const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
-
-async function frames(mode) {
- const browser = await chromium.launch();
- const page = await browser.newPage({ viewport: size });
- await page.goto(pageUrl(mode));
- const out = [];
-
- const cap = async (tag, text, delay, action) => {
- if (action) { await action(); }
- await page.evaluate(([t, x]) => window.setCaption(t, x), [tag, text]);
- await sleep(350);
- const png = PNG.sync.read(await page.screenshot());
- out.push({ rgba: new Uint8Array(png.data), w: png.width, h: png.height, delay });
- };
-
- const label = mode === 'after' ? 'AFTER (fixed)' : 'BEFORE (PR #26)';
- await cap(label, 'Repeatable \u201CFunder\u201D field \u2014 authority-controlled.', 1700);
- await cap('Step 1', 'Row 1: open the funder dropdown.', 1200, () => page.click('[data-testid="input-0"]'));
- await cap('Step 1', 'Choose \u201CNational Science Foundation\u201D.', 1300, () => page.click('[data-testid="opt-0-nsf"]'));
- await cap('Step 2', 'Add a second funder row.', 1100, () => page.click('[data-testid="add"]'));
- await cap('Step 2', 'Row 2: open the dropdown.', 1300, () => page.click('[data-testid="input-1"]'));
-
- if (mode === 'before') {
- await cap('Bug', 'BEFORE: the already-used funder is NOT greyed out.', 2100);
- await cap('Bug', 'Select the SAME funder again\u2026', 1300, () => page.click('[data-testid="opt-1-nsf"]'));
- await cap('Result', '\u26A0 Duplicate persisted \u2014 same authority twice.', 2700);
- } else {
- await cap('Fix', 'AFTER: the used funder is greyed out (disabled).', 2200);
- await cap('Fix', 'Clicking it does nothing \u2014 blocked.', 1500, () => page.click('[data-testid="opt-1-nsf"]'));
- await cap('Fix', 'Pick a different funder instead.', 1300, () => page.click('[data-testid="opt-1-erc"]'));
- await cap('Result', '\u2713 No duplicates \u2014 distinct funders.', 2700);
- }
-
- await browser.close();
- return out;
-}
-
-async function build(mode) {
- const fs = await frames(mode);
- const gif = GIFEncoder();
- for (const f of fs) {
- const palette = quantize(f.rgba, 256);
- const index = applyPalette(f.rgba, palette);
- gif.writeFrame(index, f.w, f.h, { palette, delay: f.delay });
- }
- gif.finish();
- const target = join(__dirname, 'videos', mode + '.gif');
- writeFileSync(target, gif.bytes());
- console.log('Saved', target, (gif.bytes().length / 1024).toFixed(0) + ' KB');
-}
-
-await build('before');
-await build('after');
-console.log('Done.');
diff --git a/demo/index.html b/demo/index.html
deleted file mode 100644
index 2fe70e82868..00000000000
--- a/demo/index.html
+++ /dev/null
@@ -1,257 +0,0 @@
-
-
-
-
-
-
- Repeatable dropdown – duplicate prevention
-
-
-
-
-
Submission form — repeatable “Funder” dropdown
-
-
Funder (repeatable, authority-controlled)
-
-
-
-
-
-
-
-
-
diff --git a/demo/package.json b/demo/package.json
deleted file mode 100644
index e07981156af..00000000000
--- a/demo/package.json
+++ /dev/null
@@ -1,17 +0,0 @@
-{
- "name": "dropdown-dup-demo",
- "version": "1.0.0",
- "private": true,
- "type": "module",
- "description": "Records before/after videos of the repeatable-dropdown duplicate fix (PR #26).",
- "scripts": {
- "record": "node record.mjs"
- },
- "devDependencies": {
- "playwright": "1.48.2"
- },
- "dependencies": {
- "gifenc": "^1.0.3",
- "pngjs": "^7.0.0"
- }
-}
diff --git a/demo/record.mjs b/demo/record.mjs
deleted file mode 100644
index 79845efc1e7..00000000000
--- a/demo/record.mjs
+++ /dev/null
@@ -1,82 +0,0 @@
-// Records BEFORE and AFTER videos of the repeatable-dropdown duplicate fix.
-// Usage: npm install && npx playwright install chromium && node record.mjs
-import { chromium } from 'playwright';
-import { pathToFileURL } from 'node:url';
-import { fileURLToPath } from 'node:url';
-import { dirname, join } from 'node:path';
-import { renameSync, mkdirSync, existsSync } from 'node:fs';
-
-const __dirname = dirname(fileURLToPath(import.meta.url));
-const pageUrl = (mode) => pathToFileURL(join(__dirname, 'index.html')).href + '?mode=' + mode;
-const outDir = join(__dirname, 'videos');
-const size = { width: 940, height: 720 };
-
-if (!existsSync(outDir)) {
- mkdirSync(outDir, { recursive: true });
-}
-
-const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
-
-async function step(page, tag, text, ms = 1500) {
- await page.evaluate(([t, x]) => window.setCaption(t, x), [tag, text]);
- await sleep(ms);
-}
-
-async function record(mode) {
- const browser = await chromium.launch();
- const context = await browser.newContext({
- viewport: size,
- recordVideo: { dir: outDir, size },
- });
- const page = await context.newPage();
- await page.goto(pageUrl(mode));
- await sleep(1200);
-
- const label = mode === 'after' ? 'AFTER (fixed)' : 'BEFORE (merged PR #26)';
- await step(page, label, 'Repeatable “Funder” field. Let\u2019s add two rows.', 2000);
-
- // Row 1 -> select National Science Foundation
- await step(page, 'Step 1', 'Row 1: open the funder dropdown.', 1200);
- await page.click('[data-testid="input-0"]');
- await sleep(700);
- await step(page, 'Step 1', 'Choose “National Science Foundation”.', 1400);
- await page.click('[data-testid="opt-0-nsf"]');
- await sleep(900);
-
- // Add row 2
- await step(page, 'Step 2', 'Add a second funder row.', 1300);
- await page.click('[data-testid="add"]');
- await sleep(700);
- await step(page, 'Step 2', 'Row 2: open the dropdown.', 1200);
- await page.click('[data-testid="input-1"]');
- await sleep(900);
-
- if (mode === 'before') {
- await step(page, 'Bug', 'BEFORE: the already-used funder is NOT greyed out\u2026', 2200);
- await step(page, 'Bug', 'Select the SAME funder again.', 1400);
- await page.click('[data-testid="opt-1-nsf"]');
- await sleep(900);
- await step(page, 'Result', '\u26A0 Duplicate persisted \u2014 same authority in two rows.', 3200);
- } else {
- await step(page, 'Fix', 'AFTER: the already-used funder is greyed out (disabled).', 2400);
- await step(page, 'Fix', 'Clicking it does nothing \u2014 duplicate is blocked.', 1600);
- await page.click('[data-testid="opt-1-nsf"]');
- await sleep(900);
- await step(page, 'Fix', 'Pick a different funder instead: “European Research Council”.', 1600);
- await page.click('[data-testid="opt-1-erc"]');
- await sleep(900);
- await step(page, 'Result', '\u2713 No duplicates \u2014 every row references a distinct funder.', 3200);
- }
-
- const videoPath = await page.video().path();
- await context.close();
- await browser.close();
-
- const target = join(outDir, mode + '.webm');
- renameSync(videoPath, target);
- console.log('Saved', target);
-}
-
-await record('before');
-await record('after');
-console.log('Done. Videos in', outDir);
diff --git a/demo/screenshot.mjs b/demo/screenshot.mjs
deleted file mode 100644
index 14a1fd80929..00000000000
--- a/demo/screenshot.mjs
+++ /dev/null
@@ -1,43 +0,0 @@
-// Captures still screenshots of the decisive BEFORE/AFTER states.
-import { chromium } from 'playwright';
-import { pathToFileURL, fileURLToPath } from 'node:url';
-import { dirname, join } from 'node:path';
-
-const __dirname = dirname(fileURLToPath(import.meta.url));
-const pageUrl = (mode) => pathToFileURL(join(__dirname, 'index.html')).href + '?mode=' + mode;
-const size = { width: 940, height: 720 };
-const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
-
-const browser = await chromium.launch();
-
-// BEFORE: duplicate persisted
-{
- const page = await browser.newPage({ viewport: size });
- await page.goto(pageUrl('before'));
- await page.click('[data-testid="input-0"]');
- await page.click('[data-testid="opt-0-nsf"]');
- await page.click('[data-testid="add"]');
- await page.click('[data-testid="input-1"]');
- await page.click('[data-testid="opt-1-nsf"]');
- await page.evaluate(() => window.setCaption('Before', 'Duplicate funder persisted (same authority in two rows).'));
- await sleep(400);
- await page.screenshot({ path: join(__dirname, 'videos', 'before.png') });
- await page.close();
-}
-
-// AFTER: option greyed out / blocked
-{
- const page = await browser.newPage({ viewport: size });
- await page.goto(pageUrl('after'));
- await page.click('[data-testid="input-0"]');
- await page.click('[data-testid="opt-0-nsf"]');
- await page.click('[data-testid="add"]');
- await page.click('[data-testid="input-1"]');
- await page.evaluate(() => window.setCaption('After', 'The already-used funder is greyed out and cannot be selected.'));
- await sleep(400);
- await page.screenshot({ path: join(__dirname, 'videos', 'after.png') });
- await page.close();
-}
-
-await browser.close();
-console.log('Screenshots saved to videos/before.png and videos/after.png');
diff --git a/demo/videos/after.gif b/demo/videos/after.gif
deleted file mode 100644
index af91a2874fe..00000000000
Binary files a/demo/videos/after.gif and /dev/null differ
diff --git a/demo/videos/after.png b/demo/videos/after.png
deleted file mode 100644
index 6d16330b489..00000000000
Binary files a/demo/videos/after.png and /dev/null differ
diff --git a/demo/videos/after.webm b/demo/videos/after.webm
deleted file mode 100644
index 888d40e271f..00000000000
Binary files a/demo/videos/after.webm and /dev/null differ
diff --git a/demo/videos/before.gif b/demo/videos/before.gif
deleted file mode 100644
index a8f819fe4cf..00000000000
Binary files a/demo/videos/before.gif and /dev/null differ
diff --git a/demo/videos/before.png b/demo/videos/before.png
deleted file mode 100644
index 92e14244467..00000000000
Binary files a/demo/videos/before.png and /dev/null differ
diff --git a/demo/videos/before.webm b/demo/videos/before.webm
deleted file mode 100644
index bdbfbac048b..00000000000
Binary files a/demo/videos/before.webm and /dev/null differ
diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5
index e050069efde..0816518b76b 100644
--- a/src/assets/i18n/en.json5
+++ b/src/assets/i18n/en.json5
@@ -2000,6 +2000,8 @@
"form.first-name": "First name",
+ "form.vocabulary.load-error": "Something went wrong while loading the options. Please try again.",
+
"form.group-collapse": "Collapse",
"form.group-collapse-help": "Click here to collapse",
@@ -2812,6 +2814,8 @@
"item.page.files": "Files",
+ "item.page.filesection.checksum": "Checksum:",
+
"item.page.filesection.description": "Description:",
"item.page.filesection.download": "Download",
@@ -7183,6 +7187,8 @@
"item.page.doi": "Persistent Identifier",
+ "item.page.doi.pending": "DOI registration in progress",
+
"item.page.date.available": "Date Available",
"item.page.replaced": "Relation (Is Replaced By)",