` populated during field resolution, flushed once per render pass, should be sufficient. No observable behaviour change for correctly-authored configs.
+ fields:
+ Group: "Technical"
+
+ - title: "Improve default `dataTooltip` with richer information from adapted payloads"
+ labels: ["next"]
+ body: |
+ **Context**
+ When a track has no `kind`, no `dataTooltip`, and no `tooltipOverrides` entry, the resolver synthesizes a minimal `fields` spec from the common feature-shaped record (`type`, `description`, `start`/`begin`, `end`) — see `src/tooltips/resolve.ts`. This is a reasonable floor, but it throws away a lot of information that adapters actually produce into the payload: scores, cross-references, sequence context, consequence types (for variants), AlphaMissense pathogenicity bands, and so on. Authors who haven't yet written a custom `dataTooltip` see a visibly impoverished tooltip compared to the old hard-coded ones, even when the adapter has clearly populated richer fields.
+
+ Goal: make the default tooltip pull more from what's already on the payload (including fields emitted by `calculate` transform steps) so that an out-of-the-box config produces a useful tooltip without the author writing one.
+
+ **Task**
+ Expand the default tooltip synthesizer in `src/tooltips/resolve.ts` to surface more of the adapted payload automatically, and render it via the Markdoc path so formatting (links, emphasis) matches custom tooltips.
+
+ **Scope:**
+ * Extend the fallback field list beyond `type`/`description`/`start`/`end` to include, where present: `score` (formatted per-kind), `consequenceType`, `clinicalSignificances`, `xrefs` (as linkified names where a source→URL map exists), `evidences` (count + first source), and any fields added by `calculate` steps.
+ * Emit the fallback through the Markdoc renderer rather than a flat `/
` grid, so that (a) links become anchors with the same URL-scheme allowlist as author-written tooltips, and (b) the visual treatment matches when authors progressively customize.
+ * Preserve the existing precedence: any `tooltipContent` the adapter stashed on the item still wins; any author-written `dataTooltip` still wins over the default.
+ * Document the expanded fallback set in `specs/config-approach.md` under the "Sensible out-of-the-box fallback" bullet in the Accessibility section. List the fields surfaced, in what order, and with what formatting.
+ * Add a snapshot/DOM test covering: variant record → expected default tooltip, feature record with xrefs → expected default tooltip, minimal feature record → same minimal tooltip as today (no regression).
+
+ **Notes:**
+ Keep the fallback compact — this is a safety net, not a replacement for authored tooltips. If the payload has fifteen fields, surface at most ~six; the rest are the author's call to include with an explicit `dataTooltip`. Pick the subset based on which UniProt/AlphaFold/variant adapters actually populate the fields — don't infer from type heuristics alone.
+ fields:
+ Group: "Technical"
+
+ - title: "Make `label` a Markdoc string and drop `helpPage` / `labelUrl` from the schema"
+ labels: ["next"]
+ body: |
+ **Context**
+ Group and track `label` is currently plain text. Rich-label behaviour is bolted on with two sibling fields: `helpPage` (an EBI-internal hook — renders `…`, which is interpreted by a help-article controller on uniprot.org but is dead markup everywhere else) and `labelUrl` (an external-link template with `{accession}` interpolation, used by the AlphaFold and AlphaMissense tracks). External adopters — the target audience for the config-as-data redesign — get no value from `helpPage`, and `labelUrl` duplicates what a hyperlink inside a label would express more directly.
+
+ Collapsing both into a Markdoc-rendered `label` gives us one authoring surface ("write Markdown, including a link if you want one") and removes two EBI/accession-specific fields from the schema. The Markdoc renderer is already in the bundle for `dataTooltip`, so reuse is near-free.
+
+ **Task**
+ Make `label` a Markdoc source string across groups and tracks, render it through the existing Markdoc pipeline, and remove `helpPage` and `labelUrl` from the schema. Preserve uniprot.org's in-page help-popover behaviour via a registered Markdoc custom tag rather than a schema-level field.
+
+ **Scope:**
+ * Schema + types: drop `helpPage` from `ConfigDefaults`, `GroupConfig`, `TrackConfig`; drop `labelUrl` from `ConfigDefaults` and `TrackConfig`. Update `src/schema/schema.json`, `src/schema/types.ts`, and the precedence doc comments. Extend-merger entries in `src/schema/extends.ts` (`labelUrl`, `helpPage` under scalar-field merge) go away.
+ * Renderer: in `src/protvista-uniprot.ts`, inside the `.group-label` and `.track-label` render paths, replace the three-way ternary (`labelUrl` → anchor, `helpPage` → `data-article-id` span, else plain) with a single Markdoc render. `{accession}` interpolation happens *before* Markdoc parses (same substitution pass that `labelUrl` uses today) so authors can write `[AlphaFold](https://alphafold.ebi.ac.uk/entry/{accession})` without learning Markdoc variables.
+ * Custom Markdoc tag for EBI help-popover parity: register a `{% help slug="signal" %}Signal peptide{% /help %}` tag that renders `Signal peptide`. External adopters ignore the tag; uniprot.org's existing controller keeps working. Add the tag's attribute allowlist (`slug:` is a restricted-charset string — `^[a-zA-Z0-9_#-]+$` to match existing slug values like `proteomics#1-data-from-public-mass-spectrometry-based-proteomics-resources`). Covered by the existing URL-scheme/attribute allowlist testing pattern in `src/tooltips/__spec__/`.
+ * `src/default-config.yaml` migration: convert every `helpPage: ` to `label: "{% help slug=\"\" %}{% /help %}"` (~48 entries). Convert the two `labelUrl:` entries to inline Markdown links. Drop the now-orphaned top-level `defaults.helpPage` if present.
+ * Spec rewrite: `specs/config-approach.md` — label section (currently describes `label`/`labelUrl`/`helpPage` as three separate fields), the `ProtvistaViewerConfig` interface block (L~156 and L~187), the runtime behaviour notes, and the accessibility section (tooltip-semantics paragraph mentions label rendering). Update the extends merge-rules table.
+ * Tests: `src/schema/__spec__/schema.spec.ts` (drop `helpPage`/`labelUrl` acceptance cases, add a Markdoc-label happy-path case), `src/__spec__/render-target.spec.ts` + its snapshot (regenerate under the new DOM — help-page spans become `data-article-id` spans emitted by the Markdoc tag, plain labels become Markdoc-rendered equivalent HTML), `src/schema/__spec__/extends.spec.ts` (drop `helpPage`/`labelUrl` merge cases).
+ * Migration note in `CHANGELOG`/`ROADMAP` — this is a breaking schema change and a visible shift for uniprot.org embedders (no DOM regression if the `{% help %}` tag is registered; DOM regression if they forget to register it).
+
+ **Notes:**
+ The single biggest design question is whether uniprot.org keeps using the `{% help %}` tag at all, or migrates to a consumer-side click interceptor (match `a[href^="https://www.ebi.ac.uk/help/"]`, `preventDefault`, open the popover). The interceptor approach removes the EBI-specific concept from the viewer entirely and turns `helpPage: signal` into a plain Markdown link. The custom-tag approach preserves exact DOM parity. Decide with the uniprot.org team before starting the default-config.yaml migration — the migration output differs.
+
+ Bundle-load implication: Markdoc moves from "lazy, loaded on first tooltip click" to "eager, loaded on every viewer mount" because labels render at mount. Likely in-budget (Markdoc is ~30 kB gzipped and already in the eager bundle for tooltip-default rendering when a track has no authored `dataTooltip`), but worth confirming with a bundle-size trace before landing.
+
+ Do not try to support Markdoc block-level content (headings, lists, code blocks) inside labels — labels are a single-line UI element and the author intent for "paragraph of help content" is already served by `description` (the plain-text `title=` tooltip) and `dataTooltip` (the per-datapoint Markdoc popover). Consider restricting the label-level Markdoc surface to inline nodes only (emphasis, strong, code span, link, and registered custom tags); reject block-level constructs with a validator warning.
+ fields:
+ Group: "Technical"
+
+ - title: "Add `detailOnly: true` flag on tracks to replace group-level `component:` overrides"
+ labels: ["next"]
+ body: |
+ **Context**
+ Two groups in the default config — `VARIATION` and `RNA_EDITING` — carry an explicit `component:` override with a multi-line explanatory comment, because their child tracks resolve to different Nightingale components and the aggregate-component inference would otherwise fall back to `nightingale-track-canvas` and break visual parity with the legacy viewer. Example from `src/default-config.yaml`:
+
+ ```yaml
+ - id: VARIATION
+ label: Variants
+ # Explicit because the two child kinds (`variant-counts` +
+ # `variants`) resolve to different components — without this,
+ # the collapsed-group aggregate view would fall back to
+ # `nightingale-track-canvas`, breaking parity with the legacy
+ # viewer's linegraph aggregate.
+ component: nightingale-linegraph-track
+ ...
+ ```
+
+ This is the wrong layer to express the intent. The author isn't choosing a specific component for the aggregate — they're saying "the detail `variants` track shouldn't influence what the collapsed group looks like." Pushing that truth to the track that *is* detail-only makes the group-level override unnecessary and removes the comment.
+
+ **Task**
+ Introduce an optional `detailOnly: true` flag on `TrackConfig`. When set, the track is excluded from the group's aggregate-component inference. Use the flag to clean up `VARIATION` and `RNA_EDITING` in `src/default-config.yaml`.
+
+ **Scope:**
+ * Schema + types: add `detailOnly?: boolean` to `TrackConfig` in `src/schema/types.ts` and `src/schema/schema.json`. Not present on `GroupConfig` or `ConfigDefaults` — per-track only (see Notes).
+ * Renderer: in the aggregate-component inference path (today's implementation falls back to `nightingale-track-canvas` when child kinds resolve to mixed components), filter out tracks with `detailOnly: true` before computing the component set. If all remaining tracks resolve to the same component, use it; otherwise fall back as before.
+ * Validator: add a `semantic` issue if every track in a group is `detailOnly: true` — the aggregate has no tracks to infer from and no explicit `component:` to fall back to. Error text: `"Group : all tracks are marked detailOnly, so the collapsed aggregate has no component. Set 'component:' on the group or un-mark at least one track."`.
+ * `src/default-config.yaml` migration: on `VARIATION`, remove the `component: nightingale-linegraph-track` override and the explanatory comment block; add `detailOnly: true` to the `variation` track. Same pattern on `RNA_EDITING`: remove `component: nightingale-linegraph-track` + comment, add `detailOnly: true` to the detail track (`'RNA Editing'`).
+ * Tests: `src/schema/__spec__/validate.spec.ts` — add a case for the all-detailOnly-no-component error. `src/__spec__/render-target.spec.ts` + its snapshot — regression test that `VARIATION` aggregate still renders as `nightingale-linegraph-track` after the migration. A small unit test directly against the inference helper covering {all same kind, mixed kinds, mixed kinds with one `detailOnly`}.
+ * Spec: add `detailOnly` to the `TrackConfig` interface block in `specs/config-approach.md`, one paragraph in the "Aggregate track" section explaining what it does and when to use it, and an Edge Cases row for the all-detailOnly-no-component case.
+
+ **Notes:**
+ Five design questions to pin down while writing the spec paragraph — the name `detailOnly` covers several possible semantics and the branch owner should decide explicitly:
+
+ 1. **Scope of the flag's effect.** Is `detailOnly` purely about aggregate-component inference, or does it also exclude this track's *data* from whatever payload feeds the aggregate Nightingale element? Today `VARIATION`'s aggregate data comes from a separate pipeline (`variant-counts`'s adapter on the group-level data key), so for this specific case there is no shared data pool and the answer is "inference only." But across all groups — does the aggregate ever merge multiple child tracks' payloads? If yes, `detailOnly` should probably affect both.
+ 2. **Validator floor.** All-detailOnly-on-every-track with no `component:` is the hard-error case above. What about *single*-track groups where that one track is `detailOnly`? Same error, or a softer warning ("group will never render an aggregate")? Leaning toward the same error.
+ 3. **Interaction with `filterUI`.** The detail `variation` track in `VARIATION` has `filterUI: nightingale-filter`. The filter renders in the track's label area when the group is expanded. If the track is `detailOnly`, does the filter still render when the group is *collapsed*? Today: individual tracks don't render at all when collapsed, so the question is moot. But it's worth stating in the spec that `detailOnly` does not alter *individual-track* visibility rules — it only changes aggregate inference.
+ 4. **`defaults.detailOnly` — not supported.** Setting `detailOnly: true` as a global default would mark every track `detailOnly` and break every group. Adding it at group level (`group.detailOnly`) is equally nonsense. Leaning toward: track-level only; schema rejects the field elsewhere. Document explicitly.
+ 5. **Naming symmetry for a future `summaryOnly`.** The inverse pattern — a track that *is* the aggregate and has no detail-view representation — isn't needed today but is foreseeable. Reserve the name `summaryOnly: true` for that. No work now; just avoid picking up adjacent names like `aggregateOnly` that would clash.
+
+ The cleanup applies to at least two groups in the default config (`VARIATION`, `RNA_EDITING`). Worth a quick audit of the other 13 groups before landing — if any others carry explicit group-level `component:` for the same reason, migrate them in the same commit. `STRUCTURE` and `PROTEOMICS` are plausible candidates; check their default-config entries.
+ fields:
+ Group: "Technical"
+
+ - title: "Add a generic `linegraph` semantic kind for bring-your-own-data authors"
+ labels: ["next"]
+ body: |
+ **Context**
+ Every semantic kind registered today is domain-specific — `features`, `variants`, `variant-counts`, `features-interpro`, `confidence-score`, `pathogenicity-score`, `pathogenicity-heatmap`, `rna-editing`, `rna-editing-counts`, and so on. Each bundles two things: a canonical adapter that understands a specific upstream wire format (the UniProt `/features/` stream, AlphaFold prediction API, AlphaMissense annotations, …), and a canonical component (`nightingale-track-canvas`, `nightingale-variation`, `nightingale-linegraph-track`, …). That design is deliberate and good — UniProt authors write one domain word and get the right behaviour for free, with colour ramps, filters, and tooltips wired up as a bundle.
+
+ The blind spot is the bring-your-own-data author. Say an external lab has "a list of `{position, value}` pairs I want rendered as a linegraph." None of the existing kinds fit — `variant-counts` assumes a UniProt variation response shape and runs it through a variation-specific adapter that would reject or mangle generic data. The only recourse today is to drop out of the semantic-kind surface and write a raw `component: nightingale-linegraph-track` plus a custom `adapter:` on the track, which the spec explicitly discourages ("Authors only write domain language … never Nightingale component names or adapter names").
+
+ A generic `linegraph` kind fills the gap: domain-agnostic, expects a minimal `{position, value}` record shape, renders with `nightingale-linegraph-track`. Authors who want linegraph rendering of their own data get a single-word answer; UniProt's variant-counts track keeps using its domain-specific kind unchanged.
+
+ **Task**
+ Register a built-in semantic kind named `linegraph` that maps to `nightingale-linegraph-track` with a shape-validating pass-through adapter. Document its expected data shape in the spec's semantic-kind vocabulary table. Add it to the `KnownSemanticKindName` union. Do not touch `variant-counts` or any other existing domain kind — this issue is additive.
+
+ **Scope:**
+ * Built-in adapter: add `src/schema/adapters/linegraph.ts`. Pass-through for arrays of `{position: number, value: number}` records. Reject non-array input, rows missing either field, rows where `position` or `value` aren't numbers. Error text names the offending row index and field so the author can debug their input: `"[linegraph] row 3: expected 'position' and 'value' (both numbers); got { position: '47', value: 0.9 } — 'position' is a string, not a number."`.
+ * Registry wiring: register the kind in the default kind registry (alongside wherever `variant-counts`, `confidence-score`, etc. are registered today). The kind resolves to component `nightingale-linegraph-track` and adapter `linegraph`.
+ * Types: add `"linegraph"` to the `KnownSemanticKindName` union in `src/schema/types.ts`. Add `"linegraph"` to the `KnownAdapterName` union for consistency (even though it's not an inference target from a file extension).
+ * Spec: add one row to the semantic-kind vocabulary table in `specs/config-approach.md` — kind name, canonical component, data shape (`{position, value}[]`), a sentence of guidance ("Use this kind for generic linegraph rendering when your data isn't one of the UniProt-specific variant-count sources. Keep using `variant-counts` for UniProt variation-API input.").
+ * Tests: `src/schema/adapters/__spec__/linegraph.spec.ts` — happy path (array of well-formed records), malformed-input cases (non-array, missing field, wrong types), and an integration test that renders a track with `kind: linegraph` and confirms the DOM is a `nightingale-linegraph-track`.
+ * Example in `docs/` or the spec's Examples section: a YAML snippet showing a BYOD linegraph track end-to-end, so authors searching for "how do I render my own linegraph data" find a worked answer.
+
+ **Notes:**
+ Four design decisions to make while wiring this up:
+
+ 1. **Data shape: `{position, value}` vs `{x, y}`.** `{position, value}` matches the domain language the rest of the spec uses (`position`, `begin`, `end`, `score`) and reads naturally for sequence data. `{x, y}` is more general but foreign to this codebase. Leaning toward `{position, value}`; mention the alternative in the spec so future additions of `scatter` or `heatmap` generic kinds can converge on the same vocabulary.
+
+ 2. **Validation strictness.** Pass-through (accept anything that looks vaguely correct at runtime) is simpler but defers errors to Nightingale, which tends to produce opaque failures. Shape-validating (reject malformed input at adapter time with a named error) is more work but gives the author a real debugging surface. Leaning strict — same stance the generic-format adapters in issue #1 are asked to take.
+
+ 3. **Parallel generic kinds for other components.** `track-canvas` is the obvious next candidate — the existing `features` kind is close but tied to UniProt feature-record shape. A generic `track` kind that takes `{start, end, type?, description?}` would fill the same gap for rectangular feature rendering. Out of scope for this issue, but name it now so future work doesn't clash: reserve `track`, `colored-sequence`, `heatmap`, and `variation` (lowercase) as generic-kind names. Don't register them in this issue — register them as distinct issues once a concrete need appears, so each one gets its own adapter + shape decision.
+
+ 4. **Namespace collision with adapter names.** Kinds and adapters live in separate registries in the current spec, so `kind: linegraph` and `adapter: linegraph` can coexist. The generic linegraph adapter this issue ships is named `linegraph` because it's the pass-through used by the kind — be explicit in the spec that this is a *generic* adapter, distinct from (for example) UniProt's variant-counts adapter which also feeds `nightingale-linegraph-track` but with domain-specific transforms.
+
+ Worth coordinating with issue #1 (generic-format adapters `features-json`/`features-csv`/`features-tsv`/`bed`) on the error-text format so BYOD authors get a consistent error-message vocabulary across the entire generic-kind / generic-format surface.
+ fields:
+ Group: "Technical"
+
+ - title: "Replace uniform adapter mocks in `load-data-baseline.spec.ts` with shape-correct per-adapter mocks"
+ labels: ["next"]
+ body: |
+ **Context**
+ `src/__spec__/load-data-baseline.spec.ts` currently stubs every adapter with the same uniform factory (`makeSimpleAdapter`), which returns one row per entry in `FILTER_TYPES_USED` — 35 `{ _adapter, type }` rows regardless of which adapter is being mocked. The intent was to surface loader wiring (who fires, how many times, which slot fills) without characterising adapter internals. The effect is that the snapshot shows UniProt feature types (`MOTIF`, `CHAIN`, `DOMAIN`, …) appearing under groups whose real adapters would produce entirely different shapes — `ALPHAFOLD_CONFIDENCE` (coloured-sequence string), `ALPHAMISSENSE_PATHOGENICITY` (heatmap / string), `VARIATION` group aggregate (linegraph points), and so on.
+
+ This was flagged during PR review of the `config-approach` branch as visually misleading — a reviewer scanning the snapshot reasonably parses `MOTIF` under `ALPHAFOLD_CONFIDENCE` as a pipeline bug until they drill into the test file's docstring and realise it's a mock artefact. The docstring was strengthened on that PR with a `READING THE SNAPSHOT` note explaining the convention, but the underlying fix — giving each adapter a mock that produces the right *shape* for its downstream component — was deferred to this issue.
+
+ **Task**
+ Replace the single `makeSimpleAdapter` factory with per-adapter factories that return output of the shape the real adapter would produce. Keep the "one row per filter type" pattern for adapters whose real output is feature-shaped (`uniprot-features-json`, `uniprot-proteomics-json`, `uniprot-proteins-pdb-json`, `uniprot-rna-editing-json`, `uniprot-proteomics-ptm-json`) — those are already correctly shaped. Rewrite the others:
+
+ **Scope:**
+ * `alphafold-prediction-json` — returns a coloured-sequence string of AlphaFold confidence codes, e.g. `'H'.repeat(770)` or a deterministic-but-varied pattern (`'HHHVVLLLAAA...'`). Matches the real adapter's `confidenceCategory.join('')` output.
+ * `alphamissense-average-csv` — returns a coloured-sequence string of pathogenicity codes (`H`, `V`, `L`, `A`, `l`, `h`, `p`, `P`). Matches the real adapter's `parseCSV` output (a joined string of per-position pathogenicity codes).
+ * `alphamissense-full-csv` — returns a heatmap-shaped array (matrix of per-(position, mutation) pathogenicity scores). Confirm the exact shape the `nightingale-sequence-heatmap` component expects before picking a mock; likely `[{ xValue, yValue, score }]` or similar.
+ * `uniprot-variation-json` — returns `{ sequence: string, variants: Variant[] }` matching the real variation adapter's output. Use a minimal variant fixture (1-3 entries) covering both a clinically-significant variant and a polymorphism so the filter / sort behaviour is visible.
+ * `uniprot-variation-counts-json` — returns linegraph points `[{ position: number, value: number }]` (or whatever shape the real adapter emits — confirm from `src/adapters/variation-counts-adapter.ts`).
+ * `uniprot-rna-editing-counts-json` — same pattern as variation-counts, but for RNA editing.
+ * Keep `interpro-entries-json` as-is (already has `makeInterproAdapter`; tests the `locations[].representative` flattening branch).
+ * Regenerate the `load-data-baseline.spec.ts.snap` snapshot once the mocks are in place. Expect a ~4000-line diff; review visually to confirm every group's data has a shape consistent with its component (colored-sequence strings under colored-sequence groups, linegraph points under linegraph groups, feature arrays under track-canvas groups).
+ * Verify the other tests in the same file still pass: `invokes each adapter the expected number of times`, `fetches the expected unique URL set`, `computes hasData=true/false`, `honors the variation-adapter early-return on empty raw data`, `rewrites InterPro representative fragments`, `returns an empty data object when no accession is effectively provided`. Some may need minor adjustment to the mock-expectation assertions.
+ * Remove the `READING THE SNAPSHOT` paragraph from the spec's docstring once the mocks are shape-correct — the warning is no longer needed.
+
+ **Notes:**
+ The existing `FILTER_TYPES_USED` constant stays — feature-shaped adapters still return one row per type for filter-wiring visibility. Only the non-feature-shaped adapters get custom mocks.
+
+ Confirm each mock shape against its real adapter in `src/adapters/` before committing. `alphafold-confidence-adapter.ts` returns a string joined from `confidenceCategory`. `alphamissense-pathogenicity-adapter.ts` returns a string joined from per-position pathogenicity codes. `variation-adapter.ts` returns `{ sequence, variants }`. Don't invent shapes that look plausible but don't match the real pipeline — the point is to make the test a faithful characterisation, not a creative writing exercise.
+
+ Double-check the aggregation rule in `load-data.ts` (currently "`groupData[0]` for linegraph / colored-sequence groups, `.flat()` otherwise"). With shape-correct mocks, `data["ALPHAFOLD_CONFIDENCE"]` becomes the string the adapter produced (not an array of feature records), which matches what the real coloured-sequence component consumes.
+ fields:
+ Group: "Technical"
+
+ - title: "Add a devcontainer / Dockerised dev environment for Linux-container test runs"
+ labels: ["next"]
+ body: |
+ **Context**
+ Test runs are currently coupled to the contributor's host platform. Native bindings in `node_modules` (`@rolldown/binding-*`, `@rollup/rollup-*`, `@esbuild/*`, etc.) are resolved at `yarn install` time based on the installer's OS/arch. That works fine for a contributor running tests on their own machine — Mac contributors get Mac bindings, Linux contributors get Linux bindings — but breaks down for three real-world cases:
+
+ 1. **Agentic tooling running in sandboxed Linux containers** (the case that prompted this issue). A sandbox session with the repo mounted can't run `npx vitest` because the Mac-installed bindings don't load on Linux ARM64. Attempting an additive install of the missing Linux bindings mid-session is risky: the sandbox has a 45-second-per-command timeout, and a partial install leaves `node_modules` in a corrupt half-state with dozens of empty package directories, requiring the contributor to `rm -rf node_modules && yarn install` on their Mac to recover.
+ 2. **CI pipelines on Linux runners** that inherit a contributor's macOS-installed lockfile without re-installing from scratch.
+ 3. **Remote contributors / new joiners** who want a zero-setup "clone and go" environment that matches what everyone else runs.
+
+ A standard `.devcontainer/devcontainer.json` (VS Code) + `Dockerfile` solves all three. Contributors and agents running inside the container get a fresh Linux install of `node_modules` with the right bindings; contributors running tests on their host keep doing what they already do.
+
+ **Task**
+ Add a reproducible Linux dev-container definition that bootstraps Node, installs dependencies fresh for the container's platform, and runs the same `yarn test` / `yarn build` the CI runs.
+
+ **Scope:**
+ * `.devcontainer/devcontainer.json` — VS Code remote-container config. Pinned Node version matching the one in `package.json`'s `engines` field. Post-create command: `yarn install --frozen-lockfile` (installs platform-correct native bindings inside the container, separate from the host's `node_modules`). Mount the workspace at `/workspaces/protvista-uniprot` so file edits flow back to the host.
+ * `Dockerfile` (or reuse a Microsoft-published devcontainer base image like `mcr.microsoft.com/devcontainers/javascript-node:20`). Keep it minimal — Node, yarn, git, and whatever else the test run needs (jsdom dependencies are pure JS, so no extra system packages).
+ * README section: "Running tests in a devcontainer" with a 3-line reproduction (open VS Code, click "Reopen in Container", run `yarn test`). Mention that this is the supported path for agentic tooling and for contributors without a local Node install.
+ * `.gitignore`: `.devcontainer/data` (or similar) to keep any container-generated state out of the repo.
+ * CI (optional): consider adding a second CI job that runs inside the same container image so "it works in CI" and "it works in the devcontainer" mean the same thing. Out of scope for the minimum deliverable, but noted.
+
+ **Notes:**
+ Isolation of `node_modules`: use a named volume for the container's `node_modules` so it doesn't overwrite the host's install. Pattern:
+ ```jsonc
+ "mounts": [
+ "source=${localWorkspaceFolderBasename}-node-modules,target=/workspaces/protvista-uniprot/node_modules,type=volume"
+ ]
+ ```
+ This keeps the host's macOS-bindings `node_modules` untouched — contributors who usually run tests on their host don't notice the container unless they reopen in it.
+
+ The trigger for this issue is one specific pain point, but the payoff is broader: any future sandbox / CI / remote setup inherits a reproducible Linux toolchain. Worth doing even if the immediate agentic-tooling case isn't a priority.
+
+ Out of scope: migrating the host-side dev loop to run inside a container by default. This issue is strictly about providing the option, not mandating it.
+ fields:
+ Group: "Technical"
+
+ - title: "Namespace CSS class names with a `pv-` prefix to eliminate cross-component collisions"
+ labels: ["next"]
+ body: |
+ **Context**
+ `` renders in light DOM (required because of Mol* — `createRenderRoot()` in `protvista-uniprot.ts` returns `this`). Its stylesheet lives in `document.head` and uses generic class names (`.group`, `.group-label`, `.group__track`, `.track-label`, `.track-content`, `.feature`, `.proforma`, `.mod-link`, `.credits`, `.nav-track-label`, `.aggregate-track-content`). Child components that also render in light DOM — notably `` — ship their own `