Browser & Device Compatibility Audit — v2
Audit date: 2026-04-26. Methodology: static code analysis + caniuse-style reasoning. No live browser testing.
1. Declared Targets vs Reality
No .browserslistrc, no Browserslist field in package.json. There are no declared browser targets. Vite's default targets modern browsers; Cloudflare's adapter emits standard ESM. The app is implicitly targeting "evergreen desktop browsers".
The workspace layout explicitly ships a full-screen mobile warning overlay (lg:hidden) and the navigation tabs are hidden lg:flex with no mobile fallback menu. v2/MOBILE-NOTES.md documents this as intentional: v2 is desktop-first by design. The lg breakpoint is not the practical dashboard support floor; current browser QA should target realistic desktop/laptop widths, 1280px and up.
Practical minimum:
| Browser | Minimum usable version |
|---|---|
| Chrome / Edge | 110+ (Baseline 2023) |
| Firefox | 115+ (ESR) |
| Safari | 16.4+ (WebKit 616, Baseline 2023) |
| iOS Safari | 16.4+ |
| Android Chrome | 110+ |
2. API Support Matrix
| API | Used | Chrome | Firefox | Safari | iOS Safari | Android Chrome | Notes |
|---|---|---|---|---|---|---|---|
| WebSocket | Yes — WorkspaceConnection |
✅ | ✅ | ✅ | ✅ | ✅ | Universally supported |
IndexedDB (y-indexeddb) |
Yes — Yjs persistence | ✅ | ✅ | ✅ 12.1+ | ⚠️ 12.2+ | ✅ | Safari had broken IDB in private browsing up to iOS 14; modern (16.4+) fine. Private mode on older Safari will lose local state but won't crash (y-indexeddb wraps errors). |
| WebAssembly (Typst WASM) | Yes — compiler + renderer | ✅ | ✅ | ✅ 13.1+ | ✅ 14+ | ✅ | See §6 for streaming specifics. |
| Web Workers (Typst off-main-thread) | Yes — typst-client.ts |
✅ | ✅ | ✅ | ✅ | ✅ | Module workers ({type:'module'}) need Safari 15+ and iOS 15+. |
crypto.randomUUID() |
Yes — connection.ts, Icons.svelte |
✅ 92+ | ✅ 95+ | ✅ 15.4+ | ✅ 15.4+ | ✅ | Has explicit 'randomUUID' in crypto guard — falls back to Math.random. Safe. |
navigator.clipboard.writeText() |
Yes — copy-link button | ✅ | ✅ | ✅ 13.1+ | ⚠️ 13.4+ | ✅ | Requires HTTPS (already on Cloudflare). Silently no-ops on failure (try/catch). |
| ResizeObserver | Yes — PipelineBar, KanbanBoard | ✅ | ✅ | ✅ 13.1+ | ✅ 13.4+ | ✅ | Fully supported in all targets. |
| AbortController | Yes — pipeline.svelte.ts | ✅ | ✅ | ✅ 12.1+ | ✅ | ✅ | Universally supported. |
BroadcastChannel |
No | — | — | — | — | — | Not used in v2. |
| Service Workers | No | — | — | — | — | — | Not used (per MOBILE-NOTES.md). |
requestIdleCallback |
No | — | — | — | — | — | Not used. |
| IntersectionObserver | No | — | — | — | — | — | Not used. |
| Wake Lock API | No | — | — | — | — | — | Not used. |
navigator.share |
No | — | — | — | — | — | Not used. |
CSS zoom property |
Yes — Dashboard auto-shrink | ✅ | ⚠️ FF 126+ | ✅ | ✅ | ✅ | See §4. |
color-mix(in oklab, ...) |
Yes — themeBg() |
✅ 111+ | ✅ 113+ | ✅ 16.2+ | ✅ 16.2+ | ✅ | See §3. |
oklch() color |
Yes — CSS custom props | ✅ 111+ | ✅ 113+ | ✅ 15.4+ | ✅ 15.4+ | ✅ | See §3. |
Module Workers ({type:'module'}) |
Yes — Typst worker | ✅ 80+ | ✅ 114+ | ✅ 15+ | ✅ 15+ | ✅ | Firefox 114 added module worker support. Target of FF 115+ ESR covers this. |
3. CSS Modern Features
oklch() color space
app.css declares all 6 DaisyUI semantic tokens as oklch() values. Firefox shipped oklch() in 113; Safari in 15.4. Both are within safe targets. No fallback is provided — if a browser older than these targets opens the app, all semantic colors would be invisible (rendering with no background/text color where tokens are used). This is a hard requirement for minimum version.
color-mix(in oklab, ...)
themeBg() in lib/domain/src/theme.ts generates color-mix(in oklab, var(--color-*) N%, transparent) for inline-style tinted backgrounds. This is called wherever a semi-transparent tint needs a dynamic value (e.g., overdue row borders in JobCard). Support: Chrome 111+, Firefox 113+, Safari 16.2+. Fine for target versions. No fallback — on unsupported browsers the background would be transparent rather than tinted (visual degradation only, not functional breakage).
bg-primary/10 (Tailwind opacity modifier)
These compile to color-mix() in Tailwind v4. Same floor as above.
CSS zoom property
Dashboard.svelte:1187 applies style="zoom: ${dashboardZoom}" for the auto-shrink path on narrow screens. zoom is not in the CSS spec but is universally implemented as a de-facto standard. Firefox added it in FF 126 (2024-05). Firefox ESR 115 (current LTS) does NOT support zoom. The dashboard will not shrink on Firefox ESR 115; it will render at full width regardless of screen width.
No :has(), no container queries, no @layer, no subgrid, no @scope
None of these are used in the application source. No issues here.
4. Svelte 5 Runes
Svelte 5 runes mode is forced globally (runes: ({ filename }) => ... true in svelte.config.js). Svelte 5 compiles to vanilla JS/DOM and does not ship browser-specific APIs. All major browsers at target versions support the output. No Svelte-level compatibility issues.
Svelte 5 module workers ({type: 'module'}) in typst-client.ts require Safari 15+ and Firefox 114+. Both within target range.
5. Yjs / y-indexeddb
y-indexeddb is used for local Yjs persistence. Connection code has an explicit typeof indexedDB !== 'undefined' guard before initialising it — private browsing in Safari (which blocks IDB) will gracefully skip persistence and operate as a pure WebSocket client. The app remains functional; users simply lose offline/reload state in private mode.
WebSocket sync itself (WorkspaceConnection) has no Safari-specific issues — ws.binaryType = 'arraybuffer' is supported on all targets.
6. Typst WASM
The Typst compiler (typst_ts_web_compiler) and renderer are fetched from jsDelivr CDN and loaded as URL strings via getModule: () => URL_STRING. The underlying wasm-bindgen shim attempts WebAssembly.instantiateStreaming() first and falls back to WebAssembly.instantiate(bytes) on failure (with a console warning). This is robust — even if the CDN returns non-application/wasm MIME, it recovers.
Two remaining concerns:
Module worker (
{type: 'module'}) containing a WASM load from CDN: The Typst worker runs as a module worker. Cross-origin fetches inside module workers work normally in all target browsers. No issue for the CDN case.Memory floor: The compiler + renderer together require significant heap. Devices with less than ~512 MB browser-available RAM (older Android, very constrained iOS) may OOM the worker. The worker has a crash-recovery path (
w.onerrorrecreates it), but a low-RAM device that repeatedly OOMs won't recover — Typst preview and PDF export will silently fail. This is a real concern for low-end Android.
7. Touch & Mobile UX
The app ships an explicit "Best on Desktop" full-screen warning for sub-lg (< 1024 px) viewports. Users can dismiss it and continue. Several touch-hostile patterns exist behind that gate:
The warning threshold and the practical dashboard width are not identical. At exactly 1024 px the desktop nav is visible, but dense header/dashboard controls can still overflow. Treat that as a known edge of the desktop-first implementation, not as a mobile support commitment.
Hover-revealed action buttons (cannot be activated on touch)
The following UI controls are hidden with opacity-0 group-hover:opacity-100 and have no touch alternative:
| Location | Controls hidden |
|---|---|
JobCard.svelte:39 |
Quick-action toolbar (Favorite, Follow-up, Archive) — the primary per-card CTA surface on the Kanban board |
StatusContainer.svelte:155 |
Delete-status button |
PhaseColumn.svelte:205 |
Phase rename/delete controls |
Kanban.svelte:262 |
Delete kanban column button |
WorkspaceNotes.svelte:165 |
Note timestamp label |
DataCell.svelte:869–879 |
Deadline "X" clear button overlaid on the days counter |
ColumnHeader.svelte:131 |
Column sort affordance opacity |
On touch devices, none of these can be activated because there is no hover state. Tapping a card or container triggers onclick on the card itself, not the hidden overlay. The most critical is the Kanban quick-action toolbar — there is no other path to archive or follow-up a job from the Kanban view on touch.
Column resize handles
ResizeHandle.svelte uses pointerdown (pointer events, which work on touch). This is fine — pointer events fire on touch.
Drag and drop
svelte-dnd-action@0.9.69 advertises touch support in its description and implements it. The Kanban board and CV section list should be draggable on touch. However, SortableCategoryBody.svelte sets touch-action: none on the draggable <tbody> which is correct for DnD but prevents native scroll on that element.
Navigation tabs
The main nav ([wsId]/+layout.svelte) hides all tabs with hidden lg:flex. No hamburger menu, no bottom-tab replacement. On < 1024 px, the app has no navigation — a user who dismisses the mobile warning cannot switch between Dashboard, Kanban, and CV/CL views.
Dashboard table
The dashboard is a <table class="table table-fixed"> with dynamic column widths. When dashboardZoom is 1 (no auto-shrink), the outer div has overflow-x-auto, so the table is horizontally scrollable. When zoom < 1, zoom: N is applied instead. Given Firefox ESR lacks zoom, Firefox ESR on a narrow screen would overflow without scroll.
8. Viewport / Responsive
app.html sets <meta name="viewport" content="width=device-width, initial-scale=1"> — correct standard viewport tag. No user-scalable=no or maximum-scale — pinch-zoom is allowed.
The workspace layout (px-4 sm:px-6 lg:px-8) scales down padding but the content itself (the dashboard table) does not reflow for narrow viewports. This is documented as intentional.
9. Top 5 Most-Likely-to-Break Items
#1 — CSS zoom on Firefox ESR 115 (SEVERITY: High)
Dashboard.svelte uses style="zoom: ${dashboardZoom}" for the auto-shrink path. Firefox ESR 115 (the enterprise/extended-support release, widely deployed) does not support zoom. The dashboard auto-shrink silently does nothing on FF ESR 115 — the table overflows on narrow desktops and the only fallback is overflow-x-auto (which only applies when zoom ≥ 1). No visual error, just a broken layout. Test first on Firefox 115.
Mitigation: replace zoom: with transform: scale() + matching transform-origin + explicit width: calc(100% / scale) wrapper. transform: scale() is universally supported.
#2 — Kanban quick-action toolbar inaccessible on touch (SEVERITY: High for mobile)
JobCard.svelte hides the favorite/follow-up/archive buttons with opacity-0 group-hover:opacity-100. There is no touch alternative, no long-press handler, no fallback menu. Combined with the absent mobile navigation, Kanban is close to unusable on touch. (Mitigated by the mobile warning overlay, but a user who dismisses it is stuck.)
Mitigation: add group-active:opacity-100 (CSS, zero JS cost) or wire the card onclick to open a bottom sheet. Deferred to mobile sprint per MOBILE-NOTES.md.
#3 — Missing mobile navigation (SEVERITY: High for mobile)
The workspace nav tabs are hidden lg:flex with no substitute. Below 1024 px there is no way to navigate between Dashboard, Kanban, and CV views after dismissing the warning overlay. Route changes via URL are the only escape.
Mitigation: add a mobile bottom-tab bar or hamburger drawer. Deferred per MOBILE-NOTES.md.
#4 — color-mix(in oklab) / oklch() — no fallback (SEVERITY: Low for targets, Medium for older)
Every semantic color in the UI is oklch(). themeBg() generates color-mix(in oklab, ...). Neither has a fallback. Chrome < 111, Firefox < 113, Safari < 16.2 (or iOS < 16.2) render the app with all semantic color surfaces as transparent or missing. These versions are below the practical minimum anyway, but there is no graceful degradation — the app looks broken, not just less pretty.
Mitigation: add @supports fallbacks or hex fallback custom-property values (e.g. --color-primary: #2563eb; --color-primary: oklch(...)). Low priority given explicit modern-browser targeting.
#5 — Typst WASM on low-memory devices / iOS private browsing (SEVERITY: Medium for iOS)
Two compounding issues: (a) y-indexeddb silently skips persistence in iOS private browsing — the Y.Doc starts empty every page load, so drafts in the CV editor are lost. (b) The Typst compiler + renderer (~25 MB WASM, large heap) may OOM on low-end Android or constrained iOS tabs. The worker has a crash-recovery path, but on constrained devices it will loop — preview fails silently. PDF export also fails.
Mitigation for (a): show a UI banner when IDB is unavailable (detect indexedDB === undefined). Mitigation for (b): add explicit OOM detection in the worker onerror handler and surface it to the user.
10. Recommendations
Test first (highest signal/effort ratio):
- Firefox (stable, current) — catches the
zoomregression on the dashboard auto-shrink path. One test session reveals the most impactful desktop compat issue. - Safari 16.x on macOS — validates
color-mix(),oklch(), module worker + WASM, and IndexedDB. Most of these are fine at 16.4+ but worth confirming end-to-end in a real browser.
Polyfill / fix before any public beta:
- Replace
zoom:withtransform: scale()inDashboard.svelte(one-line fix, zero risk). - Add
group-active:opacity-100to allgroup-hover:opacity-100hidden-action classes (one-liner Tailwind addition, touches multiple components).
Accept / defer:
- Mobile nav and mobile layouts — documented in MOBILE-NOTES.md, correct call.
oklch/color-mixfallbacks — out of scope until a compat floor below Chrome 111 / Firefox 113 / Safari 16.2 is needed.- Typst WASM memory on low-end devices — document minimum requirements; add a graceful error toast in the worker crash handler.
No action needed:
- WebSocket, IndexedDB (modern), ResizeObserver, AbortController, crypto APIs — all fine at target versions.
- Svelte 5 runes output — standard JS, no browser issues.
- svelte-dnd-action touch support — built in, pointer events used correctly.