element",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "navigation-role-aria",
+ "dir": "navigation-role-aria",
+ "role": "navigation",
+ "description": "navigation role via explicit ARIA role=\"navigation\"",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "noteref-role-aria",
+ "dir": "noteref-role-aria",
+ "role": "noteref",
+ "description": "A standalone noteref link, via explicit ARIA role=\"doc-noteref\", with no resolvable footnote/endnote target",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "noteref-target-in-endnotes-epub-type",
+ "dir": "noteref-target-in-endnotes-epub-type",
+ "role": "footnote",
+ "description": "A noteref's target footnote, nested inside an endnotes section via epub:type, still appears at its own location (unlike a footnote outside any endnotes section, which is suppressed there since a noteref already embeds it inline)",
+ "files": {
+ "input": "input.xhtml",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "noteref-target-in-endnotes-role-aria",
+ "dir": "noteref-target-in-endnotes-role-aria",
+ "role": "footnote",
+ "description": "A noteref's target footnote, nested inside an endnotes section via explicit ARIA roles, still appears at its own location (unlike a footnote outside any endnotes section, which is suppressed there since a noteref already embeds it inline)",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "notice-epub-type",
+ "dir": "notice-epub-type",
+ "role": "notice",
+ "description": "notice role via epub:type=\"notice\"",
+ "files": {
+ "input": "input.xhtml",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "notice-role-aria",
+ "dir": "notice-role-aria",
+ "role": "notice",
+ "description": "notice role via explicit ARIA role=\"doc-notice\"",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "pagebreak-epub-type",
+ "dir": "pagebreak-epub-type",
+ "role": "pagebreak",
+ "description": "A standalone pagebreak and one falling in the middle of a sentence, via epub:type=\"pagebreak\"",
+ "files": {
+ "input": "input.xhtml",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "pagebreak-role-aria",
+ "dir": "pagebreak-role-aria",
+ "role": "pagebreak",
+ "description": "A standalone pagebreak and one falling in the middle of a sentence, via role=\"doc-pagebreak\"",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "pagelist-epub-type",
+ "dir": "pagelist-epub-type",
+ "role": "pagelist",
+ "description": "A page-list navigation list, containing links to pagebreaks, via epub:type",
+ "files": {
+ "input": "input.xhtml",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "pagelist-role-aria",
+ "dir": "pagelist-role-aria",
+ "role": "pagelist",
+ "description": "A page-list navigation list, containing links to pagebreaks, via explicit ARIA role",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "paragraph-basic",
+ "dir": "paragraph-basic",
+ "role": "paragraph",
+ "description": "A single paragraph, the only encoding pathway for this role",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "part-epub-type",
+ "dir": "part-epub-type",
+ "role": "part",
+ "description": "part role via epub:type=\"part\", containing a heading",
+ "files": {
+ "input": "input.xhtml",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "part-role-aria",
+ "dir": "part-role-aria",
+ "role": "part",
+ "description": "part role via explicit ARIA role=\"doc-part\", containing a heading",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "preface-epub-type",
+ "dir": "preface-epub-type",
+ "role": "preface",
+ "description": "preface role via epub:type=\"preface\"",
+ "files": {
+ "input": "input.xhtml",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "preface-role-aria",
+ "dir": "preface-role-aria",
+ "role": "preface",
+ "description": "preface role via explicit ARIA role=\"doc-preface\"",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "preformatted-html-native",
+ "dir": "preformatted-html-native",
+ "role": "preformatted",
+ "description": "preformatted role via native element",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "presentation-role-aria",
+ "dir": "presentation-role-aria",
+ "role": "presentation",
+ "description": "presentation role via explicit ARIA role=\"presentation\"",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "prologue-epub-type",
+ "dir": "prologue-epub-type",
+ "role": "prologue",
+ "description": "prologue role via epub:type=\"prologue\"",
+ "files": {
+ "input": "input.xhtml",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "prologue-role-aria",
+ "dir": "prologue-role-aria",
+ "role": "prologue",
+ "description": "prologue role via explicit ARIA role=\"doc-prologue\"",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "pullquote-epub-type",
+ "dir": "pullquote-epub-type",
+ "role": "pullquote",
+ "description": "pullquote role via epub:type=\"pullquote\" on an ",
+ "files": {
+ "input": "input.xhtml",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "pullquote-role-aria",
+ "dir": "pullquote-role-aria",
+ "role": "pullquote",
+ "description": "pullquote role via explicit ARIA role=\"doc-pullquote\"",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "qna-epub-type",
+ "dir": "qna-epub-type",
+ "role": "qna",
+ "description": "qna role via epub:type=\"qna\" on a ",
+ "files": {
+ "input": "input.xhtml",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "qna-role-aria",
+ "dir": "qna-role-aria",
+ "role": "qna",
+ "description": "qna role via explicit ARIA role=\"doc-qna\"",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "region-role-aria",
+ "dir": "region-role-aria",
+ "role": "region",
+ "description": "region role via explicit ARIA role=\"region\"",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "section-html-native",
+ "dir": "section-html-native",
+ "role": "section",
+ "description": "section role via native element with no more specific semantic",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "separator-html-native",
+ "dir": "separator-html-native",
+ "role": "separator",
+ "description": "separator role via native element; a purely visual divider with no read-aloud content",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "separator-role-aria",
+ "dir": "separator-role-aria",
+ "role": "separator",
+ "description": "separator role via explicit ARIA role=\"separator\"; a purely visual divider with no read-aloud content",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "separator-with-glyphs",
+ "dir": "separator-with-glyphs",
+ "role": "separator",
+ "description": "separator role whose visible content is a decorative glyph run (\"* * *\"), not meaningful text",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "subtitle-epub-type",
+ "dir": "subtitle-epub-type",
+ "role": "subtitle",
+ "description": "subtitle role via epub:type=\"subtitle\"",
+ "files": {
+ "input": "input.xhtml",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "subtitle-role-aria",
+ "dir": "subtitle-role-aria",
+ "role": "subtitle",
+ "description": "subtitle role via explicit ARIA role=\"doc-subtitle\"",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "summary-html-native",
+ "dir": "summary-html-native",
+ "role": "summary",
+ "description": "summary role via native element",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-caption",
+ "dir": "table-caption",
+ "role": "table",
+ "description": "A table with an accessible name via aria-label — the {description} branch of table's own announcement template. Uses aria-label rather than a native element since native / implicit accessible-name folding is not yet implemented (see CONTEXTUALIZATION_PLAN.md)",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-caption-element-html-native",
+ "dir": "table-caption-element-html-native",
+ "role": "table",
+ "description": "A native with no explicit aria-labelledby — the caption becomes a separate child node with role \"caption\", not folded into the table's own description field. Directly demonstrates the caption-folding gap noted in CONTEXTUALIZATION_PLAN.md: table.md's spec assumes \"can end up in description\", but that implicit association isn't implemented. Contrast with table-caption (which uses explicit aria-label and does populate description) and table-caption-role-aria (the same un-folded-caption behavior via role=\"caption\")",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-caption-role-aria",
+ "dir": "table-caption-role-aria",
+ "role": "table",
+ "description": "An ARIA table with an explicit role=\"caption\" child (no aria-labelledby) — same un-folded behavior as table-caption-element-html-native (a separate role \"caption\" child, not folded into the table's own description), confirmed via the explicit-ARIA encoding rather than a native element",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-colspan-html-native",
+ "dir": "table-colspan-html-native",
+ "role": "table",
+ "description": "A native columnheader with colspan=\"2\" spanning two data columns — colspan is not read or tracked at all; the second data row's two cells carry no header-association information reflecting that a single header covers both of them. Relevant to the planned cell/rowheader {header}: {value} template work, which needs real column-index tracking to get this right. See table-colspan-role-aria for the aria-colspan equivalent",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-colspan-role-aria",
+ "dir": "table-colspan-role-aria",
+ "role": "table",
+ "description": "An ARIA columnheader with aria-colspan=\"2\" spanning two data columns — same as table-colspan-html-native's native colspan, aria-colspan is not read or tracked at all either, confirmed via the explicit-ARIA encoding",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-epub-type",
+ "dir": "table-epub-type",
+ "role": "table",
+ "description": "table role via epub:type=\"table\"/\"table-row\"/\"table-cell\" on neutral elements — the only encoding of table previously untested, despite extensive native/ARIA table coverage. epub:type has no columnheader/rowheader equivalent at all (only table/table-row/table-cell are mapped in epubTypeRoles), so this is necessarily a header-less data grid, same shape as table-no-headers",
+ "files": {
+ "input": "input.xhtml",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-html-native",
+ "dir": "table-html-native",
+ "role": "table",
+ "description": "A table with a header row and row headers, via native
/// ",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-in-role-none-wrapper",
+ "dir": "table-in-role-none-wrapper",
+ "role": "table",
+ "description": "A native inside a : a div has no required owned elements, so presentation doesn't reach the table, which keeps its table/row/columnheader/cell roles",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-nested-html-native",
+ "dir": "table-nested-html-native",
+ "role": "table",
+ "description": "A native
nested inside another native table's cell — confirms recursion handles it correctly (the inner table's own row/cell structure resolves independently, nested under the outer cell alongside its own text). See table-nested-role-aria for the ARIA-role equivalent",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-nested-in-presentation-table",
+ "dir": "table-nested-in-presentation-table",
+ "role": "presentation",
+ "description": "A data table nested in a layout : only the outer table's own rows/cells inherit presentation; the inner table keeps its table/row/columnheader/cell roles",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-nested-role-aria",
+ "dir": "table-nested-role-aria",
+ "role": "table",
+ "description": "An ARIA table (role=\"table\"/role=\"row\"/role=\"cell\") nested inside another ARIA table's cell — same correct recursion as table-nested-html-native, confirmed via the explicit-ARIA encoding",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-no-headers",
+ "dir": "table-no-headers",
+ "role": "table",
+ "description": "A bare data table with no columnheader/rowheader row at all — the header-less \"{value}\" branch of cell's announcement template",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-role-aria",
+ "dir": "table-role-aria",
+ "role": "table",
+ "description": "A table with a header row and row headers, via explicit ARIA table/row/columnheader/rowheader/cell roles",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-role-presentation-explicit-cell",
+ "dir": "table-role-presentation-explicit-cell",
+ "role": "presentation",
+ "description": "A in a keeps its explicit role, while its sibling with no role inherits presentation",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-role-presentation-html-native",
+ "dir": "table-role-presentation-html-native",
+ "role": "presentation",
+ "description": "A native : its rows/cells have no explicit role, so they inherit presentation (ARIA presentational inheritance to required owned elements). See table-role-presentation-role-aria for explicit roles, which don't inherit",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-role-presentation-role-aria",
+ "dir": "table-role-presentation-role-aria",
+ "role": "presentation",
+ "description": "role=\"presentation\" on a wrapper doesn't override descendants' explicit role=\"row\"/\"columnheader\"/\"cell\" (ARIA presentational inheritance skips explicit roles)",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-rowgroup-role-aria",
+ "dir": "table-rowgroup-role-aria",
+ "role": "table",
+ "description": "An ARIA table with an explicit role=\"rowgroup\" wrapper around its rows — rowgroup is not in GND's role vocabulary at all, so the wrapper is treated as an unknown role and spliced away transparently, same as native / / (see table-sectioning). Captured as a baseline confirming this is consistent across both encodings, not a native-only quirk",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-rowspan-html-native",
+ "dir": "table-rowspan-html-native",
+ "role": "table",
+ "description": "A native rowheader with rowspan=\"2\" spanning two data rows — rowspan is not read or tracked at all; the second row's cell carries no rowheader association even though the spanned header logically covers it too. Same relevance as table-colspan-html-native for the planned header-association work. See table-rowspan-role-aria for the aria-rowspan equivalent",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-rowspan-role-aria",
+ "dir": "table-rowspan-role-aria",
+ "role": "table",
+ "description": "An ARIA rowheader with aria-rowspan=\"2\" spanning two data rows — same as table-rowspan-html-native's native rowspan, aria-rowspan is not read or tracked at all either, confirmed via the explicit-ARIA encoding",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-sectioning",
+ "dir": "table-sectioning",
+ "role": "table",
+ "description": "/ / are transparent — their rows flatten directly into the table's children with no rowgroup role or boundary marker of any kind. GND's role vocabulary has no rowgroup role at all (not in roles.md), so this is presumed intentional (grouping is visual-only, not meaningful for read-aloud), but was previously unconfirmed by any fixture — a tfoot row like a totals row is indistinguishable from a tbody row",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "table-th-no-scope",
+ "dir": "table-th-no-scope",
+ "role": "table",
+ "description": "A with no scope attribute at all — falls back to a plain cell rather than columnheader/rowheader, since the converter only infers header direction from an explicit scope=\"col\"/\"row\" and does not implement HTML-AAM's implicit header-association algorithm. This is extremely common real-world markup (most authors omit scope), captured here as a baseline, not an endorsement of the current fallback",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "term-epub-type",
+ "dir": "term-epub-type",
+ "role": "term",
+ "description": "term role via epub:type=\"glossterm\" on a , nested in a ",
+ "files": {
+ "input": "input.xhtml",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "term-html-native",
+ "dir": "term-html-native",
+ "role": "term",
+ "description": "term role via native element",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "term-role-aria",
+ "dir": "term-role-aria",
+ "role": "term",
+ "description": "term role via explicit ARIA role=\"term\"",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "tip-epub-type",
+ "dir": "tip-epub-type",
+ "role": "tip",
+ "description": "tip role via epub:type=\"tip\" on an ",
+ "files": {
+ "input": "input.xhtml",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "tip-role-aria",
+ "dir": "tip-role-aria",
+ "role": "tip",
+ "description": "tip role via explicit ARIA role=\"doc-tip\"",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "toc-epub-type",
+ "dir": "toc-epub-type",
+ "role": "toc",
+ "description": "A table-of-contents navigation list, containing links to sectional headings, via epub:type",
+ "files": {
+ "input": "input.xhtml",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "toc-role-aria",
+ "dir": "toc-role-aria",
+ "role": "toc",
+ "description": "A table-of-contents navigation list, containing links to sectional headings, via explicit ARIA role",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "video-html-native",
+ "dir": "video-html-native",
+ "role": "video",
+ "description": "video role via native element, with an aria-label as the accessible description",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ },
+ {
+ "id": "video-no-description",
+ "dir": "video-no-description",
+ "role": "video",
+ "description": "video role via native element, with no accessible name at all",
+ "files": {
+ "input": "input.html",
+ "gnd": "gnd.json"
+ }
+ }
+]
diff --git a/guided-navigation/fixtures/math-html-native/gnd.json b/guided-navigation/fixtures/math-html-native/gnd.json
new file mode 100644
index 00000000..5a985c2b
--- /dev/null
+++ b/guided-navigation/fixtures/math-html-native/gnd.json
@@ -0,0 +1,14 @@
+{
+ "role": [
+ "math"
+ ],
+ "text": {
+ "ssml": "\n x 2 \n + \n y 2 \n = \n z 2 \n "
+ },
+ "textref": "#css(math)",
+ "description": {
+ "text": {
+ "plain": "x squared plus y squared equals z squared"
+ }
+ }
+}
diff --git a/guided-navigation/fixtures/math-html-native/input.html b/guided-navigation/fixtures/math-html-native/input.html
new file mode 100644
index 00000000..1af608e1
--- /dev/null
+++ b/guided-navigation/fixtures/math-html-native/input.html
@@ -0,0 +1,7 @@
+
+ x 2
+ +
+ y 2
+ =
+ z 2
+
diff --git a/guided-navigation/fixtures/math-html-native/meta.json b/guided-navigation/fixtures/math-html-native/meta.json
new file mode 100644
index 00000000..9a64687a
--- /dev/null
+++ b/guided-navigation/fixtures/math-html-native/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "math-html-native",
+ "description": "math role via native MathML markup, with a spoken-form description for read-aloud",
+ "role": "math",
+ "rolesCovered": [
+ "math"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/math-no-description/gnd.json b/guided-navigation/fixtures/math-no-description/gnd.json
new file mode 100644
index 00000000..3e8b6b1a
--- /dev/null
+++ b/guided-navigation/fixtures/math-no-description/gnd.json
@@ -0,0 +1,9 @@
+{
+ "role": [
+ "math"
+ ],
+ "text": {
+ "ssml": "\n x 2 \n + \n y 2 \n = \n z 2 \n "
+ },
+ "textref": "#css(math)"
+}
diff --git a/guided-navigation/fixtures/math-no-description/input.html b/guided-navigation/fixtures/math-no-description/input.html
new file mode 100644
index 00000000..8ce201fe
--- /dev/null
+++ b/guided-navigation/fixtures/math-no-description/input.html
@@ -0,0 +1,7 @@
+
+ x 2
+ +
+ y 2
+ =
+ z 2
+
diff --git a/guided-navigation/fixtures/math-no-description/meta.json b/guided-navigation/fixtures/math-no-description/meta.json
new file mode 100644
index 00000000..eb1180dc
--- /dev/null
+++ b/guided-navigation/fixtures/math-no-description/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "math-no-description",
+ "description": "math role via native MathML markup, with no alttext/aria-label and non-trivial children — no spoken-form description available for read-aloud",
+ "role": "math",
+ "rolesCovered": [
+ "math"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/math-role-aria/gnd.json b/guided-navigation/fixtures/math-role-aria/gnd.json
new file mode 100644
index 00000000..5fb70194
--- /dev/null
+++ b/guided-navigation/fixtures/math-role-aria/gnd.json
@@ -0,0 +1,14 @@
+{
+ "role": [
+ "math"
+ ],
+ "text": {
+ "plain": "x² + y² = z²"
+ },
+ "textref": "#css(span)",
+ "description": {
+ "text": {
+ "plain": "x squared plus y squared equals z squared"
+ }
+ }
+}
diff --git a/guided-navigation/fixtures/math-role-aria/input.html b/guided-navigation/fixtures/math-role-aria/input.html
new file mode 100644
index 00000000..59f0e3c2
--- /dev/null
+++ b/guided-navigation/fixtures/math-role-aria/input.html
@@ -0,0 +1 @@
+x² + y² = z²
diff --git a/guided-navigation/fixtures/math-role-aria/meta.json b/guided-navigation/fixtures/math-role-aria/meta.json
new file mode 100644
index 00000000..f673a67f
--- /dev/null
+++ b/guided-navigation/fixtures/math-role-aria/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "math-role-aria",
+ "description": "math role via explicit ARIA role=\"math\" on a rendered formula, with an aria-label carrying the spoken form for read-aloud",
+ "role": "math",
+ "rolesCovered": [
+ "math"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/navigation-html-native/gnd.json b/guided-navigation/fixtures/navigation-html-native/gnd.json
new file mode 100644
index 00000000..0de5820b
--- /dev/null
+++ b/guided-navigation/fixtures/navigation-html-native/gnd.json
@@ -0,0 +1,8 @@
+{
+ "role": [
+ "navigation"
+ ],
+ "text": {
+ "plain": "Link to the next chapter."
+ }
+}
diff --git a/guided-navigation/fixtures/navigation-html-native/input.html b/guided-navigation/fixtures/navigation-html-native/input.html
new file mode 100644
index 00000000..77173354
--- /dev/null
+++ b/guided-navigation/fixtures/navigation-html-native/input.html
@@ -0,0 +1 @@
+Link to the next chapter.
diff --git a/guided-navigation/fixtures/navigation-html-native/meta.json b/guided-navigation/fixtures/navigation-html-native/meta.json
new file mode 100644
index 00000000..08b5d806
--- /dev/null
+++ b/guided-navigation/fixtures/navigation-html-native/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "navigation-html-native",
+ "description": "navigation role via native element",
+ "role": "navigation",
+ "rolesCovered": [
+ "navigation"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/navigation-role-aria/gnd.json b/guided-navigation/fixtures/navigation-role-aria/gnd.json
new file mode 100644
index 00000000..0de5820b
--- /dev/null
+++ b/guided-navigation/fixtures/navigation-role-aria/gnd.json
@@ -0,0 +1,8 @@
+{
+ "role": [
+ "navigation"
+ ],
+ "text": {
+ "plain": "Link to the next chapter."
+ }
+}
diff --git a/guided-navigation/fixtures/navigation-role-aria/input.html b/guided-navigation/fixtures/navigation-role-aria/input.html
new file mode 100644
index 00000000..617f494a
--- /dev/null
+++ b/guided-navigation/fixtures/navigation-role-aria/input.html
@@ -0,0 +1 @@
+Link to the next chapter.
diff --git a/guided-navigation/fixtures/navigation-role-aria/meta.json b/guided-navigation/fixtures/navigation-role-aria/meta.json
new file mode 100644
index 00000000..b7132450
--- /dev/null
+++ b/guided-navigation/fixtures/navigation-role-aria/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "navigation-role-aria",
+ "description": "navigation role via explicit ARIA role=\"navigation\"",
+ "role": "navigation",
+ "rolesCovered": [
+ "navigation"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/noteref-role-aria/gnd.json b/guided-navigation/fixtures/noteref-role-aria/gnd.json
new file mode 100644
index 00000000..a6adcd41
--- /dev/null
+++ b/guided-navigation/fixtures/noteref-role-aria/gnd.json
@@ -0,0 +1,13 @@
+{
+ "role": [
+ "noteref"
+ ],
+ "text": {
+ "plain": "12"
+ },
+ "children": [
+ {
+ "textref": "#note12"
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/noteref-role-aria/input.html b/guided-navigation/fixtures/noteref-role-aria/input.html
new file mode 100644
index 00000000..b2f4454c
--- /dev/null
+++ b/guided-navigation/fixtures/noteref-role-aria/input.html
@@ -0,0 +1 @@
+12
diff --git a/guided-navigation/fixtures/noteref-role-aria/meta.json b/guided-navigation/fixtures/noteref-role-aria/meta.json
new file mode 100644
index 00000000..00d1fd68
--- /dev/null
+++ b/guided-navigation/fixtures/noteref-role-aria/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "noteref-role-aria",
+ "description": "A standalone noteref link, via explicit ARIA role=\"doc-noteref\", with no resolvable footnote/endnote target",
+ "role": "noteref",
+ "rolesCovered": [
+ "noteref"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/noteref-target-in-endnotes-epub-type/gnd.json b/guided-navigation/fixtures/noteref-target-in-endnotes-epub-type/gnd.json
new file mode 100644
index 00000000..fc9856ee
--- /dev/null
+++ b/guided-navigation/fixtures/noteref-target-in-endnotes-epub-type/gnd.json
@@ -0,0 +1,72 @@
+{
+ "role": [
+ "body"
+ ],
+ "children": [
+ {
+ "text": {
+ "language": "en",
+ "plain": "This text has a note.",
+ "ssml": "This text has a note ."
+ },
+ "role": [
+ "paragraph"
+ ],
+ "children": [
+ {
+ "id": "noteref1",
+ "text": {
+ "plain": "[1]"
+ },
+ "role": [
+ "noteref"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Text of the note."
+ },
+ "role": [
+ "listItem",
+ "footnote"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "role": [
+ "section",
+ "endnotes"
+ ],
+ "children": [
+ {
+ "role": [
+ "heading2"
+ ],
+ "text": {
+ "plain": "Notes"
+ }
+ },
+ {
+ "role": [
+ "list"
+ ],
+ "children": [
+ {
+ "id": "note1",
+ "text": {
+ "plain": "Text of the note."
+ },
+ "role": [
+ "listItem",
+ "footnote"
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/noteref-target-in-endnotes-epub-type/input.xhtml b/guided-navigation/fixtures/noteref-target-in-endnotes-epub-type/input.xhtml
new file mode 100644
index 00000000..cb68d696
--- /dev/null
+++ b/guided-navigation/fixtures/noteref-target-in-endnotes-epub-type/input.xhtml
@@ -0,0 +1,17 @@
+
+
+
+
+
+Fixture
+
+
+This text has a note [1] .
+
+ Notes
+
+ Text of the note.
+
+
+
+
diff --git a/guided-navigation/fixtures/noteref-target-in-endnotes-epub-type/meta.json b/guided-navigation/fixtures/noteref-target-in-endnotes-epub-type/meta.json
new file mode 100644
index 00000000..c0126658
--- /dev/null
+++ b/guided-navigation/fixtures/noteref-target-in-endnotes-epub-type/meta.json
@@ -0,0 +1,15 @@
+{
+ "id": "noteref-target-in-endnotes-epub-type",
+ "description": "A noteref's target footnote, nested inside an endnotes section via epub:type, still appears at its own location (unlike a footnote outside any endnotes section, which is suppressed there since a noteref already embeds it inline)",
+ "role": "footnote",
+ "rolesCovered": [
+ "footnote",
+ "noteref",
+ "endnotes",
+ "heading2",
+ "list",
+ "listItem"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "document"
+}
diff --git a/guided-navigation/fixtures/noteref-target-in-endnotes-role-aria/gnd.json b/guided-navigation/fixtures/noteref-target-in-endnotes-role-aria/gnd.json
new file mode 100644
index 00000000..036d9dee
--- /dev/null
+++ b/guided-navigation/fixtures/noteref-target-in-endnotes-role-aria/gnd.json
@@ -0,0 +1,69 @@
+{
+ "children": [
+ {
+ "text": {
+ "language": "en",
+ "plain": "This text has a note.",
+ "ssml": "This text has a note ."
+ },
+ "role": [
+ "paragraph"
+ ],
+ "children": [
+ {
+ "id": "noteref1",
+ "text": {
+ "plain": "[1]"
+ },
+ "role": [
+ "noteref"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Text of the note."
+ },
+ "role": [
+ "listItem",
+ "footnote"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "role": [
+ "section",
+ "endnotes"
+ ],
+ "children": [
+ {
+ "role": [
+ "heading2"
+ ],
+ "text": {
+ "plain": "Notes"
+ }
+ },
+ {
+ "role": [
+ "list"
+ ],
+ "children": [
+ {
+ "id": "note1",
+ "text": {
+ "plain": "Text of the note."
+ },
+ "role": [
+ "listItem",
+ "footnote"
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/noteref-target-in-endnotes-role-aria/input.html b/guided-navigation/fixtures/noteref-target-in-endnotes-role-aria/input.html
new file mode 100644
index 00000000..9d83e177
--- /dev/null
+++ b/guided-navigation/fixtures/noteref-target-in-endnotes-role-aria/input.html
@@ -0,0 +1,7 @@
+This text has a note [1] .
+
+ Notes
+
+ Text of the note.
+
+
diff --git a/guided-navigation/fixtures/noteref-target-in-endnotes-role-aria/meta.json b/guided-navigation/fixtures/noteref-target-in-endnotes-role-aria/meta.json
new file mode 100644
index 00000000..5b57d330
--- /dev/null
+++ b/guided-navigation/fixtures/noteref-target-in-endnotes-role-aria/meta.json
@@ -0,0 +1,15 @@
+{
+ "id": "noteref-target-in-endnotes-role-aria",
+ "description": "A noteref's target footnote, nested inside an endnotes section via explicit ARIA roles, still appears at its own location (unlike a footnote outside any endnotes section, which is suppressed there since a noteref already embeds it inline)",
+ "role": "footnote",
+ "rolesCovered": [
+ "footnote",
+ "noteref",
+ "endnotes",
+ "heading2",
+ "list",
+ "listItem"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/notice-epub-type/gnd.json b/guided-navigation/fixtures/notice-epub-type/gnd.json
new file mode 100644
index 00000000..ac63e2f4
--- /dev/null
+++ b/guided-navigation/fixtures/notice-epub-type/gnd.json
@@ -0,0 +1,15 @@
+{
+ "role": [
+ "body"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Caution: the tower stairs are uneven."
+ },
+ "role": [
+ "notice"
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/notice-epub-type/input.xhtml b/guided-navigation/fixtures/notice-epub-type/input.xhtml
new file mode 100644
index 00000000..11dd8eae
--- /dev/null
+++ b/guided-navigation/fixtures/notice-epub-type/input.xhtml
@@ -0,0 +1,11 @@
+
+
+
+
+
+Fixture
+
+
+Caution: the tower stairs are uneven.
+
+
diff --git a/guided-navigation/fixtures/notice-epub-type/meta.json b/guided-navigation/fixtures/notice-epub-type/meta.json
new file mode 100644
index 00000000..acd14fe0
--- /dev/null
+++ b/guided-navigation/fixtures/notice-epub-type/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "notice-epub-type",
+ "description": "notice role via epub:type=\"notice\"",
+ "role": "notice",
+ "rolesCovered": [
+ "notice"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "document"
+}
diff --git a/guided-navigation/fixtures/notice-role-aria/gnd.json b/guided-navigation/fixtures/notice-role-aria/gnd.json
new file mode 100644
index 00000000..b877083d
--- /dev/null
+++ b/guided-navigation/fixtures/notice-role-aria/gnd.json
@@ -0,0 +1,8 @@
+{
+ "role": [
+ "notice"
+ ],
+ "text": {
+ "plain": "Caution: the tower stairs are uneven."
+ }
+}
diff --git a/guided-navigation/fixtures/notice-role-aria/input.html b/guided-navigation/fixtures/notice-role-aria/input.html
new file mode 100644
index 00000000..57e4f718
--- /dev/null
+++ b/guided-navigation/fixtures/notice-role-aria/input.html
@@ -0,0 +1 @@
+Caution: the tower stairs are uneven.
diff --git a/guided-navigation/fixtures/notice-role-aria/meta.json b/guided-navigation/fixtures/notice-role-aria/meta.json
new file mode 100644
index 00000000..20bb968f
--- /dev/null
+++ b/guided-navigation/fixtures/notice-role-aria/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "notice-role-aria",
+ "description": "notice role via explicit ARIA role=\"doc-notice\"",
+ "role": "notice",
+ "rolesCovered": [
+ "notice"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/pagebreak-epub-type/gnd.json b/guided-navigation/fixtures/pagebreak-epub-type/gnd.json
new file mode 100644
index 00000000..5b8b746e
--- /dev/null
+++ b/guided-navigation/fixtures/pagebreak-epub-type/gnd.json
@@ -0,0 +1,36 @@
+{
+ "role": [
+ "body"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "4"
+ },
+ "role": [
+ "pagebreak"
+ ]
+ },
+ {
+ "text": {
+ "language": "en",
+ "plain": "And the next pagebreak is in the middle of a sentence.",
+ "ssml": "And the next pagebreak is in the middle of a sentence."
+ },
+ "role": [
+ "paragraph"
+ ],
+ "children": [
+ {
+ "id": "pagebreak1",
+ "text": {
+ "plain": "5"
+ },
+ "role": [
+ "pagebreak"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/pagebreak-epub-type/input.xhtml b/guided-navigation/fixtures/pagebreak-epub-type/input.xhtml
new file mode 100644
index 00000000..eff78dcc
--- /dev/null
+++ b/guided-navigation/fixtures/pagebreak-epub-type/input.xhtml
@@ -0,0 +1,12 @@
+
+
+
+
+
+Fixture
+
+
+
+And the next pagebreak is in the middle of a sentence.
+
+
diff --git a/guided-navigation/fixtures/pagebreak-epub-type/meta.json b/guided-navigation/fixtures/pagebreak-epub-type/meta.json
new file mode 100644
index 00000000..bdf0f441
--- /dev/null
+++ b/guided-navigation/fixtures/pagebreak-epub-type/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "pagebreak-epub-type",
+ "description": "A standalone pagebreak and one falling in the middle of a sentence, via epub:type=\"pagebreak\"",
+ "role": "pagebreak",
+ "rolesCovered": [
+ "pagebreak"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "document"
+}
diff --git a/guided-navigation/fixtures/pagebreak-role-aria/gnd.json b/guided-navigation/fixtures/pagebreak-role-aria/gnd.json
new file mode 100644
index 00000000..feeea9fd
--- /dev/null
+++ b/guided-navigation/fixtures/pagebreak-role-aria/gnd.json
@@ -0,0 +1,33 @@
+{
+ "children": [
+ {
+ "role": [
+ "pagebreak"
+ ],
+ "text": {
+ "plain": "4"
+ }
+ },
+ {
+ "role": [
+ "paragraph"
+ ],
+ "text": {
+ "plain": "And the next pagebreak is in the middle of a sentence.",
+ "ssml": "And the next pagebreak is in the middle of a sentence.",
+ "language": "en"
+ },
+ "children": [
+ {
+ "role": [
+ "pagebreak"
+ ],
+ "id": "pagebreak1",
+ "text": {
+ "plain": "5"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/pagebreak-role-aria/input.html b/guided-navigation/fixtures/pagebreak-role-aria/input.html
new file mode 100644
index 00000000..4c258945
--- /dev/null
+++ b/guided-navigation/fixtures/pagebreak-role-aria/input.html
@@ -0,0 +1,2 @@
+
+And the next pagebreak is in the middle of a sentence.
diff --git a/guided-navigation/fixtures/pagebreak-role-aria/meta.json b/guided-navigation/fixtures/pagebreak-role-aria/meta.json
new file mode 100644
index 00000000..c31359f3
--- /dev/null
+++ b/guided-navigation/fixtures/pagebreak-role-aria/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "pagebreak-role-aria",
+ "description": "A standalone pagebreak and one falling in the middle of a sentence, via role=\"doc-pagebreak\"",
+ "role": "pagebreak",
+ "rolesCovered": [
+ "pagebreak"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/pagelist-epub-type/gnd.json b/guided-navigation/fixtures/pagelist-epub-type/gnd.json
new file mode 100644
index 00000000..c1da3774
--- /dev/null
+++ b/guided-navigation/fixtures/pagelist-epub-type/gnd.json
@@ -0,0 +1,61 @@
+{
+ "role": [
+ "body"
+ ],
+ "children": [
+ {
+ "role": [
+ "navigation",
+ "pagelist"
+ ],
+ "children": [
+ {
+ "role": [
+ "list"
+ ],
+ "children": [
+ {
+ "role": [
+ "listItem"
+ ],
+ "children": [
+ {
+ "textref": "#pg1",
+ "text": {
+ "plain": "Page 1"
+ }
+ }
+ ]
+ },
+ {
+ "role": [
+ "listItem"
+ ],
+ "children": [
+ {
+ "textref": "#pg2",
+ "text": {
+ "plain": "Page 2"
+ }
+ }
+ ]
+ },
+ {
+ "role": [
+ "listItem"
+ ],
+ "children": [
+ {
+ "textref": "#pg3",
+ "text": {
+ "plain": "Page 3"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/pagelist-epub-type/input.xhtml b/guided-navigation/fixtures/pagelist-epub-type/input.xhtml
new file mode 100644
index 00000000..66f86d42
--- /dev/null
+++ b/guided-navigation/fixtures/pagelist-epub-type/input.xhtml
@@ -0,0 +1,17 @@
+
+
+
+
+
+Fixture
+
+
+
+
+ Page 1
+ Page 2
+ Page 3
+
+
+
+
diff --git a/guided-navigation/fixtures/pagelist-epub-type/meta.json b/guided-navigation/fixtures/pagelist-epub-type/meta.json
new file mode 100644
index 00000000..416efde5
--- /dev/null
+++ b/guided-navigation/fixtures/pagelist-epub-type/meta.json
@@ -0,0 +1,12 @@
+{
+ "id": "pagelist-epub-type",
+ "description": "A page-list navigation list, containing links to pagebreaks, via epub:type",
+ "role": "pagelist",
+ "rolesCovered": [
+ "pagelist",
+ "list",
+ "listItem"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "document"
+}
diff --git a/guided-navigation/fixtures/pagelist-role-aria/gnd.json b/guided-navigation/fixtures/pagelist-role-aria/gnd.json
new file mode 100644
index 00000000..374c2f66
--- /dev/null
+++ b/guided-navigation/fixtures/pagelist-role-aria/gnd.json
@@ -0,0 +1,54 @@
+{
+ "role": [
+ "navigation",
+ "pagelist"
+ ],
+ "children": [
+ {
+ "role": [
+ "list"
+ ],
+ "children": [
+ {
+ "role": [
+ "listItem"
+ ],
+ "children": [
+ {
+ "textref": "#pg1",
+ "text": {
+ "plain": "Page 1"
+ }
+ }
+ ]
+ },
+ {
+ "role": [
+ "listItem"
+ ],
+ "children": [
+ {
+ "textref": "#pg2",
+ "text": {
+ "plain": "Page 2"
+ }
+ }
+ ]
+ },
+ {
+ "role": [
+ "listItem"
+ ],
+ "children": [
+ {
+ "textref": "#pg3",
+ "text": {
+ "plain": "Page 3"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/pagelist-role-aria/input.html b/guided-navigation/fixtures/pagelist-role-aria/input.html
new file mode 100644
index 00000000..77a98301
--- /dev/null
+++ b/guided-navigation/fixtures/pagelist-role-aria/input.html
@@ -0,0 +1,7 @@
+
+
+ Page 1
+ Page 2
+ Page 3
+
+
diff --git a/guided-navigation/fixtures/pagelist-role-aria/meta.json b/guided-navigation/fixtures/pagelist-role-aria/meta.json
new file mode 100644
index 00000000..81c0ee58
--- /dev/null
+++ b/guided-navigation/fixtures/pagelist-role-aria/meta.json
@@ -0,0 +1,12 @@
+{
+ "id": "pagelist-role-aria",
+ "description": "A page-list navigation list, containing links to pagebreaks, via explicit ARIA role",
+ "role": "pagelist",
+ "rolesCovered": [
+ "pagelist",
+ "list",
+ "listItem"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/paragraph-basic/gnd.json b/guided-navigation/fixtures/paragraph-basic/gnd.json
new file mode 100644
index 00000000..7935f0d4
--- /dev/null
+++ b/guided-navigation/fixtures/paragraph-basic/gnd.json
@@ -0,0 +1,8 @@
+{
+ "role": [
+ "paragraph"
+ ],
+ "text": {
+ "plain": "It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife."
+ }
+}
diff --git a/guided-navigation/fixtures/paragraph-basic/input.html b/guided-navigation/fixtures/paragraph-basic/input.html
new file mode 100644
index 00000000..3a1e36e2
--- /dev/null
+++ b/guided-navigation/fixtures/paragraph-basic/input.html
@@ -0,0 +1 @@
+It is a truth universally acknowledged, that a single man in possession of a good fortune, must be in want of a wife.
diff --git a/guided-navigation/fixtures/paragraph-basic/meta.json b/guided-navigation/fixtures/paragraph-basic/meta.json
new file mode 100644
index 00000000..9dc55ae9
--- /dev/null
+++ b/guided-navigation/fixtures/paragraph-basic/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "paragraph-basic",
+ "description": "A single paragraph, the only encoding pathway for this role",
+ "role": "paragraph",
+ "rolesCovered": [
+ "paragraph"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud#paragraphs",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/part-epub-type/gnd.json b/guided-navigation/fixtures/part-epub-type/gnd.json
new file mode 100644
index 00000000..3a3d0d33
--- /dev/null
+++ b/guided-navigation/fixtures/part-epub-type/gnd.json
@@ -0,0 +1,23 @@
+{
+ "role": [
+ "body"
+ ],
+ "children": [
+ {
+ "role": [
+ "section",
+ "part"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Title of the part"
+ },
+ "role": [
+ "heading1"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/part-epub-type/input.xhtml b/guided-navigation/fixtures/part-epub-type/input.xhtml
new file mode 100644
index 00000000..d9c8901e
--- /dev/null
+++ b/guided-navigation/fixtures/part-epub-type/input.xhtml
@@ -0,0 +1,13 @@
+
+
+
+
+
+Fixture
+
+
+
+
+
diff --git a/guided-navigation/fixtures/part-epub-type/meta.json b/guided-navigation/fixtures/part-epub-type/meta.json
new file mode 100644
index 00000000..75ea4130
--- /dev/null
+++ b/guided-navigation/fixtures/part-epub-type/meta.json
@@ -0,0 +1,11 @@
+{
+ "id": "part-epub-type",
+ "description": "part role via epub:type=\"part\", containing a heading",
+ "role": "part",
+ "rolesCovered": [
+ "part",
+ "heading1"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud#sections-and-headers",
+ "inputKind": "document"
+}
diff --git a/guided-navigation/fixtures/part-role-aria/gnd.json b/guided-navigation/fixtures/part-role-aria/gnd.json
new file mode 100644
index 00000000..99a8d92f
--- /dev/null
+++ b/guided-navigation/fixtures/part-role-aria/gnd.json
@@ -0,0 +1,16 @@
+{
+ "role": [
+ "section",
+ "part"
+ ],
+ "children": [
+ {
+ "role": [
+ "heading1"
+ ],
+ "text": {
+ "plain": "Title of the part"
+ }
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/part-role-aria/input.html b/guided-navigation/fixtures/part-role-aria/input.html
new file mode 100644
index 00000000..fd781968
--- /dev/null
+++ b/guided-navigation/fixtures/part-role-aria/input.html
@@ -0,0 +1,3 @@
+
diff --git a/guided-navigation/fixtures/part-role-aria/meta.json b/guided-navigation/fixtures/part-role-aria/meta.json
new file mode 100644
index 00000000..4a06ebd1
--- /dev/null
+++ b/guided-navigation/fixtures/part-role-aria/meta.json
@@ -0,0 +1,11 @@
+{
+ "id": "part-role-aria",
+ "description": "part role via explicit ARIA role=\"doc-part\", containing a heading",
+ "role": "part",
+ "rolesCovered": [
+ "part",
+ "heading1"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/preface-epub-type/gnd.json b/guided-navigation/fixtures/preface-epub-type/gnd.json
new file mode 100644
index 00000000..cf412399
--- /dev/null
+++ b/guided-navigation/fixtures/preface-epub-type/gnd.json
@@ -0,0 +1,15 @@
+{
+ "role": [
+ "body"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "This edition adds a chapter on automation."
+ },
+ "role": [
+ "preface"
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/preface-epub-type/input.xhtml b/guided-navigation/fixtures/preface-epub-type/input.xhtml
new file mode 100644
index 00000000..b7a8f66d
--- /dev/null
+++ b/guided-navigation/fixtures/preface-epub-type/input.xhtml
@@ -0,0 +1,11 @@
+
+
+
+
+
+Fixture
+
+
+This edition adds a chapter on automation.
+
+
diff --git a/guided-navigation/fixtures/preface-epub-type/meta.json b/guided-navigation/fixtures/preface-epub-type/meta.json
new file mode 100644
index 00000000..00ca0ec4
--- /dev/null
+++ b/guided-navigation/fixtures/preface-epub-type/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "preface-epub-type",
+ "description": "preface role via epub:type=\"preface\"",
+ "role": "preface",
+ "rolesCovered": [
+ "preface"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "document"
+}
diff --git a/guided-navigation/fixtures/preface-role-aria/gnd.json b/guided-navigation/fixtures/preface-role-aria/gnd.json
new file mode 100644
index 00000000..6c603685
--- /dev/null
+++ b/guided-navigation/fixtures/preface-role-aria/gnd.json
@@ -0,0 +1,8 @@
+{
+ "role": [
+ "preface"
+ ],
+ "text": {
+ "plain": "This edition adds a chapter on automation."
+ }
+}
diff --git a/guided-navigation/fixtures/preface-role-aria/input.html b/guided-navigation/fixtures/preface-role-aria/input.html
new file mode 100644
index 00000000..28278878
--- /dev/null
+++ b/guided-navigation/fixtures/preface-role-aria/input.html
@@ -0,0 +1 @@
+This edition adds a chapter on automation.
diff --git a/guided-navigation/fixtures/preface-role-aria/meta.json b/guided-navigation/fixtures/preface-role-aria/meta.json
new file mode 100644
index 00000000..603742ee
--- /dev/null
+++ b/guided-navigation/fixtures/preface-role-aria/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "preface-role-aria",
+ "description": "preface role via explicit ARIA role=\"doc-preface\"",
+ "role": "preface",
+ "rolesCovered": [
+ "preface"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/preformatted-html-native/gnd.json b/guided-navigation/fixtures/preformatted-html-native/gnd.json
new file mode 100644
index 00000000..554e1f1e
--- /dev/null
+++ b/guided-navigation/fixtures/preformatted-html-native/gnd.json
@@ -0,0 +1,8 @@
+{
+ "role": [
+ "preformatted"
+ ],
+ "text": {
+ "plain": "10 PRINT \"HELLO\" 20 GOTO 10"
+ }
+}
diff --git a/guided-navigation/fixtures/preformatted-html-native/input.html b/guided-navigation/fixtures/preformatted-html-native/input.html
new file mode 100644
index 00000000..ab3604dc
--- /dev/null
+++ b/guided-navigation/fixtures/preformatted-html-native/input.html
@@ -0,0 +1,2 @@
+ 10 PRINT "HELLO"
+ 20 GOTO 10
diff --git a/guided-navigation/fixtures/preformatted-html-native/meta.json b/guided-navigation/fixtures/preformatted-html-native/meta.json
new file mode 100644
index 00000000..eb242c7e
--- /dev/null
+++ b/guided-navigation/fixtures/preformatted-html-native/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "preformatted-html-native",
+ "description": "preformatted role via native element",
+ "role": "preformatted",
+ "rolesCovered": [
+ "preformatted"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/presentation-role-aria/gnd.json b/guided-navigation/fixtures/presentation-role-aria/gnd.json
new file mode 100644
index 00000000..e26c28ab
--- /dev/null
+++ b/guided-navigation/fixtures/presentation-role-aria/gnd.json
@@ -0,0 +1,8 @@
+{
+ "role": [
+ "presentation"
+ ],
+ "text": {
+ "plain": "Decorative content with no semantic meaning."
+ }
+}
diff --git a/guided-navigation/fixtures/presentation-role-aria/input.html b/guided-navigation/fixtures/presentation-role-aria/input.html
new file mode 100644
index 00000000..60e0372b
--- /dev/null
+++ b/guided-navigation/fixtures/presentation-role-aria/input.html
@@ -0,0 +1 @@
+Decorative content with no semantic meaning.
diff --git a/guided-navigation/fixtures/presentation-role-aria/meta.json b/guided-navigation/fixtures/presentation-role-aria/meta.json
new file mode 100644
index 00000000..1cf1a961
--- /dev/null
+++ b/guided-navigation/fixtures/presentation-role-aria/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "presentation-role-aria",
+ "description": "presentation role via explicit ARIA role=\"presentation\"",
+ "role": "presentation",
+ "rolesCovered": [
+ "presentation"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/prologue-epub-type/gnd.json b/guided-navigation/fixtures/prologue-epub-type/gnd.json
new file mode 100644
index 00000000..3ee8a075
--- /dev/null
+++ b/guided-navigation/fixtures/prologue-epub-type/gnd.json
@@ -0,0 +1,15 @@
+{
+ "role": [
+ "body"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Before the lamp was ever lit, the rock was already feared."
+ },
+ "role": [
+ "prologue"
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/prologue-epub-type/input.xhtml b/guided-navigation/fixtures/prologue-epub-type/input.xhtml
new file mode 100644
index 00000000..61aeacb5
--- /dev/null
+++ b/guided-navigation/fixtures/prologue-epub-type/input.xhtml
@@ -0,0 +1,11 @@
+
+
+
+
+
+Fixture
+
+
+Before the lamp was ever lit, the rock was already feared.
+
+
diff --git a/guided-navigation/fixtures/prologue-epub-type/meta.json b/guided-navigation/fixtures/prologue-epub-type/meta.json
new file mode 100644
index 00000000..a5a91e76
--- /dev/null
+++ b/guided-navigation/fixtures/prologue-epub-type/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "prologue-epub-type",
+ "description": "prologue role via epub:type=\"prologue\"",
+ "role": "prologue",
+ "rolesCovered": [
+ "prologue"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "document"
+}
diff --git a/guided-navigation/fixtures/prologue-role-aria/gnd.json b/guided-navigation/fixtures/prologue-role-aria/gnd.json
new file mode 100644
index 00000000..1668a564
--- /dev/null
+++ b/guided-navigation/fixtures/prologue-role-aria/gnd.json
@@ -0,0 +1,8 @@
+{
+ "role": [
+ "prologue"
+ ],
+ "text": {
+ "plain": "Before the lamp was ever lit, the rock was already feared."
+ }
+}
diff --git a/guided-navigation/fixtures/prologue-role-aria/input.html b/guided-navigation/fixtures/prologue-role-aria/input.html
new file mode 100644
index 00000000..ebb5b7a9
--- /dev/null
+++ b/guided-navigation/fixtures/prologue-role-aria/input.html
@@ -0,0 +1 @@
+Before the lamp was ever lit, the rock was already feared.
diff --git a/guided-navigation/fixtures/prologue-role-aria/meta.json b/guided-navigation/fixtures/prologue-role-aria/meta.json
new file mode 100644
index 00000000..d20dd470
--- /dev/null
+++ b/guided-navigation/fixtures/prologue-role-aria/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "prologue-role-aria",
+ "description": "prologue role via explicit ARIA role=\"doc-prologue\"",
+ "role": "prologue",
+ "rolesCovered": [
+ "prologue"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/pullquote-epub-type/gnd.json b/guided-navigation/fixtures/pullquote-epub-type/gnd.json
new file mode 100644
index 00000000..534fce5e
--- /dev/null
+++ b/guided-navigation/fixtures/pullquote-epub-type/gnd.json
@@ -0,0 +1,16 @@
+{
+ "role": [
+ "body"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "The lamp still turns, though no one climbs the stairs anymore."
+ },
+ "role": [
+ "aside",
+ "pullquote"
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/pullquote-epub-type/input.xhtml b/guided-navigation/fixtures/pullquote-epub-type/input.xhtml
new file mode 100644
index 00000000..48ec5aa0
--- /dev/null
+++ b/guided-navigation/fixtures/pullquote-epub-type/input.xhtml
@@ -0,0 +1,11 @@
+
+
+
+
+
+Fixture
+
+
+The lamp still turns, though no one climbs the stairs anymore.
+
+
diff --git a/guided-navigation/fixtures/pullquote-epub-type/meta.json b/guided-navigation/fixtures/pullquote-epub-type/meta.json
new file mode 100644
index 00000000..c56451e5
--- /dev/null
+++ b/guided-navigation/fixtures/pullquote-epub-type/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "pullquote-epub-type",
+ "description": "pullquote role via epub:type=\"pullquote\" on an ",
+ "role": "pullquote",
+ "rolesCovered": [
+ "pullquote"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "document"
+}
diff --git a/guided-navigation/fixtures/pullquote-role-aria/gnd.json b/guided-navigation/fixtures/pullquote-role-aria/gnd.json
new file mode 100644
index 00000000..158a5286
--- /dev/null
+++ b/guided-navigation/fixtures/pullquote-role-aria/gnd.json
@@ -0,0 +1,8 @@
+{
+ "role": [
+ "pullquote"
+ ],
+ "text": {
+ "plain": "The lamp still turns, though no one climbs the stairs anymore."
+ }
+}
diff --git a/guided-navigation/fixtures/pullquote-role-aria/input.html b/guided-navigation/fixtures/pullquote-role-aria/input.html
new file mode 100644
index 00000000..49d9d7ff
--- /dev/null
+++ b/guided-navigation/fixtures/pullquote-role-aria/input.html
@@ -0,0 +1 @@
+The lamp still turns, though no one climbs the stairs anymore.
diff --git a/guided-navigation/fixtures/pullquote-role-aria/meta.json b/guided-navigation/fixtures/pullquote-role-aria/meta.json
new file mode 100644
index 00000000..b576ae75
--- /dev/null
+++ b/guided-navigation/fixtures/pullquote-role-aria/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "pullquote-role-aria",
+ "description": "pullquote role via explicit ARIA role=\"doc-pullquote\"",
+ "role": "pullquote",
+ "rolesCovered": [
+ "pullquote"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/qna-epub-type/gnd.json b/guided-navigation/fixtures/qna-epub-type/gnd.json
new file mode 100644
index 00000000..c1ca95b3
--- /dev/null
+++ b/guided-navigation/fixtures/qna-epub-type/gnd.json
@@ -0,0 +1,16 @@
+{
+ "role": [
+ "body"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Q: How often was the lamp refueled? A: Nightly, by hand."
+ },
+ "role": [
+ "section",
+ "qna"
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/qna-epub-type/input.xhtml b/guided-navigation/fixtures/qna-epub-type/input.xhtml
new file mode 100644
index 00000000..93340865
--- /dev/null
+++ b/guided-navigation/fixtures/qna-epub-type/input.xhtml
@@ -0,0 +1,11 @@
+
+
+
+
+
+Fixture
+
+
+Q: How often was the lamp refueled? A: Nightly, by hand.
+
+
diff --git a/guided-navigation/fixtures/qna-epub-type/meta.json b/guided-navigation/fixtures/qna-epub-type/meta.json
new file mode 100644
index 00000000..167aa1ca
--- /dev/null
+++ b/guided-navigation/fixtures/qna-epub-type/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "qna-epub-type",
+ "description": "qna role via epub:type=\"qna\" on a ",
+ "role": "qna",
+ "rolesCovered": [
+ "qna"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "document"
+}
diff --git a/guided-navigation/fixtures/qna-role-aria/gnd.json b/guided-navigation/fixtures/qna-role-aria/gnd.json
new file mode 100644
index 00000000..fbce24d8
--- /dev/null
+++ b/guided-navigation/fixtures/qna-role-aria/gnd.json
@@ -0,0 +1,8 @@
+{
+ "role": [
+ "qna"
+ ],
+ "text": {
+ "plain": "Q: How often was the lamp refueled? A: Nightly, by hand."
+ }
+}
diff --git a/guided-navigation/fixtures/qna-role-aria/input.html b/guided-navigation/fixtures/qna-role-aria/input.html
new file mode 100644
index 00000000..4e29c0cf
--- /dev/null
+++ b/guided-navigation/fixtures/qna-role-aria/input.html
@@ -0,0 +1 @@
+Q: How often was the lamp refueled? A: Nightly, by hand.
diff --git a/guided-navigation/fixtures/qna-role-aria/meta.json b/guided-navigation/fixtures/qna-role-aria/meta.json
new file mode 100644
index 00000000..acc5f16d
--- /dev/null
+++ b/guided-navigation/fixtures/qna-role-aria/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "qna-role-aria",
+ "description": "qna role via explicit ARIA role=\"doc-qna\"",
+ "role": "qna",
+ "rolesCovered": [
+ "qna"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/region-role-aria/gnd.json b/guided-navigation/fixtures/region-role-aria/gnd.json
new file mode 100644
index 00000000..dd12f49a
--- /dev/null
+++ b/guided-navigation/fixtures/region-role-aria/gnd.json
@@ -0,0 +1,8 @@
+{
+ "role": [
+ "region"
+ ],
+ "text": {
+ "plain": "A named region of interest within the page."
+ }
+}
diff --git a/guided-navigation/fixtures/region-role-aria/input.html b/guided-navigation/fixtures/region-role-aria/input.html
new file mode 100644
index 00000000..3c029d01
--- /dev/null
+++ b/guided-navigation/fixtures/region-role-aria/input.html
@@ -0,0 +1 @@
+A named region of interest within the page.
diff --git a/guided-navigation/fixtures/region-role-aria/meta.json b/guided-navigation/fixtures/region-role-aria/meta.json
new file mode 100644
index 00000000..510a5ebe
--- /dev/null
+++ b/guided-navigation/fixtures/region-role-aria/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "region-role-aria",
+ "description": "region role via explicit ARIA role=\"region\"",
+ "role": "region",
+ "rolesCovered": [
+ "region"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/section-html-native/gnd.json b/guided-navigation/fixtures/section-html-native/gnd.json
new file mode 100644
index 00000000..71ab77ba
--- /dev/null
+++ b/guided-navigation/fixtures/section-html-native/gnd.json
@@ -0,0 +1,8 @@
+{
+ "role": [
+ "section"
+ ],
+ "text": {
+ "plain": "The lighthouse was decommissioned in 1998, though its light still functions as a private aid to navigation."
+ }
+}
diff --git a/guided-navigation/fixtures/section-html-native/input.html b/guided-navigation/fixtures/section-html-native/input.html
new file mode 100644
index 00000000..50b865c9
--- /dev/null
+++ b/guided-navigation/fixtures/section-html-native/input.html
@@ -0,0 +1 @@
+The lighthouse was decommissioned in 1998, though its light still functions as a private aid to navigation.
diff --git a/guided-navigation/fixtures/section-html-native/meta.json b/guided-navigation/fixtures/section-html-native/meta.json
new file mode 100644
index 00000000..e58c5994
--- /dev/null
+++ b/guided-navigation/fixtures/section-html-native/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "section-html-native",
+ "description": "section role via native element with no more specific semantic",
+ "role": "section",
+ "rolesCovered": [
+ "section"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/separator-html-native/gnd.json b/guided-navigation/fixtures/separator-html-native/gnd.json
new file mode 100644
index 00000000..bc37b8ad
--- /dev/null
+++ b/guided-navigation/fixtures/separator-html-native/gnd.json
@@ -0,0 +1,26 @@
+{
+ "children": [
+ {
+ "role": [
+ "paragraph"
+ ],
+ "text": {
+ "plain": "Section one."
+ }
+ },
+ {
+ "role": [
+ "separator"
+ ],
+ "textref": "#css(hr)"
+ },
+ {
+ "role": [
+ "paragraph"
+ ],
+ "text": {
+ "plain": "Section two."
+ }
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/separator-html-native/input.html b/guided-navigation/fixtures/separator-html-native/input.html
new file mode 100644
index 00000000..0c65eb81
--- /dev/null
+++ b/guided-navigation/fixtures/separator-html-native/input.html
@@ -0,0 +1,3 @@
+Section one.
+
+Section two.
diff --git a/guided-navigation/fixtures/separator-html-native/meta.json b/guided-navigation/fixtures/separator-html-native/meta.json
new file mode 100644
index 00000000..2fa0fb53
--- /dev/null
+++ b/guided-navigation/fixtures/separator-html-native/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "separator-html-native",
+ "description": "separator role via native element; a purely visual divider with no read-aloud content",
+ "role": "separator",
+ "rolesCovered": [
+ "separator"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/separator-role-aria/gnd.json b/guided-navigation/fixtures/separator-role-aria/gnd.json
new file mode 100644
index 00000000..315d3304
--- /dev/null
+++ b/guided-navigation/fixtures/separator-role-aria/gnd.json
@@ -0,0 +1,26 @@
+{
+ "children": [
+ {
+ "role": [
+ "paragraph"
+ ],
+ "text": {
+ "plain": "Section one."
+ }
+ },
+ {
+ "role": [
+ "separator"
+ ],
+ "textref": "#css(div)"
+ },
+ {
+ "role": [
+ "paragraph"
+ ],
+ "text": {
+ "plain": "Section two."
+ }
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/separator-role-aria/input.html b/guided-navigation/fixtures/separator-role-aria/input.html
new file mode 100644
index 00000000..14289329
--- /dev/null
+++ b/guided-navigation/fixtures/separator-role-aria/input.html
@@ -0,0 +1,3 @@
+Section one.
+
+Section two.
diff --git a/guided-navigation/fixtures/separator-role-aria/meta.json b/guided-navigation/fixtures/separator-role-aria/meta.json
new file mode 100644
index 00000000..3ec11d59
--- /dev/null
+++ b/guided-navigation/fixtures/separator-role-aria/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "separator-role-aria",
+ "description": "separator role via explicit ARIA role=\"separator\"; a purely visual divider with no read-aloud content",
+ "role": "separator",
+ "rolesCovered": [
+ "separator"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/separator-with-glyphs/gnd.json b/guided-navigation/fixtures/separator-with-glyphs/gnd.json
new file mode 100644
index 00000000..a1615ca2
--- /dev/null
+++ b/guided-navigation/fixtures/separator-with-glyphs/gnd.json
@@ -0,0 +1,28 @@
+{
+ "children": [
+ {
+ "role": [
+ "paragraph"
+ ],
+ "text": {
+ "plain": "Before."
+ }
+ },
+ {
+ "role": [
+ "separator"
+ ],
+ "text": {
+ "plain": "* * *"
+ }
+ },
+ {
+ "role": [
+ "paragraph"
+ ],
+ "text": {
+ "plain": "After."
+ }
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/separator-with-glyphs/input.html b/guided-navigation/fixtures/separator-with-glyphs/input.html
new file mode 100644
index 00000000..97b7d817
--- /dev/null
+++ b/guided-navigation/fixtures/separator-with-glyphs/input.html
@@ -0,0 +1,3 @@
+Before.
+* * *
+After.
diff --git a/guided-navigation/fixtures/separator-with-glyphs/meta.json b/guided-navigation/fixtures/separator-with-glyphs/meta.json
new file mode 100644
index 00000000..bf191540
--- /dev/null
+++ b/guided-navigation/fixtures/separator-with-glyphs/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "separator-with-glyphs",
+ "description": "separator role whose visible content is a decorative glyph run (\"* * *\"), not meaningful text",
+ "role": "separator",
+ "rolesCovered": [
+ "separator"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/subtitle-epub-type/gnd.json b/guided-navigation/fixtures/subtitle-epub-type/gnd.json
new file mode 100644
index 00000000..53a23958
--- /dev/null
+++ b/guided-navigation/fixtures/subtitle-epub-type/gnd.json
@@ -0,0 +1,15 @@
+{
+ "role": [
+ "body"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "A History of Coastal Lights"
+ },
+ "role": [
+ "subtitle"
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/subtitle-epub-type/input.xhtml b/guided-navigation/fixtures/subtitle-epub-type/input.xhtml
new file mode 100644
index 00000000..40daa04c
--- /dev/null
+++ b/guided-navigation/fixtures/subtitle-epub-type/input.xhtml
@@ -0,0 +1,11 @@
+
+
+
+
+
+Fixture
+
+
+A History of Coastal Lights
+
+
diff --git a/guided-navigation/fixtures/subtitle-epub-type/meta.json b/guided-navigation/fixtures/subtitle-epub-type/meta.json
new file mode 100644
index 00000000..bbe349c4
--- /dev/null
+++ b/guided-navigation/fixtures/subtitle-epub-type/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "subtitle-epub-type",
+ "description": "subtitle role via epub:type=\"subtitle\"",
+ "role": "subtitle",
+ "rolesCovered": [
+ "subtitle"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "document"
+}
diff --git a/guided-navigation/fixtures/subtitle-role-aria/gnd.json b/guided-navigation/fixtures/subtitle-role-aria/gnd.json
new file mode 100644
index 00000000..87ca6f7f
--- /dev/null
+++ b/guided-navigation/fixtures/subtitle-role-aria/gnd.json
@@ -0,0 +1,8 @@
+{
+ "role": [
+ "subtitle"
+ ],
+ "text": {
+ "plain": "A History of Coastal Lights"
+ }
+}
diff --git a/guided-navigation/fixtures/subtitle-role-aria/input.html b/guided-navigation/fixtures/subtitle-role-aria/input.html
new file mode 100644
index 00000000..790129e9
--- /dev/null
+++ b/guided-navigation/fixtures/subtitle-role-aria/input.html
@@ -0,0 +1 @@
+A History of Coastal Lights
diff --git a/guided-navigation/fixtures/subtitle-role-aria/meta.json b/guided-navigation/fixtures/subtitle-role-aria/meta.json
new file mode 100644
index 00000000..3c38c3d4
--- /dev/null
+++ b/guided-navigation/fixtures/subtitle-role-aria/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "subtitle-role-aria",
+ "description": "subtitle role via explicit ARIA role=\"doc-subtitle\"",
+ "role": "subtitle",
+ "rolesCovered": [
+ "subtitle"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/summary-html-native/gnd.json b/guided-navigation/fixtures/summary-html-native/gnd.json
new file mode 100644
index 00000000..c14a3d6d
--- /dev/null
+++ b/guided-navigation/fixtures/summary-html-native/gnd.json
@@ -0,0 +1,8 @@
+{
+ "role": [
+ "summary"
+ ],
+ "text": {
+ "plain": "Click to expand"
+ }
+}
diff --git a/guided-navigation/fixtures/summary-html-native/input.html b/guided-navigation/fixtures/summary-html-native/input.html
new file mode 100644
index 00000000..aec1aec5
--- /dev/null
+++ b/guided-navigation/fixtures/summary-html-native/input.html
@@ -0,0 +1 @@
+Click to expand
diff --git a/guided-navigation/fixtures/summary-html-native/meta.json b/guided-navigation/fixtures/summary-html-native/meta.json
new file mode 100644
index 00000000..6724e26c
--- /dev/null
+++ b/guided-navigation/fixtures/summary-html-native/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "summary-html-native",
+ "description": "summary role via native element",
+ "role": "summary",
+ "rolesCovered": [
+ "summary"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-caption-element-html-native/gnd.json b/guided-navigation/fixtures/table-caption-element-html-native/gnd.json
new file mode 100644
index 00000000..d313bfc3
--- /dev/null
+++ b/guided-navigation/fixtures/table-caption-element-html-native/gnd.json
@@ -0,0 +1,42 @@
+{
+ "role": [
+ "table"
+ ],
+ "description": {
+ "text": {
+ "plain": "Team roster"
+ }
+ },
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Name"
+ },
+ "role": [
+ "columnheader"
+ ]
+ }
+ ]
+ },
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Ada"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/table-caption-element-html-native/input.html b/guided-navigation/fixtures/table-caption-element-html-native/input.html
new file mode 100644
index 00000000..2aebe3c2
--- /dev/null
+++ b/guided-navigation/fixtures/table-caption-element-html-native/input.html
@@ -0,0 +1,5 @@
+
+ Team roster
+ Name
+ Ada
+
diff --git a/guided-navigation/fixtures/table-caption-element-html-native/meta.json b/guided-navigation/fixtures/table-caption-element-html-native/meta.json
new file mode 100644
index 00000000..38f08eae
--- /dev/null
+++ b/guided-navigation/fixtures/table-caption-element-html-native/meta.json
@@ -0,0 +1,14 @@
+{
+ "id": "table-caption-element-html-native",
+ "description": "A native with no explicit aria-labelledby — the caption becomes a separate child node with role \"caption\", not folded into the table's own description field. Directly demonstrates the caption-folding gap noted in CONTEXTUALIZATION_PLAN.md: table.md's spec assumes \"can end up in description\", but that implicit association isn't implemented. Contrast with table-caption (which uses explicit aria-label and does populate description) and table-caption-role-aria (the same un-folded-caption behavior via role=\"caption\")",
+ "role": "table",
+ "rolesCovered": [
+ "table",
+ "caption",
+ "row",
+ "columnheader",
+ "cell"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-caption-role-aria/gnd.json b/guided-navigation/fixtures/table-caption-role-aria/gnd.json
new file mode 100644
index 00000000..f89c9f34
--- /dev/null
+++ b/guided-navigation/fixtures/table-caption-role-aria/gnd.json
@@ -0,0 +1,27 @@
+{
+ "role": [
+ "table"
+ ],
+ "description": {
+ "text": {
+ "plain": "Team roster"
+ }
+ },
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Name"
+ },
+ "role": [
+ "columnheader"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/table-caption-role-aria/input.html b/guided-navigation/fixtures/table-caption-role-aria/input.html
new file mode 100644
index 00000000..324f02f8
--- /dev/null
+++ b/guided-navigation/fixtures/table-caption-role-aria/input.html
@@ -0,0 +1,4 @@
+
diff --git a/guided-navigation/fixtures/table-caption-role-aria/meta.json b/guided-navigation/fixtures/table-caption-role-aria/meta.json
new file mode 100644
index 00000000..4ddbf638
--- /dev/null
+++ b/guided-navigation/fixtures/table-caption-role-aria/meta.json
@@ -0,0 +1,13 @@
+{
+ "id": "table-caption-role-aria",
+ "description": "An ARIA table with an explicit role=\"caption\" child (no aria-labelledby) — same un-folded behavior as table-caption-element-html-native (a separate role \"caption\" child, not folded into the table's own description), confirmed via the explicit-ARIA encoding rather than a native element",
+ "role": "table",
+ "rolesCovered": [
+ "table",
+ "caption",
+ "row",
+ "columnheader"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-caption/gnd.json b/guided-navigation/fixtures/table-caption/gnd.json
new file mode 100644
index 00000000..b060cb20
--- /dev/null
+++ b/guided-navigation/fixtures/table-caption/gnd.json
@@ -0,0 +1,81 @@
+{
+ "role": [
+ "table"
+ ],
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Name"
+ },
+ "role": [
+ "columnheader"
+ ]
+ },
+ {
+ "text": {
+ "plain": "Role"
+ },
+ "role": [
+ "columnheader"
+ ]
+ }
+ ]
+ },
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Ada"
+ },
+ "role": [
+ "rowheader"
+ ]
+ },
+ {
+ "text": {
+ "plain": "Engineer"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ },
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Grace"
+ },
+ "role": [
+ "rowheader"
+ ]
+ },
+ {
+ "text": {
+ "plain": "Admiral"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ }
+ ],
+ "description": {
+ "text": {
+ "plain": "Team roster"
+ }
+ }
+}
diff --git a/guided-navigation/fixtures/table-caption/input.html b/guided-navigation/fixtures/table-caption/input.html
new file mode 100644
index 00000000..46955dfe
--- /dev/null
+++ b/guided-navigation/fixtures/table-caption/input.html
@@ -0,0 +1,5 @@
+
+ Name Role
+ Ada Engineer
+ Grace Admiral
+
diff --git a/guided-navigation/fixtures/table-caption/meta.json b/guided-navigation/fixtures/table-caption/meta.json
new file mode 100644
index 00000000..d8bd8bdb
--- /dev/null
+++ b/guided-navigation/fixtures/table-caption/meta.json
@@ -0,0 +1,14 @@
+{
+ "id": "table-caption",
+ "description": "A table with an accessible name via aria-label — the {description} branch of table's own announcement template. Uses aria-label rather than a native element since native / implicit accessible-name folding is not yet implemented (see CONTEXTUALIZATION_PLAN.md)",
+ "role": "table",
+ "rolesCovered": [
+ "table",
+ "row",
+ "columnheader",
+ "rowheader",
+ "cell"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-colspan-html-native/gnd.json b/guided-navigation/fixtures/table-colspan-html-native/gnd.json
new file mode 100644
index 00000000..4feebc7c
--- /dev/null
+++ b/guided-navigation/fixtures/table-colspan-html-native/gnd.json
@@ -0,0 +1,45 @@
+{
+ "role": [
+ "table"
+ ],
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Name and Role"
+ },
+ "role": [
+ "columnheader"
+ ]
+ }
+ ]
+ },
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Ada"
+ },
+ "role": [
+ "cell"
+ ]
+ },
+ {
+ "text": {
+ "plain": "Engineer"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/table-colspan-html-native/input.html b/guided-navigation/fixtures/table-colspan-html-native/input.html
new file mode 100644
index 00000000..277b31ec
--- /dev/null
+++ b/guided-navigation/fixtures/table-colspan-html-native/input.html
@@ -0,0 +1,4 @@
+
+ Name and Role
+ Ada Engineer
+
diff --git a/guided-navigation/fixtures/table-colspan-html-native/meta.json b/guided-navigation/fixtures/table-colspan-html-native/meta.json
new file mode 100644
index 00000000..4b942554
--- /dev/null
+++ b/guided-navigation/fixtures/table-colspan-html-native/meta.json
@@ -0,0 +1,13 @@
+{
+ "id": "table-colspan-html-native",
+ "description": "A native columnheader with colspan=\"2\" spanning two data columns — colspan is not read or tracked at all; the second data row's two cells carry no header-association information reflecting that a single header covers both of them. Relevant to the planned cell/rowheader {header}: {value} template work, which needs real column-index tracking to get this right. See table-colspan-role-aria for the aria-colspan equivalent",
+ "role": "table",
+ "rolesCovered": [
+ "table",
+ "row",
+ "columnheader",
+ "cell"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-colspan-role-aria/gnd.json b/guided-navigation/fixtures/table-colspan-role-aria/gnd.json
new file mode 100644
index 00000000..4feebc7c
--- /dev/null
+++ b/guided-navigation/fixtures/table-colspan-role-aria/gnd.json
@@ -0,0 +1,45 @@
+{
+ "role": [
+ "table"
+ ],
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Name and Role"
+ },
+ "role": [
+ "columnheader"
+ ]
+ }
+ ]
+ },
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Ada"
+ },
+ "role": [
+ "cell"
+ ]
+ },
+ {
+ "text": {
+ "plain": "Engineer"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/table-colspan-role-aria/input.html b/guided-navigation/fixtures/table-colspan-role-aria/input.html
new file mode 100644
index 00000000..8a3d1a54
--- /dev/null
+++ b/guided-navigation/fixtures/table-colspan-role-aria/input.html
@@ -0,0 +1,4 @@
+
diff --git a/guided-navigation/fixtures/table-colspan-role-aria/meta.json b/guided-navigation/fixtures/table-colspan-role-aria/meta.json
new file mode 100644
index 00000000..0eda8a20
--- /dev/null
+++ b/guided-navigation/fixtures/table-colspan-role-aria/meta.json
@@ -0,0 +1,13 @@
+{
+ "id": "table-colspan-role-aria",
+ "description": "An ARIA columnheader with aria-colspan=\"2\" spanning two data columns — same as table-colspan-html-native's native colspan, aria-colspan is not read or tracked at all either, confirmed via the explicit-ARIA encoding",
+ "role": "table",
+ "rolesCovered": [
+ "table",
+ "row",
+ "columnheader",
+ "cell"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-epub-type/gnd.json b/guided-navigation/fixtures/table-epub-type/gnd.json
new file mode 100644
index 00000000..11ee718f
--- /dev/null
+++ b/guided-navigation/fixtures/table-epub-type/gnd.json
@@ -0,0 +1,37 @@
+{
+ "role": [
+ "body"
+ ],
+ "children": [
+ {
+ "role": [
+ "table"
+ ],
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Ada"
+ },
+ "role": [
+ "cell"
+ ]
+ },
+ {
+ "text": {
+ "plain": "Engineer"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/table-epub-type/input.xhtml b/guided-navigation/fixtures/table-epub-type/input.xhtml
new file mode 100644
index 00000000..bc5492a6
--- /dev/null
+++ b/guided-navigation/fixtures/table-epub-type/input.xhtml
@@ -0,0 +1,16 @@
+
+
+
+
+
+Fixture
+
+
+
+
+
diff --git a/guided-navigation/fixtures/table-epub-type/meta.json b/guided-navigation/fixtures/table-epub-type/meta.json
new file mode 100644
index 00000000..ff7f665a
--- /dev/null
+++ b/guided-navigation/fixtures/table-epub-type/meta.json
@@ -0,0 +1,12 @@
+{
+ "id": "table-epub-type",
+ "description": "table role via epub:type=\"table\"/\"table-row\"/\"table-cell\" on neutral elements — the only encoding of table previously untested, despite extensive native/ARIA table coverage. epub:type has no columnheader/rowheader equivalent at all (only table/table-row/table-cell are mapped in epubTypeRoles), so this is necessarily a header-less data grid, same shape as table-no-headers",
+ "role": "table",
+ "rolesCovered": [
+ "table",
+ "row",
+ "cell"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "document"
+}
diff --git a/guided-navigation/fixtures/table-html-native/gnd.json b/guided-navigation/fixtures/table-html-native/gnd.json
new file mode 100644
index 00000000..238061bf
--- /dev/null
+++ b/guided-navigation/fixtures/table-html-native/gnd.json
@@ -0,0 +1,76 @@
+{
+ "role": [
+ "table"
+ ],
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "role": [
+ "columnheader"
+ ],
+ "text": {
+ "plain": "Name"
+ }
+ },
+ {
+ "role": [
+ "columnheader"
+ ],
+ "text": {
+ "plain": "Role"
+ }
+ }
+ ]
+ },
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "role": [
+ "rowheader"
+ ],
+ "text": {
+ "plain": "Ada"
+ }
+ },
+ {
+ "role": [
+ "cell"
+ ],
+ "text": {
+ "plain": "Engineer"
+ }
+ }
+ ]
+ },
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "role": [
+ "rowheader"
+ ],
+ "text": {
+ "plain": "Grace"
+ }
+ },
+ {
+ "role": [
+ "cell"
+ ],
+ "text": {
+ "plain": "Admiral"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/table-html-native/input.html b/guided-navigation/fixtures/table-html-native/input.html
new file mode 100644
index 00000000..e16e3d34
--- /dev/null
+++ b/guided-navigation/fixtures/table-html-native/input.html
@@ -0,0 +1,5 @@
+
+ Name Role
+ Ada Engineer
+ Grace Admiral
+
diff --git a/guided-navigation/fixtures/table-html-native/meta.json b/guided-navigation/fixtures/table-html-native/meta.json
new file mode 100644
index 00000000..9d6a7cf8
--- /dev/null
+++ b/guided-navigation/fixtures/table-html-native/meta.json
@@ -0,0 +1,14 @@
+{
+ "id": "table-html-native",
+ "description": "A table with a header row and row headers, via native
/// ",
+ "role": "table",
+ "rolesCovered": [
+ "table",
+ "row",
+ "columnheader",
+ "rowheader",
+ "cell"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-in-role-none-wrapper/gnd.json b/guided-navigation/fixtures/table-in-role-none-wrapper/gnd.json
new file mode 100644
index 00000000..ad9aa32c
--- /dev/null
+++ b/guided-navigation/fixtures/table-in-role-none-wrapper/gnd.json
@@ -0,0 +1,44 @@
+{
+ "children": [
+ {
+ "children": [
+ {
+ "children": [
+ {
+ "role": [
+ "columnheader"
+ ],
+ "text": {
+ "plain": "Name"
+ }
+ }
+ ],
+ "role": [
+ "row"
+ ]
+ },
+ {
+ "children": [
+ {
+ "role": [
+ "cell"
+ ],
+ "text": {
+ "plain": "Ada"
+ }
+ }
+ ],
+ "role": [
+ "row"
+ ]
+ }
+ ],
+ "role": [
+ "table"
+ ]
+ }
+ ],
+ "role": [
+ "presentation"
+ ]
+}
diff --git a/guided-navigation/fixtures/table-in-role-none-wrapper/input.html b/guided-navigation/fixtures/table-in-role-none-wrapper/input.html
new file mode 100644
index 00000000..70eab1c5
--- /dev/null
+++ b/guided-navigation/fixtures/table-in-role-none-wrapper/input.html
@@ -0,0 +1,6 @@
+
diff --git a/guided-navigation/fixtures/table-in-role-none-wrapper/meta.json b/guided-navigation/fixtures/table-in-role-none-wrapper/meta.json
new file mode 100644
index 00000000..2c6ca312
--- /dev/null
+++ b/guided-navigation/fixtures/table-in-role-none-wrapper/meta.json
@@ -0,0 +1,14 @@
+{
+ "id": "table-in-role-none-wrapper",
+ "description": "A native inside a : a div has no required owned elements, so presentation doesn't reach the table, which keeps its table/row/columnheader/cell roles",
+ "role": "table",
+ "rolesCovered": [
+ "presentation",
+ "table",
+ "row",
+ "columnheader",
+ "cell"
+ ],
+ "sourceRef": "https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-nested-html-native/gnd.json b/guided-navigation/fixtures/table-nested-html-native/gnd.json
new file mode 100644
index 00000000..3bc548d4
--- /dev/null
+++ b/guided-navigation/fixtures/table-nested-html-native/gnd.json
@@ -0,0 +1,48 @@
+{
+ "role": [
+ "table"
+ ],
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "role": [
+ "cell"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Outer"
+ }
+ },
+ {
+ "role": [
+ "table"
+ ],
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Inner"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/table-nested-html-native/input.html b/guided-navigation/fixtures/table-nested-html-native/input.html
new file mode 100644
index 00000000..c484a720
--- /dev/null
+++ b/guided-navigation/fixtures/table-nested-html-native/input.html
@@ -0,0 +1,3 @@
+
diff --git a/guided-navigation/fixtures/table-nested-html-native/meta.json b/guided-navigation/fixtures/table-nested-html-native/meta.json
new file mode 100644
index 00000000..7290cbff
--- /dev/null
+++ b/guided-navigation/fixtures/table-nested-html-native/meta.json
@@ -0,0 +1,12 @@
+{
+ "id": "table-nested-html-native",
+ "description": "A native
nested inside another native table's cell — confirms recursion handles it correctly (the inner table's own row/cell structure resolves independently, nested under the outer cell alongside its own text). See table-nested-role-aria for the ARIA-role equivalent",
+ "role": "table",
+ "rolesCovered": [
+ "table",
+ "row",
+ "cell"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-nested-in-presentation-table/gnd.json b/guided-navigation/fixtures/table-nested-in-presentation-table/gnd.json
new file mode 100644
index 00000000..a38ef21f
--- /dev/null
+++ b/guided-navigation/fixtures/table-nested-in-presentation-table/gnd.json
@@ -0,0 +1,63 @@
+{
+ "children": [
+ {
+ "children": [
+ {
+ "children": [
+ {
+ "text": {
+ "plain": "Layout"
+ }
+ },
+ {
+ "children": [
+ {
+ "children": [
+ {
+ "role": [
+ "columnheader"
+ ],
+ "text": {
+ "plain": "Name"
+ }
+ }
+ ],
+ "role": [
+ "row"
+ ]
+ },
+ {
+ "children": [
+ {
+ "role": [
+ "cell"
+ ],
+ "text": {
+ "plain": "Ada"
+ }
+ }
+ ],
+ "role": [
+ "row"
+ ]
+ }
+ ],
+ "role": [
+ "table"
+ ]
+ }
+ ],
+ "role": [
+ "presentation"
+ ]
+ }
+ ],
+ "role": [
+ "presentation"
+ ]
+ }
+ ],
+ "role": [
+ "presentation"
+ ]
+}
diff --git a/guided-navigation/fixtures/table-nested-in-presentation-table/input.html b/guided-navigation/fixtures/table-nested-in-presentation-table/input.html
new file mode 100644
index 00000000..15e91d93
--- /dev/null
+++ b/guided-navigation/fixtures/table-nested-in-presentation-table/input.html
@@ -0,0 +1,3 @@
+
diff --git a/guided-navigation/fixtures/table-nested-in-presentation-table/meta.json b/guided-navigation/fixtures/table-nested-in-presentation-table/meta.json
new file mode 100644
index 00000000..8d9f94a2
--- /dev/null
+++ b/guided-navigation/fixtures/table-nested-in-presentation-table/meta.json
@@ -0,0 +1,14 @@
+{
+ "id": "table-nested-in-presentation-table",
+ "description": "A data table nested in a layout : only the outer table's own rows/cells inherit presentation; the inner table keeps its table/row/columnheader/cell roles",
+ "role": "presentation",
+ "rolesCovered": [
+ "presentation",
+ "table",
+ "row",
+ "columnheader",
+ "cell"
+ ],
+ "sourceRef": "https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-nested-role-aria/gnd.json b/guided-navigation/fixtures/table-nested-role-aria/gnd.json
new file mode 100644
index 00000000..3bc548d4
--- /dev/null
+++ b/guided-navigation/fixtures/table-nested-role-aria/gnd.json
@@ -0,0 +1,48 @@
+{
+ "role": [
+ "table"
+ ],
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "role": [
+ "cell"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Outer"
+ }
+ },
+ {
+ "role": [
+ "table"
+ ],
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Inner"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/table-nested-role-aria/input.html b/guided-navigation/fixtures/table-nested-role-aria/input.html
new file mode 100644
index 00000000..d00887e2
--- /dev/null
+++ b/guided-navigation/fixtures/table-nested-role-aria/input.html
@@ -0,0 +1,9 @@
+
diff --git a/guided-navigation/fixtures/table-nested-role-aria/meta.json b/guided-navigation/fixtures/table-nested-role-aria/meta.json
new file mode 100644
index 00000000..4b6250e1
--- /dev/null
+++ b/guided-navigation/fixtures/table-nested-role-aria/meta.json
@@ -0,0 +1,12 @@
+{
+ "id": "table-nested-role-aria",
+ "description": "An ARIA table (role=\"table\"/role=\"row\"/role=\"cell\") nested inside another ARIA table's cell — same correct recursion as table-nested-html-native, confirmed via the explicit-ARIA encoding",
+ "role": "table",
+ "rolesCovered": [
+ "table",
+ "row",
+ "cell"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-no-headers/gnd.json b/guided-navigation/fixtures/table-no-headers/gnd.json
new file mode 100644
index 00000000..ba8284b3
--- /dev/null
+++ b/guided-navigation/fixtures/table-no-headers/gnd.json
@@ -0,0 +1,53 @@
+{
+ "role": [
+ "table"
+ ],
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Ada"
+ },
+ "role": [
+ "cell"
+ ]
+ },
+ {
+ "text": {
+ "plain": "Engineer"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ },
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Grace"
+ },
+ "role": [
+ "cell"
+ ]
+ },
+ {
+ "text": {
+ "plain": "Admiral"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/table-no-headers/input.html b/guided-navigation/fixtures/table-no-headers/input.html
new file mode 100644
index 00000000..4ac11390
--- /dev/null
+++ b/guided-navigation/fixtures/table-no-headers/input.html
@@ -0,0 +1,10 @@
+
+
+ Ada
+ Engineer
+
+
+ Grace
+ Admiral
+
+
diff --git a/guided-navigation/fixtures/table-no-headers/meta.json b/guided-navigation/fixtures/table-no-headers/meta.json
new file mode 100644
index 00000000..fd9a6785
--- /dev/null
+++ b/guided-navigation/fixtures/table-no-headers/meta.json
@@ -0,0 +1,12 @@
+{
+ "id": "table-no-headers",
+ "description": "A bare data table with no columnheader/rowheader row at all — the header-less \"{value}\" branch of cell's announcement template",
+ "role": "table",
+ "rolesCovered": [
+ "table",
+ "row",
+ "cell"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-role-aria/gnd.json b/guided-navigation/fixtures/table-role-aria/gnd.json
new file mode 100644
index 00000000..238061bf
--- /dev/null
+++ b/guided-navigation/fixtures/table-role-aria/gnd.json
@@ -0,0 +1,76 @@
+{
+ "role": [
+ "table"
+ ],
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "role": [
+ "columnheader"
+ ],
+ "text": {
+ "plain": "Name"
+ }
+ },
+ {
+ "role": [
+ "columnheader"
+ ],
+ "text": {
+ "plain": "Role"
+ }
+ }
+ ]
+ },
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "role": [
+ "rowheader"
+ ],
+ "text": {
+ "plain": "Ada"
+ }
+ },
+ {
+ "role": [
+ "cell"
+ ],
+ "text": {
+ "plain": "Engineer"
+ }
+ }
+ ]
+ },
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "role": [
+ "rowheader"
+ ],
+ "text": {
+ "plain": "Grace"
+ }
+ },
+ {
+ "role": [
+ "cell"
+ ],
+ "text": {
+ "plain": "Admiral"
+ }
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/table-role-aria/input.html b/guided-navigation/fixtures/table-role-aria/input.html
new file mode 100644
index 00000000..c9e3ed26
--- /dev/null
+++ b/guided-navigation/fixtures/table-role-aria/input.html
@@ -0,0 +1,5 @@
+
diff --git a/guided-navigation/fixtures/table-role-aria/meta.json b/guided-navigation/fixtures/table-role-aria/meta.json
new file mode 100644
index 00000000..8c1aac79
--- /dev/null
+++ b/guided-navigation/fixtures/table-role-aria/meta.json
@@ -0,0 +1,14 @@
+{
+ "id": "table-role-aria",
+ "description": "A table with a header row and row headers, via explicit ARIA table/row/columnheader/rowheader/cell roles",
+ "role": "table",
+ "rolesCovered": [
+ "table",
+ "row",
+ "columnheader",
+ "rowheader",
+ "cell"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-role-presentation-explicit-cell/gnd.json b/guided-navigation/fixtures/table-role-presentation-explicit-cell/gnd.json
new file mode 100644
index 00000000..1f929f08
--- /dev/null
+++ b/guided-navigation/fixtures/table-role-presentation-explicit-cell/gnd.json
@@ -0,0 +1,30 @@
+{
+ "children": [
+ {
+ "children": [
+ {
+ "role": [
+ "cell"
+ ],
+ "text": {
+ "plain": "Ada"
+ }
+ },
+ {
+ "role": [
+ "presentation"
+ ],
+ "text": {
+ "plain": "Lovelace"
+ }
+ }
+ ],
+ "role": [
+ "presentation"
+ ]
+ }
+ ],
+ "role": [
+ "presentation"
+ ]
+}
diff --git a/guided-navigation/fixtures/table-role-presentation-explicit-cell/input.html b/guided-navigation/fixtures/table-role-presentation-explicit-cell/input.html
new file mode 100644
index 00000000..eca2468d
--- /dev/null
+++ b/guided-navigation/fixtures/table-role-presentation-explicit-cell/input.html
@@ -0,0 +1,3 @@
+
diff --git a/guided-navigation/fixtures/table-role-presentation-explicit-cell/meta.json b/guided-navigation/fixtures/table-role-presentation-explicit-cell/meta.json
new file mode 100644
index 00000000..8d75a45b
--- /dev/null
+++ b/guided-navigation/fixtures/table-role-presentation-explicit-cell/meta.json
@@ -0,0 +1,11 @@
+{
+ "id": "table-role-presentation-explicit-cell",
+ "description": "A in a keeps its explicit role, while its sibling with no role inherits presentation",
+ "role": "presentation",
+ "rolesCovered": [
+ "presentation",
+ "cell"
+ ],
+ "sourceRef": "https://www.w3.org/TR/wai-aria-1.2/#conflict_resolution_presentation_none",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-role-presentation-html-native/gnd.json b/guided-navigation/fixtures/table-role-presentation-html-native/gnd.json
new file mode 100644
index 00000000..14b39ca7
--- /dev/null
+++ b/guided-navigation/fixtures/table-role-presentation-html-native/gnd.json
@@ -0,0 +1,37 @@
+{
+ "role": [
+ "presentation"
+ ],
+ "children": [
+ {
+ "role": [
+ "presentation"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Name"
+ },
+ "role": [
+ "presentation"
+ ]
+ }
+ ]
+ },
+ {
+ "role": [
+ "presentation"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Ada"
+ },
+ "role": [
+ "presentation"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/table-role-presentation-html-native/input.html b/guided-navigation/fixtures/table-role-presentation-html-native/input.html
new file mode 100644
index 00000000..ef1b4a36
--- /dev/null
+++ b/guided-navigation/fixtures/table-role-presentation-html-native/input.html
@@ -0,0 +1,4 @@
+
diff --git a/guided-navigation/fixtures/table-role-presentation-html-native/meta.json b/guided-navigation/fixtures/table-role-presentation-html-native/meta.json
new file mode 100644
index 00000000..be85dd78
--- /dev/null
+++ b/guided-navigation/fixtures/table-role-presentation-html-native/meta.json
@@ -0,0 +1,13 @@
+{
+ "id": "table-role-presentation-html-native",
+ "description": "A native : its rows/cells have no explicit role, so they inherit presentation (ARIA presentational inheritance to required owned elements). See table-role-presentation-role-aria for explicit roles, which don't inherit",
+ "role": "presentation",
+ "rolesCovered": [
+ "presentation",
+ "row",
+ "columnheader",
+ "cell"
+ ],
+ "sourceRef": "https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/presentation_role",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-role-presentation-role-aria/gnd.json b/guided-navigation/fixtures/table-role-presentation-role-aria/gnd.json
new file mode 100644
index 00000000..d26e7395
--- /dev/null
+++ b/guided-navigation/fixtures/table-role-presentation-role-aria/gnd.json
@@ -0,0 +1,37 @@
+{
+ "role": [
+ "presentation"
+ ],
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Name"
+ },
+ "role": [
+ "columnheader"
+ ]
+ }
+ ]
+ },
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Ada"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/table-role-presentation-role-aria/input.html b/guided-navigation/fixtures/table-role-presentation-role-aria/input.html
new file mode 100644
index 00000000..28ac3e57
--- /dev/null
+++ b/guided-navigation/fixtures/table-role-presentation-role-aria/input.html
@@ -0,0 +1,4 @@
+
diff --git a/guided-navigation/fixtures/table-role-presentation-role-aria/meta.json b/guided-navigation/fixtures/table-role-presentation-role-aria/meta.json
new file mode 100644
index 00000000..99361c9c
--- /dev/null
+++ b/guided-navigation/fixtures/table-role-presentation-role-aria/meta.json
@@ -0,0 +1,13 @@
+{
+ "id": "table-role-presentation-role-aria",
+ "description": "role=\"presentation\" on a wrapper doesn't override descendants' explicit role=\"row\"/\"columnheader\"/\"cell\" (ARIA presentational inheritance skips explicit roles)",
+ "role": "presentation",
+ "rolesCovered": [
+ "presentation",
+ "row",
+ "columnheader",
+ "cell"
+ ],
+ "sourceRef": "https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/presentation_role",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-rowgroup-role-aria/gnd.json b/guided-navigation/fixtures/table-rowgroup-role-aria/gnd.json
new file mode 100644
index 00000000..5794f6fa
--- /dev/null
+++ b/guided-navigation/fixtures/table-rowgroup-role-aria/gnd.json
@@ -0,0 +1,22 @@
+{
+ "role": [
+ "table"
+ ],
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Name"
+ },
+ "role": [
+ "columnheader"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/table-rowgroup-role-aria/input.html b/guided-navigation/fixtures/table-rowgroup-role-aria/input.html
new file mode 100644
index 00000000..849f7fac
--- /dev/null
+++ b/guided-navigation/fixtures/table-rowgroup-role-aria/input.html
@@ -0,0 +1,5 @@
+
diff --git a/guided-navigation/fixtures/table-rowgroup-role-aria/meta.json b/guided-navigation/fixtures/table-rowgroup-role-aria/meta.json
new file mode 100644
index 00000000..ccdabce7
--- /dev/null
+++ b/guided-navigation/fixtures/table-rowgroup-role-aria/meta.json
@@ -0,0 +1,12 @@
+{
+ "id": "table-rowgroup-role-aria",
+ "description": "An ARIA table with an explicit role=\"rowgroup\" wrapper around its rows — rowgroup is not in GND's role vocabulary at all, so the wrapper is treated as an unknown role and spliced away transparently, same as native / / (see table-sectioning). Captured as a baseline confirming this is consistent across both encodings, not a native-only quirk",
+ "role": "table",
+ "rolesCovered": [
+ "table",
+ "row",
+ "columnheader"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-rowspan-html-native/gnd.json b/guided-navigation/fixtures/table-rowspan-html-native/gnd.json
new file mode 100644
index 00000000..d357ea6b
--- /dev/null
+++ b/guided-navigation/fixtures/table-rowspan-html-native/gnd.json
@@ -0,0 +1,45 @@
+{
+ "role": [
+ "table"
+ ],
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Ada"
+ },
+ "role": [
+ "rowheader"
+ ]
+ },
+ {
+ "text": {
+ "plain": "Engineer"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ },
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "2020"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/table-rowspan-html-native/input.html b/guided-navigation/fixtures/table-rowspan-html-native/input.html
new file mode 100644
index 00000000..c2899834
--- /dev/null
+++ b/guided-navigation/fixtures/table-rowspan-html-native/input.html
@@ -0,0 +1,4 @@
+
diff --git a/guided-navigation/fixtures/table-rowspan-html-native/meta.json b/guided-navigation/fixtures/table-rowspan-html-native/meta.json
new file mode 100644
index 00000000..b9b0807c
--- /dev/null
+++ b/guided-navigation/fixtures/table-rowspan-html-native/meta.json
@@ -0,0 +1,13 @@
+{
+ "id": "table-rowspan-html-native",
+ "description": "A native rowheader with rowspan=\"2\" spanning two data rows — rowspan is not read or tracked at all; the second row's cell carries no rowheader association even though the spanned header logically covers it too. Same relevance as table-colspan-html-native for the planned header-association work. See table-rowspan-role-aria for the aria-rowspan equivalent",
+ "role": "table",
+ "rolesCovered": [
+ "table",
+ "row",
+ "rowheader",
+ "cell"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-rowspan-role-aria/gnd.json b/guided-navigation/fixtures/table-rowspan-role-aria/gnd.json
new file mode 100644
index 00000000..d357ea6b
--- /dev/null
+++ b/guided-navigation/fixtures/table-rowspan-role-aria/gnd.json
@@ -0,0 +1,45 @@
+{
+ "role": [
+ "table"
+ ],
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Ada"
+ },
+ "role": [
+ "rowheader"
+ ]
+ },
+ {
+ "text": {
+ "plain": "Engineer"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ },
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "2020"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/table-rowspan-role-aria/input.html b/guided-navigation/fixtures/table-rowspan-role-aria/input.html
new file mode 100644
index 00000000..91ec107a
--- /dev/null
+++ b/guided-navigation/fixtures/table-rowspan-role-aria/input.html
@@ -0,0 +1,4 @@
+
diff --git a/guided-navigation/fixtures/table-rowspan-role-aria/meta.json b/guided-navigation/fixtures/table-rowspan-role-aria/meta.json
new file mode 100644
index 00000000..c038ca6c
--- /dev/null
+++ b/guided-navigation/fixtures/table-rowspan-role-aria/meta.json
@@ -0,0 +1,13 @@
+{
+ "id": "table-rowspan-role-aria",
+ "description": "An ARIA rowheader with aria-rowspan=\"2\" spanning two data rows — same as table-rowspan-html-native's native rowspan, aria-rowspan is not read or tracked at all either, confirmed via the explicit-ARIA encoding",
+ "role": "table",
+ "rolesCovered": [
+ "table",
+ "row",
+ "rowheader",
+ "cell"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-sectioning/gnd.json b/guided-navigation/fixtures/table-sectioning/gnd.json
new file mode 100644
index 00000000..4605f1e6
--- /dev/null
+++ b/guided-navigation/fixtures/table-sectioning/gnd.json
@@ -0,0 +1,52 @@
+{
+ "role": [
+ "table"
+ ],
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Name"
+ },
+ "role": [
+ "columnheader"
+ ]
+ }
+ ]
+ },
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Ada"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ },
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Total: 1"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/table-sectioning/input.html b/guided-navigation/fixtures/table-sectioning/input.html
new file mode 100644
index 00000000..0100fa75
--- /dev/null
+++ b/guided-navigation/fixtures/table-sectioning/input.html
@@ -0,0 +1,11 @@
+
+
+ Name
+
+
+ Ada
+
+
+ Total: 1
+
+
diff --git a/guided-navigation/fixtures/table-sectioning/meta.json b/guided-navigation/fixtures/table-sectioning/meta.json
new file mode 100644
index 00000000..cd135104
--- /dev/null
+++ b/guided-navigation/fixtures/table-sectioning/meta.json
@@ -0,0 +1,13 @@
+{
+ "id": "table-sectioning",
+ "description": "/ / are transparent — their rows flatten directly into the table's children with no rowgroup role or boundary marker of any kind. GND's role vocabulary has no rowgroup role at all (not in roles.md), so this is presumed intentional (grouping is visual-only, not meaningful for read-aloud), but was previously unconfirmed by any fixture — a tfoot row like a totals row is indistinguishable from a tbody row",
+ "role": "table",
+ "rolesCovered": [
+ "table",
+ "row",
+ "columnheader",
+ "cell"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/table-th-no-scope/gnd.json b/guided-navigation/fixtures/table-th-no-scope/gnd.json
new file mode 100644
index 00000000..d0b26b45
--- /dev/null
+++ b/guided-navigation/fixtures/table-th-no-scope/gnd.json
@@ -0,0 +1,53 @@
+{
+ "role": [
+ "table"
+ ],
+ "children": [
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Name"
+ },
+ "role": [
+ "columnheader"
+ ]
+ },
+ {
+ "text": {
+ "plain": "Role"
+ },
+ "role": [
+ "columnheader"
+ ]
+ }
+ ]
+ },
+ {
+ "role": [
+ "row"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Ada"
+ },
+ "role": [
+ "cell"
+ ]
+ },
+ {
+ "text": {
+ "plain": "Engineer"
+ },
+ "role": [
+ "cell"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/table-th-no-scope/input.html b/guided-navigation/fixtures/table-th-no-scope/input.html
new file mode 100644
index 00000000..ee9f69ae
--- /dev/null
+++ b/guided-navigation/fixtures/table-th-no-scope/input.html
@@ -0,0 +1,4 @@
+
+ Name Role
+ Ada Engineer
+
diff --git a/guided-navigation/fixtures/table-th-no-scope/meta.json b/guided-navigation/fixtures/table-th-no-scope/meta.json
new file mode 100644
index 00000000..633fe689
--- /dev/null
+++ b/guided-navigation/fixtures/table-th-no-scope/meta.json
@@ -0,0 +1,12 @@
+{
+ "id": "table-th-no-scope",
+ "description": "A with no scope attribute at all — falls back to a plain cell rather than columnheader/rowheader, since the converter only infers header direction from an explicit scope=\"col\"/\"row\" and does not implement HTML-AAM's implicit header-association algorithm. This is extremely common real-world markup (most authors omit scope), captured here as a baseline, not an endorsement of the current fallback",
+ "role": "table",
+ "rolesCovered": [
+ "table",
+ "row",
+ "cell"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/tree/main/examples/read-aloud",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/term-epub-type/gnd.json b/guided-navigation/fixtures/term-epub-type/gnd.json
new file mode 100644
index 00000000..cfee6436
--- /dev/null
+++ b/guided-navigation/fixtures/term-epub-type/gnd.json
@@ -0,0 +1,22 @@
+{
+ "role": [
+ "body"
+ ],
+ "children": [
+ {
+ "role": [
+ "glossary"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "lighthouse"
+ },
+ "role": [
+ "term"
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/term-epub-type/input.xhtml b/guided-navigation/fixtures/term-epub-type/input.xhtml
new file mode 100644
index 00000000..90dea642
--- /dev/null
+++ b/guided-navigation/fixtures/term-epub-type/input.xhtml
@@ -0,0 +1,13 @@
+
+
+
+
+
+Fixture
+
+
+
+ lighthouse
+
+
+
diff --git a/guided-navigation/fixtures/term-epub-type/meta.json b/guided-navigation/fixtures/term-epub-type/meta.json
new file mode 100644
index 00000000..abf83633
--- /dev/null
+++ b/guided-navigation/fixtures/term-epub-type/meta.json
@@ -0,0 +1,11 @@
+{
+ "id": "term-epub-type",
+ "description": "term role via epub:type=\"glossterm\" on a , nested in a ",
+ "role": "term",
+ "rolesCovered": [
+ "term",
+ "glossary"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "document"
+}
diff --git a/guided-navigation/fixtures/term-html-native/gnd.json b/guided-navigation/fixtures/term-html-native/gnd.json
new file mode 100644
index 00000000..60f87023
--- /dev/null
+++ b/guided-navigation/fixtures/term-html-native/gnd.json
@@ -0,0 +1,8 @@
+{
+ "role": [
+ "term"
+ ],
+ "text": {
+ "plain": "lighthouse"
+ }
+}
diff --git a/guided-navigation/fixtures/term-html-native/input.html b/guided-navigation/fixtures/term-html-native/input.html
new file mode 100644
index 00000000..bdc08f64
--- /dev/null
+++ b/guided-navigation/fixtures/term-html-native/input.html
@@ -0,0 +1 @@
+lighthouse
diff --git a/guided-navigation/fixtures/term-html-native/meta.json b/guided-navigation/fixtures/term-html-native/meta.json
new file mode 100644
index 00000000..754ca1f5
--- /dev/null
+++ b/guided-navigation/fixtures/term-html-native/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "term-html-native",
+ "description": "term role via native element",
+ "role": "term",
+ "rolesCovered": [
+ "term"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/term-role-aria/gnd.json b/guided-navigation/fixtures/term-role-aria/gnd.json
new file mode 100644
index 00000000..60f87023
--- /dev/null
+++ b/guided-navigation/fixtures/term-role-aria/gnd.json
@@ -0,0 +1,8 @@
+{
+ "role": [
+ "term"
+ ],
+ "text": {
+ "plain": "lighthouse"
+ }
+}
diff --git a/guided-navigation/fixtures/term-role-aria/input.html b/guided-navigation/fixtures/term-role-aria/input.html
new file mode 100644
index 00000000..6ad2bad0
--- /dev/null
+++ b/guided-navigation/fixtures/term-role-aria/input.html
@@ -0,0 +1 @@
+lighthouse
diff --git a/guided-navigation/fixtures/term-role-aria/meta.json b/guided-navigation/fixtures/term-role-aria/meta.json
new file mode 100644
index 00000000..95f949f9
--- /dev/null
+++ b/guided-navigation/fixtures/term-role-aria/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "term-role-aria",
+ "description": "term role via explicit ARIA role=\"term\"",
+ "role": "term",
+ "rolesCovered": [
+ "term"
+ ],
+ "sourceRef": "https://github.com/readium/guided-navigation/blob/main/roles.md",
+ "inputKind": "fragment"
+}
diff --git a/guided-navigation/fixtures/tip-epub-type/gnd.json b/guided-navigation/fixtures/tip-epub-type/gnd.json
new file mode 100644
index 00000000..0aab5218
--- /dev/null
+++ b/guided-navigation/fixtures/tip-epub-type/gnd.json
@@ -0,0 +1,16 @@
+{
+ "role": [
+ "body"
+ ],
+ "children": [
+ {
+ "text": {
+ "plain": "Tip: wind the mechanism fully before dusk."
+ },
+ "role": [
+ "aside",
+ "tip"
+ ]
+ }
+ ]
+}
diff --git a/guided-navigation/fixtures/tip-epub-type/input.xhtml b/guided-navigation/fixtures/tip-epub-type/input.xhtml
new file mode 100644
index 00000000..f1be3686
--- /dev/null
+++ b/guided-navigation/fixtures/tip-epub-type/input.xhtml
@@ -0,0 +1,11 @@
+
+
+
+
+
+Fixture
+
+
+Tip: wind the mechanism fully before dusk.
+
+
diff --git a/guided-navigation/fixtures/tip-epub-type/meta.json b/guided-navigation/fixtures/tip-epub-type/meta.json
new file mode 100644
index 00000000..6586391b
--- /dev/null
+++ b/guided-navigation/fixtures/tip-epub-type/meta.json
@@ -0,0 +1,10 @@
+{
+ "id": "tip-epub-type",
+ "description": "tip role via epub:type=\"tip\" on an