Skip to content

Tag Studio β€” Interaction Design Spec

Epic: #3866 Β· Product: Gevell Tags at tags.gevell.com (add it to packages/accessible-org-ui/src/products.ts when it ships) Β· Status: draft for review.

This spec covers how the editor behaves: layout, selection, keyboard, task modes, validation, bulk operations, AI review, undo, save and export. It does not cover the Rust/WASM engine internals (phase 1), except for the operation vocabulary that the UI emits. The design starts from scratch. It does not inherit the interaction model of apps/web’s tag views or apps/tag-editor.

Standards targets: the output is validated for PDF/UA-1 (ISO 14289-1, Matterhorn Protocol 1.1) and WCAG 2.1 AA content requirements. The tool itself must meet WCAG 2.2 AA and must be fully usable with a keyboard only and with NVDA, JAWS and VoiceOver.


0. Principles

  1. One structure model, many views. The canvas, tree, reader preview and inspector all render the same in-memory structure. No view keeps its own copy, so views cannot drift out of sync.
  2. Every edit is an operation. Each change is a typed, validated, reversible op with an inverse and an origin (user, fix-all, select-similar, AI, template). Nothing mutates the model any other way. Undo is reliable because it is part of the model, not added afterwards.
  3. Invalid ops are refused before they apply. Each op checks its preconditions (for example β€œLI may only be a child of L”). A refused op leaves the document unchanged and gives a plain-language reason that says what to do instead.
  4. Guided first, raw one toggle away. Task modes are the default workspace. Raw tree is a peer mode, not a hidden β€œadvanced” screen.
  5. No false green. Items that need human judgment (alt text quality, reading order meaning, heading semantics) are never shown as passing. They show as Needs review until a person marks them reviewed, and that attestation is recorded.
  6. Never alter what the page looks like. Saving is gated on a pixel-identity check. Tagging changes structure only, never appearance.
  7. Non-modal by default. Properties are edited in the inspector, with no blocking dialogs. The only modal surfaces are unsaved-changes and destructive-confirm prompts, and the save-gate failure sheet.
  8. Every pointer action has a keyboard path and a non-drag path (WCAG 2.5.7). Drag is a shortcut, never the only way.
  9. Say what happened. Every op result is announced in a live region in the words the history panel uses (β€œRetyped P to H2 Β· 1 item”).
  10. The AI proposes and the human decides. AI output arrives as proposed ops with a confidence level and a one-line reason. By default it changes nothing until someone accepts it. Users can opt in to auto-apply above a confidence threshold (Β§9.4). Auto-applied changes are marked as AI, can be undone, and never count as human review.
  11. The server holds the record. Documents, versions and the op log sync to Gevell’s servers (AWS). The browser keeps a local outbox so work continues offline and nothing is lost (Β§11.3).

Terms

TermMeaning
NodeA structure element (StructElem) such as H2, P, Figure or TD
Content itemA leaf of page content: marked-content sequence (MCR/MCID), annotation (OBJR), or untagged run
OrphanPage content that is neither tagged nor artifacted. Every orphan is a PDF/UA failure
OpAn atomic, reversible edit (see Β§3)
GroupOne or more ops applied as one undo step (bulk, fix-all, AI accept)
PrimaryThe most recently selected item in a multi-selection. The inspector shows the primary
ModeA guided task workspace (Reading order, Headings, …) or Raw tree
ProposalAn op (or group) suggested by AI or a detector, not yet applied

1. Layout

1.1 Regions (all breakpoints)

Each region is a landmark: <header>, <nav aria-label="Modes">, <main> containing the view regions, each a <section aria-labelledby> with a visible heading. Screen reader users can move between views with landmark and heading navigation as well as with F6.

RegionContents
App barFile name, sync status (β€œSynced” / β€œSyncing 3…” / β€œOffline Β· 12 changes on this device” / β€œSync problem”), version state (β€œv7 + 14 changes” / β€œv8 Β· verifying…”), undo/redo, validation summary chip, Save, Export, command palette button, help, theme
Mode railVertical tablist of task modes with per-mode issue counts
Task panelMode-specific work queue: outline, figure queue, table picker, artifact groups, and so on
Structure treeVirtualized treegrid. In guided modes it shares a column with the task panel as a split pane. In Raw tree mode it fills the column
CanvasRendered page(s) with overlays, reading-order path and orphan highlighting
ReaderScreen reader preview: linear announcements, plus a reflow tab
InspectorNon-modal properties of the primary selection
Dock tabsIssues Β· History Β· Suggestions (AI) Β· Similar
Status barPage x of N, zoom, selection count, validation progress, engine status, and a live-region echo of the last announcement

1.2 Wide desktop (β‰₯ 1920 CSS px)

All four views are visible at once, and the dock sits under the right column.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ β—§ Tag Studio annual-report-2025.pdf ☁ Synced Β· v7 + 14 changes β†Ά β†· [ βœ• 12 fail Β· ? 31 review ] ⌘K [Save] [Exportβ–Ύ]β”‚
β”œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ ≑ β”‚ READING ORDER Β· page 3 β”‚ β—€ p3/48 β–Ά 100% β–Ύ Overlays β–Ύ β”‚ READER Reflow β”‚ INSPECTOR β”‚
β”‚ RO β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ heading level 1, β”‚ H2 Β· page 3 β”‚
β”‚ 3 β”‚ β”‚ 1 H1 Annual Report 2025 β”‚ β”‚ β”‚ ①━━━━━━━━━━━━━━━━━━━━━━━━┓[H1] β”‚ β”‚ Annual Report β”‚ Type [H2 β–Ύ] β”‚
β”‚ Hd β”‚ β”‚ 2 P The fiscal year… β”‚ β”‚ β”‚ β”ƒβ‘‘β”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”[P] β”‚ β”‚ The fiscal year… β”‚ Lang [inherit]β”‚
β”‚ 1 β”‚ β”‚ 3 H2 Budget overview β—€ β”‚ β”‚ β”‚ ┃ ⑒━━━━━━━━━━━┓[H2] β—€sel β”‚ β”‚β–Άheading level 2, β”‚ Title [ ]β”‚
β”‚ Fg β”‚ β”‚ 4 β–¦ Table 1 (4Γ—3) β”‚ β”‚ β”‚ ┃ β‘£β–¦β–¦β–¦β–¦β–¦β–¦β–¦β–¦β–¦β–¦β–¦[Table] β”‚ β”‚ Budget overview β”‚ ActualText β”‚
β”‚ 7 β”‚ β”‚ 5 P Revenue grew… β”‚ β”‚ β”‚ ┃ β‘€β”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆ[P] β”‚ β”‚ table with 4 rows β”‚ [ ]β”‚
β”‚ Tb β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β–‘β–‘β–‘ untagged β–‘β–‘β–‘ ⚠ β”‚ β”‚ and 3 columns β”‚ BBox 72,540 … β”‚
β”‚ 2 β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ … β”‚ [Attributes β–Έ] β”‚
β”‚ Ar β”‚ STRUCTURE / filter β”‚ β”‚ β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Ls β”‚ β–Ύ Document β”‚ β”‚ β”‚ β”‚ Issues History β”‚
β”‚ Lk β”‚ β–Ύ Sect β”‚ β”‚ β”‚ β”‚ Suggest Similarβ”‚
β”‚ Fm β”‚ H1 Annual Report 2025 β”‚ β”‚ β”‚ β”‚ βœ• Figure has noβ”‚
β”‚ Nt β”‚ P The fiscal year… β”‚ β”‚ β”‚ β”‚ alt (p3)[Fix]β”‚
β”‚ La β”‚ β–Έ H2 Budget overview βœ•1 β”‚ β”‚ β”‚ β”‚ ? Reading orderβ”‚
β”‚ TC β”‚ Table 4Γ—3 β”‚ β”‚ β”‚ β”‚ p3 [Review] β”‚
β”‚ {} β”‚ β”‚ β”‚ β”‚ β”‚ β”‚
β”œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Page 3 of 48 Β· 100% Β· 1 selected (H2) Β· Validating… 62% β–“β–“β–“β–“β–‘β–‘ Β· Engine ready β”‚ Retyped P to H2 Β· 1 item β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Column widths: rail 56 Β· task/tree 360 Β· canvas flex (min 640) Β· reader 340 Β· inspector/dock 380. Every splitter can be resized with the pointer and with the keyboard. Each splitter is a focusable role="separator" with aria-valuenow; the arrow keys move it 16px and Home/End collapse or restore it. Layout is saved per user in localStorage (a convenience only).

1.3 Standard desktop (1280–1919 CSS px)

The reader moves into the right-hand dock as a tab. The inspector stays pinned at the top of the right column. The dock tabs are Reader Β· Issues Β· History Β· Suggestions Β· Similar.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ β—§ annual-report-2025.pdf ☁ Synced Β· v7+14 β†Ά β†· [βœ•12 ?31] ⌘K [Save] [Exportβ–Ύ] β”‚
β”œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ RO β”‚ READING ORDER Β· p3 β”‚ β—€ p3/48 β–Ά 100% β–Ύ Overlays β–Ύ β”‚ INSPECTOR H2 Β· p3 β”‚
β”‚ Hd β”‚ 1 H1 Annual Report 2025 β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Type [H2 β–Ύ] Lang[inh] β”‚
β”‚ Fg β”‚ 2 P The fiscal year… β”‚ β”‚ ①━━━━━━━━━━━━━━━━━┓[H1] β”‚ β”‚ ActualText [ ] β”‚
β”‚ Tb β”‚ 3 H2 Budget overview β—€ β”‚ β”‚ β”ƒβ‘‘β”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆβ”ˆ[P] β”‚ β”‚ [Attributes β–Έ] β”‚
β”‚ Ar β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ ┃ ⑒━━━━━━━┓[H2] β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ Ls β”‚ STRUCTURE / filter β”‚ β”‚ ┃ β‘£β–¦β–¦β–¦β–¦β–¦β–¦β–¦[Table] β”‚ β”‚ Reader Issues History β”‚
β”‚ Lk β”‚ β–Ύ Document β”‚ β”‚ β–‘β–‘ untagged β–‘β–‘ ⚠ β”‚ β”‚ Suggest Similar β”‚
β”‚ Fm β”‚ H1 Annual Report 2025 β”‚ β”‚ β”‚ β”‚ heading level 2, β”‚
β”‚ Nt β”‚ β–Έ H2 Budget overview βœ•1 β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ Budget overview β”‚
β”‚ La β”‚ Table 4Γ—3 β”‚ β”‚ table with 4 rows … β”‚
β”‚ TC β”‚ β”‚ β”‚ β”‚
β”‚ {} β”‚ β”‚ β”‚ β”‚
β”œβ”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ p3/48 Β· 100% Β· 1 selected Β· Validating 62% β–“β–“β–“β–‘ β”‚ Retyped P to H2 Β· 1 item β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Column widths: rail 56 Β· task/tree 300 Β· canvas flex (min 520) Β· right 360.

1.4 Compact desktop (1024–1279 CSS px): minimum for the multi-pane workspace

  • 1024 CSS px is the minimum width for the synchronized multi-pane workspace. This equals a 1280px screen at 125% browser zoom, which is a common setup for remediators.
  • The mode rail shrinks to icons with visible short labels (2–3 letters, as in the wireframes) and a tooltip. The tooltip is not the only label: each tab’s accessible name is the full mode name.
  • The right column becomes a drawer that overlays the canvas. It is toggled with the Inspector button or Mod+I (see Β§5, with its conflict note). The drawer is non-modal: focus is not trapped, and Esc closes it and returns focus to where it came from.
  • The task panel and tree stack vertically in the left column, with a draggable splitter.

1.5 Below 1024 CSS px: single-pane mode (required, not optimized)

This is a desktop pro tool, but WCAG 1.4.10 Reflow still applies. A remediator with low vision may run 1280px at 400% zoom, which gives a 320px viewport. The tool must remain fully functional there, without being optimized for it.

  • One view at a time. A view switcher (tablist: Task Β· Tree Β· Page Β· Reader Β· Inspector Β· Issues Β· History Β· Suggestions) sits under the app bar. Selection is shared, so choosing an item in Tree and switching to Page shows it selected and scrolled into view.
  • The app bar collapses to: file name (truncated, with the full name in title and the accessible name), save state icon + text, and an overflow menu holding Undo, Redo, Save, Export and Palette.
  • The canvas is a documented 1.4.10 exception (two-dimensional content). It pans in both directions inside its own scroll container, and the page itself never scrolls horizontally.
  • The table grid editor is also a 2D exception. It scrolls inside its container and the header row and header column stay sticky.
  • Below 1024px a dismissible notice appears once per session: β€œTag Studio works best at 1024 pixels wide or more. All features remain available one view at a time.”
  • There is no β€œunsupported” wall at any width.

1.6 Zoom, text spacing and motion

  • Browser zoom (Mod+=, Mod+-, Mod+0) is never overridden. Canvas zoom has its own keys (Β§5).
  • All UI text respects WCAG 1.4.12 text-spacing overrides. Truncation uses ellipsis plus a full-text accessible name, and never clips with overflow:hidden alone.
  • prefers-reduced-motion: no animated scrolling on sync, no path-drawing animation, and the proposal β€œghost” pulse is replaced by a static dashed style.
  • Minimum UI font size 12px. Tree rows are 28px tall by default, with a comfortable density of 36px. Hit targets are β‰₯ 24Γ—24 CSS px (WCAG 2.2 AA 2.5.8). Primary buttons are 44px.

2. Selection model and sync rules

2.1 What can be selected

A single shared selection holds either:

  • Structure selection: a set of node IDs, or
  • Content selection: a set of content-item refs (MCRs, OBJRs, orphan runs, artifact runs).

The two kinds never mix in one selection. If you shift-click an orphan while nodes are selected, the selection becomes content-only and the status bar says β€œSelection changed to content (3 items)”. This rule removes a whole class of ambiguous ops, such as β€œretype this orphan run”.

The selection also carries:

  • a primary item, which is the last one added and is what the inspector shows;
  • an anchor for Shift-range extension;
  • a scope (page range) for select-similar and mode queues.

Selection lives in the session store, not in any view.

2.2 Focus is separate from selection

  • Each view has its own focus (DOM focus or aria-activedescendant). Only the view the user is working in holds DOM focus.
  • In the tree and canvas, selection follows focus by default: arrow keys move focus and set a single selection. With Ctrl (Windows/Linux) or Cmd (macOS) held, arrow keys move focus without changing selection, and Space toggles the focused item (APG multi-select convention).
  • Other views reflect a selection change visually. They never take DOM focus and never announce it. Announcing mirror updates would double-speak every keystroke.

2.3 Sync rules

