Files
bea-edit/agents.md
2026-10-28 16:09:53 +01:00

435 lines
29 KiB
Markdown

# Project Agent Guide
## Project overview
This repository contains **juri-merger**, a German-language SvelteKit application branded as
**beA-Edit**. Users add one or more beA ZIP archives or PDF files in the browser, inspect and
reorder supported attachments, group attachments into named sub-documents (including page-level
PDF splitting), and export merged PDFs.
Nested ZIP archives and XJustiz metadata are handled client-side; there is no upload, server-side
archive processing, database, analytics, or persistence of case data.
The application also contains German imprint and privacy-policy routes.
## Technology stack
- SvelteKit 2 and Svelte 5, with runes mode forced for project source files
- TypeScript in strict mode
- Vite 8
- Tailwind CSS 4 through `@tailwindcss/vite`
- shadcn-svelte 1 with the `nova` style and CSS-variable theme tokens
- bits-ui, tailwind-variants, clsx, and tailwind-merge
- Lucide Svelte icons plus project-specific SVG icon components
- fflate for ZIP extraction and image-export ZIP creation
- pdf-lib for PDF assembly, `@cantoo/pdf-lib` for encryption, and pdfjs-dist for rendering
- `@sveltejs/adapter-node`
- PWA/offline support through `@vite-pwa/sveltekit` (Workbox precache, generated web manifest)
- pnpm with a committed lockfile
- Prettier with Svelte and Tailwind plugins
Use **pnpm**, not npm or yarn. The production container uses Node 24 and pnpm 11.15.1.
## Commands
```sh
pnpm install # Install dependencies
pnpm dev # Start the development server
pnpm build # Create the adapter-node production build
pnpm preview # Preview the production build
pnpm check # Run Svelte and TypeScript diagnostics
pnpm check:watch # Run diagnostics in watch mode
pnpm test:e2e # Run the toolbox specs (tests/e2e) and the unit specs (tests/unit)
pnpm test:pwa # PWA offline/update tests against the production build (requires pnpm build)
pnpm lint # Check repository formatting with Prettier
pnpm format # Format the repository with Prettier
```
For code changes, run at least:
```sh
pnpm check
pnpm test:e2e
pnpm lint
pnpm build
```
For documentation-only changes, a targeted Prettier check is sufficient. If pre-existing warnings
or formatting failures remain outside changed files, report them instead of making unrelated edits.
The Docker image can be built from the repository root. It installs with `--frozen-lockfile`, builds
and prunes dependencies, then runs `node build/index.js` as a non-root user on port 3000.
## Repository structure
```text
src/
├── app.html # German HTML shell
├── app.d.ts # SvelteKit application declarations
├── lib/
│ ├── actions/ripple.ts # Button ripple Svelte action
│ ├── assets/ # Favicon and local Nunito fonts
│ ├── components/
│ │ ├── AttachmentPreview.svelte # Browser-rendered PDF/image thumbnails
│ │ ├── AttachmentMenu.svelte # Per-attachment dropdown menu (move, split, remove)
│ │ ├── BatchFileList.svelte # Shared batch rows: status chips, downloads, retry
│ │ ├── BeaArchiveProcessing.svelte
│ │ ├── BeaWorkspaceControls.svelte # beA workspace toolbar, settings popover, dialogs
│ │ ├── ExhibitFileList.svelte # Reorderable Anlagen list: labels, renames, results
│ │ ├── ProcessedZipArchiveEditor.svelte
│ │ ├── PaginationPreview.svelte # Rendered page preview incl. shrink hints and page nav
│ │ ├── PdfSplitDialog.svelte # Page-level PDF splitting into Teildokumente
│ │ ├── SinglePdfTool.svelte # Shell for one-PDF tools (header, dropzone, slots)
│ │ ├── SubDocumentEditor.svelte
│ │ ├── ToolError.svelte # Shared error banner (role="alert")
│ │ ├── FileDropzone.svelte # Shared PDF/image/ZIP file picker and drop target
│ │ ├── ZipFilePicker.svelte # Compact ZIP picker button with hidden resettable input
│ │ ├── PdfPageGrid.svelte # Accessible PDF thumbnail selection grid
│ │ ├── PwaUpdatePrompt.svelte # Service-worker registration and update/offline toast
│ │ ├── RasterizationWarning.svelte
│ │ ├── ZipDropzone.svelte # beA-specific wrapper around FileDropzone
│ │ ├── icons/ # Project-specific SVG components
│ │ └── ui/ # Local shadcn-svelte component source
│ ├── services/
│ │ ├── xml-reading.service.ts # Namespace-tolerant XJustiz parsing
│ │ └── zip-inflating.service.ts # Async fflate wrapper
│ ├── batch.ts # Sequential batch engine for multi-file tools
│ ├── download.ts # Blob/download helpers (pdfBytesToBlob, downloadBlobFile)
│ ├── exhibit-naming.ts # Anlagen label, start number and file-name helpers
│ ├── latest-task.ts # "Only the latest async call wins" tokens
│ ├── pdf-compression.ts # Shared compression presets and rasterize pipeline
│ ├── pdf-errors.ts # Shared PDF error classification and German messages
│ ├── pdf-metadata.ts # PDF metadata inspection and cleaning (pdf-lib only)
│ ├── pdf-overlay.ts # Shared overlay positions, preview CSS and color parsing
│ ├── pdf-pagination.ts # Sheet-number labels and the pagination pipeline
│ ├── pdf-preview-text.ts # Canvas text measurement for HTML previews
│ ├── pdf-processing.ts # Reusable PDF merge helper
│ ├── pdf-session.svelte.ts # Shared single-PDF lifecycle (file, bytes, handle, errors)
│ ├── pdf-text-overlay.ts # Shared framed/rotated text and image overlay drawing
│ ├── pdf-thumbnails.ts # Managed pdfjs thumbnails, on-demand page previews, cleanup
│ ├── utils.ts # cn() and shared component utility types
│ └── zip-processing.ts # Archive model, transforms, and PDF export
└── routes/
├── +layout.svelte # Shared logo header and branded footer
├── +layout.ts # `prerender = true` for all routes (PWA offline documents)
├── +page.svelte # Toolbox hub and global beA ZIP drop target
├── layout.css # Tailwind, shadcn, theme, fonts, global CSS
├── tools/
│ ├── +layout.svelte # Shared back-navigation shell
│ ├── bea/+page.svelte # Existing ZIP archive workflow
│ └── {exhibits,pagination,merge,separate,stamp,watermark,encrypt,decrypt,compress,convert,rotate,metadata}/
├── datenschutz/+page.svelte # Privacy policy
└── impressum/+page.svelte # Imprint
static/ # Public logo, footer art, PWA icons, and robots.txt
components.json # shadcn-svelte registry configuration
Dockerfile # Node adapter production image
playwright.pwa.config.ts # Playwright config for `pnpm test:pwa`
tests/e2e/ # Playwright toolbox specs
tests/unit/ # Pure-function specs (run by the same Playwright config)
plan/ # Design and implementation notes
```
Generated directories such as `.svelte-kit/`, `build/`, and `node_modules/` must not be edited or
committed.
## PWA / offline mode
- `SvelteKitPWA` is configured in `vite.config.ts` with `registerType: 'prompt'`: a waiting service
worker never activates by itself. `PwaUpdatePrompt.svelte` registers the worker in the browser and
shows a toast (“Neue Version verfügbar” with “Neu laden”/“Später”, plus a one-time hint when the
app is ready offline). Never force a reload — an in-progress archive must survive a declined
update.
- All routes are prerendered (`src/routes/+layout.ts`), so route documents land in the precache and
offline navigation works for every page, including a direct deep link. `navigateFallback: '/'`
covers unknown paths.
- Precache contents come from `workbox.globPatterns`: the plugin's client/prerendered defaults plus
`mjs` (the pdfjs worker chunk, required for offline rendering) and the local fonts; the unused
`static/Nunito/**` font drop is excluded via `globIgnores`. Precache size is ~4.5 MB.
- PWA icons in `static/icons/` (192/512, maskable, apple-touch) were generated once from
`static/favicon.svg`; regenerate them with ImageMagick/rsvg if the logo changes. The manifest link
is injected client-side from `virtual:pwa-info` in the root layout, `theme-color` and the
apple-touch-icon live in `app.html`.
- `devOptions.enabled` stays `false`: `pnpm dev` never registers a service worker, so development
caches stay clean. Verify PWA behaviour only against a production build (`pnpm build` and
`pnpm preview`, or `pnpm test:pwa`, which also covers the offline processing chain and the update
toast).
- Simulating a deployment in tests requires restarting `node build/index.js`: adapter-node's static
handler (sirv, non-dev mode) walks the build directory once at startup and caches the ETags, so
swapped files are invisible to a running process. Browsers revalidate `/sw.js` via its ETag on
navigation and `registration.update()`, so the normal container restart on deploy is sufficient.
## Application flow and state
- `/` is the toolbox hub. A beA ZIP or PDF dropped anywhere on that route is queued in memory and
forwarded to `/tools/bea`; the file is not uploaded or persisted. The same `queuePendingFiles` /
`takePendingFiles` handoff carries the merged PDF from `/tools/merge` to `/tools/pagination`.
- `/tools/pagination` is a single-PDF tool: it renders one page at a time through
`loadPdfPageRenderer` (never the all-pages thumbnail array), composes every label with
`buildPaginationLabel` — the single source for both the live preview and the export — and stamps
all pages through `drawTextOverlay`. The page number is always on every page; the optional free
text (Aktenzeichen) follows its own “every page” / “first page only” scope.
- `/tools/exhibits` is the Anlagen tool and the only batch tool with an order. One dropped PDF is
one Anlage: `src/lib/exhibit-naming.ts` owns the label (`buildExhibitLabel`, no padded numbers in
the stamp) and the optional file name (`buildExhibitFileName`, `{prefixSlug}_{paddedNumber}_…`,
padding from `getExhibitNumberWidth`). Numbers are frozen per row when the run starts
(`assignments` map), so reordering or changing the start number never rewrites a created Anlage —
the list keeps showing the frozen label while pending rows follow the live order. The stamp is
`drawTextOverlay` with `framed: true`, `compensatePageRotation: true`, first page by default.
- `/tools/metadata` is a single-PDF inspection tool. `src/lib/pdf-metadata.ts` reads document
properties with pdf-lib only (no rasterization), returns a discriminated `{ status: 'encrypted' }`
result for password-protected files, and detects embedded-file markers by scanning raw bytes.
Cleaning neutralizes the standard Info dictionary fields and re-saves through pdf-lib; embedded
files and XMP streams are out of scope and are disclosed in the UI instead.
- `tools/bea/+page.svelte` owns selected files, processed archives, pending work, and the
thumbnail-width preference. Thumbnail width is the only persisted UI setting and uses localStorage.
- The loaded workspace renders `BeaWorkspaceControls.svelte`, a top-aligned toolbar with the
workspace status and two archive lifecycle actions:
- **“Neue Datei bearbeiten”** (replace) opens a file picker and, after the user confirms the
replacement dialog, calls `replaceWithZipFiles(files)` which resets the workspace and enqueues the
new files against the incremented generation. Files picked for replacement stay in temporary
component state until confirmation; cancelling the picker or the dialog must never mutate the
current workspace.
- **“Weitere Datei hinzufügen”** (append) lives in the settings popover, uses the plain append path
(`handleFilesSelected`), and never clears current archives. Keep the two labels distinct:
replace is “Neue Datei bearbeiten”, append is “Weitere Datei hinzufügen”.
- `BeaWorkspaceControls.svelte` owns only ephemeral UI state (popover/dialog open state and pending
replacement files). The route remains the source of truth for files, jobs, archives, and the
persisted thumbnail width.
- `resetWorkspace()` is the single teardown helper: it increments `archiveGeneration` and clears
selected files, processed archives, and jobs. Never clear those arrays without going through it.
- Accepted file types for the workspace (beA ZIP **and** PDF) are centralized in
`src/lib/zip-selection.ts` and shared by the dropzone, the start-page drop target, and both
compact pickers; `ZipFilePicker.svelte` resets its input after every selection so the
same file can be chosen again.
- Workspace files are processed concurrently. PDF files become single-attachment archives via
`pdfFileToArchive`; ZIP files go through `extractZipArchives`. `archiveGeneration` prevents
results from an old batch from reappearing after the user deletes all archives **or** confirms a
replacement.
- `ProcessedZipArchiveEditor.svelte` edits one archive and emits immutable replacements through
`onArchiveChange`; the route-level archive array remains the source of truth.
- Attachments may remain loose or belong to exactly one sub-document. Drag-and-drop supports
reordering and moving attachments between lists, while sub-document headers can be reordered.
- **Gruppieren** (grouping mode) offers archive-wide multi-select over loose and sub-document
attachments: create a new Teildokument from the selection, move the selection into an existing
Teildokument, select all/none. Exiting with **Fertig** or Escape clears the pending selection.
New Teildokumente are numbered via `nextSubDocumentNumber` (max existing trailing number, not
list length), and the created block's name input is focused and scrolled into view.
- Each attachment card has an **“⋯” menu** (`AttachmentMenu.svelte`) as the keyboard/touch path:
move into an existing or new Teildokument, move back to the loose list, split PDFs, and remove
(with confirmation). It must never become the only way to perform an action that drag-and-drop
also covers.
- **PDF splitting**: `PdfSplitDialog.svelte` opens per PDF attachment, renders pages through
`loadPdfDocument` + `PdfPageGrid`, and assigns page sets to named groups (manual selection,
“Jede Seite einzeln”, or “Alle N Seiten”). Cut is the default; “Seiten im Original behalten”
switches to copy. `applyPdfSplit` replaces the source with the remaining pages (or removes it),
inserts the new Teildokumente after the source's parent block, and the synthetic attachments
get unique `#seiten-…` path suffixes because `path` is the identity for keyed lists, drag
payloads, and selection.
- Per-archive export supports one flattened PDF or separate PDFs for sub-documents. Each
sub-document also has its own direct download action that exports only that sub-document's
attachments. Bulk export from the page always creates one flattened PDF per archive.
When changing archive transforms, preserve these invariants:
- An attachment move must not duplicate or discard data. Splitting cuts pages by default and only
duplicates them when the user explicitly chooses copy mode.
- Sub-document IDs remain stable across edits and reordering.
- Single-PDF order is all sub-documents in order, each attachment in order, followed by loose
attachments in order.
- Dissolving a sub-document appends its attachments to the loose list.
- Splitting inserts the new Teildokumente directly after the source's parent block (loose source:
appended at the end) so the reading order is preserved.
- State helpers in `zip-processing.ts` should return new archive/list objects rather than mutating
component-owned state.
## ZIP, XML, and PDF processing
All case-file processing is intentionally local to the browser.
- `extractZipArchives` recursively inflates nested ZIP files.
- Ignore macOS metadata (`__MACOSX`, `.DS_Store`, and `._*`) and unsupported entries.
- `xjustiz_nachricht.xml` is metadata, not an export attachment. Its sender, receiver, and declared
document names determine the generated PDF name and preferred attachment order.
- XJustiz element matching must remain namespace tolerant. Malformed or absent metadata falls back
safely instead of preventing extraction.
- Supported attachments are PDFs and the image extensions listed in `zip-processing.ts`. Update the
extension classification and downstream preview/export behavior together when adding formats.
- PDF attachments are copied with pdf-lib. JPEG and PNG files can be embedded directly; other image
formats are decoded through browser image/canvas APIs and converted to PNG first.
- Page-set copies (separate/extract tools, PDF splitting) share
`copyPagesToNewPdf(sourceBytes, pageIndexes)` in `src/lib/pdf-processing.ts` so the copy
semantics cannot drift apart.
- `src/lib/pdf-text-overlay.ts` owns text fitting, the rotated-box placement and the optional
`/Rotate` compensation. Stamp keeps `compensatePageRotation` off so its output does not change;
pagination turns it on so numbers stay at the visual position on rotated scans. `drawImageOverlay`
is the image counterpart. Share both instead of re-implementing overlay maths in a route, and take
the margin/padding presets (`TEXT_OVERLAY_MARGIN`, `TEXT_OVERLAY_PADDING`,
`MIN_TEXT_OVERLAY_SIZE`) from the module rather than redefining them locally.
- `src/lib/pdf-errors.ts` classifies PDF failures (`classifyPdfError`, `TextOverlayError`,
`isEncryptedPdfError`) and owns the German copy (`pdfLoadErrorMessage`, `pdfProcessingErrorMessage`,
both with a tool-specific fallback). All tools use it; never match on pdf-lib/pdfjs error text in a
route, and never copy the password hint locally.
- `src/lib/pdf-session.svelte.ts` (`createPdfSession`) is the shared single-PDF lifecycle for rotate,
separate and pagination: it reads the bytes once, builds the tool handle, maps load errors and
invalidates stale loads. Keep new single-PDF tools on it instead of re-declaring
`file`/`bytes`/`handle`/`isLoading`/`loadingError` and a generation counter.
- `src/lib/latest-task.ts` (`createLatestToken`) is the guard for "only the newest async call may
publish its result" (file loads, preview renders). Do not add another hand-rolled counter.
- Markup is shared the same way: `ToolError.svelte` owns the red `role="alert"` banner (never
inline the classes in a route), `SinglePdfTool.svelte` owns the header + dropzone + toolbar +
footer layout for one-PDF tools (rotate, separate — keep new ones on it), and
`PaginationPreview.svelte` owns the page preview with its shrink hints. Tool-specific copy is
passed in as props; keep it out of the shared components.
- `src/lib/pending-files.ts` hands files between routes. Queue with an explicit target
(`queuePendingFiles(files, 'bea' | 'pagination')`) and consume with `takePendingFiles(target)` so a
tool cannot silently swallow a file queued for another one.
- `src/lib/pdf-thumbnails.ts` offers two loaders: `loadPdfDocument` (all thumbnails, for pickers and
page grids) and `loadPdfPageRenderer` (one page at a time, for long documents). Both share
`PdfPageImage` and one rendering helper, revoke object URLs and tear down the pdfjs loading task —
including when the initial `getDocument` promise rejects. Superseded renders throw
`PreviewRenderCancelledError`, which callers swallow instead of logging.
- `AttachmentPreview.svelte` dynamically loads pdfjs-dist in `onMount`, copies PDF bytes before
rendering, and revokes object URLs during cleanup. Preserve those memory and data-integrity
safeguards.
- Empty archives/export groups should continue to fail explicitly rather than creating invalid PDFs.
Browser-only APIs used by this flow include `File`, `DOMParser`, `Image`, canvas, `Blob`, `URL`,
localStorage, and `crypto.randomUUID()`. Do not invoke browser-only work during SSR or at module
initialization. Keep it in event handlers, `onMount`, effects guarded by hydration, or functions only
called in the browser.
## Batch processing pattern for single-PDF tools
Tools that transform one PDF per run (compress, watermark, stamp, encrypt, decrypt) accept multiple
files. Follow the established compress retrofit when adding the pattern to another tool; see
`plan/done/batch-processing-plan.md` for the full design.
- `src/lib/batch.ts` owns the engine: `createBatchItem(file)`, `runBatch(items, process, update,
options?)`, `formatBytes`, `batchStatusLabel`/`batchStatusClass` and
`shouldWarnAboutBatchMemory`. `process` is `(file, item) => Promise<{ bytes, name }>`: the item is
passed so a tool can resolve per-row data frozen at run start (the Anlagen tool resolves its
exhibit number that way; `(file) => …` callbacks stay valid). `runBatch` processes strictly
sequentially (one PDF in flight for memory safety), never throws — per-item failures are caught
and written to the item as a German error message (a string, or a formatter that can inspect the
thrown error) — and consults the optional `shouldProcess` predicate so rows removed mid-run are
skipped.
- `src/lib/batch.ts` also owns the bulk export: `uniqueEntryName` (duplicate suffixes) and
`downloadBatchZip(items, zipBaseName)` → `{toolname}_{date}.zip`. Keep new batch tools on it
instead of building ZIPs inside a component.
- `src/lib/components/BatchFileList.svelte` renders the rows: name, size, status chip (`batchStatusClass`/`batchStatusLabel`)
(Wartet / Verarbeite… / Fertig ✓ / Fehler), per-row download, remove, and retry buttons with
accessible names including the file name, a progress line (“Datei 3 von 7 wird verarbeitet…”), a
total result-size summary, a memory warning (>20 files or any file >25 MB), and the bulk
“Alle herunterladen” ZIP action (via fflate, named `{toolname}_{date}.zip`) shown once ≥2 results
exist. Duplicate result names inside the ZIP are suffixed instead of overwriting.
- Page wiring: keep `items = $state<BatchItem[]>([])`, append newly dropped files (do not replace;
deduplicate by name + size), pass a `process(file)` callback that reads the current option state
so option changes mid-run affect only subsequent files, and gate the run button on pending items.
Files too large to rasterize (>100 MB) are marked as error rows immediately on selection instead
of blocking the batch. Keep thumbnails, previews, and page-specific selection to a single dropped
file; multi-file runs apply the tool to every page of every file.
- `src/lib/components/ExhibitFileList.svelte` is the other batch list, for tools where order and a
derived per-row label matter (currently `/tools/exhibits`): drag handle plus up/down buttons that
both call one `onMove(from, to)`, position badge, the resolved label, the renamed file name, and
the same status/download/retry/remove/ZIP surface as `BatchFileList`. Reordering and preview
selection are disabled while a run is active.
- Per-tool ZIP base names: compress uses `komprimiert`, watermark `wasserzeichen`, stamp `stempel`,
exhibits `anlagen`, encrypt `geschuetzt`, decrypt `entsperrt`. Choose an analogous German name
per tool and cover the
batch flow with Playwright tests (multi-file ZIP contents, failure continuation, retry, removal).
## Svelte conventions
- Use Svelte 5 runes and current event syntax: `$props()`, `$state`, `$derived`, `$effect`, snippets,
`{@render ...}`, and handlers such as `onclick` rather than legacy APIs.
- Keep component scripts typed with `<script lang="ts">`.
- Define a local `Props` type for non-trivial component props.
- Use callback props for child-to-parent updates; do not mutate prop objects in place.
- Use `$lib/...` aliases for application imports instead of long relative paths.
- Name reusable components in PascalCase and route files according to SvelteKit conventions.
- Preserve semantic HTML, accessible names, visible focus states, and keyboard alternatives. Do not
make drag-and-drop the only way to perform a critical action.
- Clean up global listeners, object URLs, PDF loading tasks, and other browser resources.
The Vite configuration forces runes mode for project files, so do not introduce legacy Svelte
component patterns.
## shadcn-svelte and component conventions
`components.json` configures the official shadcn-svelte registry, the `nova` style, Lucide icons, and
these aliases:
- UI components: `$lib/components/ui`
- General components: `$lib/components`
- Utilities: `$lib/utils`
Use existing local UI components before creating replacements. Follow the exports actually present
on disk: the card is composed through `import * as Card from '$lib/components/ui/card'`, while the
current button is a default import from `$lib/components/ui/button.svelte`.
When adding a registry component, use the project runner, inspect every generated file, and preserve
local customizations:
```sh
pnpm dlx shadcn-svelte@latest add <component>
```
Do not overwrite or bulk-update customized components without explicit approval. Use `cn()` for
conditional class merging and existing variants before adding one-off component styling.
## Styling conventions
Use Tailwind utilities in Svelte markup and semantic theme tokens from `src/routes/layout.css`.
- Prefer `bg-background`, `text-foreground`, `bg-primary`, `text-muted-foreground`, `border-border`,
and other configured semantic utilities over raw brand colors.
- Preserve the established Nominandum visual language: pale blue-gray background, white cards, dark
blue primary text, red/coral secondary actions, rounded containers, and Nunito typography.
- Use `flex`/`grid` with `gap-*`, responsive variants, and `size-*` when width and height are equal.
- Use `cn()` rather than constructing partial Tailwind class names. Keep complete class names visible
to Tailwind's scanner.
- Prefer component variants and theme variables over overriding UI-component colors in call sites.
- Add global CSS only for imports, tokens, font faces, animations, or behavior that utilities cannot
reasonably express.
- Tailwind 4 is configured through Vite and `layout.css`; there is no `tailwind.config` file.
- Maintain contrast, responsive layouts, drag-state feedback, and visible focus treatment.
Theme-level changes belong in `src/routes/layout.css`. Do not reintroduce the removed Skeleton theme
or refer to the old `nom-theme.css` architecture.
## Formatting
Follow the checked-in Prettier configuration:
- Tabs for indentation
- Single quotes in scripts and styles where supported
- No trailing commas
- 100-character print width
- Svelte-aware and Tailwind-aware formatting
Use `pnpm exec prettier --write <changed-files>` for targeted formatting. Avoid formatting unrelated
files solely to clean up existing differences.
## Product and content constraints
- User-facing copy is German unless a task explicitly requests another language.
- Preserve legal text in `datenschutz/+page.svelte` and `impressum/+page.svelte` unless explicitly
asked to change it.
- Keep the Nominandum logo, footer artwork, and theme consistent across routes.
- ZIP selection accepts multiple files matching `.zip`, `application/zip`, or
`application/x-zip-compressed`.
- Resetting the file input after selection is intentional so the same archive can be selected again.
- Do not add uploads, network requests, server processing, analytics, or case-data persistence without
an explicit requirement and privacy review.
## Change discipline
- Keep changes scoped and preserve unrelated work, including untracked files.
- Inspect existing components, archive helpers, and theme tokens before adding abstractions.
- Keep browser processing behavior and archive-order invariants covered when editing shared helpers.
- Do not hand-edit generated output or lockfile contents. Update the lockfile only through pnpm when
dependency changes are required.
- Avoid dependencies when the task can be completed with Svelte, browser APIs, Tailwind, or existing
packages.
- After implementation, review changed files, run relevant checks, and mention remaining warnings or
limitations.