diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..93bf04c
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,32 @@
+# Working on OpenCull
+
+OpenCull is an offline photo-culling desktop application: React/TypeScript in `src/`, Rust/Tauri in `src-tauri/src/`. Read `README.md` and `docs/HOW-IT-WORKS.md` before changing behavior.
+
+## Branches and review
+
+- Implement one feature per short-lived `feature/` branch. Use `fix/`, `docs/`, or `chore/` for other work. Never implement directly on `main`.
+- Independent features start from current `upstream/main` so the original maintainer can select them independently. Default PR target is `wispdevon/OpenCull:main`; submit upstream only when requested.
+- Fetch before choosing a base. Keep upstream synchronization in a separate PR. Do not bundle other feature commits into a standalone feature branch.
+- Combine features only on a separate `integration/` branch. Document integration-only fixes there, including conflict resolutions.
+- Inspect Git status first. Preserve unrelated edits and use separate worktrees when another branch is dirty or work runs concurrently. Never reset or stash someone else's work.
+- Keep commits and PRs focused; separate unrelated refactoring, formatting, dependency changes, and lockfile changes. Review staged diffs before committing.
+- Do not force-push shared branches. Resolve conflicts deliberately and rerun affected checks. Merge only when requested; remove merged feature branches after completion.
+
+## Verification
+
+- `npm ci` installs the locked dependencies. Use Node 22.12+ (CI uses Node 22).
+- `npm run test:web` runs frontend tests; `npm run test:rust` runs Rust library tests; `npm test` runs both.
+- `npm run build` checks TypeScript and builds the frontend. There is no lint script: do not claim one passed.
+- For desktop changes, also run `npm run tauri build -- --debug --no-bundle` and relevant runtime checks. Linux requires GTK/WebKitGTK and the Tauri prerequisites; see the README and CI.
+- Add focused regression tests for behavior, using existing Vitest and inline Rust test conventions. Do not weaken checks to make a branch pass.
+- Documentation-only work needs diff/link review, not an application rebuild.
+- PR descriptions explain the problem, resulting behavior, validation, platform limitations, and any prerequisites. Report blocked checks honestly.
+
+## Product boundaries
+
+- Keep photos private: processing stays local; no remote font or media requests.
+- Preserve original files. Card review, rejecting, and copying must never delete sources. Source deletion must be a distinct explicitly confirmed action.
+- Preserve sidecar compatibility, RAW/JPEG associations, keyboard navigation, and existing normal-folder behavior.
+- Settings belong behind the shared preferences interface. Retain existing defaults in upstream-facing features; personal defaults belong in local configuration.
+- Support Linux without introducing macOS/Windows regressions. Test on available platforms and state what was not tested.
+- Keep browser demo and native Tauri paths working. Use semantic theme tokens, accessible labels, and the existing UI component system.
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 0000000..2cf0522
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,29 @@
+# OpenCull interface design guidelines
+
+These rules apply across Home, ingest, culling, settings, menus and dialogs. Wisp is the reference theme for readable interface contrast; other presets retain their visual character.
+
+## Readability and surfaces
+
+Use semantic tokens for background, panel, row, input, selected state, text and focus. Avoid a single grey for every level. Wisp separates an ink workspace (#111827), navy panels (#162033), raised rows (#1d293d), and controls (#2b3b55). Borders (#71839f) identify controls and setting boundaries. Primary text is #f3f6ff; secondary copy is #bac8df. Violet (#b7a6ff) marks primary actions and active controls. Light mode uses paper surfaces with dark ink and a deeper violet.
+
+Normal text must reach 4.5:1 against its actual background; large text, icons and necessary control boundaries must reach 3:1. Test computed colours in every colour mode, including hover and selected states. Do not lower descriptive copy opacity until it becomes difficult to read. Colour never carries state alone: use position, border, checkmark, text or icon as well.
+
+## Hierarchy and density
+
+Keep identity and supporting text above a horizontal action row. Use an 8px spacing rhythm: 8 within groups, 16 between controls or rows, 24 between sections, 32 before image collections. Setting rows have their own surface, 1px visible border, 8px radius and 16px padding; title and description belong together, control aligned at the start. Do not put borders around every text fragment.
+
+Titles use a clear weight and size hierarchy. Home headings can scale from 36–56px with bounded container sizing. Section headings use 22–32px. Setting labels use 14px semibold; explanatory text uses 13px with 1.6 line height. Retain the user's font preference and native interface scale. Avoid fixed heights on text blocks; allow wrapping and vertical scrolling.
+
+Controls need a 40px hit area where practical, with a visible keyboard focus outline separated from their edge. Selected tabs need both a border and stronger text; checked switches need a contrasting thumb position and track. Disabled controls retain labels and never look active. Menus, dialogs and popovers use opaque readable surfaces. Dialog content stays within the viewport and scrolls on smaller windows.
+
+## Photograph-first workspace
+
+Photo pixels, marks and rating colours are never recoloured by the theme. Keep the loupe surround neutral; 18% grey is #767676. Native-aspect strips use displayed rotation and crop proportions. Keep selection rings, rank labels and resize edges visible over mixed photographs. Theme accents belong on interface controls, not as a tint over previews.
+
+Recent folder contact sheets stay visible. Compact presets may reduce card dimensions without turning images into text-only entries. Bulk actions appear after the first selection. Removing recent history must clearly distinguish itself from deleting source photos. Errors use readable text, not just red borders. Loading retains useful previews where possible.
+
+## Verification
+
+Check Home, ingest, culling and all Settings tabs in dark, light and 18% grey, at small and large windows and at 100%, 150% and 200% native interface scale. Verify keyboard focus, scrolling, switch/tab state contrast and forced colours. Use screenshots plus measured contrast; do not infer accessibility from palette names.
+
+Wisp is supplied by feature/wisp-theme and depends on feature/rapidraw-theme's preset infrastructure. It changes no personal default automatically and loads no remote fonts or assets.
diff --git a/README.md b/README.md
index 526d509..65a2dad 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,70 @@
+## Run this fork with all features
+
+The `integration/linux-features` branch combines every feature in this fork.
+No manual merging or cherry-picking is needed. Individual feature branches
+remain available for selective upstream review; fork `main` contains general
+tooling fixes rather than the combined application.
+
+Install [Rust stable](https://rustup.rs) and the
+[native Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) first:
+
+- **macOS:** install Xcode Command Line Tools (`xcode-select --install`), then
+ `brew install jpeg-turbo pkgconf`. Tauri automatically uses the macOS
+ `turbo-system` configuration.
+- **Linux:** install GTK/WebKitGTK prerequisites plus CMake and NASM (NASM is
+ needed on Intel/AMD). Tauri uses the Linux `turbo` configuration.
+- **Windows:** install the Visual Studio C++ and WebView2 prerequisites plus
+ CMake and NASM. Tauri uses the Windows `turbo` configuration.
+
+Then, using nvm (or your platform's Node version manager):
+
+```sh
+git clone --branch integration/linux-features --single-branch https://github.com/wispdevon/OpenCull.git
+cd OpenCull
+nvm install
+nvm use
+npm install --global npm@11.21.1
+npm ci
+npm run dev:desktop
+```
+
+Without nvm, install Node 22.23.3 and npm 11.21.1 using your preferred version
+manager, or run the last two commands through the pinned runtime:
+
+```sh
+npx --yes --package=node@22.23.3 --package=npm@11.21.1 --call 'npm ci && npm run dev:desktop'
+```
+
+The desktop development command starts the frontend and native backend
+together, with live frontend reloads and incremental Rust recompilation.
+It does not create a production bundle or installers. The initial native
+compile can take several minutes. On affected Linux/Wayland systems, use
+`WEBKIT_DISABLE_DMABUF_RENDERER=1 npm run dev:desktop`.
+
+All features are included: cull-before-import sessions, multiple card sources,
+specific card subfolders, configurable panels, video inclusion/playback,
+appearance modes, offline fonts, the optional RapidRAW inspired design,
+larger UI headings, XDG configuration, remembered/default view modes, Wisp contrast,
+saved interface scaling, recent session deletion, resizable stack strips,
+experimental native-aspect strips and eased zoom, and optional NVIDIA rendering profiles.
+Optional preferences are selectable in Settings; the RapidRAW design is under
+**Culling → Design theme**, and view/panel controls are under **Layout**.
+Linux settings are created in `$XDG_CONFIG_HOME/opencull/config.json`, normally
+`~/.config/opencull/config.json`. No personal configuration file is required.
+macOS and Windows use Tauri's native application-config directory instead.
+The Linux video streaming workaround is compiled only on Linux; macOS keeps
+native asset-protocol playback. `dev:desktop` sets no Linux environment flags.
+
+For a Mac tester, use the [macOS trial guide](docs/TRY-ON-MACOS.md).
+NVIDIA rendering profiles are Linux-only; macOS uses its native webview automatically.
+
+Run `npm run check` for automated checks. See the
+[feature and validation map](docs/features/VALIDATION.md) for individual PRs
+and platform limitations.
+
Table of Contents
@@ -155,7 +219,7 @@ also need the system packages on
1. Clone the repo
```sh
- git clone https://github.com/NatePhotos/OpenCull.git
+ git clone --branch integration/linux-features --single-branch https://github.com/wispdevon/OpenCull.git
cd OpenCull
```
2. Install the packages
diff --git a/docs/TRY-ON-MACOS.md b/docs/TRY-ON-MACOS.md
new file mode 100644
index 0000000..396023b
--- /dev/null
+++ b/docs/TRY-ON-MACOS.md
@@ -0,0 +1,39 @@
+# Try every OpenCull feature on macOS
+
+Share this link: https://github.com/wispdevon/OpenCull/tree/integration/linux-features
+
+The integration branch already combines the feature branches. Do not merge branches by hand or use fork main for the combined preview. Experimental features are available in Settings and remain opt-in. The discarded mountain welcome-page demo is excluded.
+
+## Prerequisites
+
+Install Xcode Command Line Tools (`xcode-select --install`), Homebrew, Rust stable from https://rustup.rs, and Node 22.23.3 (for example with nvm). Then:
+
+```sh
+brew install jpeg-turbo pkgconf
+git clone --branch integration/linux-features --single-branch https://github.com/wispdevon/OpenCull.git
+cd OpenCull
+nvm install
+nvm use
+npm install --global npm@11.21.1
+npm ci
+npm run dev:desktop
+```
+
+If Node 22.23.3 and npm 11.21.1 are already installed, omit the nvm and global npm steps. The first Rust compile can take several minutes. Keep the terminal open. This starts the Vite frontend and native Tauri backend together; it does not create an installer. A browser-only preview cannot ingest actual cards or exercise native image decoding.
+
+macOS automatically uses the Homebrew libjpeg-turbo configuration. Do not set Linux WebKit, DMA-BUF, CUDA or NVIDIA flags on a Mac. The NVIDIA profile commands intentionally apply only to Linux. Settings use the native macOS application-config directory; no personal Linux config is needed.
+
+## Things to try
+
+- Ingest from Card: navigate to a DCIM subfolder, add multiple cards to one review session, cull before copying, and resume through Recent Sessions.
+- Settings → Culling: choose Wisp, OpenCull or RapidRAW inspired design; choose colour mode and font. Wisp separates setting rows and active controls with stronger contrast.
+- Settings → Layout: interface scale (80–200%), default/remembered view, stack filmstrip placement. Drag each strip's top edge to resize; F hides both stack strips. Resize edges support arrow keys as well.
+- Settings → Culling → Experimental Features: native-aspect strips, including the stack overview; decoded JPEG dimensions allow early sizing before complete metadata.
+- Settings → Zoom → Experimental Features: eased click zoom. This affects motion curves, not the speed of CPU image decoding.
+- Select one recent item to reveal bulk controls. Deleting a session removes saved review history, never source photos.
+
+Use a copy of a small shoot for an initial trial. `npm run check` runs frontend tests/build and Rust tests. Update with `git pull --ff-only`, `npm ci`, then `npm run dev:desktop`.
+
+## Verification boundaries
+
+The earlier combined snapshot passed the macOS Apple-silicon CI tests and build. The final branch's CI additionally starts `dev:desktop`, checks Vite and the native process, and observes them through startup. This is a startup smoke test, not a visual, card-reader or performance test on the recipient's Mac. Intel macOS is not covered by the Apple-silicon runner. See the latest integration Build workflow for the exact commit and result.
diff --git a/docs/features/EXPERIMENTAL_FILMSTRIP.md b/docs/features/EXPERIMENTAL_FILMSTRIP.md
new file mode 100644
index 0000000..b146a97
--- /dev/null
+++ b/docs/features/EXPERIMENTAL_FILMSTRIP.md
@@ -0,0 +1 @@
+Settings → Culling → Experimental Features → Native-aspect filmstrip. Disabled by default. Uses displayed rotation/crop proportions at a common height, preserving the existing virtualized horizontal strip. Unknown dimensions temporarily use 3:2 until metadata arrives. No masonry CSS dependency or photo changes.
diff --git a/docs/features/EXPERIMENTAL_ZOOM.md b/docs/features/EXPERIMENTAL_ZOOM.md
new file mode 100644
index 0000000..2eb1f92
--- /dev/null
+++ b/docs/features/EXPERIMENTAL_ZOOM.md
@@ -0,0 +1 @@
+Settings → Zoom → Experimental Features → Smooth click zoom. Disabled by default. Click/keyboard/face/marquee zoom uses a fast-start, slow-stop cubic curve with 280–340 ms durations. Reduced motion disables these transitions. Wheel zoom and dragging remain immediate. Existing sharp rerasterization after motion is retained. This is an experimental motion option, not a promise of native macOS performance.
diff --git a/docs/features/INTERFACE_SCALE.md b/docs/features/INTERFACE_SCALE.md
new file mode 100644
index 0000000..8a5a2e7
--- /dev/null
+++ b/docs/features/INTERFACE_SCALE.md
@@ -0,0 +1,7 @@
+# Interface scale
+
+Saved desktop preference under Settings → Culling in the standalone feature, and Layout in the combined app. Uses Tauri Webview.setZoom so pixels, rems, menus, controls, and pointer coordinate systems scale together. Supports 80–200%, default 100%, plus Reset. Uses shared preferences. Browser demos retain browser zoom controls rather than simulating page zoom with CSS transforms.
+
+macOS requires 11+ (Tauri API requirement). Linux and Windows use their native webview zoom implementation. A narrowly scoped core:webview:allow-set-webview-zoom permission is added. Errors remain visible and unsuccessful changes are not persisted.
+
+Settings stay within the viewport, tabs wrap, settings content scrolls on short windows, and the culling toolbar wraps rather than clipping controls as usable CSS space decreases.
diff --git a/docs/features/NVIDIA_GRAPHICS.md b/docs/features/NVIDIA_GRAPHICS.md
new file mode 100644
index 0000000..5dc2940
--- /dev/null
+++ b/docs/features/NVIDIA_GRAPHICS.md
@@ -0,0 +1,9 @@
+# NVIDIA graphics rendering
+
+Independent feature branch: feature/nvidia-gpu-acceleration. Uses the native graphics stack for webview compositing, photo transformations, WebGL and supported 2D canvas rendering. It does not implement CUDA image decoding or change ONNX inference. WebKit still decides which operations are accelerated.
+
+Linux: `npm run dev:desktop:nvidia` requests hardware compositing, enables WebGL and supported accelerated canvas, and removes inherited software/compositing-disable overrides before GTK initializes. It uses `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and `WEBKIT_DMABUF_RENDERER_DISABLE_GBM=1` rather than disabling the DMA-BUF renderer and prefers XWayland when an X display is available and no backend was explicitly requested, because direct DMA-BUF/GBM presentation failed with this test machine’s proprietary driver. This leaves accelerated compositing enabled while changing buffer transport. A native CSS-only animation probe on this RTX 3060 Ti allocated 53 MiB in the WebKit graphics process; no WebGL probe was needed to produce that allocation. `npm run dev:desktop:compatibility` retains the DMA-BUF workaround; `npm run dev:desktop:software` explicitly requests a software path for comparison. The ordinary desktop command remains automatic and changes no environment flags. Packaged binaries accept `--graphics=nvidia`, `--graphics=compatibility`, `--graphics=software`, or `OPENCULL_GRAPHICS`. Linux-only code and dependencies leave macOS/Windows native rendering unchanged.
+
+Settings → Storage → Graphics acceleration provides a renderer probe. An NVIDIA WebGL renderer is evidence for that context, not proof that every rendering operation is GPU accelerated. Verify native WebKit process GPU device use and frame timings as well. These profiles do not install drivers or edit system configuration. On hybrid systems, choose the discrete GPU through the system's normal offload launcher.
+
+Reference: https://docs.webkit.org/Ports/WebKitGTK%20and%20WPE%20WebKit/Graphics.html and the WebKitGTK HardwareAccelerationPolicy API. The NVIDIA profile requests Always; it does not claim a specific performance improvement. Compatibility remains available for driver/compositor issues.
diff --git a/docs/features/README.md b/docs/features/README.md
new file mode 100644
index 0000000..d3f49cf
--- /dev/null
+++ b/docs/features/README.md
@@ -0,0 +1,15 @@
+# Independent feature initiative
+
+Approved feature branches (each starts at upstream main):
+
+- `feature/cull-before-ingest`: reuse culling before copying; local source-safe marks; choose files at finish; copy or import in place.
+- `feature/multi-card-session`: combine sources; conservatively pair RAW/JPEG across cards; one session.
+- `feature/configurable-ui`: settings for panel visibility and sizes, preserving defaults.
+- `feature/appearance-modes`: dark, light, 18% grey; preserve default dark.
+- `feature/font-selection`: system, offline Space Grotesk, custom local family; preserve default system.
+- `feature/video-culling`: ignore toggle, supported playback, import selection; default ignore.
+- `feature/xdg-config`: atomic native settings, Linux XDG path, migration from webview preferences.
+
+No feature requires another. Combined behavior and overlap resolutions belong on `integration/linux-features`. No automatic merges or upstream submissions.
+
+Personal configuration: Space Grotesk, 18% grey, existing panel visibility/sizes, videos ignored. Card workflow reviews first, then chooses Copy (initially all non-rejects) or Import in place (only selected members). No card writes during review or implicit deletion.
diff --git a/docs/features/RECENT_SESSIONS.md b/docs/features/RECENT_SESSIONS.md
new file mode 100644
index 0000000..a2343b9
--- /dev/null
+++ b/docs/features/RECENT_SESSIONS.md
@@ -0,0 +1 @@
+Depends on feature/cull-before-ingest. Card sessions are retained individually and shown under Recent Sessions. Legacy latest.json remains readable. Resume Latest Session compares card review save time with the last successfully opened normal folder. Missing cards produce a reconnect message. Source photos are never modified by session history.
diff --git a/docs/features/STACK_FILMSTRIP.md b/docs/features/STACK_FILMSTRIP.md
new file mode 100644
index 0000000..7b1a111
--- /dev/null
+++ b/docs/features/STACK_FILMSTRIP.md
@@ -0,0 +1 @@
+Settings → Culling → Stack filmstrip placement. Classic is the default. Keep normal filmstrip placement puts active stack frames in the shared full-width bottom filmstrip; the scene overview remains above. The existing show-filmstrip preference applies.
diff --git a/docs/features/VALIDATION.md b/docs/features/VALIDATION.md
new file mode 100644
index 0000000..9d7d123
--- /dev/null
+++ b/docs/features/VALIDATION.md
@@ -0,0 +1,139 @@
+# Linux feature integration validation
+
+The seven feature branches start independently at upstream release 0.4.1. Each has its own fork PR; none requires the others. `integration/linux-features` combines them and resolves their overlapping settings, session, and ingest interfaces. General Node/toolchain fixes are committed directly to fork `main` and merged into integration.
+
+## Review units
+
+| Change | Branch | Fork PR |
+|---|---|---|
+| Agent workflow | `docs/agent-workflow` | [#1](https://github.com/wispdevon/OpenCull/pull/1) |
+| Workspace controls | `feature/configurable-ui` | [#2](https://github.com/wispdevon/OpenCull/pull/2) |
+| Offline fonts | `feature/font-selection` | [#3](https://github.com/wispdevon/OpenCull/pull/3) |
+| Dark/light/18% grey | `feature/appearance-modes` | [#4](https://github.com/wispdevon/OpenCull/pull/4) |
+| XDG configuration | `feature/xdg-config` | [#5](https://github.com/wispdevon/OpenCull/pull/5) |
+| Cull before import | `feature/cull-before-ingest` | [#6](https://github.com/wispdevon/OpenCull/pull/6) |
+| Multi-card sources | `feature/multi-card-session` | [#7](https://github.com/wispdevon/OpenCull/pull/7) |
+| Video playback | `feature/video-culling` | [#8](https://github.com/wispdevon/OpenCull/pull/8) |
+| RapidRAW inspired design | `feature/rapidraw-theme` | [#10](https://github.com/wispdevon/OpenCull/pull/10) |
+| Precise card subfolders | `feature/ingest-subfolders` | [#11](https://github.com/wispdevon/OpenCull/pull/11) |
+| Larger headings and review guidance | `feature/ui-changes` | [#12](https://github.com/wispdevon/OpenCull/pull/12) |
+| Default and remembered views | `feature/view-mode-preferences` | [#13](https://github.com/wispdevon/OpenCull/pull/13) |
+
+The design and subfolder branches also start directly from upstream. The UI
+branch starts from `feature/cull-before-ingest`, because its instruction text
+describes that workflow; its PR targets that branch to isolate the UI diff.
+
+## Additional welcome, folder, and heading checks
+
+The latest integration passes 52 frontend tests and a Linux debug desktop
+build. Standalone design/subfolder/UI branches pass their frontend tests and
+TypeScript/Vite builds. No Rust logic changed in these additions.
+
+The in-app browser remained unavailable, so a separate headless Chromium
+instance checked the rendered screens. No existing user browser was controlled.
+Theme checks covered selection, reload persistence, offline font application,
+font opt-out, restoration of the default design, and 1440px/600px layouts without
+horizontal overflow or JavaScript errors. Combined light/dark/grey modes keep
+the expected native colour scheme and grey's `#767676` photo canvas.
+
+Folder checks observed no photo scan when navigating CARD or DCIM, a scan of
+only `DCIM/100CANON` after selecting it, and an explicit parent selection with
+**Use this folder**. Combined multi-source selection retained earlier sources.
+Heading checks verified a 24px ingest heading, 14px instruction immediately
+below it, no duplicate sidebar instruction, and 20px shared dialog titles.
+
+View preferences add a default opening view and an enabled-by-default
+**Remember last view** control under **Settings → Layout**. The standalone
+branch passes 35 frontend tests and a TypeScript/Vite build. Combined browser
+checks verify Filmstrip/Loupe, Stack/Scene, and Grid across folder changes and
+reloads, explicit defaults with remembering disabled, and keyboard changes
+written to preferences. The runtime check follows the UI's nonblocking folder
+opening path and waits for the listing rather than for background metadata.
+
+The updated executable opened successfully in the user's Wayland session with
+`WEBKIT_DISABLE_DMABUF_RENDERER=1`. An unmodified launch previously exited with
+a Wayland protocol error. The previous app window was left running so its
+pending card selection was preserved.
+
+## Verified locally
+
+- Clean-clone quickstart: a fresh remote clone of `integration/linux-features`
+ installed with pinned `npm ci` and passed `npm run check` (52 frontend tests,
+ production frontend build, and 283 Rust tests). The clone contained no local
+ patches or dependency symlinks. Cargo reused cached native dependencies.
+- `npm run dev:desktop` passed its Node guard, started Vite on port 1420, and
+ opened the real desktop app in the user's Wayland session. This uses the
+ development URL and Rust watcher rather than a packaged frontend. The Linux
+ graphics workaround was supplied only to that local process's environment.
+- Platform review: Linux video streaming and XDG path code/tests are guarded by
+ Linux target configuration. macOS uses its native app-config location,
+ asset-protocol video playback, and `turbo-system` JPEG feature. macOS setup/CI
+ now explicitly installs Homebrew `jpeg-turbo` and `pkgconf`. Native macOS
+ verification is tracked by [the cross-platform run](https://github.com/wispdevon/OpenCull/actions/runs/38796608300);
+ it was still in progress when the local desktop development session opened.
+- Toolchain: Node 22.23.3, npm 11.21.1. Clean `npm ci`, then `npm run check`: 46 frontend tests, frontend production build, and 283 Rust library tests pass on the combined branch. Fork main separately passes its 31 frontend tests, production build, and 268 Rust tests.
+- Desktop: `npm run build:desktop -- --debug --no-bundle` succeeds on Linux with libjpeg-turbo enabled.
+- Native self-test: opens files, loads previews, transfers without changing originals, and exercises the new features through real Tauri IPC and the existing store.
+- Separate synthetic RAW/JPEG cards pair into shared entries. Local marks survive save/load; reviewing creates no source sidecars. Selected-only copying includes companions, and import in place retains only selected members and their marks.
+- MP4 metadata, playback, pause, and seeking work on Linux. WebKitGTK cannot play the custom `asset:` scheme directly, even with a working H.264 decoder. The Linux feature uses a bounded, token-protected loopback HTTP stream, with range/authorization tests. Other systems retain Tauri's scoped asset protocol.
+- Offline Space Grotesk loads. Theme controls change the actual root theme; dark/light/grey appearances and layout controls render. Native screenshots were inspected outside the repository.
+- A malformed config remains byte-for-byte intact and displays a recovery banner while the app remains usable. An unwritable config root creates no partial file and displays the same recovery path.
+- SHA-256 hashes and file membership of all nine synthetic source files remain unchanged after native checks. Test photos, videos, snapshots, and session data are outside the repository.
+
+## Reproducing native feature checks
+
+Use an isolated XDG config/data/cache directory. The combined native `--selftest ` runs baseline checks and the additional integration checks. Supply two or more still images; include a playable MP4 to test video. An optional `selfTestSources` array in the temporary config can point at separate RAW and JPEG source folders to verify cross-card pairing. This option is read only by the integration self-test, not normal application flows.
+
+The browser plugin reported `Browser is not available: iab`; rendered validation used the real Linux GTK/WebKit application under a private X display instead. No external browser or existing user browser session was controlled.
+
+These fixtures validate session, pairing, copying, persistence, and media delivery. They are synthetic previews with RAW filenames, not real sensor-data captures. Original RAW parsing retains its existing Rust tests. Physical card-reader behavior and macOS/Windows runtime behavior still need reports from those systems; the fork's cross-platform build is separate from local Linux validation.
+
+## Personal configuration
+
+`$XDG_CONFIG_HOME/opencull/config.json` (normally `~/.config/opencull/config.json`):
+
+```json
+{
+ "theme": "grey",
+ "font": { "choice": "space-grotesk", "family": "" },
+ "ignoreVideos": true
+}
+```
+
+Existing panel defaults remain unchanged. These are personal values; standalone feature branches preserve the original first-run defaults.
+
+
+## October 16 follow-up
+
+- Combined frontend: 57 tests pass; TypeScript/Vite production build passes.
+- Combined Rust library: 285 tests pass, including retained session history and legacy session compatibility.
+- Browser runtime: mixed native-aspect widths, fixed full-width stack filmstrip geometry, and the experimental 320ms cubic click-zoom transition verified after a fresh dev-server launch.
+- Linux native preview fix: WebKitGTK dev-origin CORS allowlist enables opencull://localhost previews. Reproduced failure with the DMABUF flag both set and unset; verified JPEG and real RW2 preview dimensions after the fix.
+- Stack placement lives under Layout in the combined app. Standalone branch uses Culling.
+- New standalone branches: feature/recent-sessions (depends on cull-before-ingest), feature/stack-filmstrip-layout, feature/experimental-filmstrip-aspect, feature/experimental-smooth-zoom.
+- Native macOS runtime remains unverified locally.
+
+
+## Interface scale and compact home follow-up
+
+- Interface scale uses native webview page zoom, with a saved 80–200% range and Reset. Located under Settings → Layout in the combined app. Native Linux inspection verified 150% (960×600 CSS viewport) and 200% (720×450), bounded Settings dialogs, no horizontal overflow or errors, and 200% retained after reload.
+- Typography uses clamped container-relative heading sizes with rem bounds and fixed fallbacks. Identity stays above the horizontal toolbar in both visual themes.
+- RapidRAW recent-folder contact sheets remain visible. Its session cards measured 304px versus 437px in the default preset at the same 1440px viewport; card layouts and toolbar hierarchy verified in browser runtime.
+- User rejected the monochrome mountain demo. demo/rapidraw-welcome is excluded from integration and the app continues to use its original opening page.
+- Frontend suite: 58 tests pass; combined production frontend build passes. Native macOS scaling has not been run locally.
+
+## Contrast, strips, history deletion and graphics follow-up
+
+- 61 frontend tests pass; production frontend build passes. 288 Rust tests pass, including session removal/promotion, legacy cleanup and graphics-profile choice.
+- Wisp's measured setting title/detail contrast meets 4.5:1, and row boundaries meet 3:1, in dark, light and grey. Active tabs have explicit boundaries. Guidelines are in DESIGN.md.
+- Browser runtime verifies session deletion, bulk controls hidden before selection, source-history separation, whole-window OpenCull layout after theme switching, both strip resize controls, and hide/show for both classic stack strips.
+- Native aspect covers both the active-frame and scene overview strips. Decoded upright JPEG dimensions supply early proportions; full camera dimensions remain separate and supersede the preview ratio.
+- This NVIDIA/Wayland test hit protocol error 71 with direct DMA-BUF. The narrower FORCE_SHM/DISABLE_GBM transport keeps the accelerated renderer enabled. An isolated CSS-only WebKit animation used the RTX 3060 Ti with 53 MiB allocated; no WebGL probe was required. NVIDIA profile requests hardware compositing/WebGL/available 2D canvas acceleration and uses this transport. CPU decoding remains separate. No zoom-performance gain is claimed without representative timings.
+- The earlier combined macOS Apple-silicon build passed. Fork main's new macOS dev startup smoke passed, checking the native process and Vite frontend. Final combined commit CI is still being checked; Intel Mac runtime is not covered.
+
+
+## Final native NVIDIA check
+
+The actual OpenCull WebKit process (not an isolated WebGL probe) was observed using the RTX 3060 Ti with a 311 MiB graphics allocation while rendering real RAW previews. No failed previews were found in the inspected sample. This uses FORCE_SHM/DISABLE_GBM presentation, keeping hardware compositing enabled. The broad WEBKIT_DISABLE_DMABUF_RENDERER flag is cleared only for this opt-in profile. Native Linux desktop is running via npm run dev:desktop:nvidia. GPU acceleration does not imply CPU image decoding or AI inference has moved to GPU.
+
+Additional feature PRs: [Wisp theme](https://github.com/wispdevon/OpenCull/pull/19), [resizable stack strips](https://github.com/wispdevon/OpenCull/pull/20), [NVIDIA rendering](https://github.com/wispdevon/OpenCull/pull/21). Wisp depends on the RapidRAW preset infrastructure; the resize and NVIDIA branches start from upstream. The combined macOS dev-startup smoke at source fb2a83a passed; final source 4b7316d CI is running after the Linux-only transport correction and diagnostics clarification.
diff --git a/docs/features/appearance-modes.md b/docs/features/appearance-modes.md
new file mode 100644
index 0000000..a3d6cd0
--- /dev/null
+++ b/docs/features/appearance-modes.md
@@ -0,0 +1,5 @@
+# Appearance modes
+
+Settings → Culling → Appearance selects Dark (unchanged default), Light, or 18% grey. Selection persists and applies immediately. The neutral grey canvas is sRGB #767676, approximately 18% linear luminance; panels use neutral darker tones. Interface neutrals use semantic CSS roles while image pixels and rating colours remain unchanged. Native controls follow the explicit colour scheme.
+
+Verify all three modes on home, card ingest, culling, settings, menus, and dialogs, including a small desktop window. Run frontend unit tests and production build. No other feature branch is required.
diff --git a/docs/features/configurable-ui.md b/docs/features/configurable-ui.md
new file mode 100644
index 0000000..969a21c
--- /dev/null
+++ b/docs/features/configurable-ui.md
@@ -0,0 +1,5 @@
+# Configurable workspace
+
+Settings → Layout controls filmstrip, face panel, EXIF panel, histogram, information overlay, rating bar, and filter bar. Size controls share the existing persisted dimensions with drag handles; side panels remain constrained to the workspace. Defaults and shortcuts are unchanged.
+
+Validation: frontend unit suite and production build; visually check small windows and restarting after a layout change. No other feature branch is required.
diff --git a/docs/features/cull-before-ingest.md b/docs/features/cull-before-ingest.md
new file mode 100644
index 0000000..61d8513
--- /dev/null
+++ b/docs/features/cull-before-ingest.md
@@ -0,0 +1,9 @@
+# Cull before importing
+
+Ingest from Card → select a source → Start Culling opens the existing workspace directly on the card. Review keeps ratings, flags, colours and crops in a local saved session; source XMP and embedded JPEG writes are bypassed even when normally enabled. Resume card session on Home restores the latest session; disconnected cards must be reconnected.
+
+Finish review initially selects all non-rejects, with All/None and individual selection. Copy selected files uses the existing staged, size-checked transfer path with collision renaming and writes marks/crops only at the destination. A RAW and its JPEG travel together. Completed copies are recorded for retry after cancellation or failure; retries retain the chosen destination. Import in place opens only chosen members without copying or source writes. Sessions live in the native application data directory, separately from configuration.
+
+Move and Ship cannot bypass this workflow. Deleting remains an explicit source action and always asks during card review. Reject never deletes. Ordinary folder culling retains its existing sidecar behavior.
+
+Validation: frontend tests include local-only marks/crops/undo and forced deletion confirmation. Rust tests check unchanged source bytes, destination sidecars, paired copying, source-destination refusal, and session persistence. Run `npm test`, `npm run build`, and a native desktop smoke test. This branch works independently; integration with multi-card selection is separate.
diff --git a/docs/features/font-selection.md b/docs/features/font-selection.md
new file mode 100644
index 0000000..0783f3a
--- /dev/null
+++ b/docs/features/font-selection.md
@@ -0,0 +1,5 @@
+# Interface fonts
+
+Settings → Culling → Interface font offers System (unchanged default), bundled Space Grotesk, or a single installed font family. Selection previews and applies immediately and persists. Space Grotesk is included under the SIL Open Font License in `public/fonts/SpaceGrotesk-OFL.txt`; no network font service is used. Technical values retain monospace styling.
+
+Verify with `npm run test:web`, `npm run build`, and a restart with Space Grotesk and an unavailable custom font selected. This branch requires no other feature.
diff --git a/docs/features/ingest-subfolders.md b/docs/features/ingest-subfolders.md
new file mode 100644
index 0000000..0e98edd
--- /dev/null
+++ b/docs/features/ingest-subfolders.md
@@ -0,0 +1,18 @@
+# Choose a specific folder on a card
+
+Branch: `feature/ingest-subfolders`, based directly on upstream `a8c7f32`.
+
+Opening a card now browses its folders without selecting the whole card.
+Open `CARD → DCIM → 100CANON` to select that leaf folder. Camera directory
+names no longer stop navigation. Mixed folders containing both photographs
+and subfolders can also be browsed. **Use this folder** explicitly selects
+the currently displayed parent when a whole directory is wanted.
+The system **Browse Folder** picker follows the same browse-first rule.
+
+This changes source selection only. It works independently of multi-card,
+culling-session, and appearance features. Original files are untouched.
+
+Validation: 31 frontend tests and TypeScript/Vite build passed. Browser runtime
+check verified zero scan requests while opening CARD/DCIM, a single scan of
+`/DCIM/100CANON` when selected, and an explicit DCIM scan with **Use this folder**.
+No browser JavaScript errors were observed.
diff --git a/docs/features/multi-card-session.md b/docs/features/multi-card-session.md
new file mode 100644
index 0000000..b5a55cf
--- /dev/null
+++ b/docs/features/multi-card-session.md
@@ -0,0 +1,9 @@
+# Multiple cards, one shoot
+
+Ingest from Card now accumulates selected cards/folders in a removable source list. All selected sources are scanned recursively, including nested DCIM folders, and displayed in one combined selection with source labels. Overlapping folders do not duplicate photos. Removed/disconnected sources are reported before copying.
+
+A cross-source RAW/JPEG pair requires one RAW and one JPEG with matching filename stem, camera make/model, and capture timestamp. Missing metadata, differing camera/time, or multiple candidates keep entries separate. Existing same-folder pairs remain intact. The JPEG supplies the preview and both files travel together under the existing collision/cancellation rules.
+
+Standalone: copies the selected combined shoot through existing ingest and opens the destination. Integration with cull-before-ingest is implemented separately, so this feature has no prerequisite branch.
+
+Validation: `npm test`, `npm run build`; Rust tests cover cross-card pairs, ambiguous/missing metadata, differing timestamps/cameras, overlapping roots, and disconnected sources. Native checks should include actual JPEG-only and RAW-only cards.
diff --git a/docs/features/rapidraw-theme.md b/docs/features/rapidraw-theme.md
new file mode 100644
index 0000000..7b0a00e
--- /dev/null
+++ b/docs/features/rapidraw-theme.md
@@ -0,0 +1,29 @@
+# RapidRAW inspired design preset
+
+Branch: `feature/rapidraw-theme`, based directly on upstream `a8c7f32`.
+No other feature branch is required. Select **Settings → Culling → Design theme
+→ RapidRAW inspired**. OpenCull's existing design remains the default.
+
+The reference is RapidRAW's first library screen at
+[`MainLibrary.tsx`](https://github.com/CyberTimon/RapidRAW/blob/e90a9bba7422ed833892e4945f06da35a65470d2/src/components/panel/MainLibrary.tsx):
+a split image/action layout, prominent identity, neutral surfaces, and direct
+session actions. This is an original implementation using OpenCull's existing
+folder, settings, keyboard, card detection, and recent-folder controls.
+No RapidRAW code, photographs, or logos are distributed.
+
+The image panel uses the latest available recent folder's local preview, or
+bundled original SVG artwork. Small windows show the action panel alone.
+Space Grotesk is bundled offline under its SIL Open Font License. The preset
+includes that font, with an option to use the normal interface font instead.
+Selecting another design removes the preset font override without changing
+the separate interface-font preference.
+
+Preferences `visualTheme` and `visualThemeFont` use the shared preferences API,
+so they also persist through the optional XDG configuration feature.
+With appearance modes installed, dark, light, and 18% grey remain independent
+of the design preset. Rating colours and photograph pixels are unchanged.
+
+Validation: 33 frontend tests and production TypeScript/Vite build passed.
+Chromium runtime checks cover selecting the theme, persistence after reload,
+font override opt-out, returning to the default design, and desktop/small
+window layouts with no horizontal overflow or JavaScript errors.
diff --git a/docs/features/ui-changes.md b/docs/features/ui-changes.md
new file mode 100644
index 0000000..0b6879d
--- /dev/null
+++ b/docs/features/ui-changes.md
@@ -0,0 +1,22 @@
+# Clearer UI headings and ingest instructions
+
+Branch: `feature/ui-changes`. Prerequisite: `feature/cull-before-ingest`.
+This branch starts from that feature because the moved **Review first**
+instruction describes its cull-before-copy workflow. Review its PR against
+`feature/cull-before-ingest` to see only the UI changes.
+
+- Ingest uses a semantic 24px heading. The 14px **Review first** instruction
+ sits immediately below it, above the whole workspace, rather than in the
+ destination sidebar. Header height follows the wrapped instruction text.
+- Home branding increases to 32px, recent-folder headings to 18px, the current
+ culling folder to 16px, and shared dialog/confirmation titles to 20px.
+- Source section labels increase to 14px, with 12px card/drive subheadings.
+- The unused destination sidebar is removed during card review. Previews use
+ the available width, with **Start Culling** in a bottom action bar. In the
+ combined app, the video inclusion toggle remains in Settings.
+
+These changes are independent of the optional design theme. Actions, photo
+selection, keyboard shortcuts, and file operations retain their behavior.
+
+Validation: frontend tests, TypeScript/Vite build, and browser runtime checks
+of the ingest heading/instruction placement and shared dialog title sizes.
diff --git a/docs/features/video-culling.md b/docs/features/video-culling.md
new file mode 100644
index 0000000..17bcaec
--- /dev/null
+++ b/docs/features/video-culling.md
@@ -0,0 +1,9 @@
+# Video culling
+
+Ignore videos is enabled initially, in Settings → Culling and the card browser. Disable it to list camera video containers alongside photos, keep/reject/rate them, copy them, and play supported codecs in the loupe. Native playback controls provide pause, seek, and volume; their keyboard events stay with the player. Unsupported codecs show a message and remain selectable for import. No transcoding or codec downloads are performed.
+
+Playback uses a bounded, token-protected loopback stream on Linux because WebKitGTK's media backend rejects custom asset schemes. Other platforms use Tauri's range-capable asset protocol, with an empty initial scope. Only a video already belonging to an open session is granted individual file access. Videos use a thumbnail icon and skip image preview warming, histogram processing, and face analysis. Normal folder/video sidecars follow existing mark behavior; integration with source-safe card review is separate.
+
+Linux playback depends on system WebKitGTK/GStreamer codec support. Formats are recognized as containers, not promises that every codec inside them will decode.
+
+Validation: `npm test`, `npm run build`, native desktop smoke checks with a playable video, unsupported video, video-only source folder, seeking, and source-copy preservation. No other feature branch is required. Tauri asset protocol reference: https://v2.tauri.app/reference/javascript/api/namespacecore/#convertfilesrc
diff --git a/docs/features/view-mode-preferences.md b/docs/features/view-mode-preferences.md
new file mode 100644
index 0000000..a988ff7
--- /dev/null
+++ b/docs/features/view-mode-preferences.md
@@ -0,0 +1,24 @@
+# Default and remembered view modes
+
+Branch: `feature/view-mode-preferences`, directly based on upstream `a8c7f32`.
+No other feature is required.
+
+**Settings → Culling → View mode** provides a default opening view:
+Filmstrip (Loupe), Stack (Scene), or Grid. **Remember last view** is enabled by
+default. With it enabled, toolbar, keyboard, and other view changes persist
+across folders and restarts. With it disabled, each folder opens in the chosen
+default, while the current folder can still be switched freely.
+
+First use still starts in Loupe. Invalid saved modes fall back to the valid
+default. Changing the default does not interrupt the current culling view.
+Filmstrip panel visibility remains its existing separate setting.
+
+The shared preferences API stores `defaultViewMode`, `rememberViewMode`, and
+`viewMode`. Their mode values are `loupe`, `scene`, or `grid`. With XDG settings
+installed they are written into the native JSON configuration automatically.
+In the combined integration, these controls live under **Settings → Layout**.
+
+Validation: 35 standalone frontend tests and TypeScript/Vite production build.
+Browser runtime checks cover every toolbar mode across folder changes and
+page reloads, configured defaults with remembering disabled, and keyboard
+changes saved to preferences. No JavaScript errors were observed.
diff --git a/docs/features/xdg-config.md b/docs/features/xdg-config.md
new file mode 100644
index 0000000..469fdb2
--- /dev/null
+++ b/docs/features/xdg-config.md
@@ -0,0 +1,13 @@
+# Native configuration
+
+Linux settings are stored at `$XDG_CONFIG_HOME/opencull/config.json`, or `~/.config/opencull/config.json` when XDG_CONFIG_HOME is unset or not absolute. Other platforms use the native Tauri application config directory. The file is a JSON object of existing preference keys. Unknown keys survive writes. Restart after editing manually.
+
+Startup migrates webview preferences only into missing keys, then imports the application so stores see file values immediately. Existing file values win. Writes are serialized and use a synced temporary file and rename; malformed or unreadable files are left untouched and reported visibly. Changes remain in webview storage if native initialization fails. `session.*` manifests remain separate from preferences.
+
+Personal settings, when the corresponding independent features are installed:
+
+```json
+{"theme":"grey","font":{"choice":"space-grotesk","family":""},"ignoreVideos":true}
+```
+
+Run `npm test`, `npm run build`, and desktop startup tests with an XDG override, missing directory, malformed file, and an unwritable parent. This feature does not require appearance or font branches.
diff --git a/index.html b/index.html
index 43c5b34..fbb5e9f 100644
--- a/index.html
+++ b/index.html
@@ -2,6 +2,7 @@
+
OpenCull
diff --git a/package.json b/package.json
index 398fbee..653af39 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,10 @@
"pretest:web": "npm run check:node",
"prebuild:web": "npm run check:node",
"predev": "npm run check:node",
- "check": "npm run test:web && npm run build:web && npm run test:rust"
+ "check": "npm run test:web && npm run build:web && npm run test:rust",
+ "dev:desktop:nvidia": "node scripts/dev-graphics.mjs nvidia",
+ "dev:desktop:compatibility": "node scripts/dev-graphics.mjs compatibility",
+ "dev:desktop:software": "node scripts/dev-graphics.mjs software"
},
"dependencies": {
"@tailwindcss/vite": "^4.3.3",
diff --git a/public/fonts/SpaceGrotesk-OFL.txt b/public/fonts/SpaceGrotesk-OFL.txt
new file mode 100644
index 0000000..cb512b9
--- /dev/null
+++ b/public/fonts/SpaceGrotesk-OFL.txt
@@ -0,0 +1,93 @@
+Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk)
+
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/public/fonts/SpaceGrotesk.ttf b/public/fonts/SpaceGrotesk.ttf
new file mode 100644
index 0000000..a1b2e6c
Binary files /dev/null and b/public/fonts/SpaceGrotesk.ttf differ
diff --git a/public/theme-landscape.svg b/public/theme-landscape.svg
new file mode 100644
index 0000000..b73d4da
--- /dev/null
+++ b/public/theme-landscape.svg
@@ -0,0 +1,14 @@
+
diff --git a/scripts/dev-graphics.mjs b/scripts/dev-graphics.mjs
new file mode 100644
index 0000000..cbabba6
--- /dev/null
+++ b/scripts/dev-graphics.mjs
@@ -0,0 +1,10 @@
+import { spawn } from "node:child_process";
+import { fileURLToPath } from "node:url";
+const profile = process.argv[2];
+if (!["nvidia", "compatibility", "software"].includes(profile) || process.platform !== "linux") {
+ console.error("Graphics profiles are Linux-only. Use npm run dev:desktop for native macOS or Windows rendering."); process.exit(1);
+}
+const cli = fileURLToPath(new URL("../node_modules/@tauri-apps/cli/tauri.js", import.meta.url));
+const child = spawn(process.execPath, [cli, "dev", ...process.argv.slice(3)], { stdio: "inherit", env: { ...process.env, OPENCULL_GRAPHICS: profile } });
+child.on("error", (e) => { console.error(e.message); process.exitCode = 1; });
+child.on("exit", (code) => { process.exitCode = code ?? 1; });
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index 714ec29..0726dde 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -1623,6 +1623,12 @@ dependencies = [
"pin-project-lite",
]
+[[package]]
+name = "http-range"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
+
[[package]]
name = "httparse"
version = "1.10.1"
@@ -4043,6 +4049,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http",
+ "http-range",
"jni 0.21.1",
"libc",
"log",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 1796562..c7cfada 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -36,7 +36,7 @@ turbo-system = ["dep:turbojpeg", "turbojpeg/pkg-config"]
tauri-build = { version = "2", features = [] }
[dependencies]
-tauri = { version = "2", features = [] }
+tauri = { version = "2", features = ["protocol-asset"] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
index 0a553e9..11efb0f 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -5,6 +5,7 @@
"windows": ["main"],
"permissions": [
"core:default",
+ "core:webview:allow-set-webview-zoom",
"core:window:allow-start-dragging",
"core:window:allow-toggle-maximize",
"core:window:allow-internal-toggle-maximize",
diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs
index 97e84ce..a97a5ff 100644
--- a/src-tauri/src/commands.rs
+++ b/src-tauri/src/commands.rs
@@ -224,7 +224,7 @@ const TIME_BUDGET: Duration = Duration::from_millis(1500);
/// on screen before either — but the capture times are read here, by the
/// short route, because the order of the folder depends on them.
#[tauri::command]
-pub async fn open_folder(app: AppState<'_>, path: String, include_subfolders: bool, kinds: Option) -> Result {
+pub async fn open_folder(app: AppState<'_>, path: String, include_subfolders: bool, kinds: Option, members: Option>, ignore_videos: Option) -> Result {
let kinds = kinds.unwrap_or_default();
let folder = PathBuf::from(&path);
if !folder.is_dir() {
@@ -234,7 +234,18 @@ pub async fn open_folder(app: AppState<'_>, path: String, include_subfolders: bo
let scanned_folder = folder.clone();
let (result, read) = tauri::async_runtime::spawn_blocking(move || {
let folder = scanned_folder;
- let result = scan::scan_folder_of(&folder, include_subfolders, kinds);
+ let result = match members {
+ Some(images) => {
+ for image in &images {
+ if !image.path.is_file() || !image.display.is_file() { return Err(format!("Reconnect the source containing {}", image.name)); }
+ let meta = image.path.metadata().map_err(|e| e.to_string())?;
+ let modified = meta.modified().ok().and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok()).map(|d| d.as_millis() as u64).unwrap_or(0);
+ if meta.len() != image.size || modified != image.modified { return Err(format!("{} changed since this session was saved. Start a new card review to avoid applying marks to a different file.", image.name)); }
+ }
+ scan::ScanResult { images, subfolder_count: 0, left_out: 0 }
+ }
+ None => scan::scan_folder_media(&folder, include_subfolders, kinds, !ignore_videos.unwrap_or(true)),
+ };
let known = shared.times.load(&folder);
let deadline = Instant::now() + TIME_BUDGET;
@@ -246,7 +257,7 @@ pub async fn open_folder(app: AppState<'_>, path: String, include_subfolders: bo
.par_iter()
.map(|image| {
times::remembered(&known, image)
- .or_else(|| (Instant::now() < deadline).then(|| raw::capture_time(&image.path)))
+ .or_else(|| (Instant::now() < deadline).then(|| if scan::is_video(&raw::extension(&image.path)) { None } else { raw::capture_time(&image.path) }))
})
.collect()
});
@@ -255,10 +266,10 @@ pub async fn open_folder(app: AppState<'_>, path: String, include_subfolders: bo
if learned {
shared.times.save(&folder, &result.images, &read);
}
- (result, read)
+ Ok((result, read))
})
.await
- .map_err(|e| e.to_string())?;
+ .map_err(|e| e.to_string())??;
let timed = read.iter().all(Option::is_some);
let times: Vec