Source β†’ TargetRule
Any β†’ CanvasHighlights the bounding boxes of all content under each selected node. If the primary’s first content item is off-screen, scroll it into view (instant with reduced motion, otherwise 150ms). If any part of the primary is already visible, do not scroll. Multi-page selections show a page-strip badge ”+ on pages 4, 7, 12” with clickable links
Any β†’ TreeExpands ancestors of the primary and scrolls it into view (virtualized). Other selected rows are marked but not expanded to. If a selected node is hidden by the current filter, a non-modal bar above the tree says: β€œ1 selected item is hidden by the filter. [Show it] [Clear filter]β€œ
Any β†’ ReaderScrolls to the first utterance generated by the primary and outlines every utterance from selected nodes
Any β†’ InspectorShows the primary. For a multi-selection it shows the shared fields; fields that differ show β€œMixed (3 values)”. Editing a mixed field applies to all selected items as one group
Canvas clickSelects the innermost node that owns the clicked content. Clicking the same point again (within 600ms, or with Alt+click) walks up one ancestor per click, cycling back to the innermost after the root. A click on orphan content selects that orphan run (content selection). A click on artifact content selects the artifact run. Empty space clears the selection
Hover (pointer only)A β€œpeek” highlight in the other views, without changing selection and without any announcement. Keyboard users get the same peek on the focused row
After an opEvery op returns a result selection: wrap selects the new parent, unwrap selects the former children, retype keeps the node, artifact selects nothing and moves focus to the next item in the mode queue. Undo and redo restore the selection that was current before or after that op
Mode switchThe selection is kept if the new mode can show it. For example, a Figure selection carries into Figures mode and opens that figure in the queue. Otherwise it is kept but the mode shows β€œYour selection (H2) isn’t part of Figures. [Go to Headings]β€œ
Page changePaging the canvas does not clear the selection. The page strip shows where selected items live

2.4 Selection tools (pointer and keyboard parity)

ActionPointerKeyboard / non-drag alternative
Select oneClickArrow keys in tree or canvas
Add/removeMod+clickMod+arrows then Space
RangeShift+click (tree: row range; canvas: reading-order range)Shift+arrows
Box selectDrag a marquee on empty canvasMark mode Alt+B: arrows move a crosshair, Enter sets the first corner, arrows (with Shift for Γ—10) size the box, Enter finishes. Each corner is announced as a percentage of the page (β€œCorner at 12% across, 30% down”)
LassoShift+drag on empty canvasMark mode + L switches to a polygon: Enter adds a vertex, Mod+Enter closes it
Select text rangeDrag across textIn the Reader, Shift+arrows extends across utterances, then β€œSelect content” (Alt+Enter)
Select all on pageβ€”Mod+A in canvas (the page), or in tree (siblings of focus; press again for the whole visible tree)
ClearClick empty spaceEsc (first press closes any open popover, second press clears selection)

3. Operation vocabulary (what the UI emits)

The UI calls the engine only through these ops. Each has preconditions, an inverse, and a human-readable label template (i18n key ts.op.<name>.label). Ops are batched into groups when they come from one gesture.

OpArgsInverseNotes
retypenode, newTyperetype to oldRefused if the parent/child rules break (for example TD outside TR)
wrapnodes[] (contiguous siblings), newType, attrs?unwrapCreates a parent
unwrapnodewrapRemoves the node and promotes its children in place
movenode(s), newParent, indexmove backUsed by drag, cut/paste and Alt+arrows
reorderparent, permutationinverse permutationReading-order edits within one parent
mergeNodesa, b (same type, adjacent)splitNodeFor example a P split across a page break
splitNodenode, childIndexmergeNodes
tagContentcontentRefs[], parent, index, typeuntagContentOrphan β†’ new node
untagContentcontentRefs[]tagContentLeaves content orphaned (flagged)
splitContentcontentRef, glyphOffsetjoinContentSplits a marked-content sequence, for example a bullet glyph from list text. Engine phase 1
artifactcontentRefs[] | node, kind (Pagination/Layout/Page/Background), subtype? (Header/Footer/Watermark/PageNum)unartifactArtifacting a node artifacts all of its content and removes the node
unartifactcontentRefs[]artifactContent becomes orphan, then is tagged by a follow-up op in the same group
setAttrnode, key, valuesetAttr oldAlt, ActualText, E, Lang, ID, Title; Table: Scope, Headers, RowSpan, ColSpan, Summary; List: ListNumbering; Layout: BBox, Placement; any owner/key in Raw mode
setAnnotannot, key, valueoldLink /Contents, widget /TU (tooltip)
linkAnnotannot, nodeunlinkAnnotAdds an OBJR; also sets /StructParent
setTabOrderpage, S|R|ColdForms and Links modes offer S (structure order), which PDF/UA requires
setDockey, valueoldLang, Title, DisplayDocTitle, PDF/UA identifier (XMP), Suspects
setRoleMap / setClassMapname, value | nulloldRaw mode
attestissueKey, reviewer, note?unattestRecords β€œreviewed by a human”. This changes the report, not the PDF. It is still in the op log so it can be undone and replayed

Every op is undoable, with no exceptions. Acrobat’s Content and Reading Order panels have no undo, and some axesPDF operations cannot be undone. In Tag Studio, an operation without a tested inverse cannot ship: the engine’s op registry rejects any op type that lacks one, and CI round-trips op β†’ inverse on the corpus. This guarantee appears in the product copy and the help site.

Empty tags: β€œRemove empty tags” is a document-wide command (palette + Issues fix-all). It emits one group of unwrap ops on nodes with no content and no children (keeping TD/TH cells that hold a table shape, and Form nodes with widgets).

Group envelope (as seen in the history panel): { groupId, label, origin: user|fixAll|similar|ai|template|mode, ops[], selectionBefore, selectionAfter, validationDelta: {fixed, introduced}, at }.


4. Views

4.1 Page canvas

Rendering: a PDF page raster tiled at the current zoom, with a vector overlay layer (SVG) on top. In both themes the page is always drawn as printed; dark mode never inverts the page. Only the surrounding canvas background changes (bone #F7F5F1 in light, ink #0E1116 in dark).

Overlays (each can be toggled from the β€œOverlays” menu or the command palette; defaults are set per mode):

OverlayRenderingDefault on in
Tag boxesStroke in the tag-family color and stroke pattern (Β§14), label chip at top-left (β€œH2”, β€œP”, β€œFig”)All modes
Reading-order pathNumbered ink badges (white number on #0E1116, with a white 2px ring) at each block’s start, joined by a 2px ink polyline with arrowheads. Page-to-page continuation shows ”→ p4”Reading order, Raw
OrphansDiagonal hatch fill in orphan color at 25% opacity, dashed stroke, and a ”!” glyph chip labelled β€œUntagged”Always on; cannot be turned off while orphans exist (a toggle would hide a failure)
ArtifactsDotted gray stroke, 40% white wash over the content, chip β€œArt”Artifacts mode, Raw
ProposalsDashed stroke in the target type’s color, chip β€œSuggested H2 Β· 92%β€œWhenever the Suggestions queue has items for this page
Tab orderNumbered squares (as opposed to the round reading-order badges)Forms, Links
IssuesSmall severity glyph at the node’s top-right (βœ• fail, ? review)All modes

Filters: β€œShow only: [Headings] [Figures] [Tables] [Lists] [Links] [Forms] [Artifacts] [Untagged] [Issues]” (checkbox menu), and β€œDim everything else” (on by default in guided modes).

Selection style: a 2px ink (#0E1116) stroke with a 2px white outer halo, plus 8Γ—8 corner handles. This double stroke passes 3:1 against any page background. Keyboard focus on a canvas item uses the brand focus ring (3px solid #046307, 3px offset; #5CCB60 in dark mode) drawn outside the selection halo, so focus and selection are never confused. The palette excludes green (Β§14), so the focus ring is never mistaken for a tag color.

Canvas ARIA: role="application" with aria-roledescription="page canvas" and aria-label="Page 3 of 48". It is a single tab stop. Its content items are exposed through aria-activedescendant pointing into an offscreen role="listbox" mirror, in which each option’s name is the node announcement (Β§4.2.3). The listbox mirror lets screen readers announce the focused item without the SVG overlay needing an accessibility tree. The application role is justified because the canvas has custom 2D navigation. Its keys are listed in the help dialog (?), and aria-describedby gives a short key hint: β€œArrow keys move between items in reading order; Alt plus arrows moves spatially; press question mark for all keys.”

4.2 Structure tree

4.2.1 Rows and columns

Each row shows: disclosure Β· tag chip (family color + pattern + text label) Β· content preview Β· issue badges Β· page number(s).

Content preview rules. β€œ<MCID 12>” is never shown as a node’s preview.

NodePreview
Text containerFirst 80 characters of its actual extracted text, whitespace collapsed; ActualText wins if set (shown with an β€œAT” marker)
Figure/Formula32px thumbnail + alt text in quotes, or β€œNo alt text” in the error style (text, not color only)
Table”Table Β· 4 rows Γ— 3 cols Β· 1 header row”
L”List Β· 5 items Β· numbered”
LinkLink text + ”→ example.gov/budget”
Form widgetField name + β€œTooltip: …” or β€œNo tooltip”
Content item (Raw mode only)β€œText: The fiscal ye…” / β€œImage 640Γ—480” / β€œAnnot: Link” and, in muted mono, MCID 12 Β· p3

4.2.2 ARIA pattern

  • The container is role="treegrid" with aria-label="Structure tree", aria-multiselectable="true", aria-rowcount set to the total number of visible (expanded) rows, and aria-colcount="4".
  • Each row is role="row" with aria-level, aria-setsize, aria-posinset, aria-expanded (only on rows that have children), aria-selected, and aria-rowindex, which is required because rows are virtualized.
  • The cells are role="gridcell" for Tag, Content, Issues and Page. There is one tab stop, using roving tabindex on rows. Focus is on the row by default (APG treegrid β€œrow-first” behavior). Right arrow on an expanded row moves into the cells, where Left and Right move between cells and Left from the first cell returns to the row.
  • A row’s accessible name is built explicitly (aria-label on the row). The implementation does not rely on concatenated cells, because verbosity differs across screen readers.
  • Virtualization renders the viewport Β± 50 rows. Focus is never placed on a row that isn’t mounted, and the virtualizer mounts the target row before moving focus. This holds for 10k+ nodes.
  • Drag and drop is pointer-only sugar. Every drop has a keyboard op (Β§5.3).

4.2.3 Row announcement text

Template (i18n ts.tree.row.name, ICU):

{tagSpoken}, {preview}{issues, select, 0 {} other {, {issues, plural, one {# issue} other {# issues}}: {firstIssue}}}{pages}

The level, set size and position come from the ARIA attributes, so the screen reader adds β€œlevel 3, 2 of 5”. The template must not repeat them. tagSpoken uses a spoken form, not the raw tag: β€œHeading 2”, β€œParagraph”, β€œFigure”, β€œTable”, β€œList item”, β€œArtifact”. In Raw mode it becomes β€œH2 (Heading 2)”, so experts hear the literal tag.

Examples (NVDA-style rendering; the ARIA suffix is added by the AT):

  • β€œHeading 2, Budget overview, 1 issue: heading level skipped from 1, page 3, level 3, 2 of 5, collapsed”
  • β€œFigure, No alt text, 1 issue: missing alternate text, page 4, level 4, 1 of 3”
  • β€œParagraph, The fiscal year closed with…, page 3, level 3, 4 of 5, selected”
  • β€œUntagged content, 3 runs, page 3” (orphan group pseudo-row, only in Raw mode and Reading order)

Rules for drop indicators:

  • A 2px ink insertion line with a left circle marks β€œinsert between” (sibling). A full-row outline with a 12% tint marks β€œinsert as last child”.
  • An illegal target shows a struck-through line and a tooltip giving the reason (β€œLI can only go inside L”).
  • The drop position is also written as text in the status bar: β€œMove β€˜Budget overview’ after β€˜Table 1’, inside Sect”.

4.2.4 Search and filter

/ focuses the filter field above the tree. It accepts free text (matching previews) and facets, which are chips with autocomplete:

  • type:Figure, has:alt, no:alt, issue:any, issue:<ruleId>, page:3-7, lang:fr, origin:ai, changed:session
  • Saved presets: β€œFigures with no alt”, β€œHeadings out of sequence”, β€œEmpty tags”, β€œTables without headers”, β€œLinks without text”, β€œUntagged content”, β€œChanged this session”.

Filtering shows matching rows with their ancestor path as muted context rows (aria-level intact). A live count (β€œ23 matches”) appears in a polite live region, debounced to 500ms.

4.3 Screen reader preview (Reader)

The Reader is a linear, virtualized list of utterances, one per announceable unit in structure order, rendered with the grammar in Β§4.3.2. It is role="list" inside a <section>, so it can be read with browse-mode or virtual-cursor navigation. It is deliberately not an application widget, so screen reader users can read it with their normal reading keys. Each utterance is a list item holding a button, β€œEdit …”, that jumps selection to the node and focuses the inspector field that produced the text.

Tabs: Reader (announcements) Β· Reflow (the document rendered as semantic HTML at the reading width, which shows what reflow and Liquid Mode would produce).

Verbosity profile selector: Generic (default) Β· NVDA-like Β· JAWS-like Β· VoiceOver-like. The profiles only change role words and ordering, for example β€œheading level 2” versus β€œheading 2” and whether the role comes before or after the name. A persistent caption reads: β€œPreview approximates what assistive technology announces. Always spot-check with a real screen reader.” We do not claim the preview is equivalent to a real screen reader.

4.3.1 Inline editing

Pressing Enter on an utterance’s Edit button opens an inline editor for the governing text:

  • a figure edits Alt;
  • a heading offers a level picker and ActualText;
  • a link edits the annotation’s Contents;
  • a text node with ActualText edits ActualText.

Mod+Enter commits the edit (as a setAttr op) and Esc cancels it. After a commit, the utterance re-renders and the new text is announced politely.

4.3.2 Announcement grammar (Generic profile)

Notation: {x} is a value, […] is optional, | separates alternatives. Strings are ICU templates in ts.reader.*.

StructureUtterance
H1–H6heading level {n}, {text}
H (unnumbered)heading, {text} + flag β€œUnnumbered heading, level unknown to most AT”
P, Span, Quote, BlockQuote{text} (BlockQuote: block quote, {text} … out of block quote)
Llist, {count, plural, one {# item} other {# items}} … out of list
LI{Lbl} {LBody text}; nested list: list, {count} items, nesting level {depth}
Tabletable with {rows} rows and {cols} columns [, {caption}] … out of table
TD (first visit to a row)row {r}, {rowHeaders}, column {c}, {colHeaders}, {text}; later cells in the same row omit the row part
TH{text}, column header | row header (from Scope)
Figuregraphic, {Alt}; missing Alt: graphic + flag β€œNo alternate text; AT may read the file name or skip it”
Figure + Captiongraphic, {Alt} then {caption text}
Formulamath, {Alt}
Linklink, {text}; if annotation Contents differ: link, {text} + hint (description: {Contents})
Notefootnote, {text} (from a Note following a Reference)
Reference{text}, link to note {n} when an ID/Ref pair resolves
TOC / TOCItable of contents, list, {n} items / {text}
Form widget{role: edit text|check box|radio button|combo box|button}, {TU}[, required][, {state}]; missing TU: {role}, unlabeled + flag
Lang change{text} with a leading chip (French). Generic mode inserts the phrase β€œswitches to French” once
E (expansion)Speaks the expansion instead of the text, with a chip abbr
ActualTextSpeaks ActualText instead of the content; chip AT
ArtifactNot announced. With the β€œShow skipped” toggle, a muted row reads (skipped: running header "Annual Report 2025")
OrphanNot reliably announced. Shown as an error row β€œ(untagged: may be read out of order or not at all) {text}β€œ
Page boundaryA thin divider β€œpage 4” (visual only; not an utterance)

Flags are rendered as text chips and linked to the Issues entry. They are never color-only.

4.4 Inspector

The inspector is a non-modal panel that always shows the primary selection. Sections are disclosure widgets (<details>-style buttons with aria-expanded):

  1. Identity: Type (combobox of allowed types for this parent, with a type-ahead filter and invalid types shown disabled with the reason), page(s), node path breadcrumb (each crumb is a button that selects that ancestor).
  2. Text alternatives: Alt, ActualText, E. There is a character counter, and lint hints appear inline below the field (β€œStarts with β€˜image of’; AT already says β€˜graphic’”).
  3. Language: Lang (combobox of BCP 47 tags with names such as β€œFrench (fr)”; β€œinherit” shows the effective value β€œinherits en-US from Document”).
  4. Type-specific: Table (scope, headers, IDs, spans, summary) Β· List (ListNumbering) Β· Link (Contents, target URL read-only) Β· Form (TU, field name read-only) Β· Note (ID).
  5. Layout: BBox (x, y, w, h with units; a β€œRecompute from content” button), Placement.
  6. Attributes (Raw mode, or when expanded): an owner/key/value table, plus + Add attribute.
  7. Issues on this node: a mini-list linked to the Issues panel.

Commit rules: text fields commit on blur, Enter (single-line) or Mod+Enter (multi-line). Each commit is one setAttr op. Typing does not create an op per keystroke, and native text undo works inside the field until it is committed. Esc reverts the field to its committed value. A pending uncommitted edit is shown by a left border and the text β€œNot applied”.

Multi-selection: shared fields are editable and apply to all as one group (β€œSet Lang = fr on 12 nodes”). Fields that don’t apply to every selected type are hidden and a note lists them: β€œAlt applies to 3 of 12 selected; select Figures only to edit.”

Document (nothing selected, or root selected): Title, DisplayDocTitle, document Lang, PDF/UA identifier, Tab order summary, RoleMap and ClassMap counts (links to Raw mode).


5. Keyboard map

5.1 Conventions and conflict policy

  • Mod means Ctrl on Windows/Linux and Cmd on macOS. When a binding says Ctrl literally it is Ctrl on every platform.
  • No shortcut ever uses the NVDA key (Insert/CapsLock), the JAWS key (Insert/CapsLock), or the VoiceOver key (Ctrl+Option). No shortcut uses Ctrl+Alt+Arrows, which NVDA and JAWS use for table navigation, or Ctrl+Alt+<letter> / Ctrl+Alt+<digit>, which are AltGr combinations on European layouts (for example AltGr+Q = @ on German keyboards).
  • No Alt+<letter> for the letters that open browser menus on Windows: D, E, F, H, S, T, V, B (Chrome: Alt+F/E, Alt+D address bar; Firefox: Alt+F/E/V/S/B/T/H; Edge: Alt+F).
  • Browser zoom and tab keys are never captured: Mod+=, Mod+-, Mod+0, Mod+1..9, Mod+T/W/N, Mod+Tab, Mod+L, Mod+Shift+T, Mod+F (Find in page is left to the browser; our search is /).
  • Alt+<letter> accelerators only fire when focus is in a non-text widget (tree, canvas, grid, queues). In text fields they are passed through, because macOS Option+letter types characters.
  • Single unmodified letters are used only inside our own composite widgets, which screen readers switch into focus/forms mode automatically. They are never bound in the Reader, because that is a browse-mode document.
  • The keymap can be customized (Settings β†’ Keyboard). The editor refuses bindings from the reserved lists above and names the conflict: β€œCtrl+Alt+Down is used by NVDA and JAWS to move down a table column.”
  • ? (in a widget) or Mod+/ (anywhere) opens the keyboard help dialog, filtered to the current view and mode.

5.2 Global

KeysActionNotes
F6 / Shift+F6Next / previous region (Mode rail β†’ Task β†’ Tree β†’ Canvas β†’ Reader β†’ Inspector β†’ Dock β†’ App bar)Browsers usually let pages capture F6; verify in usability testing (Β§16, scenario U14). Fallback: Mod+K β€œGo to …” and landmark navigation
Mod+KCommand palette (every command, mode and panel, with shortcut hints)Palette is a combobox + listbox dialog
Mod+ZUndo (group)In a text field with uncommitted edits, native text undo runs instead
Mod+Shift+Z, Ctrl+Y (Win)Redo
Mod+SSave a version (runs the gate, Β§11). Edits sync continuously anyway; Save makes a verified PDF versionpreventDefault of the browser’s Save Page
Mod+Shift+SSave as new version with a note
Mod+EnterCommit / Accept (context: inspector field, proposal, fix preview, mark-mode)
EscClose popover β†’ cancel inline edit β†’ clear selection (one per press)NVDA may also switch from focus to browse mode on Esc; focus stays put (see Β§5.6)
Mod+/Keyboard help
Mod+IToggle inspector drawer (compact layouts); focus inspector (wide)Firefox Ctrl+I = Page Info; captured only when focus is inside the app. Palette alternative β€œGo to Inspector”
Mod+Shift+. / Mod+Shift+,Next / previous modeOrdering follows the rail
Alt+PageDown / Alt+PageUpNext / previous issue in the whole document (selects the node, announces the issue)
Mod+Shift+JJump to page… (dialog with number field)Chrome Ctrl+Shift+J = DevTools console on Windows. Conflict, see Β§5.6; palette β€œGo to page” is the primary route

5.3 Structure tree (focus in treegrid)

KeysActionOp
↑ / ↓Previous / next row (selection follows)β€”
β†’Expand; if expanded, move to first child; with cell focus, next cellβ€”
←Collapse; if collapsed, move to parentβ€”
Home / EndFirst / last visible rowβ€”
PageUp / PageDownΒ± one viewportβ€”
*Expand all siblings (APG)β€”
Mod+↑/↓Move focus without changing selectionβ€”
SpaceToggle selection of the focused rowβ€”
Shift+↑/↓, Shift+Home/EndExtend selectionβ€”
Mod+ASelect siblings; press again to select all visibleβ€”
Printable characterOpens the inline Retype combobox seeded with that character (like typing into a spreadsheet cell). Enter commits, Esc cancels. Only types allowed under the parent are listedretype
F2 / EnterOpen Retype with the current typeretype
Alt+↑ / Alt+↓Move node up / down among siblingsmove
Alt+←Promote (outdent: become next sibling of parent)move
Alt+β†’Demote (indent: become last child of previous sibling)move
Ctrl+[ / Ctrl+]Promote / demote (alternative for Alt+←/β†’, see conflict C1)move
Mod+X then Mod+VCut then paste after the focused row; Mod+Shift+V pastes as last child. Cut rows are dimmed until pasted or Esc. Nothing is removed until the pastemove
Alt+WWrap the selection in a new parent (type prompt)wrap
DeleteUnwrap (remove the tag and keep its children and content)unwrap
Shift+DeleteMark as artifact (kind prompt with a sensible default)artifact
Alt+M / Alt+PMerge with next sibling / split at focused childmergeNodes / splitNode
Alt+LSelect similar (Β§8)β€”
Alt+IFocus the issue badge of this row / open its issueβ€”
/Focus filter fieldβ€”
Alt+RFind on page: reveal on canvas (scroll only; ignores the β€œdon’t scroll if visible” rule)β€”

5.4 Canvas (focus in page canvas)

KeysActionOp
↑/↓ or ←/β†’Previous / next item in reading order (the path numbers)β€”
Alt+arrowsSpatial move to the nearest item in that directionβ€”
Alt+Shift+↑Select parent (walk up); Alt+Shift+↓ back to childβ€”
Space, Shift+arrows, Mod+AToggle, extend, select all on pageβ€”
PageDown / PageUpNext / previous pageβ€”
+ / - / 0 / 1Canvas zoom in / out / fit page / 100% (unmodified; browser zoom untouched)β€”
Alt+BMark mode (keyboard box select, Β§2.4); L inside switches to lassoβ€”
Printable character (with a content or box selection)Tag as… type-ahead combobox (β€œh2”, β€œfig”, β€œp”). Enter appliestagContent / retype / wrap
EnterOpen Tag as… with the suggested typesame
Shift+DeleteArtifactartifact
DeleteUntag (content becomes orphan). Asks for confirmation the first time per session, because it creates failuresuntagContent
Alt+LSelect similarβ€”
OCycle overlay preset (All Β· Mode default Β· Issues only Β· None but orphans)β€”
NToggle reading-order numbersβ€”
Alt+RFind in tree: reveal the focused item’s node in the tree (expands ancestors, clears a filter that hides it after asking)β€”

Find works in both directions (page β†’ tree and tree β†’ page) and is also on each view’s context menu, so neither view ever loses track of the other. This is the Acrobat problem where the reading-order view desyncs from the tree.

5.5 Mode-specific keys

These apply only while focus is in that mode’s task panel or grid. Every one is also a visible button.

ModeKeys
Reading orderAlt+↑/↓ move block; Alt+C β€œclick-sequence” tool (then Enter on canvas items in the new order; Mod+Enter finish); Alt+1..3 apply 1/2/3-column heuristic (proposal)
Headings1–6 set level of focused outline row; Alt+←/β†’ promote/demote level; 0 demote heading to paragraph (P)
FiguresMod+Enter save alt & next; Alt+N next without saving; Alt+A mark decorative (artifact); Alt+G request AI suggestion
Tables (grid)Arrows move cell; Shift+arrows range; Alt+C toggle column header; Alt+R toggle row header; Alt+M merge cells; Alt+P split cell; Alt+Y cycle Scope (Column/Row/Both); Alt+J edit Headers/IDs for cell
ArtifactsSpace toggle group; Alt+A artifact selected groups; Alt+U un-artifact; Alt+X exclude page from group
ListsAlt+W make list from selection; Alt+β†’/← nest/unnest item; Alt+N cycle numbering
LinksMod+Enter save & next; Alt+G AI description
FormsAlt+↑/↓ move in tab order; Mod+Enter save tooltip & next
NotesAlt+K link reference ↔ note
LanguageAlt+G detect languages (proposal)
TOCAlt+G generate from headings (proposal)
Suggestions dockMod+Enter accept; Alt+X reject; Alt+E edit then accept; ↓/↑ next/prev proposal
Issues dockEnter go to node; Alt+F apply first fix; Alt+Shift+F fix all like this (preview)

Alt+F is on the Windows reserved list. Exception: in the Issues dock it is captured only while focus is on an issue row (a non-text widget), where Chrome/Firefox menu activation is suppressed by preventDefault. It is listed as conflict C3 in case testing disproves this.

5.6 Screen reader and browser conflicts

#BindingConflicts withResolutionStatus
C1Alt+← / Alt+β†’ (tree promote/demote)Windows browser Back/Forward. macOS Option+←/β†’ = word navigation (only in text fields, which we skip)Captured only in the treegrid with preventDefault. A beforeunload guard protects unsaved work if capture fails. Alternative keys Ctrl+[ / Ctrl+] are always availableResolved with alternative; verify Firefox capture
C2F6 region cycleChrome/Edge/Firefox use F6 to focus the address barPages can intercept F6 keydown in current Chromium and Gecko; if not, users fall back to Mod+K β€œGo to …” and landmarksUnverified β€” test U14
C3Alt+F in Issues dockChrome/Edge/Firefox File/app menuCaptured only on issue rows. The button β€œFix” is always adjacentUnverified β€” may need rebinding to Alt+Enter
C4Mod+Shift+J jump to pageChrome/Edge DevTools console (Windows/Linux)Primary route is palette β€œGo to page”; binding is user-rebindableUnresolved β€” no universally free combo; kept for macOS only (Cmd+Shift+J free in Safari/Chrome mac), off by default on Windows
C5Mod+IFirefox Page Info (Ctrl+I)Captured only when focus is in the app; palette alternativeResolved
C6Printable-character retype / tag-asNVDA and JAWS browse-mode single-letter navigation (H, T, L, F, K, B …); VoiceOver Quick Nav single-key modeThe tree and canvas are composite widgets (treegrid, application), so NVDA/JAWS switch to focus/forms mode automatically. VoiceOver users with single-key Quick Nav on must turn it off (VO+Q / Quick Nav toggle); the tree’s aria-describedby hint says so on first focus per session. Accidental retype is harmless: nothing commits without EnterResolved with a hint; verify in U10
C7Arrow keys in tree/canvasVoiceOver arrow Quick Nav (Left+Right toggles) intercepts arrowsSame hint as C6. With Quick Nav off, VO passes arrows to the widgetDocumented
C8EscNVDA: Esc in auto focus mode can drop to browse modeOur handler still runs (NVDA passes the key on). Focus stays on the same element, so Space/Enter or NVDA+Space returns to focus mode. No action is lostDocumented; test U10
C9Mod+Shift+. / , mode cyclingNone known (Chrome/Firefox free; macOS Cmd+Shift+. = show hidden files, only in file dialogs)β€”Resolved
C10Alt+number (reading-order columns)Firefox on Linux: Alt+1..9 switches tabsOnly in the Reading order task panel; buttons adjacent; rebindablePartially unresolved on Linux Firefox
C11Ctrl+Y redo (Windows)Firefox Windows: none; Chrome: History on some builds (Ctrl+H, not Y)β€”Resolved
C12* expand allJAWS: none in forms mode; NVDA: none in focus modeβ€”Resolved

6. Task modes

Every mode has the same frame:

  • Task panel (left): a queue or outline, with a progress meter (β€œ14 of 23 figures reviewed”) in text and a <progress> element.
  • Canvas (center): the mode’s overlay preset, with items outside the mode dimmed.
  • Inspector (right): trimmed to the fields the mode needs, with a β€œMore properties” disclosure.
  • Done criteria: the mode’s rail badge turns to a check with text (β€œFigures: done”) when there are 0 failures in that category. It never shows a check while Needs-review items remain unattested. It shows β€œFigures: 3 to review” instead.

Mode switching never loses the selection or the undo history. Each mode’s first visit shows a dismissible 2-line explainer (not a tour).

6.1 Reading order

Goal: the top-level structure order matches the logical reading sequence on every page, and there are no orphans.

β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚β–ΆRO β”‚ READING ORDER page 3 of 48β”‚ β—€ p3 β–Ά Path ● Numbers ● β”‚ INSPECTOR β”‚
β”‚ Hd β”‚ ⚠ Possible column interleave β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ P Β· page 3 β”‚
β”‚ Fg β”‚ [Apply 2-column order] β”‚ β”‚ ①━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ β”‚ β”‚ Order: 4 of 9 β”‚
β”‚ Tb β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ Continues on p4? [ ] β”‚
β”‚ Ar β”‚ β”‚ ≑ 1 H1 Annual Report 2025 β”‚ β”‚ β”‚ β”‚β‘‘ col A β”‚ β”‚β‘£ col B β”‚ β”‚ β”‚ β”‚
β”‚ Ls β”‚ β”‚ ≑ 2 P The fiscal year… β”‚ β”‚ β”‚ β”‚ ↓ β”‚ β”‚ ↓ β”‚ β”‚ β”‚ [Merge with next β–Έ] β”‚
β”‚ Lk β”‚ β”‚ ≑ 3 P Our revenue… β—€ β”‚ β”‚ β”‚ β”‚β‘’ β”‚ β”‚β‘€ β”‚ β”‚ β”‚ β”‚
β”‚ Fm β”‚ β”‚ ≑ 4 P Expenses fell… β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚
β”‚ Nt β”‚ β”‚ ≑ 5 Figure "Bar chart…" β”‚ β”‚ β”‚ β–‘β–‘ page footer "3" (untagged) β–‘β–‘ ⚠ β”‚ β”‚ β”‚
β”‚ La β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ TC β”‚ Untagged on this page: 1 [β–Έ] β”‚ [Click-sequence ✎] [Tag as… ] [Artifact]β”‚ β”‚
β”‚ {} β”‚ [β—€ Prev page] [Next page β–Ά] β”‚ β”‚ β”‚
β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Steps:

  1. Enter the mode. The queue opens on the first page with an order issue, or on page 1. A detector flags likely problems: column interleave, a z-order-driven sequence (a caption read before its figure), a top-to-bottom sweep across columns, and orphans. These appear as banners with actions.
  2. Review the path. The numbered path is drawn on the canvas. The task panel lists the page’s top-level blocks in order, and each row has a drag handle (≑) and a position number.
  3. Fix order, using any of these:
    • Drag rows in the list, or blocks on the canvas β†’ one reorder(parent, perm).
    • Alt+↑/↓ on a row β†’ move (one position), announced as β€œMoved to position 3 of 9”.
    • Click-sequence tool (Alt+C): click (or press Enter on) blocks in the intended order. The badges renumber live as you go. Mod+Enter commits β†’ one reorder group; Esc abandons with no op. Blocks you didn’t click keep their relative order after the sequenced ones, and a notice says so.
    • Tag as List: when blocks in the sequence (or a canvas box selection) look like list items, β€œTag as…” offers List, which runs the Lists-mode β€œMake list” group (Β§6.6), including nesting by indent. Acrobat’s reading-order tool has no List type.
    • Apply N-column order banner β†’ a proposal group of reorder ops, previewed as a ghost path until accepted.
  4. Resolve orphans: β€œUntagged on this page” expands to a list of orphan runs. For each run, choose β€œTag as…” β†’ tagContent(refs, parent, index, type) at the position implied by its location, or β€œArtifact” β†’ artifact(refs, kind).
  5. Cross-page continuity: if a block continues on the next page, the β€œContinues on p4” checkbox β†’ mergeNodes(a, b) (a paragraph split by a page break becomes one P).
  6. Tab order (end of mode): if any page has annotations and /Tabs β‰  S, a banner offers β€œUse structure order for Tab key on all pages” β†’ group of setTabOrder(page, S).
  7. Review attestation: reading-order meaning cannot be machine-verified. Each page shows β€œReading order on this page: Needs review Β· [Mark reviewed]” β†’ attest(ro:page3). The page queue shows reviewed pages with a check and the reviewer’s initials.

Ops emitted: reorder, move, mergeNodes, tagContent, artifact, setTabOrder, attest.

6.2 Headings

Goal: a logical heading hierarchy with no skipped levels, where every visually-heading text is tagged as a heading and nothing non-heading is.

β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ RO β”‚ HEADINGS OUTLINE β”‚ β—€ p3 β–Ά Headings only ● β”‚ INSPECTOR β”‚
β”‚β–ΆHd β”‚ Level Text Page β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ H3 Β· page 3 β”‚
β”‚ Fg β”‚ [1] Annual Report 2025 1 β”‚ β”‚ ━━[H1]━━━━━━━━━━━━━━━━━━━━ β”‚ β”‚ Level β‘  β‘‘ ●⑒ β‘£ β‘€ β‘₯ β”‚
β”‚ Tb β”‚ [2] Message from the CEO 2 β”‚ β”‚ ━━[H2]━━━━━━━━━━ β”‚ β”‚ ⚠ Skips level 2 β”‚
β”‚ Ar β”‚ [3] βœ• Budget overview 3β—€ β”‚ β”‚ ━━[H3]━━━━━ β—€ β”‚ β”‚ Font: Archivo 18 B β”‚
β”‚ Ls β”‚ [2] Operations 5 β”‚ β”‚ β”ˆβ”ˆβ”ˆ Revenue (P, looks like β”‚ β”‚ Similar looking: 6 β”‚
β”‚ Lk β”‚ [ ] ? "Revenue" (P) looks 3 β”‚ β”‚ a heading) [Make H3] β”‚ β”‚ [Select similar] β”‚
β”‚ Fm β”‚ like a heading [H3β–Ύ] β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚
β”‚ Nt β”‚ Doc title: [Annual Report 2025] β”‚ β”‚ β”‚
│ La │ Level map: 24pt→H1 18pt→H2 14→H3 │ │ │
β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Steps:

  1. The outline lists every H/H1–H6 in document order, indented by level, with a level chip (number + β€œH” + indentation, never color alone). Skipped levels show a βœ• and the text β€œSkips level 2”.
  2. Candidate headings: a detector clusters text by visual signature (font, size, weight, spacing before) and lists P nodes that look like headings as ”?” rows with a suggested level.
  3. Set levels by pressing 1–6 on an outline row, using the inspector’s level radio group, or using Alt+←/β†’ for relative moves β†’ retype(node, Hn). 0 β†’ retype(node, P).
  4. Level map (optional): β€œApply level map” assigns levels by font-size cluster across the whole document β†’ a proposal group of retype ops for review (see Β§9 for how proposals are shown).
  5. Select similar from any heading applies the same level to everything that looks like it β†’ one retype group.
  6. Document title: prefilled from the first H1. Commit β†’ setDoc(Title) + setDoc(DisplayDocTitle, true).
  7. Optional sectioning: β€œWrap sections under headings” β†’ proposal group of wrap(nodes, Sect). Off by default, because PDF/UA does not require it.
  8. Attest: β€œHeading text describes its section: Needs review” β†’ attest(headings:doc).

Ops: retype, setDoc, wrap, attest.

6.3 Figures

Goal: every Figure has meaningful Alt or is artifacted as decorative, with a caption association and a BBox.

β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ RO β”‚ FIGURES 14 of 23 reviewed β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ FIGURE 15 Β· p12 β”‚
β”‚ Hd β”‚ β–“β–“β–“β–“β–“β–“β–“β–“β–“β–“β–‘β–‘β–‘β–‘β–‘ β”‚ β”‚ β”‚ β”‚ ( ) Informative β”‚
β”‚β–ΆFg β”‚ Filter: [Needs alt β–Ύ] β”‚ β”‚ [ large figure crop ] β”‚ β”‚ ( ) Decorative β†’ β”‚
β”‚ Tb β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ β”‚ artifact β”‚
β”‚ Ar β”‚ β”‚ β–£ 13 Logo (decorative) βœ“ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ ( ) Text in image β†’β”‚
β”‚ Ls β”‚ β”‚ β–£ 14 "Bar chart of…" βœ“ β”‚ β”‚ Context before: "…grew 12% as shown β”‚ ActualText β”‚
β”‚ Lk β”‚ β”‚ β–£ 15 No alt text βœ• β—€ β”‚ β”‚ below." Caption: "Figure 3: Revenue β”‚ Alt text β”‚
β”‚ Fm β”‚ β”‚ β–£ 16 No alt text βœ• β”‚ β”‚ by quarter" β”‚ [ ]β”‚
β”‚ Nt β”‚ β”‚ β–£ 17 "image1.png" βœ• β”‚ β”‚ β”‚ [ ]β”‚
β”‚ La β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ ✦ Suggestion (High, 91%): "Bar chartβ”‚ 0/250 Β· lint: β€” β”‚
β”‚ TC β”‚ β”‚ of revenue by quarter; Q4 highest atβ”‚ Caption: [Linked βœ“]β”‚
β”‚ {} β”‚ β”‚ $4.2M." Reason: chart with legible β”‚ [Save & next βŒ˜β†΅] β”‚
β”‚ β”‚ β”‚ axis labels. [Use] [Edit] [Dismiss] β”‚ [Skip] β”‚
β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Steps:

  1. The queue holds every Figure/Formula node plus image content that is not a Figure (orphan images, and images tagged P), grouped by status: Needs alt Β· Suspicious alt (file name, β€œimage”, single word, over 250 characters, or identical to nearby text) Β· Has alt (needs review) Β· Reviewed Β· Decorative. The default filter is β€œNeeds attention”.
  2. The focused figure shows a large crop plus context (the text before and after in reading order, and the detected caption).
  3. Choose its role (radio group):
    • Informative β†’ setAttr(Alt) (+ retype(node, Figure) if it wasn’t one; + tagContent if it was orphan)
    • Decorative β†’ artifact(node, Layout)
    • Text in image β†’ setAttr(ActualText) (retype to P/Span where appropriate)
    • Math β†’ retype(Formula) + setAttr(Alt)
  4. Alt text field: live lint for β€œimage of”, β€œpicture of”, file names, duplicated caption text (hint: β€œCaption already read; alt can be shorter”), or length > 250 (hint to use a long description in adjacent text).
  5. Caption: if a Caption node is detected nearby, β€œLinked βœ“β€ means it is a child of the Figure, or Figure and Caption are wrapped together. β€œLink caption” β†’ move(caption, figure, 0).
  6. BBox is computed automatically on save of Alt if missing β†’ setAttr(BBox) in the same group.
  7. AI assist: Alt+G or β€œSuggest” β†’ proposal inline (Β§9). β€œUse” puts the text into the field unapplied; Mod+Enter commits it as origin ai-accepted (or ai-edited if changed).
  8. List view (bulk, the Grackle-style alt-text panel): a toggle above the queue switches to a grid with the columns thumbnail Β· page Β· role Β· Alt (editable) Β· lint Β· AI band. Arrow keys move between cells, Enter edits, and Mod+Enter commits a cell. Each commit is its own setAttr group, so undo stays granular. Sorting by page or lint lets a remediator sweep 200 figures without opening each one.
  9. Mod+Enter = save + next. Saving counts as a human attestation of this figure’s alt (the person read the image and wrote or accepted the text), so no separate attest is needed. The history records who saved it.

Ops: setAttr(Alt|ActualText|BBox), retype, tagContent, artifact, move.

6.4 Tables

Goal: a correct Table/TR/TH/TD structure, header cells with Scope (simple tables) or Headers/IDs (irregular tables), correct spans, and optional THead/TBody/TFoot.

β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ RO β”‚ TABLES 2 of 6 done β”‚ TABLE GRID Β· Table 3 Β· page 7 [Grid ● | Canvas] β”‚ CELL B1 β”‚
β”‚ Hd β”‚ βœ“ Table 1 p3 4Γ—3 β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Type (●)TH ( )TDβ”‚
β”‚ Fg β”‚ βœ“ Table 2 p5 6Γ—4 β”‚ β”‚ A β”‚ B β”‚ C β”‚ D β”‚ β”‚ Scope [Column β–Ύ] β”‚
β”‚β–ΆTb β”‚ βœ• Table 3 p7 5Γ—4 β—€ β”‚ β”Œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ ID [t3-b1 ] β”‚
β”‚ Ar β”‚ ? Table 4 p9 12Γ—6 β”‚ β”‚ 1 β–€β”‚ Region β–€ β”‚[Q1 β–€] β—€β”‚ Q2 β–€ β”‚ Q3 β–€ β”‚ β”‚ Spans ↔1 ↕1 β”‚
β”‚ Ls β”‚ βœ• Table 5 p9 3Γ—2 β”‚ β”œβ”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ Headers (auto) β”‚
β”‚ Lk β”‚ ? Table 6 p14 2Γ—2 β”‚ β”‚ 2 β”‚ North β–₯ β”‚ 4.1 β”‚ 4.3 β”‚ 4.8 β”‚ β”‚ β”‚
β”‚ Fm β”‚ β”‚ β”‚ 3 β”‚ South β–₯ β”‚ 3.2 β”‚ 3.0 β”‚ 3.9 β”‚ β”‚ Announces as: β”‚
β”‚ Nt β”‚ Structure: THead βœ“ β”‚ β”‚ 4 β”‚ East β–₯ β”‚ 2.7 β•Žmergedβ•Ž 2.9 β”‚ 3.1 β”‚ β”‚ "row 2, North, β”‚
β”‚ La β”‚ Header rows: [1] β”‚ β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ column Q1, 4.1" β”‚
β”‚ TC β”‚ Header cols: [1] β”‚ β–€ column header β–₯ row header β•Ž span β”‚ β”‚
β”‚ {} β”‚ Pattern: Simple (Scope) β”‚ [Header row] [Header col] [Merge] [Split] [Headers/IDs…] β”‚ [Preview row β–Ά]β”‚
β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Steps:

  1. Pick a table from the queue. Detection covers Table nodes and table-like orphan or P regions, which get β€œMake table” β†’ a proposal that builds wrap(Table), wrap(TR) and tagContent(TD) groups from the detected grid.
  2. The grid editor replaces per-cell properties dialogs. Every header, scope, span and Headers/ID edit happens in place, and bulk selection of rows, columns or ranges applies TH/TD and Scope in one group. It is an ARIA grid built from the structure (rows = TR, cells = TH/TD with spans resolved). It can toggle between an abstract grid and β€œCanvas”, which draws the grid over the page. Header cells are marked by a glyph (β–€ column header, β–₯ row header) and a bold label, in addition to the fill.
  3. Header rows/columns: set the count on the task panel spinbutton or with Alt+C/Alt+R on a cell β†’ retype(TDβ†’TH) and setAttr(Scope) for each cell as one group. If the table has no THead, β€œPut header rows in THead” (default on) β†’ wrap(rows, THead) + wrap(rest, TBody).
  4. Spans: select a range and press Alt+M (Merge) β†’ setAttr(ColSpan/RowSpan) on the surviving cell + move content from the absorbed cells + unwrap of the empty cells. This is refused if the absorbed cells hold content that would change reading order, and the refusal explains why. Alt+P (Split) reverses it.
  5. Pattern detection: the task panel shows β€œSimple (Scope is enough)” or β€œIrregular: needs Headers/IDs”. Irregular tables have multi-level headers, spanning headers, or headers mid-table. For irregular tables, β€œGenerate Headers/IDs” β†’ group of setAttr(ID) on all TH plus setAttr(Headers) on all TD, computed from the grid. The per-cell editor (Alt+J) is a checklist of all TH cells in the table, where ticking one adds its ID.
  6. Split/merge tables: a table that continues across a page break β†’ β€œMerge with Table 4 (p8)” (mergeNodes, then unwrap of the duplicated header rows, which become artifacts via artifact when they repeat). β€œSplit table here” β†’ splitNode.
  7. Announcement preview: the inspector’s β€œAnnounces as” shows the Reader grammar for the focused cell. β€œPreview row ▢” plays the whole row into the live region. Screen reader users hear the same string by pressing Alt+Q.
  8. Flatten layout table: β€œThis is a layout table” β†’ unwrap of Table/TR/TD down to their content (proposal preview first), because PDF/UA has no layout-table role.
  9. Attest: β€œHeader associations are correct: Needs review” β†’ attest(table:t3).

Ops: retype, setAttr(Scope|ID|Headers|RowSpan|ColSpan|Summary), wrap, unwrap, move, mergeNodes, splitNode, tagContent, artifact, attest.

6.5 Artifacts

Goal: running headers and footers, page numbers, decorative rules, backgrounds and watermarks are artifacted, and real content is not.

β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ RO β”‚ REPEATING CONTENT (detected) β”‚ Page strip: 1 2 3 4 5 … 48 β”‚ GROUP β”‚
β”‚ Hd β”‚ [βœ“] Header "Annual Report 2025" β”‚ β”Œβ”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β”β”Œβ”€β”€β”€β”€β”€β” β”‚ Header β”‚
β”‚ Fg β”‚ top band Β· 46 of 48 pages β—€ β”‚ β”‚β–€β–€β–€β–€β–€β”‚β”‚β–€β–€β–€β–€β–€β”‚β”‚β–€β–€β–€β–€β–€β”‚β”‚ β”‚ … β”‚ Kind [Paginβ–Ύ]β”‚
β”‚ Tb β”‚ β–’β–’β–’β–’β–’β–’β–’β–’β–’β–’ thumbnails β”‚ β”‚ β”‚β”‚ β”‚β”‚ β”‚β”‚ p4 β”‚ β”‚ Sub [Headerβ–Ύβ”‚
β”‚β–ΆAr β”‚ [βœ“] Page number "{n}" bottom-right β”‚ β”‚ β–„ β”‚β”‚ β–„ β”‚β”‚ β–„ β”‚β”‚excl.β”‚ β”‚ 46 pages β”‚
β”‚ Ls β”‚ 48 of 48 pages β”‚ β””β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”˜β””β”€β”€β”€β”€β”€β”˜ β”‚ Excluded: p4 β”‚
β”‚ Lk β”‚ [ ] Logo image (same XObject) β”‚ β–€ header band β–„ page number β”‚ (title page) β”‚
β”‚ Fm β”‚ top-left Β· 47 of 48 β”‚ β”‚ [Include p4] β”‚
β”‚ Nt β”‚ [ ] Horizontal rule Β· 48 pages β”‚ β”‚ β”‚
β”‚ La β”‚ ── Already artifacts: 212 runs [β–Έ] β”‚ β”‚ β”‚
β”‚ TC β”‚ ── Tagged but looks like artifact: 3 β”‚ β”‚ β”‚
β”‚ {} β”‚ [Artifact 2 selected groups] β”‚ β”‚ β”‚
β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Steps:

  1. Detection runs on entry (with progress if > 2s). Content is grouped by signature: position band (top/bottom 12%, margins), normalized text (digits β†’ {n}, so β€œPage 3 of 48” matches), image XObject identity, and vector path hash. Groups list a sample, coverage (β€œ46 of 48 pages”) and thumbnails.
  2. Review a group: selecting it highlights every instance on the page strip and canvas. Pages where the item is missing or differs are listed. Individual pages can be excluded (Alt+X), for example a title page where β€œAnnual Report 2025” is real content.
  3. Apply: β€œArtifact N selected groups” β†’ one group of artifact(refs, Pagination, Header|Footer| PageNum) (or Layout/Background for rules and images). This includes untagContent/unwrap for instances that were wrongly tagged (for example P β€œ3” per page), all in the same group.
  4. Reverse check: β€œTagged but looks like artifact” and β€œArtifacted but looks like content” (text in the body band, over 20 words, or a heading-sized font inside an artifact) are listed for review β†’ artifact / unartifact + tagContent.
  5. The Already artifacts browser can unartifact anything.

Ops: artifact, unartifact, untagContent, unwrap, tagContent.

6.6 Lists

β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚β–ΆLs β”‚ LISTS β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ LIST Β· p6 β”‚
β”‚ β”‚ βœ“ List 1 p2 Β· 4 items Β· β€’ β”‚ β”‚ [L]┏━━━━━━━━━━━━━━━━━━━━━━━━━┓ β”‚ β”‚ Numbering β”‚
β”‚ β”‚ βœ• Looks like a list p6 β—€ β”‚ β”‚ ┃[Lbl]β€’ [LBody]Reduce cost┃ β”‚ β”‚ [Disc β–Ύ] β”‚
β”‚ β”‚ 5 paragraphs start with β€’ β”‚ β”‚ ┃[Lbl]β€’ [LBody]Grow sales ┃ β”‚ β”‚ Items 5 β”‚
β”‚ β”‚ [Make list] β”‚ β”‚ ┃ [Lbl]– nested item ┃ β”‚ β”‚ Nested lists 1 β”‚
β”‚ β”‚ ? List 3 p9 Β· Lbl missing β”‚ β”‚ ┗━━━━━━━━━━━━━━━━━━━━━━━━━┛ β”‚ β”‚ Announces: "listβ”‚
β”‚ β”‚ [Split labels] β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ 5 items" β”‚
β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Steps:

  1. The queue holds existing L nodes with issues (LI outside L, missing Lbl/LBody, flat lists that look nested) plus candidates: runs of sibling P that start with a bullet glyph, numbering pattern or hanging indent.
  2. Make list: select the items (the candidate pre-selects them) β†’ one group: wrap(items, L), a wrap(item, LI) for each item, splitContent at the label boundary, tagContent(Lbl) + tagContent(LBody), and setAttr(ListNumbering) inferred from the glyph (β€’β†’Disc, 1.β†’Decimal, a.β†’LowerAlpha).
  3. Nest: Alt+β†’ on an item β†’ move into a new or existing sub-L inside the previous LI’s LBody. Alt+← unnests.
  4. Split labels for lists that lack Lbl β†’ splitContent + tagContent for each item. If the label cannot be split without changing the content stream (a glyph shared in one text show operator), the op is refused and β€œKeep label inside LBody” is offered, which is valid under PDF/UA.
  5. Numbering: Alt+N cycles β†’ setAttr(ListNumbering).

Ops: wrap, splitContent, tagContent, setAttr(ListNumbering), move, unwrap.

β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚β–ΆLk β”‚ LINKS 31 Β· 4 need attention β”‚ canvas: link boxes β”‚ LINK Β· p11 β”‚
β”‚ β”‚ βœ• "click here" β†’ gevell.com/… p11β—€ β”‚ numbered in tab order β”‚ Link text (tagged) β”‚
β”‚ β”‚ βœ• (no tagged text) annot only p12 β”‚ β”‚ "click here" β”‚
β”‚ β”‚ ? "https://example.gov/…" (raw URL) β”‚ β”‚ ⚠ Not descriptive β”‚
β”‚ β”‚ βœ“ "Budget portal" p3 β”‚ β”‚ Description (Cont.)β”‚
β”‚ β”‚ Tab order: 3 pages not structure [Fix]β”‚ β”‚ [Budget portal ]β”‚
β”‚ β”‚ β”‚ β”‚ [Save & next βŒ˜β†΅] β”‚
β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Steps:

  1. The queue lists every Link annotation, joined with its tagged text. Issues: annotation with no Link node (the OBJR is missing), a Link node with no annotation, a Link without text, generic text (β€œclick here”, β€œmore”, β€œlink”), a raw URL, and the same text pointing to different targets.
  2. Missing structure: β€œTag link” β†’ wrap(textRuns, Link) + linkAnnot(annot, node). The text runs are found under the annotation rectangle, and you can adjust them with Mark mode.
  3. Description: edit the annotation’s Contents β†’ setAnnot(Contents). This is recommended when the visible text is not descriptive (visible text can’t be changed without altering the page).
  4. Tab order: the banner β†’ setTabOrder(page, S) group.

Ops: wrap, linkAnnot, unlinkAnnot, setAnnot(Contents), setTabOrder.

6.8 Forms

β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚β–ΆFm β”‚ FORM FIELDS 18 Β· tab order β–Ύ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ FIELD "applicant_nm" β”‚
β”‚ β”‚ ≑ 1 Name βœ“ "Full name" β”‚ β”‚ [1]Name [____] β”‚ β”‚ Type: text β”‚
β”‚ β”‚ ≑ 2 DOB βœ• no tooltip β—€ β”‚ β”‚ [2]DOB [____] β—€ β”‚ β”‚ Tooltip (TU) β”‚
β”‚ β”‚ ≑ 3 Email βœ“ "Email address" β”‚ β”‚ [3]Email [____] β”‚ β”‚ [Date of birth, MM/DDβ”‚
β”‚ β”‚ ≑ 4 Agree [x] ? "Check"(vague) β”‚ β”‚ [4][x] I agree β”‚ β”‚ Required: yes (read) β”‚
β”‚ β”‚ Tab order = structure order βœ“ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ Announces: "edit β”‚
β”‚ β”‚ β”‚ β–  = tab stop number β”‚ text, Date of birth, β”‚
β”‚ β”‚ β”‚ β”‚ required" β”‚
β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Steps:

  1. List every widget in the current tab order (numbered squares on the canvas). Issues: no TU, a TU that duplicates the field name (β€œText1”), a widget missing a Form struct element/OBJR, and tab order β‰  structure order.
  2. Tooltip: the field is prefilled with the nearest visible label text (suggestion, not applied). Commit β†’ setAnnot(TU).
  3. Structure: β€œTag field” β†’ tagContent/wrap(Form) + linkAnnot(widget, formNode) at the position of its label in reading order.
  4. Tab order: drag or Alt+↑/↓ reorders the structure position of Form nodes (move), and setTabOrder(page, S) makes Tab follow it. The list mirrors the result immediately.

Ops: setAnnot(TU), wrap, tagContent, linkAnnot, move, setTabOrder.

6.9 Notes

  1. Detect superscript reference markers and note bodies (small font, bottom band or end section).
  2. For each pair: retype(body, Note) + setAttr(ID) (unique, required by PDF/UA) + optionally wrap(marker, Reference), then Alt+K links them (stored as a structure Ref/ID association). If the author made the marker a link, linkAnnot is used.
  3. Placement: offer β€œMove note after its reference” β†’ move (off by default; the reading-order trade-off is explained inline).

Ops: retype, setAttr(ID), wrap, move, linkAnnot.

6.10 Language

  1. Document language: required. It is prefilled from metadata or detection and committed with setDoc(Lang).
  2. Passages (Alt+G): a detector proposes spans whose language differs from the document’s, with a confidence level. Accept β†’ setAttr(Lang) on the node, or wrap(runs, Span) + setAttr(Lang) for inline phrases.
  3. Filter lang:* in the tree shows everything with explicit Lang.

Ops: setDoc(Lang), setAttr(Lang), wrap.

6.11 TOC

  1. Detect an existing TOC page (dot leaders, page numbers, links).
  2. β€œStructure TOC” β†’ wrap(TOC), a wrap(TOCI) for each entry, and linkAnnot when entries are links. Nesting follows heading levels.
  3. β€œCheck against headings” compares TOC entries with H nodes (missing entries or mismatched text) and lists the differences for review. There is no auto-fix.
  4. Bookmarks (Outlines) from headings: β€œGenerate bookmarks” β†’ setDoc(Outlines) (a document-level op, shown in the change report).

Ops: wrap, linkAnnot, setDoc(Outlines).

6.12 Raw tree (expert)

β”Œβ”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ {} β”‚ RAW TREE / type:Span lang:fr [Contents]β”‚ canvas β”‚ INSPECTOR (full) β”‚
β”‚ β”‚ β–Ύ StructTreeRoot β”‚ β”‚ Span Β· p8 β”‚
β”‚ β”‚ β–Ύ Document β”‚ β”‚ Type [Span β–Ύ] std β”‚
β”‚ β”‚ β–Ύ Part β”‚ β”‚ Lang [fr ] β”‚
β”‚ β”‚ β–Ύ Art β”‚ β”‚ Attributes β”‚
β”‚ β”‚ P Bonjour Γ  tous… β”‚ β”‚ Owner Key Value β”‚
β”‚ β”‚ β–Ύ Span (lang fr) β”‚ β”‚ Layout BBox […] β”‚
β”‚ β”‚ MCR MCID 4 Β· p8 "Bonjour" β”‚ β”‚ [+ Add attribute] β”‚
β”‚ β”‚ OBJR Link annot Β· p8 β”‚ β”‚ ClassMap: β€” β”‚
β”‚ β”‚ RoleMap (3) ClassMap (0) ParentTree βœ“ IDTree βœ“β”‚ β”‚ Raw: /S /Span /P 41 0β”‚
β””β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • Every standard PDF 1.7 structure type is available in Retype, plus RoleMapped custom types (shown as β€œCustom β†’ maps to P”).
  • Content items (MCR/OBJR) are visible as leaf rows, and the Contents toggle hides or shows them.
  • RoleMap editor: a table of custom β†’ standard with validation (no cycles, and no mapping of standard types, which PDF/UA forbids) β†’ setRoleMap. ClassMap editor β†’ setClassMap.
  • Attribute editor: any owner and key. Unknown keys are allowed with a warning.
  • Read-only diagnostics: ParentTree and IDTree consistency, and the object number/generation for cross-reference with other tools.
  • Raw mode still uses ops. There is no direct object editing.

7. Validation panel and fix-all

7.1 Model

  • The engine validates incrementally. Each op returns a validationDelta, and a full pass runs in the background after open and after bulk groups. While results are stale, the summary chip reads β€œChecking…”, and it never shows old numbers as current.
  • Each result has: ruleId (Matterhorn checkpoint, for example 13-004, and/or WCAG SC, for example 1.1.1), severity, node/content refs, message, and zero or more fixes.

Status vocabulary (text + icon shape + color; never color alone):

StatusIconMeaning
Failβœ• in octagonMachine-verified failure
Needs review? in triangleA human must judge. Never shown as passing
Reviewedβœ“ with a person glyph, β€œReviewed by LA Β· 09-28”A human attested (an attest op)
Passβœ“ in circleMachine-verified checkpoint only
Not applicableβ€”For example, no tables in the document
Could not check⊘The check errored or the input is unsupported. Counted with failures in the summary (it is not a phantom pass)

7.2 Panel layout

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ISSUES βœ• 12 fail Β· ? 31 review Β· ⊘ 0 β”‚
β”‚ Group by: [Rule β–Ύ] Show: [Fail βœ“][Review βœ“] β”‚
β”‚ Scope: [Whole document β–Ύ] β”‚
β”‚ β–Ύ βœ• Figure without alternate text (13-004, β”‚
β”‚ WCAG 1.1.1) Β· 7 β”‚
β”‚ [Fix all like this… (AI suggest)] β”‚
β”‚ βœ• Figure p4 "chart" region [Go][Fixβ–Ύ]β”‚
β”‚ βœ• Figure p12 [Go][Fixβ–Ύ]β”‚
β”‚ β–Ύ βœ• Content not tagged (01-005) Β· 3 β”‚
β”‚ [Fix all like this…] β”‚
β”‚ β–Ύ ? Reading order logical (09-001) Β· 48 pages β”‚
β”‚ [Review in Reading order mode] β”‚
β”‚ β–Έ βœ“ Document title set (07-001) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • Grouping: by Rule (default), Page, Mode, or Severity. Each group heading is an h3, and each issue row is a list item with a Go button (selects the node and moves focus to the canvas or tree, whichever was last used) and a Fix menu button.
  • Selecting any node filters the panel to β€œIssues on selection” with a β€œShow all” toggle.
  • The summary chip in the app bar is a button that opens the panel.
  • Rules that need a human link to the mode that supports the review, rather than offering a fix.

7.3 Fix and fix-all flow

  1. Fix (one issue): the menu lists the fixes the engine offers for that rule, with the exact op described (β€œMark as artifact (Layout)”, β€œTag as P under Sect”). Choosing one applies it as a group and announces β€œFixed: Content not tagged, page 3. 11 failures remain.”
  2. Fix all like this…: shown only when the fix is deterministic (no judgment needed), for example artifacting page numbers, setting Scope on first-row THs, setTabOrder(S), adding a missing BBox, or retyping H β†’ Hn where the level is unambiguous. It opens a preview sheet inside the panel. The sheet is non-modal, and focus moves to its heading:
    • β€œApply β€˜Mark as artifact (Pagination/PageNum)’ to 47 issues on 47 pages”
    • a checklist of every instance (virtualized) with thumbnails; each can be unticked
    • a β€œShow on canvas” toggle that steps through instances (↓/↑)
    • a before/after validation delta: β€œFixes 47 Β· introduces 0”
    • buttons: Apply to 47 (Mod+Enter) Β· Cancel
  3. Apply β†’ one group, origin fixAll, labelled β€œFix all: artifact page numbers (47)”. One undo reverts all of them.
  4. Judgment fixes (alt text, heading level meaning) have no Fix all. The group instead offers β€œSuggest for all (AI)…”, which routes to the Suggestions queue (Β§9) as proposals, never applied directly.
  5. If some ops in a fix-all are refused (their preconditions fail), the others still apply, and the result says β€œApplied 45 of 47. 2 skipped: [view]” with each reason. Skipped items stay in the list.

8. Select similar

  1. Start from any selection: Alt+L, the Similar dock tab, or the context menu β†’ Similar tab opens, seeded with the primary’s signature.
  2. Signature chips (toggle buttons with aria-pressed). The defaults on are marked with ●:
    • Visual: ● font family Β· ● size (Β±0.5pt) Β· ● weight/style Β· color Β· position band (top/bottom/left margin, column)
    • Structural: ● current tag type Β· parent type Β· depth Β· has alt / no alt
    • Content: text pattern (starts with bullet / number / β€œFigure N:” / all caps / same text), same image XObject, size Β±10%
    • Scope: this page Β· page range Β· whole document (default)
  3. The live result shows β€œ43 matches on 12 pages” (polite announcement, debounced), a list of matches with page and preview, and canvas highlights with the β€œDim everything else” style. Each match has an exclude checkbox.
  4. Select all matches (Mod+Enter) β†’ the shared selection becomes the match set. The inspector shows mixed values, and any op now applies to all of them as one group, origin similar, labelled β€œRetype 43 similar items to H3”.
  5. Shortcut actions in the tab: β€œRetype all to…”, β€œArtifact all”, β€œSet Lang…”, which create the same groups directly.
  6. β€œSave as rule” (phase 5): stores the signature plus the op as a replayable template for later documents.

A result set larger than 500 shows a warning chip (β€œLarge selection. Review a sample before applying.”) and a β€œReview 10 random samples” stepper.


9. AI suggestion review

9.1 What a suggestion is

A suggestion is a proposal: { id, ops[] (or group), target refs, confidence (0–1), band (High β‰₯ 0.85, Medium 0.6–0.85, Low < 0.6), reason (one line), model, cost, createdAgainst (document state hash) }. Proposals never change the document until accepted. The feature is branded β€œGevell AI”. For audit purposes, the history panel and the change report show the model identifier recorded by the AI service for every AI-originated group, whether accepted or auto-applied.

9.2 Requesting suggestions

  • Per item (Alt+G in Figures, Links, Language, TOC) or in bulk from a mode or issue group (β€œSuggest alt for 23 figures”).
  • Before a bulk request, a confirmation shows the estimated credits/cost and count, because every AI call is cost-tracked per the platform rules. Requests run in the background with a determinate progress bar in the Suggestions tab and status bar (β€œGenerating 23 suggestions… 9 done”). You can keep working meanwhile.

9.3 Review queue

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ SUGGESTIONS 23 pending Β· 4 accepted Β· 1 rej. β”‚
β”‚ Group: [Figures alt β–Ύ] Sort: [Confidence β–Ύ] β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚ β”‚ ✦ Figure p12 Β· High (91%) β—€ β”‚ β”‚
β”‚ β”‚ Set Alt: "Bar chart of revenue by quarter;β”‚ β”‚
β”‚ β”‚ Q4 highest at $4.2M." β”‚ β”‚
β”‚ β”‚ Why: legible axis labels and caption. β”‚ β”‚
β”‚ β”‚ [Accept βŒ˜β†΅] [Edit] [Reject] [Show] β”‚ β”‚
β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚
β”‚ β”‚ ✦ Figure p4 Β· Low (48%) β”‚ β”‚
β”‚ β”‚ Set Alt: "Decorative swirl" β†’ artifact? β”‚ β”‚
β”‚ β”‚ Why: no text, repeated on 3 pages. β”‚ β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ [Accept all High (12)…] β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • The confidence is always shown as a word + number (β€œHigh, 91%”), never as a color bar alone.
  • Ghost preview: the focused proposal is drawn on the canvas as a dashed overlay with a β€œSuggested” chip, and as an italic ghost row in the tree labelled β€œ(suggested)”. The Reader shows the would-be utterance with a β€œSuggested” chip.
  • Accept β†’ applies the ops as a group, origin ai, labelled β€œAI: set alt on Figure p12”. The proposal moves to Accepted.
  • Edit β†’ opens the inspector with the proposed values prefilled but not applied. Committing applies with origin ai-edited.
  • Reject β†’ no document change. It is recorded in the history as a non-document entry (for quality metrics), and Restore in the Rejected list brings it back.
  • Accept all High (N)… β†’ the same preview-sheet pattern as fix-all (Β§7.3), with a sample stepper. One group, origin ai. Low-confidence items are never included in bulk accept. They must be handled one at a time.
  • Staleness: if a proposal’s targets change after it was generated (an op touches them), it is marked β€œOutdated β€” the item changed since this was suggested. [Regenerate] [Dismiss]” and cannot be accepted as-is.
  • Auto-apply is off by default. When a user turns it on, proposals at or above their threshold are applied on arrival (Β§9.4). Proposals below the threshold, Outdated proposals, and proposals for types the user excluded stay in this queue as normal.

9.4 Auto-apply (user setting)

Settings β†’ Gevell AI β†’ Auto-apply. This is a per-user setting. It is also shown, and can be changed, from the Suggestions tab header (β€œAuto-apply: on, 90% and above Β· [Change]”) so its state is never hidden.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Settings Β· Gevell AI ───────────────────┐
β”‚ Apply suggestions automatically β”‚
β”‚ ( ) Off β€” every suggestion waits for review ← default β”‚
β”‚ (●) On, when confidence is at least [ 90 ]% (80–99) β”‚
β”‚ β”‚
β”‚ Apply automatically for: β”‚
β”‚ [βœ“] Alt text for figures [βœ“] Heading levels β”‚
β”‚ [βœ“] Passage language [βœ“] List detection β”‚
β”‚ [ ] Mark as decorative (artifact) [ ] Table headers β”‚
β”‚ β”‚
β”‚ Auto-applied changes are marked "AI (auto)", can be undone β”‚
β”‚ one at a time or as a batch, and still need human review β”‚
β”‚ where a check requires it. β”‚
β”‚ [Save settings] β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • The controls are a radio group (Off / On) and a labelled number input (type="number", min 80, max 99, step 1, inputmode="numeric") with a visible range hint. The 80% floor is fixed (agreed 2026-09-28) so that Medium-band suggestions can never auto-apply. The type checklist defaults to the four types shown as ticked. Artifacting and table headers are unticked by default because a wrong change removes or reshapes content that screen reader users rely on.

  • Organization lock: in the organization admin settings (Admin β†’ Gevell AI), admins get the option β€œAllow members to auto-apply AI suggestions”. It is on by default. When an admin turns it off:

    • Auto-apply is off for every member, whatever each member had set. Members’ own choices are kept and come back if the lock is lifted.
    • In Settings, the section stays visible and readable. The radio group, threshold and type checklist are read-only (aria-readonly), with a lock icon and the text β€œTurned off by your organization’s admin. Contact your admin to change it.” This text is linked to the group through aria-describedby, so the reason isn’t color- or icon-only. The controls stay focusable, so screen reader users can find and hear the explanation.
    • The Suggestions tab header reads β€œAuto-apply: off (set by your organization)”.
    • Taking effect is immediate: a suggestion batch already running finishes as normal review items, and nothing more is auto-applied. Changes that were already auto-applied stay in history as they are.
    • The admin change is recorded in the organization audit log (who, when). The change report shows the auto-apply state that was in effect for each AI batch.
  • When it runs: whenever a suggestion batch completes (Β§9.2). Auto-apply never triggers a new AI request or cost by itself. The request and cost confirmation are unchanged.

  • How it applies: each qualifying proposal becomes its own group (origin ai-auto). The groups from one suggestion batch are wrapped in a batch entry. The batch goes through the same precondition checks as any op, and refused ops stay in the queue with their reason.

  • History: the batch entry reads β€œAI auto-applied 18 suggestions (90% and above)”. It shows an β€œAI (auto)” origin chip and expands to one row per change. Each row shows its label, the model identifier, the confidence (β€œHigh, 94%”), and the one-line reason.

    β”œ 14:40 AI auto-applied 18 suggestions (β‰₯ 90%) AI (auto) [Undo batch]
    β”‚ β”œ Set alt on Figure p12 Β· model gv-vision-2026-09 Β· High 94% Β· "chart with legible axes" [Revert]
    β”‚ β”œ Retype P to H3 p14 Β· model gv-structure-2026-09 Β· High 91% Β· "matches 18pt bold cluster" [Revert]
    β”‚ β”” … 16 more
  • Undo: Mod+Z right after the batch undoes the whole batch as one step. Undo batch in history does the same at any time, subject to the dependency check in Β§10.1. Revert on a row undoes that single change as a new forward group. Undone auto-applied proposals return to the queue as Pending, marked β€œAuto-apply undone”, and they are never auto-applied again. That prevents an undo from being silently re-applied.

  • Review status is unchanged by auto-apply. An auto-applied change never produces an attest. A check that needs human review (alt text quality, heading meaning, language of passage) stays Needs review with an extra β€œAI (auto)” chip. In Figures mode those items appear under the filter β€œAuto-applied β€” needs review”. The reviewer confirms with Mod+Enter (which records the attestation) or edits. Tree rows and canvas chips carry a text β€œAI” marker until the item is reviewed.

  • Announcement (polite, once per batch): β€œApplied 18 AI suggestions automatically. They still need review. Undo with Control Z.”

  • Reports: the change report lists auto-applied changes in their own section (model, confidence, reviewed yes/no). The conformance report treats unreviewed auto-applied items as Needs review.


10. Undo and history

10.1 Semantics

  • The unit of undo is the group: one keystroke, one fix-all (47 ops), one AI bulk accept, one click-sequence commit. Mod+Z undoes the most recent group. Announcement: β€œUndid: Fix all: artifact page numbers (47). Redo with Control Shift Z.”
  • Linear stack. A new group after undo discards the redo stack. The discarded groups stay in the history panel, collapsed as β€œ3 undone changes (discarded)”, with Re-apply available if they still apply cleanly (validated as new ops). This covers accidentally undoing and then typing.
  • Selective revert from the history panel (β€œRevert this change”) makes a new forward group of the inverse ops. The engine first checks that later groups don’t depend on the same nodes. If they do, the panel lists the dependent changes and offers β€œUndo back to here” (a linear undo of everything after it) instead. Nothing is reverted silently out of order.
  • Undo of AI-accepted ops reverts the document and returns the proposals to Pending in the Suggestions queue (not lost, no re-charge). Undo of an AI bulk accept returns all N.
  • Undo of fix-all restores all instances, and the issues reappear in the panel.
  • Undo of attest removes the reviewed mark, and the item returns to Needs review.
  • Undo of auto-applied AI changes: one at a time or as a batch (Β§9.4).
  • Undo survives save, reload and device changes. Every group is written to the local outbox (IndexedDB) immediately and synced to the server op log (Β§11.3). The server log is the record of truth. Saving creates a version marker in history (β€œSaved v7”), and undo can go past it, which puts the document ahead of its last version again. Opening the document on any device restores its history from the server. The op log is kept for the life of the document and is never trimmed. Deleting the document is the only thing that removes it.
  • Undo is per session and linear across the synced log. If another session appended groups (Β§11.3 take-over), Mod+Z only undoes this session’s own groups when they don’t depend on later ones. Otherwise the history panel’s dependency flow applies.
  • Undo restores selection and view: the selection, active mode and canvas page from before the group.
  • Inline text fields use native text undo until committed. After commit, Mod+Z undoes the setAttr group.

10.2 History panel

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ HISTORY Filter: [All origins β–Ύ] πŸ” β”‚
β”‚ ● Now β”‚
β”‚ β”œ 14:32 AI: set alt on Figure p12 ai β†Ά β”‚
β”‚ β”œ 14:30 Fix all: artifact page numbers (47) β†Ά β”‚
β”‚ β”‚ fixAll Β· fixed 47 Β· introduced 0 β”‚
β”‚ β”œ 14:21 Retype 43 similar items to H3 similar β”‚
β”‚ β”œ 14:05 ── Saved v7 ── β”‚
β”‚ β”œ 13:58 Reorder page 3 (click-sequence) β”‚
β”‚ β–Έ 3 undone changes (discarded) [Re-apply] β”‚
β”‚ β”” 13:40 Opened annual-report-2025.pdf (v6) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • role="list". Each entry shows its label, time, origin chip, validation delta, and actions (Show affected, Revert this, Undo back to here). Expanding an entry lists its ops in plain language.
  • The filters (origin, page, node) and a search field make an audit trail usable in 3,000-op sessions.
  • The entry labels come from the same ts.op.* templates as the live announcements.

11. Save and export

11.1 Save (Mod+S)

  1. Pre-flight (≀ 1s): make sure no inline edit is uncommitted (offer to commit it or discard it).
  2. Serialize in the engine (WASM worker): incremental write where possible.
  3. Pixel-identity gate: render every page of the original and the candidate at 150 dpi (with a 300 dpi recheck of any page that differs) and compare exactly. Separately, verify that the content streams differ only in marked-content operators (BDC/BMC/EMC) and property-list references. Progress: a determinate bar β€œChecking page 18 of 48…”. Pages already proven unchanged by stream analysis skip rendering.
  4. Pass β†’ upload the new version to the server, show β€œSaved v8” in the app bar, and announce β€œSaved version 8. Pages verified identical.” Validation summary numbers are stored with the version. A veraPDF PDF/UA-1 recheck then runs on the server in the background (Β§11.1.2).
  5. Fail β†’ by default nothing is saved. A modal Save blocked sheet (this is one of the few modals) appears:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Save blocked: page appearance changed ─────────────────────────┐
β”‚ 2 of 48 pages would look different after saving. Tagging must never change β”‚
β”‚ what a page looks like, so this version was not saved. β”‚
β”‚ β”‚
β”‚ Page 7 β–ˆβ–ˆβ–ˆβ–ˆ 0.4% of pixels differ [View diff] β”‚
β”‚ Page 12 β–ˆβ–ˆ 0.1% of pixels differ [View diff] β”‚
β”‚ β”‚
β”‚ Likely cause: "Split cell B4 (Table 3)" at 14:12 β€” content split inside a text run. β”‚
β”‚ [Undo that change] [Show all changes to page 7 and 12] β”‚
β”‚ β”‚
β”‚ [Keep editing] [Save with pixel changes…] [Report a problem] β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • View diff opens a diff viewer: before / after / difference (a magenta+ink overlay with an outline around changed regions, and a text summary β€œChanged region at 72,540, 110Γ—18 pt”), with keyboard stepping between regions.
  • Likely cause: the engine bisects the unsaved groups (replaying subsets through the gate on the affected pages only) to name the responsible change(s). A progress indicator shows while this runs.
  • Save with pixel changes… is the deliberate override (Β§11.1.1). Blocking stays the default. The button is never the primary (filled) button, and Enter in the sheet activates Keep editing.
  • The engine error is logged with context (structured logger), and an unexpected engine failure (as opposed to a legitimate diff) goes to error alerting.

11.1.1 Save with pixel changes: confirm step

Choosing Save with pixel changes… replaces the sheet’s contents with a confirm step in the same modal. Focus moves to its heading.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Save version 8 with pixel changes? ────────────────────┐
β”‚ These pages will look different from version 7: β”‚
β”‚ Page 7 0.4% of pixels Β· 1 region [View diff] β”‚
β”‚ Page 12 0.1% of pixels Β· 1 region [View diff] β”‚
β”‚ β”‚
β”‚ Reason (required, kept in the change report) β”‚
β”‚ [ Kerning shift from splitting the Q4 cell; not visible at 100%. ] β”‚
β”‚ β”‚
β”‚ [ ] I have viewed the differences and accept them on pages 7 and 12 β”‚
β”‚ β”‚
β”‚ After saving, Gevell re-renders every page and runs a veraPDF check. β”‚
β”‚ Results appear on the version and in the change report. β”‚
β”‚ β”‚
β”‚ [Back] [Save version 8 with changes] β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
  • Required: a reason (textarea, labelled, at least 10 characters) and the acknowledgement checkbox. The primary button is not disabled, because disabled buttons can’t be focused and give no reason. If either is missing, activating it shows inline errors linked with aria-describedby, and focus moves to the first invalid field: β€œEnter a reason for accepting the page changes.” / β€œConfirm you have viewed the differences.”
  • The pages listed are the ones found by the save gate. A β€œView diff” was not required, but the acknowledgement says so explicitly.
  • Saving writes version 8 flagged β€œPixel changes: pages 7, 12”. It is a normal version (not a side copy) and becomes the current version. Version 7 remains restorable.
  • History adds β€œSaved v8 with pixel changes (pages 7, 12) Β· reason: …”, attributed to the user.
  • The announcement (polite) reads: β€œSaved version 8 with pixel changes on 2 pages. Verification running.”

11.1.2 Post-save verification and how results are shown

After every save the server runs a verification job on the stored file. Pixel-change saves get the full treatment:

CheckRuns afterWhat it does
veraPDF recheck (PDF/UA-1 profile)Every saveIndependent validation of the stored file. Results are merged into the version’s conformance data (they don’t replace our checks); disagreements are flagged
Per-page render-diff reportSaves with pixel changesServer renders every page of the new version and of the original source file (not only the previous version) at 150 dpi, re-checks differing pages at 300 dpi, and records per page: % pixels changed, changed-region boxes, diff image, and the op groups that touched the page

Where the results appear:

  1. Version chip in the app bar: β€œv8 Β· pixel changes Β· verifying…” (with a progress indicator) β†’ β€œv8 Β· 2 pages changed Β· veraPDF 0 failures” β†’ or, if problems are found, β€œv8 Β· verification found problems” in the error style (text + icon).
  2. Versions panel (History tab β†’ Versions): each version lists its verification status, the render-diff report (an accessible HTML page with per-page before / after / diff images, alt text describing each changed region, and a data table of the numbers) and the veraPDF report.
  3. Announcements: a polite message when verification finishes (β€œVerification finished for version 8: 2 pages changed as accepted; veraPDF found no failures.”). It is assertive only if verification finds problems.
  4. Problems are: pages changed that the save gate did not report, a larger change than accepted (more than 2Γ— the accepted % or new regions), or new veraPDF failures. In that case a non-modal banner appears on the workspace: β€œVersion 8 changed more than you accepted: page 9 also differs. [View report] [Restore version 7] [Keep version 8]”. Restore creates version 9 from version 7’s content plus the op log after it, re-run through the gate. Nothing is deleted.
  5. If the verification job fails (the job itself errors), the status is β€œCould not verify”, which is treated like a failure in reports, never as a pass. It retries automatically, and the error goes to alerting.
  6. Change report (Β§11.2): a β€œPages with visual changes” section at the top listing every changed page with a diff thumbnail, % pixels changed, region count, the accepting user, the reason, and the responsible changes. The conformance report states β€œPage appearance differs from the source on pages 7, 12 (accepted by LA, 2026-09-28)” and includes the veraPDF result.
  7. Each later save carries the flag forward (β€œdiffers from source on pages 7, 12”) until a version is saved that is identical to the source on those pages again.

11.2 Export (Export β–Ύ)

OutputContents
Tagged PDFThe saved version (the export is disabled while unsaved; the button says β€œSave first”)
Change report (accessible HTML + tagged PDF)β€œPages with visual changes” first when any exist (Β§11.1.2); summary counts by op and origin; before/after validation; every group in plain language with page links; AI-accepted, AI-edited and AI auto-applied items (model, confidence, reviewed yes/no) listed separately
Conformance report (accessible HTML + tagged PDF)veraPDF result for the version, PDF/UA-1 Matterhorn checkpoint table (Pass / Fail / Needs review / Reviewed by / N/A / Could not check) and a WCAG 2.1 AA SC table for document content. Needs-review items are listed as not yet verified, and Reviewed items name the reviewer and date. There is no single β€œcompliant” badge unless every applicable item is Pass or Reviewed
Op log (JSON)A replayable group/op list for audit and templates

The export runs with a progress indicator. Report generation is cost-free (no AI). If the version’s verification is still running, the export dialog says so and offers β€œExport when verification finishes” (the default) or β€œExport now”. Export now puts β€œVerification pending” on the report.

11.3 Sync, autosave, conflicts and offline

What lives where: the source PDF, every saved version, verification reports and the op log live on Gevell’s servers (AWS: files in S3, op log and version metadata in the database). The browser runs the engine (WASM) against a local copy, and keeps an outbox in IndexedDB.

Autosave = op-log sync. There is no β€œunsaved work” in the sense of losable edits:

  • Every group is written to the outbox immediately, then sent to the server in order (batched, within about 2 seconds). Each group carries the server sequence number it was based on.
  • Saving (Mod+S) creates a verified PDF version, which is a separate step. Closing the tab with changes since the last version does not lose them. On reopen: β€œYou have 14 changes since version 7. [Keep working] [Save a version]”.
  • beforeunload warns only when the outbox has not finished syncing, not merely when there is no new version.

Sync status indicator (app bar; text + icon, never icon or color alone; a button that opens a details popover):

StateTextDetails popover
Synced”Synced”Last sync time
Syncing”Syncing 3…”Count pending
Offline”Offline Β· 12 changes on this device""Your changes are stored on this device and will sync when you’re back online.”
Sync problem”Sync problem Β· retrying”Error in plain language, next retry time, [Retry now], [Download a backup of my changes] (op log JSON)
Read-only”Read-only Β· open elsewhere”See the conflict flow below

Announcements only fire on transitions to Offline, back to Synced after being offline (β€œBack online. 12 changes synced.”), and to Sync problem (assertive). Routine syncing is silent.

Same document open twice. Editing needs a document lease, held by one session and renewed by heartbeat every 15 seconds:

  1. If you open a document whose lease is held (another tab of yours, detected at once through BroadcastChannel, or another user or device, detected through the server), a non-modal choice appears before the workspace becomes editable: β€œThis document is being edited in another tab / by Laura Hopkins on another device (since 14:02).” [Open read-only] Β· [Take over editing] Β· [Open a copy].
  2. Read-only follows the live op log: new groups appear, the history updates, and edit commands are disabled with the reason β€œRead-only while this document is edited elsewhere.”
  3. Take over asks the other session to flush its outbox, then moves the lease. The other session switches to read-only and shows β€œEditing moved to another tab / to Larry Anglin at 14:31. Your changes up to then are synced.” (assertive, since it interrupts work).
  4. If the previous holder is unreachable (crashed or offline), take-over waits for the lease to expire (up to 45 seconds, with a countdown and progress) and then proceeds.
  5. Open a copy creates a new document from the current version plus synced ops.

Offline:

  • Editing, validation (WASM), select-similar, undo/redo and the pixel-identity gate all keep working.
  • AI requests, the veraPDF recheck and version upload need the network. Those buttons stay focusable and explain why: β€œNeeds a connection. Your request will not be queued, to avoid unexpected costs.”
  • Save while offline runs the local gate and then queues the version upload: β€œVersion 8 will be uploaded when you’re back online.” A Download PDF now option gives a local copy marked β€œnot yet uploaded” in the file name.
  • Reconnecting after someone else took over (your lease expired while offline): your outbox groups are rebased onto the server head. Each group is re-validated against the current state. Groups that still apply are appended, labelled β€œSynced after reconnecting”. Groups that no longer apply go to a β€œCouldn’t sync” list in the History tab. Each one has its reason, [Re-apply] (after you edit the conflict) and [Discard]. [Save all as a copy] creates a separate document from your offline state. Nothing is silently dropped.

Recovery: after an engine crash or browser restart, the workspace rebuilds from the latest version plus the server op log plus any unsynced outbox groups, in that order.


12. Loading, empty and error states

12.1 Progress rule

Anything that may exceed 2 seconds shows a progress indicator. It is determinate when the total is known, and it has text, a <progress> element, and a polite announcement at start, at 25% steps (at most every 5s) and at the end. The UI stays interactive during background work unless the work blocks editing (save gate).

OperationIndicator
UploadDeterminate bytes (β€œ12.4 of 30 MB”)
Parse / openDeterminate by objects. Progressive: page 1 and the top tree levels become interactive before the whole document is parsed; the status bar shows β€œLoading structure… 40%β€œ
Page renderPer-page skeleton (gray page with the page number) then tiles. No spinner for < 300 ms
Full validationStatus bar bar + the summary chip reads β€œChecking…”
Detection (artifacts, lists, headings)Task-panel skeleton rows + β€œScanning 48 pages… 12”
Select similar on large docsCount rises live; β€œStill searching (page 30 of 480)β€œ
AI suggestionsPer-request determinate count; per-item skeleton in the queue
Save gate / bisect / exportDeterminate by page; can be cancelled (except the final write)
Version uploadDeterminate bytes
Post-save verification (render diff + veraPDF)Version chip β€œverifying…” + determinate page count in the Versions panel; runs on the server and does not block editing
Lease take-over waitCountdown + progress (up to 45 s)

12.2 Empty states

SituationWhat the user sees
No file openA drop zone with a file picker button, recent files (with version and date), and a short note that the file is uploaded to Gevell and edited in your browser. The keyboard path is the button itself
Untagged PDF (no StructTreeRoot)β€œThis PDF has no tags. Screen readers will read it poorly or not at all.” Options: Start in Reading order mode (build structure page by page) Β· Auto-tag first (proposal-based, reviewed like AI suggestions) Β· Tag manually in Raw tree. No option is preselected
No text layer (scanned)β€œThis PDF is images of text, so it has nothing to tag yet. It needs OCR first.” A link to the OCR path (see Q2). Figures mode still works for true images
Mode has nothing to do”No tables found in this document.” + β€œTag a region as a table” (Mark mode) + a check in the rail with the text β€œTables: none”
Filter with no matches”No nodes match type:Figure no:alt.” + Clear filter
All issues resolved”No failures. 6 items still need review.” (Never β€œAll clear” while any review items remain)

12.3 Errors

  • Op refused (precondition): nothing changes. An assertive announcement plus a toast (error toasts use role="alert"; everything else is polite) states the reason and the alternative: β€œCan’t move LI here. List items must be inside a List (L). Wrap it in a list first with Alt+W.”
  • Password-protected: a password field (labelled, autocomplete="off"). A wrong password says β€œThat password didn’t open the file. Check caps lock and try again.”
  • Encrypted with permissions that forbid modification: open read-only with a banner explaining that saving needs the owner password.
  • Corrupt / unsupported (for example XFA forms, PDF 2.0 in phase 1): a specific message with the detected reason, plus β€œOpen read-only to inspect” when parsing got far enough.
  • Engine crash / out of memory (WASM): the workspace shows β€œThe editor stopped unexpectedly. Your changes up to 14:32 are safe.” and Recover, which reloads the engine and rebuilds from the server op log and the local outbox (Β§11.3). The error is logged and alerted.
  • Network lost: the work continues offline (Β§11.3). Nothing is lost.
  • Same document open elsewhere: the lease flow in Β§11.3. Real-time co-editing is phase 6.
  • Large documents (over 2,000 pages or 200k nodes): a warning on open with the expected memory use, and a page-range open option.

13. Live-region announcements

Two always-mounted regions: polite (role="status") and assertive (role="alert", errors only). The status-bar echo mirrors the last message visually (WCAG 4.1.3). Messages are short, lead with the result, and follow the i18n templates ts.op.<name>.done.

EventText (en)
Retype”Retyped Paragraph to Heading 2.” (multi: β€œRetyped 43 items to Heading 3.”)
Move”Moved Budget overview to position 3 of 9 in Section.”
Promote/demote”Promoted to level 2.” / β€œDemoted into Table 1.”
Wrap / unwrap”Wrapped 5 items in List.” / β€œRemoved List tag; 5 items kept.”
Artifact”Marked 47 page numbers as artifacts.”
Fix”Fixed: Figure without alternate text, page 12. 11 failures remain.”
Undo / redo”Undid: Retype 43 items to Heading 3.” / β€œRedid: …”
AI ready”12 suggestions ready for Figures.” (once per batch)
Accept”Accepted suggestion: alt text on Figure, page 12.”
Validation done”Check complete: 12 failures, 31 need review.”
Save”Saved version 8. Pages verified identical.”
Save with pixel changes”Saved version 8 with pixel changes on 2 pages. Verification running.”
Verification done”Verification finished for version 8: …” (assertive if problems found)
Auto-apply”Applied 18 AI suggestions automatically. They still need review. Undo with Control Z.”
Sync transitions”Offline. Changes are stored on this device.” / β€œBack online. 12 changes synced.” / β€œSync problem…” (assertive)
Lease moved”Editing moved to another tab at 14:31. This window is now read-only.” (assertive)
Save blocked (assertive)β€œSave blocked. Two pages would change appearance. Details are open.”
Refused op (assertive)as in Β§12.3

Selection sync, hover and background scrolling are never announced.


14. Overlay color palette

14.1 Design rules

  • Nine tag families. Each is encoded three ways: color, stroke pattern, and a text label chip. Color is never the only signal (WCAG 1.4.1). With labels on (the default), every overlay can be identified without color vision.
  • No green in the palette, so the brand focus ring (#046307 / #5CCB60) is unique on the canvas. This also avoids blue–green adjacency with the brand blue.
  • Light variant colors (used for canvas strokes in both themes, because the page is always drawn as printed, and for UI chips in light mode) are β‰₯ 4.5:1 on white. This means white chip text passes AA, and strokes exceed the 3:1 non-text minimum.
  • Dark variant colors (UI chips, tree tags and legend in dark mode) are β‰₯ 4.5:1 against the dark surfaces and carry ink (#0E1116) text.
  • Every canvas stroke is drawn with a 1px white outer keyline, so it stays β‰₯ 3:1 against dark or colorful page backgrounds (flyers, reversed text).

14.2 Palette

Family (tags)Light (canvas + light UI)on whiteon boneDark UIon #161B23on #0E1116on #1D2330Stroke patternChip
Heading (H, H1–H6)#B347005.505.05#F0862E6.717.346.11Solid 3pxH2
Text (P, Span, Quote, BlockQuote, Code, Caption)#4A54647.657.03#B9BEC89.2710.148.43Solid 1pxP
Figure (Figure, Formula)#A3238F6.576.04#F08BDF7.798.527.09Solid 2px + corner ticksFig
Table (Table, TR, TH, TD, THead, TBody, TFoot)#0B4FC77.106.52#6FA3F56.787.416.17Solid 2px + inner cell gridTbl/TH/TD
List (L, LI, Lbl, LBody)#735A006.586.04#E6C34A10.1011.059.19Dashed 6-3, 2pxL/LI
Link / Note (Link, Annot, Reference, Note)#6B2FA88.097.43#C9A6FF8.549.357.77Dotted 2px + underlineLnk
Form (Form)#0E1116 (ink)18.9117.37#F7F5F1 (bone)15.8717.3714.44Double 1px-2-1pxFrm
Artifact#6B76864.604.23 †#8A94A65.656.185.14Dotted 1px + 40% white washArt
Untagged (orphan)#C4001A6.255.74#FF8A807.578.286.89Dashed 4-2, 2px + diagonal hatch fill! Untagged
Containers (Document, Part, Sect, Div, Art, TOC, TOCI…)#4A54647.657.03#B9BEC89.2710.148.43Long-dash 10-4, 1px, no fillSect

† Artifact on bone is 4.23: that passes 3:1 for strokes, but artifact chips must sit on white or on the chip’s own fill, not on bone. Chips in light mode always use white text on the family fill (ratios = the β€œon white” column, all β‰₯ 4.5). Chips in dark mode use ink text on the dark-variant fill (ratios = the β€œon #0E1116” column, all β‰₯ 6.1).

UI state colors (not tag families):

ElementLightRatioDarkRatio
Focus ring#046307 3px, 3px offset7.52 on white, 6.91 on bone#5CCB608.37 on #161B23, 9.16 on ink
Selection2px ink #0E1116 + 2px white haloβ‰₯ 3:1 on any background (by construction)same (the page is drawn as printed)same
Reading-order badgeWhite on ink circle with white ring18.91same18.91
Error text/chips#B3261E on #FBE4E25.38#F28B84 on #33110F7.17
Review/warning#A24A00 on #FBEADC5.09#F0862E on #2C1B0C6.43

14.3 Color-vision check (simulated, CIELAB Ξ”E between family pairs)

Simulated with Machado et al. (2009) matrices at full severity. Pairs under Ξ”E 12 are β€œhard to tell apart by color” and are deliberately separated by stroke pattern:

VisionClosest pairs (Ξ”E)Disambiguated by
TypicalText/Artifact 14 Β· Table/Link 25Solid vs dotted+wash; solid+grid vs dotted+underline
DeuteranopiaHeading/Orphan 4 Β· List/Orphan 7 Β· Heading/List 11Solid 3px vs dashed+hatch fill; dashed vs hatch; solid vs dashed
ProtanopiaHeading/List 0 Β· Table/Link 10 Β· Text/Artifact 14Solid 3px vs dashed; solid+grid vs dotted; solid vs dotted+wash
TritanopiaText/Artifact 14 Β· Table/Artifact 20Stroke patterns + chips

With nine families, perfect separation by hue alone is impossible under protanopia with AA contrast (all colors must sit in the dark half of the luminance range), so hue collisions are expected. The stroke pattern and label make each family unique. Usability scenario U12 checks this with participants who have color-vision deficiencies. A β€œPatterns only” overlay setting renders all families in ink with their patterns and chips, for users who prefer no color. A high-contrast mode follows forced-colors: active: strokes use CanvasText, selection uses Highlight, and focus uses system focus colors.


15. i18n

All UI strings are externalized from day one: flat JSON in locales/{lang}.json with one level of dotted keys, ICU MessageFormat for plurals and select, and Intl for numbers, dates and percentages. Tag names in Raw mode (H2, LBody) are not translated. Their spoken forms are.

NamespaceHoldsExample key
ts.shell.*App bar, status bar, landmarks, region headingsts.shell.saveState.unsaved
ts.mode.<mode>.*Mode names, explainers, task panel labels, done criteria (readingOrder, headings, figures, tables, artifacts, lists, links, forms, notes, language, toc, raw)ts.mode.figures.queue.needsAlt
ts.tree.*Treegrid labels, row name template, filters, presetsts.tree.row.name
ts.canvas.*Overlay menu, mark-mode announcements, canvas descriptionts.canvas.mark.corner
ts.reader.*Announcement grammar templates and profiles (Β§4.3.2)ts.reader.heading = heading level {level}, {text}
ts.inspector.*Field labels, hints, lint messagests.inspector.alt.lint.imageOf
ts.tag.<Type>.*name, spoken, description for each structure typets.tag.LBody.spoken
ts.op.<op>.*label (history), done (announcement), refused.<reason>ts.op.move.refused.liOutsideL
ts.rule.<ruleId>.*Validation message, fix labels, help link text (ruleId uses mp-13-004, wcag-1-1-1)ts.rule.mp-13-004.title
ts.issues.*Panel chrome, statuses, fix-all sheetts.issues.status.needsReview
ts.similar.*Signature chip labels, result countsts.similar.result.count
ts.ai.*Suggestion queue, confidence bands, reasons scaffolding, cost confirmts.ai.band.high
ts.history.*History panel chrome, origin chips, versions panelts.history.origin.aiAuto
ts.sync.*Sync status, lease/conflict flow, offline messagests.sync.state.offline
ts.verify.*Post-save verification, render-diff report, veraPDF summaryts.verify.chip.problems
ts.settings.*Settings, including AI auto-apply and the org-locked statets.settings.ai.autoApply.lockedByOrg
ts.save.* / ts.export.*Gate progress, blocked sheet, report headingsts.save.blocked.title
ts.keys.*Key names and shortcut descriptions, platform variants (.mac, .win)ts.keys.undo.mac
ts.state.*Empty, loading, error statests.state.untagged.title
ts.progress.*Progress texts for >2s operationsts.progress.validating

Reports (change and conformance) use the same catalog under ts.export.report.*, so they can be generated in the user’s locale. The Reader’s grammar is in the user’s UI locale; the spoken document content stays in the document’s language (and is marked up with lang in the Reader HTML).


16. Usability-test scenarios

Participants: at least 5 professional remediators (Acrobat Pro users), 3 of them screen reader users (NVDA, JAWS, VoiceOver on macOS), and 2 with color-vision deficiency. Test files come from the tag-tree golden corpus (#3856). Success = task complete without moderator help, within the time target, with no unintended ops in the history.

#ScenarioMeasures / pass bar
U1Open an untagged 12-page two-column newsletter and produce a correct reading order with no orphans≀ 15 min; zero orphans; path correct; used click-sequence or column proposal
U2Fix a skipped heading level and apply the level map document-wide, then undo the level map≀ 3 min; one undo restores all; participant can say what was undone
U3Write alt text for 10 figures, mark 2 as decorative, link 1 caption≀ 10 min; no suspicious-alt lint left
U4Repair a 3-level-header irregular table with a spanning header (Headers/IDs), confirming via announcement preview≀ 8 min; announcement preview matches expected for 5 sampled cells
U5Artifact running headers, footers and page numbers on a 48-page report while keeping the title-page header as content≀ 4 min; exclusion used; fix count correct
U6Convert bullet paragraphs to a nested list with labels≀ 5 min; Lbl/LBody present; nesting correct
U7Fix 3 β€œclick here” links and set tab order to structure≀ 4 min
U8Add tooltips to 8 form fields and reorder tab order to match the visual layout≀ 6 min
U9Use select-similar to retype 40 mis-tagged captions (P β†’ Caption), excluding two false matches≀ 3 min; exclusions correct
U10Screen reader only: move a paragraph under a different heading, promote a heading, retype a node and read the result in the Reader, with no pointerCompletes on NVDA, JAWS and VoiceOver; every op announced; no lost focus; notes on C6–C8
U11Review 20 AI alt-text suggestions: accept High, edit 2, reject 1, then undo the bulk accept and confirm the proposals return to PendingUnderstands confidence; no Low items bulk-accepted; undo understood
U12Color-vision participants: identify the tag family of 10 highlighted regions with labels on, then with β€œPatterns only”β‰₯ 9/10 correct in both
U13Save after an op that changes page appearance (seeded), then find and undo the cause from the Save blocked sheetFinds cause ≀ 2 min; understands nothing was saved
U13bSame file: choose Save with pixel changes, complete the confirm step, then find the verification result and the changed-pages list in the change reportCompletes confirm without help; can state which pages changed and whether verification passed
U14Keyboard only: cycle regions with F6 across Chrome, Edge, Firefox, Safari; fall back to Mod+K where F6 failsRecords which browsers pass C2
U15Work at 400% zoom (320px) in single-pane mode: fix one figure and one headingCompletes; no horizontal page scroll outside the canvas
U16Use fix-all for 47 page-number issues, untick 2, apply, and read the change reportReport lists 45 fixes and origin fixAll
U17Compare against Acrobat Pro: same file, same 5 tasks, timed (benchmark from epic phase 0)Tag Studio β‰₯ 30% faster median; fewer errors
U18Recover after a forced engine crash mid-sessionNo lost ops; participant trusts the recovery message
U19Open the same document in two tabs, take over editing in the second, then go offline in the first and reconnectUnderstands read-only state; no lost or silently dropped changes; can explain the β€œCouldn’t sync” list if seeded
U20Turn on AI auto-apply at 90%, run suggestions on 30 figures, then find the auto-applied items, revert one, undo the rest as a batch, and review twoFinds them via history and the β€œAuto-applied β€” needs review” filter; knows they are not yet reviewed

17. Competitor pain points designed out, and parity

Seen in (Acrobat Pro / Grackle / Siteimprove / our older editors)Tag Studio answer
Tags, Content and Order are separate panels that each hold their own model and driftOne model, synchronized views (Β§2)
Modal Properties dialogs that hide the pageNon-modal inspector (Β§4.4)
Unreliable undo and β€œundo not available” after Reading Order tool actionsEvery edit is an op with an inverse, and undo is group-level and persisted (Β§10)
Nodes shown as β€œ<MCID 12>” or β€œContainer”Real content previews (Β§4.2.1)
Drag-only reordering; tiny drop zonesKeyboard ops, cut/paste move, explicit drop indicators with text (Β§4.2.3, Β§5.3)
Checkers that mark β€œPassed manually” by default or collapse review items into passesNeeds review is never a pass; attestation is explicit and reported (Β§7)
Auto-tag that silently rewrites structureProposals with preview and confidence (Β§9)
Saving that silently re-renders or re-encodes contentPixel-identity gate (Β§11)
Acrobat Content / Reading Order panels have no undo; some axesPDF ops can’t be undoneEvery op has a tested inverse, and none ship without one (Β§3)
Acrobat’s reading-order view desyncs from the tag treeBoth are renderings of one model; bidirectional Find on page / Find in tree (Β§5.4)
Acrobat’s reading-order tool has no List typeTag as List from the sequence or canvas, including nesting (Β§6.1, Β§6.6)
Per-cell table properties dialogs (Acrobat, CommonLook)In-place grid editor with bulk TH/TD, scope, spans, Headers/IDs (Β§6.4)
AI alt text without confidence (several tools)Every proposal shows its confidence band, number and reason (Β§9)
No competitor documents how its editor works with a screen readerThis spec does: ARIA patterns, announcements and conflicts (Β§4.2, Β§5.6, Β§13), and U10 tests them

Parity features we must match (from the phase-0 competitor teardown): wrap in a new container, move up a level, merge, and delete empty tags (axesPDF, CommonLook) β†’ wrap, promote, mergeNodes, Remove empty tags. Tag Similar by font style (CommonLook) β†’ Select similar (Β§8). List building from a selection, including nested lists (CommonLook) β†’ Β§6.6. Table grid with bulk TH/TD, scope, headers, spans, merge/split and flatten layout table β†’ Β§6.4. One-click document-wide fixes (axesPDF) β†’ Fix all (Β§7.3). A color-coded structure preview and screen reader preview (PAC, axesPDF) β†’ overlays and Reader (Β§4.1, Β§4.3). Bulk alt-text and links panels (Grackle) β†’ Figures list view and Links mode (Β§6.3, Β§6.7). An AI first pass with a review queue β†’ Β§9 and the β€œAuto-tag first” empty state (Β§12.2).


18. Open questions for Larry

  • Q1: Does saving Alt text in Figures mode count as human attestation (as specified), or should there be a separate explicit β€œReviewed” step for audit-heavy customers?
  • Q2: For scanned PDFs, should Tag Studio hand off to Gevell PDF for OCR, or refuse outright?
  • Q3: Should Mod+Shift+J (Jump to page) be dropped on Windows (conflict C4), keeping only the palette?
  • Q4: Should verbosity profiles name the products (β€œNVDA-like”) or stay generic (β€œProfile A/B/C”) to avoid implying equivalence? Resolved 2026-09-28: pixel-diff saves are allowed as a confirmed override (Β§11.1.1); documents, versions and the op log sync to the server (Β§11.3); AI auto-apply is an opt-in user setting with a fixed 80% floor that org admins can lock off (Β§9.4); the op log is kept for the life of the document (Β§10.1).