Skip to content

Technical documentationInternal

For the team and anyone taking the product over. Not linked from the site and not indexed.

Technical documentation pages

Shared shell and components

Scientist workflow entry and export choices

The Bench header's Import data opens and focuses Your data inside the existing Quick start control, including from Library and other Bench views. The separate disclosure is Study recovery → Restore a Study. Existing app import review, units, diagnostics and retention controls remain app owned. Fresh task launch links acknowledge pending work before clearing the previous Study/Experiment/task context. Superseded or unmounted intents cannot navigate later or clear a newly chosen destination; failed saves provide feedback.

Study export choices retain their location and are ordered PDF report, Reopenable Study file, Complete Study ZIP. PDF help states that it cannot restore a Study. The Study file uses the shared guarded recovery operation; the ZIP and all other report paths retain their existing closure. The existing storage line links to Study recovery where a real Study exists. Memory-mode failures remain visible after routine information is dismissed.

Bench execution surfaces

Study Work includes a separately labelled Bench sessions list, alongside saved tasks and planned Experiments. Sessions are not scientific result snapshots. Each execution Object opens the intent-loaded session controller and panel; its frozen plan, all preparation/dispense rows and captured qualifications remain readable without the original Calc task. The list includes marked, skipped and unmarked counts and the retained source label/revision. Material lists and the Materials progress milestone exclude execution in their presentation only; actual owning membership remains intact. The session shows its current owning Study/Experiment and a separate source link when available. A changed source revision is disclosed without replacing or recalculating the frozen plan.

The phone panel uses 48 px action controls, stacked labelled quantities and explicit Done/Skip/Note/What differed actions. Next only navigates. Skip and removing a mark require reasons; amounts begin empty and require a chosen unit. Corrections append to the retained history. Finish requires a closing note for unmarked steps and changes neither individual marks nor Study/Experiment state. Draft guards preserve unfinished text across refused navigation and failed saves. An uncertain save exposes Check saved entry, using the same operation.

Reminders are researcher-selected foreground timers. The local controller records only explicitly confirmed monotonic segments. Visibility loss, page exit, clock discontinuity or a substantial callback gap invalidates its token without adding a saved entry. Reopening requires explicit takeover and review of remaining time. Reaching zero does not mark a step or silently write history. No notifications or background timing service is introduced.

The saved-session report uses retained formatters and the existing print kit. All rows, entered quantities, correction history and qualifications travel with the source/method/hash receipts; exact saved JSON is also downloadable. It includes the retained plate map and source/role key, current entered status and mark counts, retained closing notes, and a report-generation timestamp separate from the plan's capture time. A 384-well map uses four labelled quadrants; the full dispense table remains available. The screen map uses the same retained source/role codes and a visible text key; 384-well maps use four twelve-column quadrants at phone width. Session history and reminder targets resolve saved row/field identities to their retained human-readable labels, while exports retain the original identifiers. The five new lazy-module budgets have provisional CTO ceilings pending measured production sizes; existing ceilings are unchanged. Browser and visual acceptance are recorded separately in the Bench/Patent progress record.

Everything an app builds on lives in src/components/suite/: the app definition contract, the shell that frames every workspace, the primitives and class recipes, the theme, tokens and accents, the export writers and the accessibility, motion and keyboard helpers. This page describes each part as the code implements it, names the design-system section it implements and the tests that guard it, and ends with a walkthrough for adding a panel or view to an app.

Storage (the project store, Objects, Studies, the #s= fragment) is described in Storage and state; the network layer and the policy-navigation boundary in Security and network; the registry in Registry and content. This page links to them rather than repeating them.

Design-system references below are to docs/design/suite-design-system.md (written "DS §n"). That document is normative but carries dated overrides at its top (D-0028, D-0029, D-0035, D-0036, D-0038 to D-0040), and docs/design/research-workspace-principles.md supersedes its older composition rules. Where the code follows a later decision rather than the body of the design system, this page says so.

What lives in src/components/suite/

The folder holds 427 files (about 61,500 lines including tests, counted on 10 October 2026).

Folder or file What it holds Design system
app-definition.ts The AppDefinition type and the rail, inspector, command, storage, share and object-tab types. Types only. §11.3
host.tsx ToolHost, the handful of page services a tool tree uses (useHost()). §11.3
app-entries.tsx AppEntriesProvider and useAppEntry(slug): full registry entries for the part of the page that needs them. registry split memo
shell/ AppShell and everything it renders: top bar, Research sidebar, Tools & sections, Inspector, status bar, menu sheet, notices, About, jobs, commands, layout state, the no-flash theme script. §2, §6.1 to §6.6
recipes.ts Whole-literal Tailwind class recipes (buttons, inputs, chips, panels, type, stage controls, links). §6.0
form/ Field, Input, NumberField, Select, Combobox, SuggestionInput, Segmented, Slider, Switch, Checkbox and radio, Textarea, FileDrop, number and date formatting. §6.20, rule 18
overlay/ Dialog, Drawer and Sheet, Popover, Menu, Tooltip, Toast, CommandPalette, ShortcutsOverlay, the layer stack and Esc ladder, Floating UI wrapper. §2.4, §6.7 to §6.14, §6.24
layout/ Panel, SubjectHeader, DiscoveryHeader and DiscoveryStart, Tabs and ViewTabs, SplitPane, Stage, media-query hooks. §6.11, §6.22, §6.32, §8
feedback/ Banner, EmptyState, ErrorBox, Skeleton, Progress and the loading ladder, InlineStatus, Kbd, LimitsBanner and drawer, ToolBoundary, ToolLoading, ViewerGate, optional sound. §6.15 to §6.21, §6.31, §6.32, §7.5
a11y/ announce() and the one live region, focus helpers, skip links, the shortcut registry. §9
motion/ animate() on the Web Animations API, usePresence, useReducedMotion; effects.ts, the interaction effects and their switches. §7
theme/ The shared theme preference and store, ThemeScope, Settings model and panel, chart and stage colour readers. §3, §6.28, §6.29
tokens.css, tokens.ts, accents.css, brand.css, print.css Semantic tokens, their JavaScript mirror, generated per-app accents, the brand layer, print rules. §3.2, §4.1, §5, §6.26
chart-palette.ts, data/ Chart palette and geometry, DataTable, KpiTile, PercentileBar, ProvenanceChip and the provenance model, ExportMenu and the CSV, ZIP, JSON, SVG, PNG and print writers. §3.4, §6.12, §6.18, §6.26, §6.30
icons.tsx, mark.tsx, wordmark*.tsx, suite-mark.tsx, education-mark.tsx Shell glyphs, app marks and lockups. §4.2, §4.3, §4.5
objects/ Object store, refs, verbs (Pin, Send to, Add to study), object and study pages, Bench library, SuiteFrame. build-spec-objects
workspace/ Bench home panels, Save to Bench and Record result controls, Study panels, recorded-result views. D-0038 to D-0040
onboarding/, walk/, use-walk-action.ts WelcomeCard, CoachMarks, the guided walk (J3) bar and actions. §6.23, D-0022
navigation/ PolicyNavigationBoundary and usePolicyRouter. See Security and network.
store/, net/ Covered in Storage and state and Security and network. The store's UI components are listed below. §6.25, §6.27
engines/rdkit/ The shared RDKit worker and its client. See Engines.
literature/ The Literature desk components. See Literature.
marketing/ Server-rendered public-page blocks (*.marketing.tsx). §10
build-info.ts, format-date.ts Build hash and time; UK date formatting.

Bench and report loading boundaries

Bench imports Study limits from objects/study-limits.ts and read-only saved-view/app helpers from objects/study-display.ts; the existing model and Study modules preserve their public exports. Those views no longer load Study validation or bundle-building code for presentation helpers.

Quick start's existing Examples panel is workspace/bench-quick-examples.tsx and loads only when Examples is selected. Start and Import retain their existing rows. The Library fragment-search disclosure reads its title from fragment-search-entry.ts, with the form and result copy kept behind the existing search intent. The examples catalogue and walk copy arrive with that panel; loading, failure and retry use the existing optional-control feedback. content/apps/example-study-entry.ts holds the unchanged visible entry and load-feedback copy, while the scientific example story stays with the existing requested example loader. The full content object retains its original fields and values.

The PDF section-selection dialog imports its own unchanged copy from content/apps/study-export-dialog.ts. Report-only text lives in content/apps/study-export-report.ts; the original study-export-builder.ts content module retains the complete public content object. Report selections, defaults and scientific source qualifications are unchanged. Report generation also imports only its unchanged brief/status/paper-origin labels from workspace-story-report.ts, workspace-experiments-report.ts and study-workflow-report.ts; the original content objects compose those same single-source values.

The populated example's existing science module prepares its unchanged scientific story, source inputs and paper-evidence fields before any writes. The orchestrator retains the original scope checks, actual note creation, recorded-at time and seed-receipt sequence. Citation capture delegates to the original strict function through that loaded module only after the note has been persisted, so its reference and revision are the saved note's. Both existing example chunk budgets and the complete action closure are measured.

How a workspace is assembled

A request for /<segment>/app is served by src/app/(workspace)/[app]/app/page.tsx, which renders WorkspaceFrame (workspace-route.tsx in the same folder). The chain from there:

  1. WorkspaceFrame emits ThemeScript (the inline no-flash script), a ThemeScope on <html>, a <noscript> message and SuiteWorkspace.
  2. SuiteWorkspace (src/apps/definitions.ts) holds one next/dynamic loader per app, keyed by slug and mode, created once and kept, so the same component renders before and after the chunk arrives and the workspace is never remounted by a load. Each loader imports src/apps/<slug>/entry.tsx; the loader table and preloadWorkspace live in src/apps/preload.ts, which the shell's navigation can import without importing AppShell. preloadWorkspace(slug) only fetches that chunk into the bundler's cache: the product page's live preview awaits it, and the sidebar calls it when a person points at or focuses one app's row. Nothing preloads every app. While the chunk loads, ShellSkeleton (built from the light client index) stands in where no server HTML exists. The skeleton is the shell's own frame (the sidebar column at --rail from lg, the bar, the status band) with the workspace loader centred in the work area (feedback/workspace-loader.tsx; the CEO, 14 October 2026: an app should not arrive in parts): three bars in the app's accent rising in turn above "Loading <app>…", still under reduced motion, with ToolLoading's slow rung (after 15 s it says so and offers Reload). AppShell then keeps the same loader over its grid of main and the inspector (WorkspaceCurtain, pointer-events: none, its loader sticky in the window's work area, the whole curtain aria-hidden and its line no status region, announce={false}, so the app's one status line stays the only one) until useSettled reports the work area settled: fonts ready, then no [data-skeleton] (every Skeleton carries it), [data-tool-loading] or [aria-busy="true"] inside main for six frames running, or 4 s at most so it never holds the app back. It fades over 200 ms and does not return for the life of the page. Not in a preview.
  3. The entry module is a client component that calls render(definition, app): the app's AppDefinition and its full registry entry (content/apps/<slug>.entry.ts). Each entry sets displayName = "drybench-entry:<slug>", the marker scripts/size-check.mjs uses to find the chunk.
  4. SuiteWorkspace's render prop creates AppShell with { app, definition, mode, page, initial }.
  5. AppShell (shell/app-shell.tsx) reads the Workspace access scopeEpoch (useWorkspaceAccess). When the account scope changes in full mode it clears the address with history.replaceState, shows ToolLoading and remounts AppShellFrame under a new key, so the previous account's in-memory science is never shown.
  6. AppShellFrame builds the layout state, the host, the shell context, the command and export registries and the overlays, then renders definition.Main inside the grid with preview and initial props. If the route names a page (/<segment>/app/<page>), it renders AboutContent for about or definition.pages[page] instead, under a back link.

The shared workspace route therefore carries the shell and the light index but no app's definition, entry or copy; each app's code downloads only on its own workspace. See Architecture.

Full mode and preview mode

AppShell has two modes. The product page's live preview renders SuiteWorkspace with mode="preview".

Concern Full (workspace page) Preview (product page)
Landmarks <main id="main"> A role="region" labelled with the app name; main is a div with role="group"
Ids As given Suffixed with a useId() value (ids(name)), so the product page has no duplicate ids
Keys Shell keys registered globally (useShortcuts("shell", …)) Only Mod+k, ?, / and ], handled on the frame's onKeyDown; setShortcutRoot(root) limits app keys to the frame
Theme ThemeScope on <html> from the route ThemeScope targeting the shell root
Remembered layout localStorage keys (below) In memory only
Address #s= fragment through writeUrlState(…, { mode: "fragment" }) memoryUrl(); the page address is never written
Object verbs createObjectVerbs(slug) previewVerbs(slug): upsert and link do nothing
Saving, Export, Share, Settings, app switcher, walk Available Absent
Scroller The window The shell root (overflow-y-auto)
Navigation Research sidebar (ResearchNavigation) The app's own Rail

The suite's own pages use SuiteFrame

Bench (/o), object pages, Study pages, Literature and the education tools render inside SuiteFrame (objects/suite-frame.tsx) rather than AppShell. It draws the same ShellBar, Research sidebar (RailColumn plus ResearchNavigation), menu sheet and status bar (D-0028; UX audit SHELL-2), with a breadcrumb trail instead of an app lockup, the command palette with object, study and identifier results, toasts, the layers element and a suite host whose verbs carry "suite". An optional accent prop gives the page the accent of the app that owns the object type.

The app definition contract

What an app folder holds

Every launch app has a folder src/apps/<slug>/. The files below are the conventions found across the ten apps; not every app has every file.

File Role
definition.ts export const definition: AppDefinition. Kept light: it is in the app's entry chunk. Main should itself load the workspace with next/dynamic.
entry.tsx Client component: render(definition, app), with displayName = "drybench-entry:<slug>".
workspace.tsx The Main component: usually a ToolBoundary around a next/dynamic import of the real workspace with a skeleton fallback.
views/ The workspace's views.
store.ts A tiny external store (publish…, read…, use…) through which the workspace exposes its state and actions to the parts the shell renders outside the workspace tree (commands, shortcuts, share link).
page-ids.ts PAGE_IDS: the app's own static pages under /<segment>/app/<id>. Empty for all ten apps today; about is the shell's.
objects.ts Pure module: TAB_TYPES, objectInbound (how the app opens a type through Send to) and the object data shapes.
object-tabs.ts, object-tab.tsx objectTabs: one { load } per type the app shows a tab for on an object page; the tab components.
inspector.tsx Static inspector content, loaded with next/dynamic from the definition.
exports.ts, provenance.ts, report.ts Export items, provenance blocks and print-report models.
tasks.marketing.tsx, task-islands.tsx, islands/ Task pages and their client islands. See Registry and content.
palettes.ts Optional light and dark colour pairs; tokens.test.ts imports every such file and checks the dark set (below). Present for Pipeline, Molecule and Protein.

Shared maps over every app sit at the top of src/apps/: definitions.ts (the entry loaders and SuiteWorkspace), page-ids.ts (pageIdsFor), objects.ts (objectInbound, objectTabTypes), object-tabs.tsx (tab loaders for object pages), send-to-rules.ts, tasks.ts and explainers.ts. Each is written once so no app edits it, and each is a Record<LaunchAppSlug, …>, so a new slug fails type checking until it is added.

AppDefinition

Everything is optional except Main. The registry entry carries what pages and the CSP read; the definition carries what only code can (DS §11.3; conflict C09 keeps storage kinds, export capabilities and fragment keys in code).

Field Type What the shell does with it Apps that set it today
Main ComponentType<WorkspaceMainProps> Renders it in main with preview and initial All
rail RailSection[] The first-paint local sections, shown in Tools & sections (full mode) or the app rail (preview). useRail() or host.setRail() replaces them while mounted. All (Bioreactor and Calc pass [] and set sections from the workspace)
inspector { title, kicker?, Content } or "selection" Static content, or content published with inspector.open(node, opts) All but Bioreactor; Protein and Pipeline use "selection"; Cell uses the shared SourcesInspector
inspectorLayout { defaultState?, startHidden?, dockMinMain? } Hidden by default; explicit docked or docked-from-xl opt-in; startHidden suppresses the column until the workbench publishes its start/work state Calc and Sequence opt into methods for active inputs (D-0060); Pipeline and Cell keep their dockMinMain
LimitsNotice ComponentType<LimitsBannerProps> Optional contextual-summary renderer in the single notice position below the work Assay suppresses only its C50 summary outside dose-response, keeping the full drawer
commands Command[] Added to the palette after the shell's own All but Cell
shortcuts Shortcut[] Registered under owner app:<slug> while mounted; may not use a reserved key All
widgetKeys WidgetKey[] Listed in the ShortcutsOverlay, never registered Protein
sources () => SourceRef[] The status bar's freshness chip shows the oldest Protein, Molecule, Pipeline
statusChips () => ReactNode Extra status-bar chips Molecule (engine chip)
exports () => ExportItem[] Export menu items beside those registered with useExports Pipeline
search { label, focus() } / focuses it; otherwise / opens the palette in search mode Protein, Molecule, Pipeline, Calc, Sequence
share ShareSpec Copy link, the share dialog and the "Not in this link" fallback All but Cell
usesUrlState boolean Settings offers the address-bar preference All but Cell
aboutSections, settings ExtraSection[] Appended to About and licences, or to Settings aboutSections: Protein only
methods { group(): string | null } The id of the calculator, analysis, tool, view or step open now, read from the app's published store; the methods and checks dialog opens at the group that answers to it (D-0085) All ten apps
objectTabs Partial<Record<ObjectType, ObjectTab>> Object-page tabs (also registered in src/apps/object-tabs.tsx) All but Cell
storage, session, onSave StorageSpec, SessionSpec, function The shell's local project saving: autosave after shell.storage.changed(), ProjectChooser, SaveStatus, ConflictDialog, ⌘S None of the ten apps; only the dev kit's demonstration workspace
pages, TopBarActions, paperMain, palettes Static pages, extra top-bar actions, a light paper main in dark theme, palette pairs None today

The "apps that set it" column was read from the nine definition.ts files on 10 October 2026. The important consequence is that the shell's own project-saving path (contracts §4; DS §6.25) is not used by any shipped app: apps save through the Workspace controls (WorkCaptureControls, below) instead. The path is still exercised by the dev kit and by e2e/smoke/shell-a11y.spec.ts ("saving flows (dev kit demonstration workspace)").

src/apps/definitions.test.ts checks one rule of the contract: an app whose definition gives share.notInLink must also give fileKind and onShareFile.

The rail model

RailSection has an id, an optional title, ordered (render as <ol>), collapsed (what the 56 px rail shows: "icons", "rule", "hide" or "popover"), an optional icon, items and an empty line. Items are one of four kinds (app-definition.ts):

Kind Element Behaviour
section <button> Scrolls to the panel and focuses its heading through scrollToPanel (shell/scroll-to-panel.ts), never writing location.hash. Scroll-spied with aria-current="true"; aria-disabled with an explanation while the panel is not on the page.
view <button> aria-current="page" on the open view; runs onSelect.
link <a href> A real link (works in a new tab); onNavigate may call preventDefault() to handle a plain click in place. Optional numeral index and badge.
action <button> Runs onSelect; pressed gives aria-pressed.

The shell turns every non-action item into a "Go to" palette command (shellCommands in app-shell.tsx).

The host

useHost() (host.tsx) returns the ToolHost the shell provides, or standaloneHost outside a shell.

Member Suite host Standalone host
kind "suite" "site"
announce(text) announce() into the one live region No-op
links linksFor(slug) (src/lib/handoff/links.ts): cross-app links carrying via=<slug> linksFor() without via
assetBase "" ""
scrollContainer() null (window), or the shell root in preview null
id(name) Suffixed in preview Identity
url Fragment read and write, or memory in preview Query string
objects Object verbs carrying the app's slug Verbs carrying "suite"
setSubject, openInspector, setRail Present Absent
registerExport Full mode only Absent
walk Full mode only (walkHost) Absent

Calc and Sequence pass preserveExplicit: true when their blank/work defaults change: explicit choices on this visit win, while docked choices remain separate from floating-open state on resize. Other apps retain their existing transient-reset behaviour.

Apps should build every element id through host.id() so the same view works in a preview.

The shell context

useShell() throws outside a shell; useShellOptional() returns null (shell/shell-context.tsx). The value holds app, definition, mode, page, root, ids, rail (state, sections, toggle, setTransient("collapsed" | null), set), inspector (available, state, width, setWidth, open, close, toggle, setTransient("hidden" | null, { preserveExplicit?: boolean })), announce, density, the resolved theme, jobs, subject and setSubject, open (palette, shortcuts, settings, about, limits, coachMarks, menu), reportInbound, arrivedObject and storage (showConflict, changed). useRail(sections) shows a memoised section list while the calling component is mounted and restores the definition's on unmount.

The shell frame

Regions and geometry

The frame is the UI redesign of 13 October 2026 (D-0082, docs/board/decisions/), on the desktop scale of D-0016 and the Research sidebar of D-0028: the shell root is a row; the sidebar ([data-shell-region="rail"], RailColumn, a sticky top-0 column the full height of the window) stands at the left from lg, and the body column ([data-shell-body]) beside it stacks the top bar, the notices, the grid of main and the docked inspector, and the status bar.

Constant (SHELL in tokens.ts) Value (px at the 16 px root)
strip / top / status 0 / 56 / 26
rail / railCollapsed 216 / 76
inspector (xl) / inspectorLg 360 / 320
inspectorMin / inspectorMax 280 / 520

On desktop (min-width: 70.4rem) tokens.css sets html { font-size: 110% } (D-0016), so every rem length is a tenth larger; code that measures the shell converts with designPx() and rootScale() (shell/use-shell-layout.ts). The breakpoints are lg = 70.4rem, xl = 88rem, 2xl = 105.6rem (BREAKPOINTS); md and sm keep Tailwind's defaults. Media queries in rem resolve against the browser's initial 16 px, so lg starts at 1,126.4 CSS px and xl at 1,408 CSS px.

The shell root carries data-app, data-mode, data-rail, data-inspector, data-mounted and data-rail-motion. data-rail says what is drawn: expanded only for the labelled column at xl, collapsed for the tiles at lg and for the collapsed choice at xl (the rows read it to switch to tiles; 13 October 2026). The sidebar's width is var(--rail-collapsed) at lg and var(--rail) at xl, and .suite-shell[data-rail="collapsed"] sets --rail to the collapsed width; its width transition, and the rail's label, brand and padding transitions, run only under data-rail-motion, which useRailMotionReady (shell/use-shell-layout.ts) sets on the AppShell and SuiteFrame roots from the second frame after mount, so the remembered rail state that arrives with hydration is applied without sliding and only a person's toggle animates; the width transition fires suite:layout (LAYOUT_EVENT) at its end. The grid (data-shell-grid, and data-research-grid in full mode) holds main alone, plus var(--inspector) while docked (tokens.css); its column transition is disabled while resizing and under reduced motion and fires the same event. The window scrolls; the sidebar and the inspector column are sticky with their own overflow (DS §2.3).

In full mode the rail column renders ResearchNavigation (shell/research-navigation.tsx, DS §6.3, D-0028), the same component the suite pages and the phone menu use. Its order is:

  1. StudyContext: the tab's working Study selector (see Storage and state for the research context).
  2. The Bench group, coloured with --color-suite-accent regardless of the active app: Bench (/o, named by BENCH_NAME), then Literature (/literature). The Studies and Objects rows that sat indented under Bench (UI-03 of 11 October 2026) were removed at the CEO's word of 14 October 2026: the Bench's own count cards (BenchStats, each a link over the card to #studies, #pinned, #activity and #objects), its Recent studies and its Pinned objects reach every Study and every saved object. These are plain <a> elements, so a click loads a new document at the top of the page (Bench and Literature use a different policy from the apps); the policy boundary turns a cross-boundary click into a document load. suitePlace reads the place from the path: the Bench link is the current page (aria-current="page") on /o and every view of it, and current (aria-current="true") on a Study page (/study/…) or an object page (/o/<type>/<id>); Literature is the current page on /literature. Nothing in the group is current inside an app or an education tool.
  3. Literature (/literature), in its own hairline box, followed in the same list by the apps whose workspaceGroup is "literature" (Patent, D-0079; a tile beside Literature's, not indented, with no group heading).
  4. App groups from APP_INDEX by workspaceGroup: Discovery, Bench & process, and Learn. Learn lists EDUCATION_CATEGORIES from content/education-tools.ts as <details> groups, opening the group of the current tool; Cell sits inside it.
  5. A footer line, "On this device / Work is kept in this browser" (content/apps/workspace-navigation.ts).

App links use the last safe address recorded for that app in this browser (useAppAddresses, shell/last-address.ts) and fall back to appHref(slug). They are ordinary next/link navigations (a new page starts at its top); pointing at or focusing one starts preloadWorkspace for that app alone. Clicking the current app on its own workspace does nothing, so its state is kept.

The compact rail is one Tab stop (UI-04): useRovingRail gives the current destination, else the first row, tabindex="0" and the rest -1; ArrowUp and ArrowDown move between the rows (wrapping), Home and End jump, and the item last focused keeps the stop, also through a change of address or hash while it holds focus (REL-36). The skip link before the rail stays first. From lg the column draws ResearchNavigation compact, the sidebar of the UI redesign of 13 October 2026 (D-0082): while the sidebar is expanded (xl, unless collapsed), each destination is a 32 px row, its mark in a 26 px badge and its short name beside it (the label is aria-hidden; the link's name is the app's full name), the current app's row washed in its glow with a 3 px bar at its left; while collapsed (data-rail="collapsed" on the shell root, and always at lg) the same rows draw as 36 px icon keys, the mark alone (16 October 2026, e60bed8): the label fades and is clipped to zero width, and the link keeps the app's full name (aria-label) and a tooltip. This replaces the 64 × 40 px labelled tiles of the 11 October fix round (UI-01) that D-0082 describes as "the 76 px tiles"; the column is still 76 px wide. D-0093 (17 October 2026) keeps the icon-only collapsed rail; the CPO recommended restoring the tiles, citing touch screens at lg, which show no tooltips. Groups are split by hairlines. Studies and Objects have no rows of their own (the CEO, 14 October 2026): they are Bench views, reached from the Bench, and the Bench row is aria-current="true" on them. The Bench group is sticky inside the scrolling list (useRailScroller, below), so Bench never leaves view and the sole roving tab stop always belongs to that list (REL-18); at 1366 × 768 the whole list, Cell included, fits between the brand and the footer without scrolling. The working Study is the header's chip rather than a rail entry. Every app keeps its own row except Cell, which is listed under Learn (D-0034, REL-200): the collapsed tiles show one Learn tile, which widens the column (below), and the expanded column lists Learn's subjects as <details> groups in place of the tile, so the expanded column has no Learn tile (the browser helper openLearnNavigation presses it only while the column is collapsed, REL-235). The Learn tile is the current destination on an education tool's page. The non-compact ResearchNavigation (the subjects and the Study selector) is the Menu sheet's list.

The column is a stage surface (data-surface="stage", class suite-rail): tokens.css draws it on the rail tokens (--color-rail, -raised, -edge, -fg, -fg-muted: the concept's #102832 family in light; the violet-graphite chrome in the workspace dark), and the accent inside it resolves to each app's glow (accents.css), so the rows, keys and focus rings read as on dark chrome in both themes. RailBrand (rail-column.tsx) sits above the list: the rail's toggle at its left in both states, then the serif initial of SUITE_NAME, the name and "Research workspace" (faded out and clipped while collapsed) (workspaceNavigation.label, which wraps to two lines in the 168 px column rather than truncating), decorative beside the Bench row.

RailColumn (shell/rail-column.tsx) gives the list the room between the brand and the footer; its useRailScroller hook fades whichever end of a scroll box has more beyond it (edgeMask from layout/tab-overflow.ts) and scrolls the current destination into view, on a new address and again when the column widens (a data-rail change on the shell root shows rows the tiles hid, such as the current Learn subject). The column scrolls the list itself for the Menu sheet and a preview's Rail; the compact rail passes scroller={false} and uses the hook on its own list containing the sticky Bench group. That group suppresses the top fade, and the compact rail draws its bottom fade as RailFade, a zero-height sticky anchor at the list's foot with a 24 px gradient into the rail's own surface above it (pointer-events: none, aria-hidden, not drawn in forced colours), rather than as a mask on the box (useRailScroller(list, { fade: "overlay" }), whose fadeEnd says when): a masked box is a stacking context to axe, which then read the rows scrolled under the opaque Bench group as text on the page's paper (REL-236; CPO memo of 17 October 2026). The Menu sheet's and a preview's lists keep the mask. focusin reveals an arrow-key destination below the sticky group with room for its focus ring and above the bottom fade. The scrolling container adds no second tab stop. RailFooter pins the sidebar's foot under it: the project chooser trigger for apps that save, Settings (onSettings), About and licences with the version under it (v0.2.0 · Preview form), and the device line ("On this device / Work is kept in this browser", the suite mark in a 20 px circle). Since 15 October 2026 (the CEO) the footer's rows are text-ui-sm and About and licences text-ui-xs over its version line (which wraps rather than truncates), the project name under the chooser text-ui-xs, and the device line text-ui-xs over a text-ui-2xs detail that wraps rather than truncates; while the sidebar is collapsed these are 32 px icon keys that wrap in the 76 px column on fine pointers (44 px keys in a column on coarse pointers), and the version and device line are not shown. The research sidebar's toggle is in the brand row in both states (RailBrand collapse), so its footer draws none (collapseInRow={false}); the app preview's footer keeps its own. From lg the toggle reads "Collapse navigation" on the expanded column and "Expand navigation" on the collapsed one, and writes the shared suite:suite:rail choice (collapsed or expanded); the Learn tile expands it. Esc and Collapse close the overlay and return focus to the column's toggle; a press outside or a choice also closes it.

Rail state (useRailLayout):

Width Full workspace Preview
below lg sheet: the Menu button opens AppMenuSheet sheet
lg to xl collapsed (the tiles) unless the person expanded it; the toggle, [ and the Learn tile widen it to the labelled list beside main as full, in memory
xl and up expanded (the labelled sidebar) unless the person collapsed it; the toggle and [ switch them expanded or collapsed, in memory

An app may collapse the rail for one view with setTransient("collapsed"), which never overwrites the stored choice.

From lg the sidebar is always a column beside the page, never an overlay (the CEO, 14 October 2026). useRailLayout keeps one choice for every width in suite:suite:rail (collapsed or expanded; without one, the list from xl and the tiles below; the first-paint script in theme-script.tsx applies the same rule). The column's width follows --rail (lg:w-(--rail)) and transitions over --motion-move (300 ms, ease-sine-in-out), so widening or narrowing pushes the page rather than covering it. The expanded width is --rail-open (10.5 rem, 168 px at the 16 px root; 13.5 rem until the CEO asked for it 20 to 30 % slimmer on 15 October 2026). The list (.suite-rail) fills the column, which clips it (overflow-x: clip, which keeps the column's sticky list sticky); since 16 October 2026 data-rail follows the state at once and the labels fade and narrow (max-width, opacity) while the column's width transition runs, rather than the open list staying drawn until the tiles replace it. collapse still sets closing for the transition's length (data-rail-closing on the root, which sets --rail to the collapsed width); reduced motion swaps at once. The former lg overlay and its rail-widen animation were removed.

The current destination is drawn by its row's tint alone (bg-accent/12, /15 in the compact rail) with the semibold label; the row has no accent bar on its curved edge (the CEO, 14 October 2026).

Tools & sections and Find on this page

In full mode the app's RailSections are not shown as a second rail. AppSectionMenu (shell/app-section-menu.tsx) renders the app's page tools, a nav labelled "<app>: page tools" at every width, with:

  • PageFind (shell/page-find.tsx, page-find-model.ts): literal text search over the rendered main region, at most PAGE_FIND_LIMIT (500) matches, highlighting with the CSS Custom Highlight API where available and an outline otherwise. It never changes app state or makes a request. Enter and Shift+Enter move between matches; the first Esc clears the query.
  • "Tools & sections": a Popover containing Rail variant="sheet", shown only when the app has sections.
  • The Inspector toggle, shown only when the app has an inspector, with aria-expanded and aria-controls naming the docked inspector or the drawer.

Below lg (and in the server render) they are a sticky row at the top of main, Find leading it (SHELL-15). The row measures its height into --app-subnav-height; app-section-menu.css adds it to scroll-padding-top and starts the inspector drawer beneath it. From lg, once mounted, they are portalled into the bar's [data-header-tools] bank as flat icon keys, which ShellTools places after Export and an app's extras and before the workspace search (D-0082). Find is the last key, so it sits in one place in every app whichever tools an app adds (SHELL-15); it opens its field in a Popover, which stays open while it has a query. Both forms carry app-subnav, so a focused stage (html[data-stage-focus]) hides them.

Inspector

shell/inspector.tsx (DS §6.4) renders DockedInspector (a sticky column with a SplitPane-style resize gutter: 280 to 520 px, arrows 16 px, Shift 64 px, Home and End, Enter or double click resets) and OverlayInspector (a non-modal right Drawer from md, a bottom sheet below). Content comes from a static inspector.Content or from inspector.open(node, { title, kicker, focus }); with nothing published it shows InspectorEmpty. A content change slides in 8 px over 200 ms unless motion is reduced; focus moves to the title only when focus: true (an explicit Inspect).

Docking follows one rule for every app (shell/inspector-dock.ts, DISC-8 of the 10 October 2026 audit): the inspector docks only from lg and only while viewport − railWidth − width ≥ dockMinMain, where dockMinMain defaults to DEFAULT_DOCK_MIN_MAIN = "40rem". The rail's width is an input to the check and is never changed by it. useInspectorLayout repeats the comparison inline (the code comment explains the React Compiler only memoises the hook without the call); inspector-dock.test.ts tests the pure function. Where it cannot dock, the inspector opens only on an explicit request and changes form on resize without closing. The person's choice is stored in suite:<slug>:inspector-open ("1"/"0") and the width in suite:<slug>:inspector. Before hydration gridMode starts without an inspector column for the hidden default and for startHidden opt-ins; a conditional method inspector opens only after its workbench publishes active inputs (D-0060).

Top bar, status bar and menu sheet

  • TopBar (shell/top-bar.tsx, D-0082) supplies the app's content to ShellBar (shell/shell-bar.tsx), which SuiteFrame also draws: a 56 px white bar under a hairline, inside the body column. Left: the Menu pill below lg, then the breadcrumb (described next) and after it the workspace search as a fixed-width field from lg (16rem, 20rem from xl, the ⌘K hint beside its label; an icon at the right below lg; the CEO's feedback of 14 October 2026). The breadcrumb (nav "Breadcrumb"): Bench (a link to /o, from sm), the app's mark and short name (a p with id app-title that labels main and carries the full name as its accessible text; not a heading since D-0084, so the subject in the body is the page's one H1) and the subject set by setSubject, each after an aria-hidden slash that sits outside the crumb so the crumb holds the plain name; the status slot (SaveStatus) from sm. SuiteFrame draws the same breadcrumb with its own trail. The working Study chip shows at every width on every research workspace page (the sidebar's column, expanded or collapsed, has no Study entry; the non-compact list in the lg overlay and the Menu sheet carries its own selector beside it), naming "Without a Study" when none is chosen; it opens the chooser as a menu under it (StudyContext, below). Then ShellTools (Export and app extras from md; the page tools from lg, above) beside ShellActions from sm: the More circle, with no separate Sources key (the CEO, 14 October 2026; About and licences, with the sources, is in More, the Help items and the sidebar footer) (PageActionsMenu, the suite mark on ink: Copy link, Settings, shortcuts, the theme choice as a radio group, the Help items, and the apps' Start fresh and All apps, which load a new document as the former switcher's links did). A 2 px sweep runs along the bottom edge while a long job runs. A running guided walk's WalkBar renders after the sticky top wrapper (walk slot), as a fixed card (below, "Onboarding and guided walks").
  • StatusBar (shell/status-bar.tsx, DS §6.5, §9.2) is 26 px and sticky at the foot of the body column, on the canvas under a hairline (the redesign's quiet footer line). Left from sm: the freshness chip (FreshnessChip slot), app chips and the network indicator for apps whose registry lists it; the bar shows state only, the "Not medical advice." statement having moved to the head of the limits line (UI-07, below). Centre: AnnounceRegion, the app's one polite live region, as visible text. Right from sm: Jobs (while a job of 10 s or more is listed), Limits, and the ? and ⌘K keys as buttons. A 2 px sweep shows while requests are in flight through suite/net.
  • AppMenuSheet (shell/menu-sheet.tsx) is the left drawer (md) or bottom sheet below lg: the Research sidebar, and whatever the bars no longer show at that width (save status below sm, Copy link, Export and extras below md, theme, Help and apps below sm).
  • The Apps switcher (shell/app-switcher.tsx, DS §6.6) was removed with D-0082: the sidebar names every app, the Learn row opens the education tools' full list, and More carries Start fresh and All apps.

The notepad (D-0087)

Notepad (workspace/notepad.tsx) is mounted by AppShell (not in a preview, with the app's short name and subject as the entry's place) and by SuiteFrame (with its crumbs as the place), beside the Toaster and outside main, so Find on this page and figure exports never read it. It is a 48 px round key filled with the page's accent, the notepad glyph (a ruled sheet with a pencil at its foot, 24 px; the CEO, 14 October 2026, in place of the note sheet) in the accent's on-colour (white in light, the dark ink on dark's mint), inside a 2 px surface ring with the float shadow so it stands out (the CEO's feedback of 14 October 2026; a neutral 44 px key before), fixed at the bottom right above the status bar, at z-(--z-chrome), hidden in print and in stage focus. While a draft is held it shows a surface dot ringed in the accent and is named "Notepad, draft kept". tokens.css sets --notepad-clear (3.5 rem) on html:has([data-notepad-key]), which lifts the toast stack (overlay/toast.tsx) above the key, and moves the key left of a docked inspector ([data-shell-grid][data-inspector="docked"] from lg, docked-xl from xl). The palette's "Open notepad" (shell.notepad, suite.notepad) calls openNotepad, a window event the key listens for; there is no single-key shortcut.

The panel (workspace/notepad-panel.tsx) loads on the first hover, focus or open. From sm it is a non-modal card above the key (title, a visible close key, Esc closes and returns focus to the key); below sm it is the non-modal bottom Sheet. It holds an optional Title (Input, 120 characters; the CEO's request of 14 October 2026) and a plain Textarea (20,000 characters, the field's own count), which are one draft per account (notepad-model.ts: localStorage under suite:notepad:<scope> as { text, title, at }, a draft kept before titles reading with none, held in memory when refused, "Kept in this browser" or the refusal said in the pad; another tab's typing arrives by the storage event). "Save to" offers:

  • Notes of <working Study>, when a Study is chosen and the person can write: studyStore().update appends ### <title> · Notepad, <date and time> · <place> (the title only when given) and the text as its own section to StudyData.description (appendEntry), each line escaped with escapeNoteText, since the notes are Markdown. The update reads the Study as it is now, so it appends and never overwrites; past 20,000 characters it is refused with the count. The Study page's notes editor and the Bench note pick the entry up, or report the change through their own revision check when they hold an edit. The choice names its Study and falls back to a note, never to another Study, when the working Study changes.
  • New note in Objects: createMine("note", { label: <title, else first line>, data: { text } }, { by: "suite" }), not pinned; the confirmation links to the note and offers Pin.

A save clears the pad and confirms through InlineStatus (the status bar's live region) with Open Study or Open note; a failure keeps the text and offers Try again. Without write access the pad says so and offers Copy and Download. The ⋯ holds Copy text, Download as text (drybench-notepad.txt, the title, a blank line and the text exactly as typed; Copy text the same) and Clear, which offers Undo. Settings › Data lists the notepad with Clear notepad. Copy is in content/apps/notepad.ts; e2e/smoke/notepad.spec.ts covers the key, the kept draft, both destinations and the phone sheet.

Notices and arrival

Under the top bar, in order (shell/notices.tsx): the arrival strip ("Came from {app}: {context}", plus one line per ignored hand-off key), the offline banner, and storage notices for apps that save. On mount the shell reads via, obj and the hand-off keys this app does not read (readArrival), then removes via, obj and ignored query keys from the address. A #s=handoff=<id> address makes the shell read the hand-off record and write its parameters into the fragment before Main mounts (waitingForHandoff renders ToolLoading meanwhile). An app adds its own dropped values with shell.reportInbound. See Storage and state.

Commands, palette and keys

The palette lists, in group order: Search {app}, Go to, Actions, Recent, Apps, Help, then any app groups (paletteOrder). An object or identifier result is one row, its "Open" command, with its "Open in <app>" and "Add to study" commands (Command.parent, set in objects/palette.ts) nested under it: the row says "N more", Right (or the pointer affordance) lists them indented beneath it and Left folds them back (UI design review of 11 October 2026, UI-05). Every command remains a flat listbox option, without a nested button; the pointer affordance is hidden from assistive technology. The parent option's accessible description names its expanded/collapsed state and the Right/Left keyboard route. Both routes keep focus in the combobox. The shell contributes: search (when definition.search exists), Go to for every rail item, Copy link and Share this view, Sessions or Projects (apps that save), Save (Mod+S), Toggle rail ([), Toggle inspector (]), Stage focus (f, for apps with a stage), Theme: Light / Dark / System (apps that allow dark), Settings (Mod+,), every other app, All apps, Your objects, Keyboard shortcuts (?), About, Limits and "Show me around" (only while a coach target is present). Apps add definition.commands and may register more while mounted with useCommands(list) (shell/commands.ts); each AppShell owns its own registry. Recent command ids are kept in suite:<slug>:palette-recent (at most five). When saved objects and Studies cannot be read, the object results add a "could not be searched" row (Your objects; the other results stand) rather than leaving them silently missing, so a failure never reads as no matches.

CommandPalette (overlay/command-palette.tsx, DS §6.7) is a modal <dialog> with a combobox. Matching (overlay/palette-match.ts) scores a whole-label prefix highest, then a word prefix, a substring, then letters in order; keywords score 5 lower and group names 40 lower. When open with a non-empty query, it also lazy-loads sniffScientificIntent (src/lib/omnibox/scientific-sniffer.ts, copy in content/apps/omnibox.ts), which recognises inline calculations (C1V1, mass from molarity, oligo Tm), public accessions (PDB, UniProt, DNA sequences) and 14 cross-app bench-task intents (kind: "task-intent", opening the target app's #s=task=…, #s=op=… or #s=view=… address). The result count is announced after 500 ms of no typing and retracted on close. With objects, typed text also finds the person's objects, Studies and full public identifiers (objects/palette.ts). useObjectCommands returns the commands with the outside lookups' state (LookupState: UniProt and PubChem pending or failed, from useUniprotSuggestionState and usePubchemSuggestions); a status line above the key hints says "Looking up UniProt and PubChem…" while they answer, or that a source could not be reached and the list comes from the person's records and the local index, so a short list never reads as all there is ([data-palette-lookup]; the design audit of 14 October 2026). Answers to an older query are dropped (each result keeps its query).

Keys go through the registry in a11y/shortcuts/registry.ts (DS §9.3, contracts §7):

  • Reserved keys: ?, /, Escape, [, ], f, Mod+k, Mod+s, Mod+, (RESERVED_KEYS). Only owner "shell" may bind them. A conflict or reserved key throws in development; in production the later registration is ignored.
  • Single keys match event.key, ignore Shift for printable keys, never fire with Ctrl, Meta or Alt, never in a typing target (isTypingTarget: inputs, textareas, selects, contentEditable, anything inside .msp-plugin or [data-shortcuts="off"]), and never while a modal layer is open except Escape and ?.
  • The Settings preference singleKeys is "on", "off" or "alt" (fire only with Alt, matched on event.code); /, ? and Escape stay available in every mode.
  • A shortcut with scope fires only inside [data-shortcut-scope~=scope].
  • useShortcut and useShortcuts (use-shortcut.ts) install one bubble-phase keydown listener on window, so a widget handler runs first and can claim a key with preventDefault().

ShortcutsOverlay (DS §6.24) is generated from the registry plus widgetKeys and the shell's display-only rows (Escape, f), grouped Everywhere, Navigation, This view, Viewer, Tables, Editing. It hides the Save row when the app has nothing to save.

Settings, About, Limits and Help

The palette, Settings, About, shortcuts and coach marks are next/dynamic imports mounted on first open (the "dialog group", DS §7.3) and then kept for their exit animation.

  • SettingsPanel (theme/settings-panel.tsx, DS §6.29): Appearance (theme where the app allows dark, density, reduce motion, larger text), Numbers (extra digits; significant figures only for apps that declare units), Viewer, Keyboard, Data (the StorageSummary slot for apps that save, the hosts the app contacts, the address-bar preference when usesUrlState, clearing this app's preferences), About, then definition.settings.
  • AboutDrawer and AboutContent (shell/about.tsx): Sources and licences, Limits, Regulatory position, Privacy (with the host table), Version, then definition.aboutSections. The same content is the static page /<segment>/app/about.
  • LimitsBanner and LimitsDrawer (feedback/limits-banner.tsx, DS rule 6, §6.31; instrument design system §2 rule 3 and §4.4; UI design review of 11 October 2026, UI-07): the shell renders the banner once, below every workspace state in main (D-0060; moved from above the work by the CEO's feedback of 14 October 2026) (not in previews, the About or the Terms page), so apps do not place it. It is one notice line that leads with the app's standing statement (lead, which the shell passes as adviceLine(app): "Not medical advice." or "Not medical or investment advice."; "Limits." for Bioreactor and Cell, which have none), then on first open the first full sentence of limits[0] (limitLine in feedback/limit-line.ts: never a sentence cut mid-way, and a sentence that restates the lead is skipped; the CEO's feedback of 14 October 2026) (up to two lines at every width; the whole limit is the line's title) and "Read all N limits". Reading the limits or dismissing them collapses the line to the statement and the link, which stay on the page; the collapse lasts 30 days in suite:<app>:limits-seen (or the session with remember="session"). data-limits-notice is "open" or "collapsed". Assay sets showSummary from its published dose-response state; when false the notice retains its standing statement and link, without the C50 summary or its title. The drawer lists every limit, the shared statements (statementsFor, shell/about-statements.ts) and the sources.
  • HelpMenu and PageActionsMenu hold Keyboard shortcuts, Show me around, "What this app does not do", Report an error (About at the terms), Report a bug, Give feedback and Request a feature (each a mailto: from contactMailto in content/apps/contact.ts: its own subject tag, [drybench/<slug>] Bug report and so on, and the app, build and page without its fragment; the Menu sheet lists the same three), About and Settings.
  • MethodsDialog (shell/methods-dialog.tsx, DS §6.35, D-0085): each app's methods and checks, a §6.9 Dialog at size lg (bottom-anchored and at most 85 dvh on phones), mounted on first open like the rest of the dialog group. shell.open.methods(target?) opens it; with no target it opens at definition.methods.group(), read as the dialog renders, so a press before the workspace has published still lands; { method } opens at the group holding that method (groupForMethod, the open group first). The ways in: the limits line's "Methods and checks" at every width (LimitsBanner's methods prop, which Assay's own notice passes through); the Menu sheet's Help row; the palette's shell.methods command; a chip's "How this number is computed" in the app's own full workspace (ProvenanceDetails, through useShellOptional; elsewhere it stays the docs link); and MethodsLink (shell/methods-link.tsx) in Calc's and Sequence's method inspectors. The dialog's content is the app's sheet (content/apps/<slug>.methods-sheet.ts), loaded on first open through src/apps/methods-sheets.ts and cached for the tab; see Registry and content. Its links end with "All of this on one page" (/methods/<segment> at the open group's heading, D-0085; headingId from src/lib/technical-docs/heading-id.ts, so the dialog does not load the Markdown renderer) and a "Copy methods and reference checks" button ([data-methods-copy], formatMethodsAndReferenceChecks in content/apps/methods-sheet.ts, matching the label in NotebookCopyControl, content/apps/workspace-recorded-methods.ts) that copies a plain-text summary of the open group, its methods and versions, citations, oracle verification sets, the scope note on what a reference check does and does not show, and the current #s= workspace address when within LINK_MAX_LENGTH. Its sections, in order: what the three levels mean and what a check does not show; the standing statements, limits[0] and "Read all N limits"; the open group, expanded; every other group as a native <details> with its lowest level in the summary; the sheet's app-wide checks; registry.oracles and registry.sources; and About (no docs page link: the CEO's instruction of 14 October 2026; if the sheet fails to load, the message links to /methods/<segment>). Limits and About close the dialog first and open after MS.base + 40. No count, percentage or score for the app is shown anywhere (the CSO's ruling R3).

Slots for the store and data UI

shell/slots.ts lists seven slots the shell renders when filled; shell/slot-adapters.tsx fills them with thin adapters, each loading its component with next/dynamic on first use or after first paint.

Slot Component Where it appears
SaveStatus store/save-status.tsx Top bar status slot from sm; first line of the Menu sheet
FreshnessChip shell/freshness-chip.tsx (a ProvenanceChip line) Status bar left, for the oldest of definition.sources()
ShareLinkDialog store/share-link-dialog.tsx "What is in this link", "Share this view"
ExportMenu data/export-menu.tsx Top bar from md; Menu sheet below, where its onDone (after a finished export, never after a failure or a cancelled figure dialog) closes the sheet as Copy link does (CPO ruling, 12 October 2026; DS §6.26). A failure's title names the item ("Export failed: {item}", failureTitle; itemName takes the label up to its first ": ", trimmed, one trailing "." or "…" removed, the CPO's ruling of 21 October 2026, docs/board/memos/2026-10-21-cpo-export-failed-item-name.md; export-failure-names.test.ts holds every app's export labels to 40 characters or fewer), and its reason is the error's own words with the first letter upper-cased (reasonOf), both in data/export-failure-text.ts (the CPO's ruling of 21 October 2026, docs/board/memos/2026-10-21-cpo-rel378-export-retry-words.md, 3 and 4). In the sheet (trigger="row") a failure is said under the Export row, an ErrorBox inside a role="alert" region with Try again, not a toast; if the sheet closes with it unretried, the danger toast takes it over with quiet (CPO ruling 2 of 12 October 2026; REL-378, the phone export retry). Retries go through module-scope handles every mounted menu registers (data/export-retry.ts, registerRetry and retryExport: the menu the failure came from first; only if it has gone, another live menu in the same retryScope, which ExportMenuSlot sets to the app's slug so the TopBar's menu, mounted but hidden below md, takes a retry from a closed sheet; a menu with no scope, a table's, gets no fallback; each handle resolves the id against its latest items under its own lock and absorbs a retry while busy), and the held failure (sheetFailure) is module state owned by the sheet's menu, so closing the sheet removes neither (the CTO's memo of 21 October 2026, docs/board/memos/2026-10-21-cto-rel378-export-retry-mechanism.md). The TopBar menu's staying mounted below md (shell-bar.tsx, hidden md:flex) is what the phone toast's retry relies on; menu-sheet-export.spec.ts guards it
ProjectChooser store/project-chooser.tsx Rail footer and palette, when definition.storage exists
StorageSummary store/storage-summary.tsx Settings › Data, when definition.storage exists
ConflictDialog store/conflict-dialog.tsx shell.storage.showConflict(payload)

Copy link (shell/copy-link.tsx) copies share.link() (default: the current address), swaps its label to "Copied" for 1.5 s and shows one info toast with "What is in this link". When share.notInLink() returns a line, it opens the share dialog instead and copies nothing.

Jobs

shell/jobs.ts is the job registry (DS rule 19, §6.21, §7.5): running, done, failed or cancelled, with a progress fraction or null, a detail line and a Cancel that says what it does ("Stop now", or "Stop after this page" for a job registered with stopsAfterStep). The Jobs popover (jobs-popover.tsx) lists jobs that ran for 10 s or more until dismissed. Escape never cancels a job. jobs.test.ts covers the rules.

First paint and remembered layout

ThemeScript (shell/theme-script.tsx, DS §3.3) runs before the shell markup, from the server's HTML only: a copy React creates on a client navigation would never run, so the client draws it as an inert type="text/plain" block (React does not warn about data blocks), with suppressHydrationWarning for that one attribute. Besides the theme (below) it sets data-rail="collapsed" from suite:suite:rail and --inspector-saved from suite:<slug>:inspector when the stored width is between 280 and 572 (520 at the desktop scale). AppShell removes both hints after hydration, when its own state takes over. All localStorage reads go through guarded helpers (shell/local-value.ts: useLocalValue, writeLocal), which return null on the server and during hydration and notify other tabs through the storage event.

PolicyNavigationBoundary is mounted in the root layout, and the shell imports usePolicyRouter as its router, so palette commands, rail links and app switching load a new document when they cross a CSP boundary. This is described in Security and network.

Primitives and recipes

Recipes

recipes.ts (DS §6.0) exports whole-literal class strings so Tailwind finds every class in the file. Each is built from semantic tokens only; the site's original string is quoted in a comment beside it.

Group Recipes
Focus focusRing, regionRing (scrollable regions), groupRing (radio chip groups)
Inputs inputBase (a flat 36 px channel, 16 px text on phones, 14 px from sm; inside a Field the label sits above a 40 px channel), inputProse, textareaBase
Buttons buttonPrimary and buttonAccent (filled with the accent), buttonQuiet (white with a hairline), buttonDanger, buttonIcon (control height; 44 px on coarse pointers), buttonSmall, buttonText; all flat, 3 px corners (--radius-control; the polish pass of 14 October 2026, which superseded D-0082's 7 px)
Chips and segments chipBase, chipOn, chipOff, chip(on) (pills since D-0082: a tinted accent wash when on), segmentGroup, segment(on, size), segmentThumb
Panels and type panel (the card radius, --radius-card 3 px, and the resting shadow --shadow-rest, 0 2px 6px rgb(30 25 15 / 0.1), the CEO's feedback of 14 October 2026; the Bench, Study and library cards take the same two tokens, dashed placeholders the radius only; the recipe carries .ui-card, so the shadow drops on a card inside a card, a dialog, a drawer, a popover or the inspector and is none in dark, D-0086; 16 px padding on phones, 20 px from sm), panelHeading, subjectHeading (Source Serif 4, 500; 34 px phone, 43 px from sm, 20% above D-0037's 28 and 36 px at the CEO's request of 14 October 2026), subjectPlaceholder, headlineAnswer (the serif face for one headline number), panelHeadingDense (17 px, Inter), panelKicker (the small bold accent eyebrow, sentence case), monoLabel, metaLine, metaText, disclosureSummary, note (a left-ruled aside, capped at max-w-3xl for a readable measure, REL-222; a site that must span a wider element would add max-w-none with a comment, and none does today), callout (a note box on the canvas), pill, kickerTag (a tinted pill label), featureTag, inlineFlag
Stage controls stageButton, stageCluster, stagePlate, stageHint, stageToast
Links and inline link, linkAccent, idLink, arrowLink, smiles, spinnerDot, spinnerDotMark
Site strings site* exports, re-exported unchanged under their old names by src/components/tools/{chips,form-styles,panel-styles,stage-controls}.ts for the carried-over tool trees. Suite code never uses them.

Rules stated in the file: do not combine two recipes that both set a background or text colour on one element; three recipes change shape with density (inputBase, the buttons, chipBase). The ui-action, ui-nav-item, ui-field, ui-link and ui-hover: names are utilities defined for the hover and press states.

Form primitives

All live in form/ and implement DS §6.20 and rule 18 (values stored at full precision, displayed rounded, never coerced silently).

Component Notes
Field Label above the control, help and error ids wired into aria-describedby; the error lives in an always-present role="alert" element (empty:hidden) so a message inserted on blur is read once. kind="group" labels a group with role="group". Controls read the ids with useField().
Input Optional leading icon, clear button (Esc clears), busy state; type="search" gets role="searchbox".
NumberField A text input, never type=number, so the mouse wheel never changes a value; the phone keyboard comes from keypad() in field-logic.ts (the full keyboard for a field that takes negatives or allowExponent, the digit pad for a non-negative integer, else the decimal pad). Parses 1e-6, 1,000, 0.5, ~5 (field-logic.ts); shows sigFigs out of focus and the full value in focus; validates on blur and Enter; live passes each parsable keystroke through. Inside EntryValidity it reports whether its text is readable so a calculator withdraws its answer.
Select Native <select> styled with selectRecipe.
Combobox Static list or async source; the source is called 280 ms after the last keystroke, from two characters, with an AbortSignal; ARIA combobox pattern; suggestion count announced after 500 ms of quiet.
SuggestionInput, SearchInput, SearchPopup, SearchResult Synchronous suggestions whose matching the parent owns; the popup is shown in the top layer and follows its anchor. Enter on a highlighted suggestion picks it; Enter without one submits the typed text and closes the list, so it never covers what the search answers under the field; typing opens it again. Focus opens the list unless openOnFocus is false, which Pipeline sets while its not-found message or an error stands under the field.
ChoiceGroup A <fieldset> with a mono <legend> for checkboxes and radios.
Segmented At most four options that set one value; role="group" with aria-pressed; sliding thumb after mount.
Slider, Switch, Checkbox, Radio, Textarea, FileDrop Native inputs where possible; Switch is role="switch" with an optional On/Off word; Textarea grows to 8 rows and counts characters; FileDrop checks type, emptiness and size before handing files over.
format.ts formatSignificant, formatQuantity (per-quantity decimals plus extraDigits), formatExportNumber (full precision, . decimal, no grouping), isoUtc, UK dates and local times with offset. Pure; format.test.ts.

Overlays

Component Behaviour (from the module headers) DS
Dialog Native <dialog> with showModal(); enters through @starting-style (200 ms), exits with data-state="closing" (150 ms) before closing; Esc through the ladder; a busy dialog refuses to close and says why; Tab and Shift+Tab are wrapped by hand inside the modal; focus returns to the opener or the nearest panel heading; page scroll locked. §6.9
Drawer / Sheet Same base with side right (420 px), left (280 px) or bottom. modal={false} (inspector drawer) uses show(), no scrim, no trap, and sits between the top and status bars so the chrome stays usable. The sheet's grab bar closes past 40 % of its height or faster than 0.5 px/ms. Drawers and sheets enter in --motion-drawer (220 ms) and leave in --motion-base (200 ms). §6.10
Popover Rendered after its trigger in the DOM and shown in the top layer (popover="manual"), positioned by Floating UI (offset 6, flip, shift 8 px, height capped at min(60vh, 420px)); within="workspace" keeps it between the bars and inside main. Its sticky header row holds a visible Close and, where the body has no heading of its own, the visible title (the label by default), which also names the dialog, so no popover opens with a row that holds only a close button (UI-08); the Save to Bench and Task details panel, Find on this page and Tools & sections pass one. After Escape or a choice it returns focus to its trigger; if the choice removed the trigger, to the first control that survives where the trigger sat. §6.14
Menu Roving focus and type-ahead (500 ms buffer); menuitem, menuitemcheckbox, menuitemradio; disabled items stay focusable. The panel establishes left alignment and normal whitespace independently of its trigger's table cell. Multiline Send to descriptions determine their row height; Library's compact sizing applies only to the direct row controls. The opaque pinned Close row reaches the panel's bottom border without a padding gap exposing scrolled text beneath it. Choosing an item returns focus to the trigger, closes, then runs the action, so a dialog the action opens restores focus correctly. No submenus. §6.14
Tooltip Text only, at most 240 px; 300 ms hover intent; on :focus-visible; never on touch; aria-hidden when the trigger's label already says the same. §6.13
toast() / Toaster At most three; success and info 6 s, warning 10 s, Undo 10 s, danger and any other action toast until dismissed (toast-store.ts). Success and info are announced once through announce(); warning and danger are role="alert" and not also announced; a toast marked quiet (its words already read once, as the phone Menu sheet's export failure is) is neither (isAlert). Toasts never take focus. §6.8
OverlayCloseButton The visible close control every persistent popup carries (principle 6).

Supporting modules: layers.tsx (SuiteLayers, the #suite-layers portal element; useEscapeLayer; lockScroll), layer-stack.ts (the Esc ladder: the most recent reachable layer, then stage focus, then nothing; Esc never navigates or cancels a job), floating.ts (imports exactly computePosition, autoUpdate, offset, flip, shift, size from @floating-ui/dom), menu-keys.ts (rovingIndex, type-ahead). Each pure module has a test.

Popovers and menus position with floating.ts (position, follow); useFloatingPanel in popover.tsx passes stayInView: true, so shift() works on the cross axis too and a panel whose trigger scrolls away stays inside its bounds (DS §6.14, 13 October 2026; shiftOptions is unit-tested in floating.test.ts, the behaviour in e2e/smoke/popover-in-view.spec.ts at 1408 and 375 px). Tooltips keep main-axis shift only.

Layout primitives

Component Notes DS
Panel, PanelTitle <section aria-labelledby> with the panel recipe (the card radius and resting shadow), an optional kicker drawn as the small bold accent eyebrow (indexLabel, sentence case; passed only where it carries a source name since D-0084), a serif 20 px h2 (panelHeadingDense; 22 px under the 110 % root) that takes programmatic focus (tabIndex={-1}), an aside slot and ticks (accent corner marks for the one specimen panel); a level-3 title is sans 15 px (panelHeadingSub), so each panel carries one serif line (D-0084). Quiet by default: no mandatory accent rail or divider (principle 5). Server component. §6.32, §8.1
ViewToolbar The row under a page's section tabs (D-0084): a view's own selector at the left, a small muted <label for> beside the select (toolbarLabel), and the page's action keys at the right (actions, in R1 order: the split Save to Bench first). A selector scoped within the selected tab (Calc's calculator, Sequence's operation, Assay's analysis, the library's view) lives here. No app has a Layout menu: an app's views are its tabs (the CEO, 14 October 2026). Wraps on a narrow container. Server component. atlas
DisclosureList, DisclosureRow A panel's optional sections as native <details> rows separated by hairlines (divide-y), each summary carrying the row's name and a decorative chevron that turns when the row opens; the open state may be controlled (open, onToggle). Callers keep the content mounted so Find on this page reaches it, and never put an error, a changed-input warning or a limitation behind a row (D-0084). atlas
CopyButton (feedback/copy-button.tsx) The suite's one copy key (DS §7.4; the design audit of 14 October 2026), which Protein, Molecule, Assay and Sequence use: the label names what is copied; success shows a tick and "Copied" for 1.5 s, announced once; failure shows "Copy failed" with the remedy in its title and announcement until the next press; a press while a copy is in flight is ignored; the key holds one width whichever word shows. variant small, quiet or icon (the state in the accessible name). §7.4
ReadoutStrip A subject's headline numbers as a row of stat cards (<dl>, white cards with the card radius and resting shadow; the tinted tiles take the radius only, as they sit inside a panel): a small bold muted label, the value in tabular figures (the one keyValue in the accent), an optional unit and a detail line for the qualifier or provenance (D-0082); icon and tint draw the Bench's glyph tiles (D-0083); tone="tint" draws the atlas's soft accent-tinted tiles with a short accent dash and no hairline (.ui-readouts[data-tone=tint], D-0084). A withheld value shows its reason, never a dash. instrument DS §4.4
SubjectHeader The subject block of every bench tool: a 4 px accent bar beside the app's H1 (H2 in a preview), identifier lines beneath, and optional search and mode rows. Work-action keys now sit at the right end of the tab region rather than beside the title (CEO, 23 October 2026). principles, R1 and R2
DiscoveryStart, DiscoveryHeader The shared start and loaded header of Protein, Molecule and Pipeline, and Assay's start. The start retains its optional aside preview/chooser; the loaded header keeps compact search above the subject and its source details. Action keys follow in the tab region, leaving the H1 and identifiers their own width. principles
Tabs kind="line" (underline tabs on a hairline that spans the content width (tabList; tabListFit fits the row to its tabs, D-0084) with a sliding 3 px accent indicator, the selected tab in the accent deep at the same weight as the rest, so choosing a tab never widens it or moves its neighbours (the design audit of 14 October 2026); automatic activation, sideways scroll with edge fades; D-0082), "segment" (the recessed track), "steps". §6.11
ViewTabs, viewTabId Line tabs for an app-owned panel without remounting the subject; clears the last announcement on a tab change. The actions slot places Save work and the other keys at the right end of the tab region, on a right-aligned line beneath the tabs below a 48 rem container. Tabs alone scroll sideways with edge fades; the selected tab carries the 3 px accent underline. WorkCaptureControls keeps routine Study ownership and membership inside Work details; status and warnings appear as small unboxed text above the keys, outside the row's height on wide screens and on a compact line at narrow widths. AppShell's data-inspector state hides the tab-region keys while the Inspector is open, including its docked, drawer and sheet forms. Views without tabs use an equivalent right-aligned action line. §6.11
SplitPane Resizable gutter with keyboard steps of 16 and 64 px (split-math.ts), persisted per app, suite:layout on release. §6.22
Stage Frame for 3D and canvas viewers: data-surface="stage" or "paper", §8.2 heights (320 px phone, 380 px from sm, clamp(380px, 56vh, 720px) from lg), the control cluster, wheel and touch arming rules, stage focus mode. Charts and live 3D views use mode="theme" and follow the suite theme (principle 8). rule 1, §3.4, §8.2, §8.3
WorkflowGuide A native <details> for optional guidance.
useMediaQuery, useInserted SM_UP, COARSE_POINTER; whether a component was inserted after load (entry motion only then).

Feedback

Banner (tones info, success, warning, danger; a glyph so colour is not the only cue; role="alert" only when inserted after an action), EmptyState (dashed for empty, solid for no match or offline; the cards' 3 px corners; the action placeholder of the never-empty rule, D-0084: one sentence and one real verb), ErrorBox (not live; focusOnRetry), Skeleton and LoadingStatus (hidden for 300 ms, then a 1.6 s pulse, static under reduced motion), Progress (the loading ladder), InlineStatus and PendingIndicator, PendingLabel and useRunGuard (pending-label.tsx: a key's word turns to its pending word, "Fitting…" or "Calculating…" from content/apps/interaction-feedback.ts, reserving the wider of the two, while a second press during the run is ignored; the Assay fits, the Bioreactor measured review), Acknowledgement, Kbd, ToolBoundary (the one role="alert" crash surface), ToolLoading (says so and offers a reload after 15 s), ViewerGate (holds back a heavy viewer until clicked or visible) and the opt-in sound.ts (local synthesis only after an explicit action; off by default).

Since D-0086 the same feedback/copy-button.tsx also exports useCopyAction (the behaviour for a control drawn elsewhere) and takes write (a caller's writer, for a table copied as text and HTML; the tick waits for it), silent with onCopied (the caller announces, so the live region hears it once), dataAttributes, and variant="button" as an alias of quiet; the icon variant shows its word as a tag beside the key. Beyond the four apps above, the Calc answers, Plasmid's sequence, the data grid, the Assay results table, design notes and method paragraphs, and the label dialog use it.

The loading ladder (feedback/ladder.ts, DS rule 19 and §7.5): nothing for 300 ms; a skeleton or bar to 2 s; words and one announcement at 2 s; the slow sentence with Keep waiting at 6 to 8 s (default slowMs 6,000); the recovery verb and one announcement at 15 s; a stalled flag after 20 s without progress; alternatives at 40 s without stopping the job. InlineStatus sends its message to the live region and retracts it when it stops holding (status-retraction.ts).

Objects UI

objects/ implements build-spec-objects (docs/design/build-spec-objects.md). The data side (store, refs, links, Studies, research context) is in Storage and state. The UI parts:

Component Role
FigureSpec.physical A figure with a physical size: the export menu opens PhysicalFigureDialog (formats SVG, PNG, TIFF, PDF; backgrounds per format; the size in mm; resolution in dpi with pixel size for PNG and TIFF, greyed past the device's canvas limits; the TIFF size and alpha lines, the PDF note and its substitutions; the app's notes) and writes through the app's write, never the record (the CPO's §D.24; Assay from A-0 commits 4 and 5).
WorkCaptureControls sessionUnsaved An app's dirty flag for unnamed work: "Unsaved changes" in the control's status slot with the app's project download beside it, quiet, never for a named task (the unit-of-work design §3.9; Assay from A-0 2d).
ObjectActions, MoreActions, SendToLink The object verbs on every row, card and panel: variant="bar" (More (⋯) holding Pin, then Send to, Add to Study or "Add molecule to Study", and Open page; D-0102) or "row" (one More menu: lead items, Send to, then Pin first in Keep, Add to Study and Open page; pinInMenu defaults to true and pinInMenu={false} exposes Pin as its own key; a dense table may shorten the row keys from its own class on the group, as Pipeline's trials do with [&_.ui-key]:[--key-h:1.5rem], while the key's coarse-pointer rule keeps 44 px on touch, UI-27). The object is upserted when a verb is used. A row may lead its More menu with the work's own acts under their own label (leadItems: { label, items }, Assay's "This assay": Open a file…, Download project file, Repeat): a labelled MenuGroup of ordinary items first, then Send to and Keep as before, then moreItems with Delete last after its separator; one arrow-key order; nothing rendered when empty; the label from content. Only acts on the current work itself go there (open, download, repeat); anything else is the CPO's first (the CPO's ruling of 12 October 2026).
ObjectPage /o/<type>/<id>: identity header, Overview (cached facts per source with licence and date, the person's numbers), one tab per app that declares one (loaded on open, inside ObjectTabFrame with the app's own host, accent and error boundary; the tab's address is memory only), Mine, Used in. The open tab is kept as tab= in the fragment.
MinePanel, Numbers, Trail Notes, numbers, tags, files (up to 2 MB each) and versions kept on an object; where a number came from.
StudyPage, StudyPicker, StudyContext, StudyChoices /study/<id>, the "Add to study" picker, the working-Study chip and its chooser: a Menu under the chip (objects/study-context-menu.tsx, loaded with the menu) with the Studies as a radio group, "Without a Study" first, archived ones behind a checkbox, "Open Study" and a way to a first Study; a choice applies at once with the D-0046 rules (pending work saved first, the Study re-read, a changed or archived destination refused with a toast and a status line) and the menu closes (UI design review of 11 October 2026, UI-02).
ObjectList, ObjectLibrary Bench at /o with its hash views (#studies, #work, #pinned, #activity, #objects) and Library (List, the default, or Cards, suite:library:layout; D-0084, D-0120).
HomeParts, HomeExample, ExampleParts, RemoveExample Bench panels and the guided walk's example card and removal dialog.
CandidateList (objects/candidate-list.tsx, copy content/apps/candidate-list.ts) The records a typed name matches, none chosen (the CPO's ruling 7 of 17 October 2026; built with E10, reused by E9's compound identity). Source-agnostic: props query, candidates ({ id, name, identifier, distinguishing, matched: { on, text } }), source ({ name, release, accessionPrefix }), onUse(row), optional onKeepTyped(). The heading "{N} records match “{query}”. None has been chosen.", announced once per result set through one polite status region; each row with the name, the identifier in mono, the line that tells it apart, "Matched on: primary name" or "Matched on: synonym “…”" and a quiet "Use this" whose name includes the record; no row preselected and no key handler, so Enter in the caller's field chooses nothing; at most CANDIDATE_LIST_MAX_ROWS (10) rows, then "Show all N" for that result set; with no candidates, the no-match sentence naming the source, its release and its accession prefix; "Keep as typed text" when offered. Rows full width and 44 px targets below sm and on coarse pointers. content/apps/candidate-list.test.ts pins the words and that the component names no source.
ObjectPickerPill, ObjectPickerDialog (objects/object-picker-*.tsx, copy content/apps/object-picker.ts) The @Object picker that fills one field from a saved object (Calc's molarity, A280 and C1V1). The cell-line target (E10) carries no number: its pill reads "Choose the cell line", and its dialog body, objects/cell-line-picker.tsx (loaded when it opens), lists the saved cell lines and looks a name or accession up in the Cellosaurus snapshot with resolveCellLine, showing the outcome in a CandidateList (even a single match is a candidate to choose); a chosen record is stored by value with its passport, unless this browser already holds that line with one; "Keep as typed text" stores a mine:cell-line record. The selection carries the record's rev and its body as payload. Used by Assay's plate details (one per plate).
CellLineCard, CellLinePassport (objects/cell-line-passport.tsx, marker drybench-island:cell-line-passport, code row cell-line-passport-chunk) A cell line's card details ("Cell line · CVCL_… · species", the disease, the caution-tone "Cellosaurus annotation" quoting the comment) from RecordDetails, and its page's Overview sections (Identity, Origin, Annotations, STR profile, Sources and limits) from ObjectPage, both loaded on demand. The quote is Cellosaurus release {N} ({date}), record {CVCL_…}: “{comment}” with a link to the entry. A page opened before the record exists reads the snapshot and carries the record with its passport. Copy in content/apps/cell-lines.ts.
useObjects hooks, useLearn, loadRuntime Load the store on first use (import("./runtime")); useLearn refreshes facts of records the person already has and never creates one (D-0045).

The verbs module (verbs.ts) is what every host carries; its implementation (verbs-impl.ts), the store and Send to's planning load on the first call.

The discovery start reads discoveryStartText and discoveryGuidanceText from the pure content/apps/discovery-guidance.ts leaf. The public workflowGuidanceText object still composes the same summary, discovery instructions and Bench guidance, and workflow-guidance.ts re-exports discoveryStartText. This keeps the Bench copy out of discovery-only import owners without changing a word or interaction.

Workspace UI

workspace/ holds the D-0038 to D-0040 controls for saved work and Studies. The records they write are described in Storage and state; access modes in Server, accounts and billing.

  • WorkCaptureControls is the shared Save to Bench and Record result control that bench tools pass to SubjectHeader as actions. Props: app, label, objectRef, payload (JSON), schema, optional subjectRefs, captureRecorded(work) (the app's closure that builds the ResultSnapshot and InputSnapshots for this render), onSaved, onNamed, beforeSave, disabled, saveHelp, freeLine (optional: without save access, the app's own sentence of fact and its free actions shown in place of the suite's paid link; Assay passes its free/paid line, roadmap-v0-depth Stage A, exit criterion 7; apps that leave it out keep the paid link), details and children (the app's own actions, which stay usable without save access). Naming, the actual destination and duplicate creation sit in a details panel opened on demand (D-0040). save-focus.ts decides where focus goes when a save settles.
  • WorkItemsPanel, WorkOutcome, SavedWork, RecordedResult, ResultHistory, ResultComparison (descriptive only, never pools or computes statistics), InputPicker, WorkNoteEditor, WorkOrganisationDialog, WorkDestination.
  • Study panels: StudyBrief (the callout), StudyProgress, StudyStatusMenu, StudyTaskKey, StudyContextPanel, StudyResultsTable, useStudyWorkspace, StudyNextSteps, StudySummary, ExperimentsPanel, StudyRecoveryControls (the .drybench-study.json recovery file), MeetingView, NotebookCopyControl, SourceImpact, ScientificChanges.
  • Bench: BenchHome parts (bench-home.tsx: the greeting BenchGreeting, the counts strip BenchStats, the placeholder BenchPlaceholder, the tools grid with its quote card, the section heads, the view links and the relative time Ago), bench-figure.tsx and bench-figures.ts (the generated figures), bench-studies.tsx (the Study cards with their covers and ⋯ menu), bench-pins.tsx (the record rows RecordTable, the pinned table and its type chips), bench-quick-start.tsx (the Templates, Example data and Import tabs), BenchNote (edits StudyData.description with baseRev), bench-priority.ts (presentation-only ordering).
  • Access: access.tsx (useWorkspaceAccess), account.tsx, backups.tsx, entry-banner.tsx, ManagementDisclosure, use-research-draft-guard.ts.

lazy-result-views.tsx loads the recorded-result and comparison views on first use and retries a failed download on request.

Onboarding and guided walks

  • WelcomeCard (DS §6.23; decision C22): a first-open frame with one to three actions, optional steps, "Show me around" for apps with coach marks and "Don't show this again" stored under storageKey(slug, "welcome"). A small inline guard hides a dismissed card before first paint.
  • CoachMarks: an opt-in tour of at most three [data-coach="<id>"] targets, never started or advanced automatically. The palette offers it only while a target exists (useCoachTargets). The targets come from the registry entry's coachMarks.
  • Guided walks (D-0022, J3): walk/use-walk-shell.ts reads a walk key from the fragment on arrival (before the app reads the address) and removes it; walk/run.ts keeps the run in sessionStorage["suite:walk:run"]; walk/walk-bar.tsx loads with next/dynamic only while a walk runs or a key arrived; walk/definitions.ts holds the step definitions; walk/slots.ts the filled copy. Apps register step actions with useWalkAction(id, action), which goes through host.walk and does nothing in a preview or without a run. After each step action the bar waits until no action has been registered or removed for 60 ms (at least 60 ms, at most 2 s), so a component the action replaced has mounted and registered its own before the next action runs (REL-27). Next never stays on "Working on step n…" (REL-426): every path that does not leave the page clears it, and a watchdog (STEP_WATCHDOG_MS, 45 s) stops a step still waiting after that long. It bumps the bar's revision, so every closure the step captured is stale and a late answer cannot move the run under a retry, logs Walk step n stopped after 45 s, waiting on: … with the stage it was on (saving open work, an action id, the example study, or the hand-off that should open the next page) to the console, and shows the walk's failed line on the bar as well as announcing it. Before this, Next returned without clearing its busy state whenever the run stopped being current mid-step (another tab's run, a change of workspace scope), and a hand-off that never left the page kept the label for good. In the changed state the step text stays open and its Show/Hide toggle is hidden, so Next's explanation is on the page; on the phone that state's controls may wrap to three rows within the 300 px bar (the CPO's amendment of 13 October 2026). objects/write-context.ts marks what the store writes during a run so "Remove the example" can find it by run id.
  • The bar's placement (D-0105, updated 24 October 2026). The bar is a fixed floating card (.walk-bar-shell, tokens.css) beside the step's target over a visual-only scrim: html[data-walk-spot] .suite-shell::after, a box at the complete target rectangle whose 9999 px spread shadow dims the rest of the shell. This implements the CEO's floating-overlay decision rather than the earlier sticky-top bar. useWalkTarget (walk/walk-bar.tsx) syncs placement on scroll, resize, DOM changes and focus, without a polling interval, and writes --spot-* and data-walk-dir on <html>. At desktop widths the card chooses above/below while protecting focus, section navigation and the Inspector; when neither side fits it uses the existing compact edge dock. The edge dock's target clearance uses the actual sticky chrome and available space instead of an assumed 88 px arrival position. Below 640 px it remains a bottom dock whose bottom meets the rendered StatusBar's top, or the visual viewport's bottom when the keyboard is open. Phone controls use an explicit Back/Next row (with Restore retained when changed) and a three-column secondary grid for Go, the text disclosure and Leave; changed-input state uses two columns while the disclosure is absent. Complete secondary labels wrap at their existing font size; every key remains at least 44 px tall and Leave at least 44 px wide. Phone block padding and the title/control gap are 4 px. This prevents the longer “Show the step text” label from pushing Leave onto a third row when the guide compacts. Each visible header and section-row rectangle contributes its actual bottom edge, including the browser’s sticky placement. Together with final card height and StatusBar bounds these define the usable target band; there is no fixed 116 px phone target position. geometry.ts computes the scroll needed to keep a fitting target wholly in that band. On explicit “Go to the number”, an expanded phone step uses its existing text disclosure to compact when the scientific target needs the space, then focuses and scrolls once using the final card height. The text remains available through the same disclosure; changed-input guidance remains expanded. A target too tall even for the compact band keeps its useful start and complete content in ordinary page flow, with its lower content reachable by scrolling; this does not claim simultaneous whole-panel clearance. During a phone walk, Assay’s existing “First imported readings” scrolling region (the direct region under [data-detected]) has a maximum height of the smaller of 24 rem and the measured viewport minus header, section row, StatusBar, guide and two 8 px clearances. Its name, focusability and all eight preview rows remain; internal scrolling makes the final row reachable without covering the focused region. The outer recognised-import target and its complete content remain the walk target. ResizeObserver reconciles target, card, body and chrome growth until the person scrolls or interacts, so late layout does not silently move a fitting target under the dock, and later intentional scrolling is respected. “Go to the number” explicitly restores placement. After a real focus move, one animation-frame check protects the same still-connected active element if native focus scrolling leaves it under the card; the correction is instant so the global smooth-scroll rule cannot replace focus protection. New focus, user interaction and step cleanup cancel stale work. Explicit Go retains its smooth motion unless reduced motion is requested. Observers, animation-frame work and listeners are removed with the step; one per-step AbortController owns the window, document and visual-viewport listeners and preserves their capture/passive options. A press or focus outside the target/card lifts the scrim, which returns on a step change or Go. Phone scroll padding and the toast stack retain the measured card height (--spot-bh/--walk-clear). The scrim remains at --z-coach, the card one level above, and the target's content-sourced “Step n” tag below chrome; no accent focus ring is added. Reduced-motion preferences still disable movement transitions and smooth scrolling. The unchanged hard target predicates in the plate/J3 journeys and e2e/support/walk.ts remain separate from the softly reported focus assertion under D-0080; no oversized-target exemption is added. walk/geometry.test.ts covers measured fitting phone/desktop targets, late card growth, stationary clear targets and the honest oversized-panel start. e2e/objects/j3-focus-clearance.spec.ts separately models one late browser scroll after synchronous focus handling and uses the unchanged focus-clear predicate; its controlled scheduling evidence is distinct from the unchanged J3 journey. See Known issues for browser outcomes and remaining oversized-target limits; the existing 10 KB walk budget is unchanged.

Motion and reduced motion

motion/animate.ts (DS §7.3) wraps the Web Animations API with the suite easings (EASE, from tokens.ts: expo-out cubic-bezier(0.16, 1, 0.3, 1), expo-in-out, sine-in-out, sine-out, cubic-in, standard) and durations (MS: instant 100, fast 150, base 200, move 300, slow 500, draw 600 ms). It exports animate, stagger (60 ms steps, capped at 6), flip, countUp, drawPath, keyframes fadeUp, fadeUpSm, pop and prefersReducedMotion. No motion library is a dependency. The ANIMATIONS tokens (tokens.ts, mirrored as --animate-* in tokens.css) loop only for the existing busy indicators (indeterminate, pulse-soft, dot-pulse); shimmer is one 1.2 s sweep (DS §3.6, "Nothing loops"). The glow-pulse keyframe added on 16 October 2026 was removed: DS §1 rules out glows.

Reduced motion has three sources, honoured identically (DS rule 11, §9.5): the OS media query, the Settings switch (html[data-motion="reduce"], with MOTION_EVENT = "suite:motion"), and for opt-in decoration (mark draw, skeleton pulse) the browser's data saver. Under any of them, and when the tab is hidden, animate() applies the final keyframe and resolves at once. tokens.css repeats the site's 0.01 ms collapse under html[data-motion="reduce"] and under prefers-reduced-motion, leaving out ::backdrop; html[data-theme-instant] disables transitions while a theme swaps under reduced motion. Nothing inside a closed <details> (below its summary) transitions: Chromium never advances an animation in that hidden content, so a transition started there (a key enabled while its disclosure is shut) never finished and the page never settled. useReducedMotion() is false on the server and during hydration; usePresence(open, exitMs) keeps an element mounted through its exit. Entry motion never runs on first paint (useInserted, shellMounts in AppShell).

Interaction effects (the motion pass of 14 October 2026)

motion/effects.ts holds the suite's acknowledgements and the expressive effects, with their switches. Every effect is presentation only: it starts after the operation it acknowledges has succeeded, never delays it, never changes a result, a record or the address, and leaves the final state meaningful without it. Visual proxies are aria-hidden and removed when they finish (or after 1.2 s).

  • Switches (internal, no setting). EFFECTS.expressive turns every expressive effect off at once; each has its own flag (travel, expand, compareAssembly, inkCurve, recordedStamp, and the opt-in launcherLight and launcherTilt, off). For review, html[data-effects="core"] turns the expressive effects off without a rebuild and html[data-effects="all"] turns every one on. expressive(effect) is false under either reduced-motion source and in a hidden tab; effectEnabled(effect) ignores motion, for an effect that also carries information (the recorded stamp's time stays, still, under reduced motion); motionAllowed() gates the core tints; motionAllowed({ saveData: true }) also honours the data saver, for a surface that treats save-data like the other two sources (the Bioreactor visual layer, BR11).
  • Core feedback (not under the switch). trace(el, { tone }) tints an element's own background 14 % with the accent, success or danger colour and lets it settle over 700 ms (TRACE_MS), with fill: "none" so nothing inline is left behind. On an SVG element it tints the element's own fill instead of a background: the one shared trace for HTML and SVG (suite DS §7.2, BR11.6); accent passes the app's accent explicitly where the element does not resolve it inside [data-app], and saveData honours the data saver. Users: InlineStatus on a new success eventKey (opt out with acknowledge={false}), NumberField after a unit conversion (with an aria-hidden "Converted to …" receipt for 1.6 s; the conversion itself is announced), Molecule's comparison columns added later, the Assay plate's wells after an exclusion decision (danger) or inclusion (accent), the arrival section (below) and the return highlight.
  • Keys. .ui-key (instrument.css) answers hover in 130 ms and press in 80 ms with a sunken face and an inset shadow, never a translate or scale (the old .ui-action press scale in globals.css is gone). .ui-pending holds a pending ring back for 150 ms so a fast run never flashes it. Acknowledgement's persistent tick draws its path in 140 ms.
  • Panels and lists. ViewTabs slides one indicator (data-tab-indicator, data-live after the first placement) under the selected tab; DisclosureList's chevron rotates; an open <details> body fades in over 160 ms (closed within that time, Chromium leaves the fade unfinished in the closed body until it opens again, so the browser helper settle() does not wait on animations inside a closed disclosure, REL-206); the search popup settles 3 px over 160 ms and its active option carries a 2 px accent edge; a windowed DataTable's sticky head takes a shadow only once scrolled (.ui-head-shadow-host, a scroll-driven animation where supported).
  • Arrival (A5, with B1). arrive({ section, label, via }) runs after a destination has decoded and reported a hand-off: a name card travels from the arrival strip ([data-arrival-strip], shell/notices.tsx) into the section that holds what arrived, from the sender's accent (accentOf(slug)) to the receiver's, and the section is then tinted once; when the travel cannot run (effect off, either end off screen) the section is only tinted. Calc's compound card uses it.
  • Return (A6). rememberReturn(ref) keeps the record a list opened in sessionStorage (suite:return, one key, 30 minutes; opening a record is a full page load, so memory would not survive it); highlightReturn(root) brings that row back into view (block: "nearest", focus not moved) and tints it once. The Objects library uses it. A record that no longer exists is not found and nothing is selected instead.
  • B1, travel. travel(from, to, label): a 420 ms quint-out name card, no overshoot. Add to Study (object-actions.tsx) flies the record's name to the bar's Study control only when that control shows the Study it was added to ([data-study-context="header"][data-study-id]); the toast receipt always stands.
  • B2, expand. The Stage's Focus FLIP (layout/stage.tsx) runs 320 ms expo-out when expressive("expand"), else it is instant. Focus is offered wherever the shared Stage is used (focusMode defaults to true): Protein's structure, Molecule's map and shape in the default layout, Cell's tour. Molecule's Compare layout draws its own map, without a Stage.
  • B5, recorded stamp. workspace/recorded-stamp.tsx: after Record result, an outlined accent "Recorded · time" stamp beside the receipt (content/apps/workspace-work.ts, capture.recordedStamp), settling from 102 % with a short trace; the receipt's own tint is turned off for that event so only one acknowledgement plays. Not shown for a save, a memory-only record or a failure.
  • B6, launchers. workspace/launcher-light.tsx lights the Bench's tool cards under a fine pointer (data-lit, --lx, a shadow offset) and tilts them at most 1° (data-tilt); both off by default.

The Assay curve draw (B4) and Molecule's comparison assembly (B3) are described on their app pages.

Accessibility helpers

  • One live region. announce(text, { kind }) (a11y/announce.ts, DS rule 9, §9.2) writes to the single AnnounceRegion the status bar renders. It coalesces for 400 ms (COALESCE_MS), drops identical text within 1 s (DEDUPE_MS), writes progress at most every 5 s (PROGRESS_MS), queues at most 3 messages 1.5 s apart (QUEUE_MAX, QUEUE_GAP_MS), and drops messages when no region is mounted. clearAnnouncement(text) normalises whitespace, removes matching pending/queued text and empties the region if it shows that text. An explicit nonempty target matching the last emission also releases only that emission's dedupe identity, even if its text is already absent; a later explicit identical request follows normal coalescing and queue spacing (REL-518). Clearing unrelated or blank text leaves dedupe intact. No-argument clear drops all waiting text and empties the region but deliberately retains dedupe history. Targeted withdrawal does not reset progress cadence or the active queue-gap timer and never republishes a message by itself. A pre-existing deliberate-repeat callback can still restore withdrawn text if cleared between its blanking write and next animation frame (REL-519, open); the bounded dedupe amendment does not change that scheduler. announceOnArrival(text) carries a message through sessionStorage["suite:announce:arrival"] to the next page. answer-text.ts builds a calculator's first-answer sentence from [data-answer] cards.
  • Focus. a11y/focus.ts (DS rule 13, §9.1): focusAfterRender(target, { fallback }), focusHeading(target, { scroll }), nearestHeading, focusableIn, restoreFocus(invoker, fallback), and trackStableFocus()/lastStableFocus(), which the shell starts on mount so a layer opened from a toast or a menu item can return focus to the control the person used. Busy controls use aria-disabled, not disabled.
  • Skip links. The root layout's "Skip to content" targets #main; InspectorSkipLink adds "Skip to inspector" while it is docked. useSkipLinkFocus focuses the target without writing the fragment, so #s= state is never lost.
  • Target sizes. Recipes add [@media(pointer:coarse)]:min-h-11 or size-11 (44 px) to controls.

Theme

One preference covers every page (D-0029). The mechanism is summarised in Architecture and the keys in Storage and state; the modules are:

Module Role
theme/preference.ts THEME_STORAGE_KEY = "suite:theme", values light, dark, system, DEFAULT_PREFERENCE = "light" (D-0035). createThemePreferenceStore keeps the choice in memory when storage is blocked and migrates a valid suite:<slug>:theme or, for "suite", drybench:landing-tone once.
theme/store.ts Scopes (a target element per ThemeScope), the resolved theme, THEME_EVENT = "suite:theme" on every change, the storage event and the prefers-color-scheme query; an app whose themes lacks "dark" resolves to light. Updates theme-color metas from CANVAS_LIGHT and CANVAS_DARK.
theme/theme-scope.tsx Applies theme and appearance settings to <html> (or the preview's shell root) while mounted; removes workspace hints on unmount.
shell/theme-script.tsx The inline first-paint script: data-theme, data-motion, data-density, data-text, and the layout hints.
theme/appearance.ts, theme/settings.ts suite:settings JSON: sounds, reducedMotion, density, largerText, singleKeys, significantFigures (2 to 8, default 4), extraDigits (0 to 2), units, viewer.defaultStage, viewer.labelScale, addressBar (live or static). sanitizeSettings validates every field on read. SETTINGS_EVENT = "suite:settings".
theme/theme-toggle.tsx, settings-button.tsx, settings-panel.tsx changeTheme (the 300 ms crossfade and one announcement), used by the More menu's theme group, Settings and the palette; the ThemeToggle button is no longer drawn in the bar (D-0082); the Settings button; the panel.
theme/chart-theme.ts chartTheme(root?) reads the --chart-* tokens with getComputedStyle from the nearest themed element and returns literal colours, cached per element until THEME_EVENT; useChartTheme() re-renders on change and returns the light set on the server.
theme/stage-theme.ts, theme/viewer-surface.ts Literal stage colour tables (obsidian, paper) for MVS scenes, 3Dmol, canvases and RDKit drawing; syncMolstarSurface resets a Mol* canvas background.

DS §3.4's rule applies: SVG attributes and canvas fills take literal colours read at draw time, never var(), so exported SVGs keep real colours.

Tokens, accents and print

  • tokens.css (imported at the end of src/app/globals.css, after brand.css and before accents.css) declares the semantic colours in @theme static (light), the dark and stage overrides under [data-theme="dark"], [data-surface="stage"], the paper surface re-declaring the light column ([data-surface="paper"], R42), motion durations, the z ladder (--z-base 0 to --z-toast 70, --z-showreel 100; DS §2.4), chart tokens, density variables (html[data-density="compact"]), shell variables on .suite-shell, the custom variants dark: and stage:, keyframes, the D-0016 desktop scale and the reduced-motion collapses.
  • The light semantic colours are the redesign's (D-0082): canvas #f6f8f7 (workspace pages override it with paper beige #f3f0e8, CANVAS_WORKSPACE_LIGHT, under html[data-theme="light"][data-suite-shell] and [data-suite], the CEO's feedback of 14 October 2026; the html and body take it too, so it runs to the window's foot; public pages keep the cool canvas; since D-0086 the same block gives workspaces the warm neutrals beside it, COLOR_WORKSPACE_LIGHT: hairline #e0dacf, sunken and skeleton #eeeae2, soft line #ebe7de, key and field line #d8d2c6, input edge #857e71), hairline #dfe6e5, ink #18313b, muted #4a5b63 (kept at the 7:1 rule), flat white keys and fields (--color-key-top and --color-key agree; --color-key-edge is the key line), and the sidebar's --color-rail, -raised, -edge, -fg and -fg-muted (the workspace dark block overrides them with its chrome). .suite-rail[data-surface="stage"] draws the sidebar on those tokens.
  • Cards and corners (D-0086, the polish pass of 14 October 2026): --radius-control (3 px, beside the CEO's --radius-card of the same day) gives keys, fields, selectors and the section track the cards' corners (rounded-control); chips and status pills stay pills, and round wells and avatars stay round. .ui-card (in instrument.css; the panel and panelGrid recipes and the discovery start carry it) adds --shadow-rest, and drops it on a card inside a card, a dialog, a drawer, a popover or the inspector, so shadows never nest; both dark sets set --shadow-rest to none. A navigable card (.ui-card-link, the Bench tool and Study cards) answers a pointer with a stronger border and a slightly deeper shadow and never moves. Line tabs keep one weight in every state, so selecting a tab never shifts its neighbours.
  • tokens.ts mirrors every value for JavaScript: EASINGS, D, Z, SHELL, COLOR_LIGHT, COLOR_DARK, COLOR_DARK_RESOLVED, TEXT_UI (the text-ui-* scale from 11 px 2xs to 32 px big; below the 14 px body only the three steps 11, 12 and 13 px, the CPO's ruling of 17 October 2026, guarded by type-steps.test.ts), BREAKPOINTS, SPACING_UI, DENSITY, shadows, CHART_LIGHT, CHART_DARK, CHART_GEOMETRY, STAGE_OBSIDIAN, STAGE_PAPER, and the gate references (GATE_SURFACES, AMBER, ALERT). It is a plain module safe for tests and server code.
  • accents.css is generated by scripts/accents.mjs (npm run accents) from ACCENTS and APP_IDENTITY in the registry: per slug, the base, deep and glow colours and hue on [data-app="<slug>"], the swap to the glow on dark and stage surfaces, and the swap back on a paper surface. --color-suite-accent is separate and does not change with the app (D-0036). node scripts/accents.mjs --check exits 1 when the file is stale.
  • brand.css is the maker's brand layer (brand colours and fonts, brand utilities, base rules): --font-sans (Inter), --font-display (Bricolage Grotesque, the public pages), --font-serif (Source Serif 4, the workspace subject face since D-0082) and --font-mono (IBM Plex Mono). Suite code may not use its colours (below).
  • print.css (imported by the (workspace) layout) prints A4 or Letter with 16 mm margins, sets every dark-overridden token back to its light value, hides the shell chrome and anything with data-print="hide", and expands data-print-expand regions.

Icons and marks

  • icons.tsx (DS §4.5): 55 glyphs (ICONS) on a 24 × 24 grid, 2 px stroke in currentColor, square caps, mitred joins, no fill; aria-hidden unless title is given. No icon library is imported; a new glyph is added here in the same grammar.
  • mark.tsx (DS §4.2): MARKS, one per registry MarkKey, at most three paths with exactly one specimen point (a zero-length path M x y h.01 whose square cap draws a 2 × 2 px square) standing clear of the strokes. Mark can draw itself once per page load (draw), skipped under reduced motion.
  • wordmark-layout.tsx, wordmark.tsx (DS §4.3): the lockup "{SUITE} | mark Short." in bar, hero and card sizes; the root carries data-app.
  • suite-mark.tsx: the filled Bench mark used for the suite (D-0035). education-mark.tsx: marks for the education tools.

The flask glyph also identifies the methods and checks command in the palette (D-0085).

marks.test.ts checks that every registry mark exists and is used, the path-count and specimen-point grammar, the spacing of the point, that mark.tsx and icons.tsx draw with the shared attributes, and the exact icon set.

Data display, charts and the chart palette

  • chart-palette.ts: SLOTS (eight categorical colours, assigned in order), NEUTRAL, COMPARISON, their dark counterparts, and chartSeries(theme). The header records that the light slots were checked with the dataviz palette validator (worst adjacent deutan ΔE 10.4, normal-vision ΔE 19.8) and that slots 2 and 4 are below 3:1 on white, so charts using them need a legend, direct labels or a data table. That check is recorded in the comment only; it is not re-run by a test. The dark set keeps each slot's OKLCH hue and chroma with lightness 0.74. tokens.test.ts checks both sets equal CHART_LIGHT and CHART_DARK. Today only Molecule's map view (src/components/tools/molecule-workbench/map-view.ts) imports this module directly; the other charts read the same values through chartTheme().
  • data/chart-geometry.ts: linearScale, linearTicks (steps of 1, 2, 2.5 or 5 × 10ⁿ), logTicks (whole log10 units, halves when fewer than three) and path helpers. Pure; used by Bioreactor's run chart and Assay's chart.
  • data/data-table/ (DS §6.12): DataTable pages at 25 rows and windows the body when every column is nowrap and there are more than 200 rows (ten rows of overscan); pagerAtHead (default false, so no other table moves) repeats the pager above the table, labelled "{name} pages, above the table" (Pipeline's trials, UI-27); roving row focus; <dl> cards below sm; aria-sort, aria-selected, aria-rowcount. model.ts holds sorting (stable, missing last), filtering, paging, windowing and selection, with model.test.ts. Phone cards also accept opt-in column presentation: cardFullWidth gives long values reading width, and cardPairWith places two labelled short facts beside each other. Pairing uses only fields rendered in that row, never drops visible fields, and leaves desktop order, visibility controls and exports unchanged. Columns without these options keep their original cards.
  • The data grid's model, src/lib/grid/ (DS §6.34; A-0's fourth design deliverable): pure, no React. A DataGrid (v: 1) is one table of numbers in one of three shapes, columns (one column per group; paired when rows are matched subjects), grouped (a row factor and column groups of subColumns replicates) and xy (an X column and Y groups with replicates). A cell is a finite number or null; null is blank, never zero, and never counted. Text that is not a number ("n.d.") is kept in text and reads as missing; a comma inside digits is read by evidence (classifyNumber, commaEvidence, readCellWith; the CSO's addendum of 13 October 2026, item 3): "1,234.5" and "1,234,567" are thousands, "1,5" a decimal comma, and the ambiguous "1,234" is thousands or a decimal comma only when the same paste holds an unambiguous cell of that kind (or a semicolon delimiter, or the explicit decimal-comma setting), else text ("comma is ambiguous"); conflicting evidence keeps every comma cell as text; spaces are stripped only between groups of three digits; a typed "1,234" is text without the setting; the paste statement counts each reading (readings); an excluded cell keeps its value, is listed in excluded and is never read. Edits (setCell, clearCells, toggleExcluded, ensureRows, ensureGroups, removeRow, removeGroup, the label setters) return a new grid and never mutate. Engines read through groupValues, columnsOf, pairedValues (a row counts only when both cells hold values; the rest counted as dropped) and xyPoints, each returning the values with their addresses and the blank, excluded and text counts. toLong and fromLong convert between wide and long (values beyond one per subject in a column table counted, never averaged). readGrid is the storage reader (refuses a malformed grid); it and GRID_LIMITS, X_GROUP, cellKey and parseCellKey live in read.ts, so a record reader imports them without the rest of the model (model.ts re-exports them). paste.ts: readPastedTable reads a whole pasted table (a first row with no number as the header, and a second header row only under a merged first row, so a text first data row such as "n.d." is data; a merged header as sub-columns; row labels only from a first column with no number at all, a mostly-text first column with numbers being read as data with its text counted and the statement saying so (labelsAsData); a one-column paste of decimal commas kept as one column; long tables with a group and optional subject column, the decimal comma by the delimiter) and returns a PasteStatement of what was read; pasteBlock pastes a tab-separated block at a cell as a spreadsheet does (growing rows and groups, counting what it cannot place); copyBlock copies a rectangle out; gridToText writes the whole grid back as tab-separated text that readPastedTable reads as the same grid (header words passed in by the caller; an optional rowTitle keeps the row labels' heading). Limits: 10,000 rows and 200 data columns. pzfx.ts reads the .pzfx project file's column, grouped and XY tables into grids (see the Assay page, Import). Tests: grid.test.ts, pzfx.test.ts.
  • data/data-grid/ (DS §6.34; the CPO's memo of 13 October 2026): DataGrid, the editable grid over src/lib/grid/, loaded on demand by its consumer. From sm, a role="grid" table (aria-rowcount, aria-colcount, aria-multiselectable), one tab stop with a roving active cell (data-grid-active), sticky group and sub-column headers and row labels, fixed column widths (table-fixed, so an edit never widens a column), in-place editing (Enter, F2 or a printable key; Enter, Tab and their Shift forms save and move; Escape cancels), blocks with Shift, Delete to blank, X to exclude or include (when the consumer allows exclusions), and the native copy and paste events for tab-separated blocks (copyBlock, pasteBlock). Below sm, one column at a time: a column select with previous and next keys and a list of labelled inputMode="decimal" inputs, each with an Exclude toggle. Each cell state is a visible marker word and a state word in the cell's aria-label ("1,234, text, comma is ambiguous, read as missing"; "blank"); the grid writes to announce() only for what an act did. Rows beyond RENDER_ROWS (500) are not rendered and the statement says so. keys.ts is the keyboard without React (keyAction, clampPos, blockOf, afterEdit), tested by keys.test.ts (including that x and X never start an edit). gridText writes a grid as text with the grid's own header words. Words: content/apps/data-grid-ui.ts (and data-grid-view.ts for a consumer's Text and Table switch). pairing={false} hides the columns shape's "Rows are matched" for a consumer with no pairing (Counts and the false discovery rate; the CPO's design-check ruling, 14 October 2026).
  • KpiTile and kpiGrid (DS §6.18): loading, ready, stale and n/a states; count-up and flash only for a person's own change.
  • PercentileBar (DS §6.20 D-04): a static role="img" picture with a hidden <dl>, never a range input.
  • ProvenanceChip (kind chip, line or method) and ProvenanceMark (DS rule 5, §6.30): sources resolve through the app's registry entry via useAppEntry, so an unknown source id throws.
  • ScrollBox (sideways scroll with edge fades and a focusable region), ExampleChip (published, synthetic or run examples, info tone), freshness.ts (oldestSource), clipboard.ts (copyText with a fallback that keeps focus and selection).

Export writers

All writers are in data/ and implement contracts §5 (docs/design/shared-contracts.md) and DS §6.26. The Study bundle and recovery file are described in Storage and state.

Protein's recorded-image history loads its app-owned reader lazily through recorded-result.tsx. Capture uses insertPngText before freezing and hashing the retained PNG, embedding capture-time provenance. History exports those exact verified PNG bytes and a separate input/result JSON receipt through the existing download writer. Its ordinary live-view PNG export continues to use insertPngText; experimental provenance now names the successful coordinate provider and consumed metadata sources, including RCSB when used. A retained PNG is not re-encoded or relabelled with current live provenance. Text-only Study projections explicitly state that the image is omitted and its bytes are unverified in that view.

Writer Format and rules Test
csv.ts RFC 4180, CRLF, UTF-8 with BOM always, quoting when a field holds the separator, ", CR or LF; numbers at full precision with . and no grouping; dates ISO 8601 UTC; units in header parentheses; optional ASCII headers; OWASP formula escaping (a text cell starting with =, +, -, @, tab or CR gets a leading '; numbers never escaped). TSV too. csv.test.ts
csv-bundle.ts buildTabularExport (decision C07): a ZIP with the CSV, <base>.provenance.json, <base>.README.txt and, for share-alike rows, <base>.CHEMBL-LICENCE.txt is primary when a row carries a notice that must travel; otherwise the plain CSV with per-row provenance columns (source, source_version, retrieved_at_utc, app_version, attribution). The other form is always offered. When the provenance block carries methods, the README has "Methods and parameters": "Method: {name}, version {version}" per method, never a file path, then "Software: {appName} {version}, build {buildHash}" (readmeTexts; the CPO's memo of 12 October 2026 on the method line, ruling 5); nothing precedes the CSV header. Figure exports carry the same methods and software in the provenance block they embed. Pure, Worker-safe. csv-bundle.test.ts
zip.ts Store-only ZIP (no compression), UTF-8 names with flag bit 11, CRC-32, MS-DOS times; refuses ZIP64 sizes (65,535 entries, 4 GiB) and climbing names; readZip for tests and checks. zip.test.ts
json-export.ts Data export envelope { $schema, format: "mbio-suite-export", formatVersion: 1, kind, schemaVersion, app, generatedAt, provenance, data } and project file { $schema, format: "mbio-suite-project", formatVersion: 1, suite, app, exportedAt, project, records, provenance? } with local-only key and storage fields removed. toJson writes non-finite numbers as null, -0 as 0, Maps and Sets as arrays, Dates as ISO UTC. export-files.test.ts
svg.ts Clones the chart SVG and inlines computed presentation values as literal #rrggbb attributes with separate opacity; named font stacks (fonts not embedded); drops data-export="exclude" and <foreignObject>; provenance JSON in <metadata id="mbio-provenance">; optional caption; width capped at 16,384 px; result checked with DOMParser. export-files.test.ts
png.ts From an SVG rasterised in an idle callback or from a canvas; scales 1, 2, 4 capped by MAX_TEXTURE_SIZE/MAX_RENDERBUFFER_SIZE and the browser's canvas area; provenance as iTXt chunks (Title, Source, Creation Time, Provenance) after IHDR. export-files.test.ts
print-data.ts, print-report.tsx, print-styles.ts The print report (decision C01: no print route). Fixed order: title, key figures, figures, tables, notes, then an appendix that cannot be left out (methods, limits in full, regulatory position and statements, sources with version, build, browser and time). PrintReport renders it on a paper surface; while mounted it adds an @page running footer with "page n of N" (the header notes Chrome and Edge print the page numbers). printNow() waits for fonts and images, then opens the browser's print dialog. print-styles.test.ts
download.ts exportFilename: {slug}-{subject}-{kind}-{YYYYMMDD}-{HHMM}.{ext} with the subject slugified to 40 characters and -2, -3 for repeats in the same minute; project files {slug}-{kind}-{name}-{YYYYMMDD}.{kind}.json; downloadBlob, and saveBlob using the File System Access API where present.
provenance.ts ProvenanceBlock (schema version 1) built by buildProvenanceBlock; LicenceId; attributionLines in a fixed order (uniprot, alphafold, chembl, ctgov, openfda, fda); citationText, sourcesTable, snapshotDifferences. licenceState is always "preview" and no tier line is written while pricing is indicative. provenance.test.ts; chipState (a chip's state: stale past its age, "date unknown" when no source carries a date, else the first source's; a method's provenance with no source and the method's name and version is "computed", never "date unknown", the Assay closing walk's W4 of 14 October 2026; DS §6.30 "States"); chipProvenanceProblem (null when a chip's provenance names a source, or a method with its name and version; a guard test runs it over every Assay method record), re-exported by provenance-chip.tsx

The figure core (src/lib/figure/, Stage A). A second SVG path, built for figures at a physical size rather than copied from the page: layoutFigure lays a FigureDocument out as a scene in millimetres and sceneToSvg writes it as a string with no DOM (width="85mm", viewBox in millimetres, <metadata id="mbio-provenance">, one <g id="series-<id>"> per series with points, bars, curve, interval and marker groups, real <text> in the FONT_STACKS.sans stack, literal colours resolved from roles by the caller, no CSS or <foreignObject>, a white rectangle first when the background is white). Group names that are not XML names are escaped (svgId). Tested in src/lib/figure/figure.test.ts. No Export menu uses it yet; svg.ts stays for the other apps. tiff.ts writes baseline TIFF 6.0 (little-endian, uncompressed, RGB composed on white or RGBA with ExtraSamples 2 when transparent, the resolution in dots per inch, the provenance JSON in ImageDescription with non-ASCII escaped) from RGBA strips the caller draws (at most 1 Mpx each), returned as parts for a Blob, refused at 100 MB (TiffTooLarge); pixelSize gives px = round(mm × dpi / 25.4). Tested against pipeline/assay-analyser/fixtures/figure/tiff-expected.json in src/lib/figure/tiff.test.ts. pdf.ts writes one page of PDF 1.4 from a scene (sceneToPdf): the MediaBox in points, one content stream deflated by CompressionStream("deflate") (zlib format; uncompressed where the API is absent), Helvetica and Helvetica-Bold in WinAnsiEncoding and Symbol for Greek, none embedded, any other character written as a documented ASCII form (PDF_SUBSTITUTES, else "?") and returned in substitutions, Info (Title, Subject, Creator, Producer, CreationDate, the source line as /Source and the provenance block in full as /Provenance, as PNG carries them; the closing walk's W1) and the provenance as an embedded provenance.json. helveticaMeasure measures text with the AFM widths in afm-widths.ts so a figure laid out for PDF anchors its text exactly. Tested against pdf-expected.json in src/lib/figure/pdf.test.ts.

The app print report uses the browser's "Save as PDF" under print.css. Study reports additionally have a direct PDF download in Manage Study: workspace/study-report-pdf.ts lays out the same immutable report document as the readable HTML action, and figure/pdf.ts provides scenesToPdf alongside the compatible one-page sceneToPdf. Pages share the standard fonts and embedded provenance receipt, with individual content streams and one page tree. AFM-measured line wrapping splits long identifiers and URLs, table rows can continue onto another page, figures stay together, and every page has a number. The renderer refuses more than 1,000 pages rather than truncating content. Unsupported visible characters are listed in a final section and announced after the download. The content-owned formatter in content/apps/workspace-study-report.ts names the source and every supplied replacement code point in order, retaining repeats: U+2032 (prime) printed as U+0027 (apostrophe). Its bounded printable-ASCII names cover the existing encoder substitutions and their output characters; unknown names retain the bare code point, including U+003F (question mark) for the encoder's fallback. It describes the existing receipt without changing the encoder, fonts, original document or collected substitution order (REL-576). Whitespace consumed by wrapping is still not listed. The HTML action keeps the original Unicode. Both modules are lazy, with their own budgets in scripts/size-budgets/study-report.json. No PDF dependency or server is added.

The menu: useExports(items) (export-registry.tsx) registers a view's items with the shell's ExportProvider while mounted; host.registerExport and definition.exports() add more. ExportMenu (export-menu.tsx) orders object groups first, then Data, Figure, Report, Project, Share (orderItems), shows at most ten items (nine and "More formats…" beyond that, MENU_MAX), keeps unavailable items as aria-disabled with the reason, and opens the figure dialog for figure items (SVG or PNG, the allowed backgrounds, scales with a size estimate, Copy PNG where supported). csvExportItems() (data/csv-export-items.ts, re-exported by export-menu.tsx) gives the two CSV items of the C07 rule; an app's export list imports it from the leaf so its workspace chunk does not carry the menu (Protein's exports.ts; the CTO's memo of 21 October 2026 on REL-378, 4).

Schema versions. Every JSON export and project file carries $schema set to <suite origin>/schemas/<slug>/<kind>-v<n>.json (schemaUrl in json-export.ts, schemaPath in content/apps/routes.ts). store/schema.ts has toJsonSchema to produce such files from the validator definitions, and its header says they are published from a prebuild step. As of 10 October 2026 no prebuild step calls toJsonSchema (it is called only from schema.test.ts), there is no public/schemas/ directory and no route serves /schemas/…, so the $schema URLs in exported files do not resolve. The header also names a different path (public/apps/<slug>/schemas/…) from the one schemaPath builds.

Engine wrappers

engines/rdkit/ holds the shared RDKit.js worker used by Molecule and Assay: protocol.ts (request and response messages, error codes invalid, too-large, network, engine, crashed), client.ts (request ids, timeouts, restart after a crash, cancel or timeout, and an engine status for the status chip) and rdkit.worker.ts (loads vendor.rdkitJs and the WebAssembly on the first request; closes itself after an RDKit failure). The chemistry the worker runs is imported from Molecule's tool tree (src/components/tools/molecule-workbench/chem.ts, engine.ts, library.ts). Mol* and 3Dmol.js have no wrapper here; they are loaded by their app trees through src/lib/load-script.ts. See Engines.

Other shared folders

  • store/ UI: ProjectChooser (DS §6.25), ShareLinkDialog (§6.27; updated 19 October 2026 for Milestone B: the canonical #s= link, "This link contains" from describeState(schema, state), explicit statements that anyone with the link can open it without an account, that opening it recreates editable inputs and settings under current methods rather than a frozen historical result while local saved work/notes stay in this browser, when a portable file is needed instead (inputs over 2,000 characters, plate readings or saved records; content/apps/store-ui.ts), a secondary Download {fileKind} file action when available, Copy citation, the "Not in this link" warning banner when local data is omitted, and the long-link fallback over 2,000 characters), UnnamedDraftBanner (workspace/unnamed-draft-banner.tsx: local unnamed draft recovery and discard with undo), SaveStatus, ConflictDialog, StorageSummary, and NotesMarkdown/NotesPlain (an allowlist parser in markdown-allowlist.ts: paragraphs, emphasis, lists, inline code and http(s) links only; never dangerouslySetInnerHTML). The data layer is in Storage and state.
  • literature/: the Literature desk, search, paper reader, evidence table and review notebook. See Literature.
  • marketing/: the *.marketing.tsx blocks for the landing, product, docs, regulatory and verification pages, the suite header and footer (SuiteChrome, MarketingScope) and social images. They render on the server and are exempt from the brand-colour lint rule.

The developer kit

/dev-kit and /dev-kit/workspace (src/app/(workspace)/dev-kit/) exist only in builds made with SUITE_KIT=1; otherwise both call notFound(). They are not linked, not in the sitemap and not named in robots.txt (see Deployment and environment).

  • /dev-kit is the primitive gallery (KitFrame with a theme switch): sections for tokens (with WCAG contrast ratios from kit-contrast.ts), identity, motion, shell (a preview-mode AppShell), the workspace demonstration, layout, overlay, feedback, data (chips and KPIs, exports, tables, store UI), form, store and marketing. Each section is in sections/<area>.tsx.
  • /dev-kit/workspace renders DemoWorkspace: a demonstration app in a full-mode AppShell borrowing Pipeline's registry entry, with a storage kind that has versions, a session serialiser, exports from the definition, useExports and host.registerExport, a share spec with a URL schema, a consent-gated request and an in-place print report. Its projects are kept under the namespace dev-kit-demo. This is the only place the shell's local project-saving path runs.

The design system's §11.6 step 3 still describes a /apps/_kit route guarded by NODE_ENV === "production"; the code moved to /dev-kit and the SUITE_KIT guard.

Rules enforced by tests and lint

Rule Where it is enforced
Suite code uses semantic colour tokens, never brand colour utilities (bg-white, text-ink, border-hairline, …) or raw hex ESLint suite/no-brand-colour in eslint.config.mjs (matcher in scripts/lib/brand-colour.mjs) for src/components/suite/** and src/apps/**; exempt: *.marketing.tsx, tokens.ts, chart-palette.ts, tests, and in recipes.ts the site* declarations and the two buttonDanger hover hexes #8a2d19 and #ffb3b3. recipes.test.ts applies the same check to the recipes.
Every recipe is a whole literal; no recipe transforms case or uses wide tracking meant for capitals recipes.test.ts
The site* strings equal the tools' pre-move strings and the four tools modules re-export them under their old names recipes.test.ts
No uppercase in any class string in src (table headers included) and no text-transform: uppercase in stylesheets outside the showreel no-uppercase-labels.test.ts (principle 7: transforming µL reads "ML" and mangles formulae and gene names)
tokens.css and tokens.ts agree (@theme static, :root, density, shell variables, dark and stage block, paper block, reduced-motion twin) tokens.test.ts
Contrast gates: §3.2 semantic pairs in both themes, primary actions and input boundaries, skeleton bars, §5.7 status colours, §3.4 chart and viewer colours, §4.1 accent triples, hue separation and sharing, the chart focus ring's two tones at 3:1 tokens.test.ts
accents.css is up to date with the registry tokens.test.ts (and node scripts/accents.mjs --check)
Every app palettes.ts dark colour reaches 4.5:1 on the dark panel tokens.test.ts
print.css resets every dark-overridden token to its light value tokens.test.ts
The Tailwind build emits what the token layer relies on (variable-reading utilities, @theme static, dark: and stage: variants, arbitrary-variable utilities, opacity-modifier shape, both reduced-motion collapses, the brand layer) npm run tailwind-check (scripts/tailwind-check.mjs), tested under npm test by scripts/tailwind-check.test.mjs
Mark and icon grammar marks.test.ts
Inspector docking rule shell/inspector-dock.test.ts
Shortcut grammar, reserved keys, matching and firing a11y/shortcuts/registry.test.ts
Announcer coalescing, dedupe, progress cadence, queue and carry-over a11y/announce.test.ts
Pure rules of overlays, layout, feedback, forms, jobs, last address, page find, theme and settings overlay/*.test.ts, layout/*.test.ts, feedback/*.test.ts, form/*.test.ts, shell/*.test.ts, theme/*.test.ts
Export formats and provenance data/*.test.ts
Product names only in content/app-names.ts; copy rules content/apps.test.ts, see Registry and content
Client code does not import the full registry ESLint no-restricted-imports, see Registry and content

Browser checks of the shell are in e2e/smoke/ (for example shell-a11y.spec.ts: focus return from menus and dialogs, Tab containment in the share dialog, saving flows in the dev kit, reduced motion across the shell's layers; suite-theme.spec.ts, settings.spec.ts, popup-close.spec.ts, page-find.spec.ts, app-navigation.spec.ts). See Build, test and CI. The five guard tests above (tokens, recipes, no-uppercase-labels, marks, inspector-dock) passed, 40 of 40, when run with node --test on 10 October 2026.

Size budgets: scripts/size-budgets/suite.json budgets the Bench and object-page routes (225 KB each), the walk chunk (10 KB) and public routes; each app's file budgets its definition and workspace chunks on the shared /[app]/app route. The separate shell-core, dialog-group, DataTable, motion and Floating UI budgets named in DS §7.3 and §11.7 have no entries of their own.

Building a new panel or app view

The steps below follow the Bioreactor app (src/apps/bioreactor-simulator/), which uses most of the shell's surfaces without an inspector, with notes from Calc (src/apps/lab-calculators/) for an inspector. Product names and all words go in content/; nothing below is typed in a component.

1. Put the words in content

Add the view's copy to the app's content module (Bioreactor: content/apps/bioreactor-simulator.workspace.ts, exporting workspaceCopy and VIEW_IDS). Headings, button labels, announcements, empty states and command labels all come from there.

2. Give the view an address

If the view or its inputs should survive a reload or travel in a link, add keys to the app's UrlSchema (Bioreactor: bioreactorSchema in src/components/tools/bioreactor-simulator/address.ts, with view: { label, check: checks.oneOf(VIEW_IDS), default }). Read and write through useUrlState(schema), which returns { state, set, ready, … }; set pushes history for a subject change and replaces otherwise. Never put private inputs in the query string; see Storage and state. A key that a hand-off may carry must also be in HANDOFF_PARAMS (content/apps/params.ts).

3. Compose the view inside the workspace

Bioreactor's BioreactorWorkspace (views/bioreactor-workspace.tsx) shows the pattern:

const host = useHost();                        // ids, announce, setSubject, objects
const url = useUrlState(bioreactorSchema);     // address state
<SubjectHeader heading={<h1 className={subjectHeading}>…</h1>}
               actions={<WorkCaptureControls …>{<ObjectActions …/> or <MoreActions …/>}</WorkCaptureControls>}
               modes={<Field><Select …/></Field> …} />
The shell renders `<LimitsBanner>` at the bottom of main from the registry entry (`app-shell.tsx`); apps do not render it.
<ViewTabs id={host.id("bioreactor-tabs")} panelId={host.id("bioreactor-view-panel")} value={view} items={…} onSelect={setView} />
<div id={host.id("bioreactor-view-panel")} role="tabpanel" aria-labelledby={viewTabId(…)} tabIndex={0}>
  <Panel titleId={host.id("run-heading")} kicker={…} title={copy.run.heading}> … </Panel>
</div>

Rules this follows:

  • One subject heading (h1, or h2 when preview is true) with subjectHeading; the shell's subject is set separately with host.setSubject?.(…) in an effect.
  • Every id goes through host.id(), so the view works inside a product page's preview.
  • Panels use Panel with a titleId; its heading takes focus, so focusHeading(host.id("run-heading"), { scroll: true }) can move focus after a view change.
  • Primary views are ViewTabs in the workspace (principle 3); Tools & sections must not repeat them. Bioreactor and Calc therefore pass rail: [] in the definition and set their few secondary sections from the workspace with useRail(memoisedSections); Bioreactor's are its presets.
  • Wrap anything that can crash (a viewer, a worker-backed chart) so the page survives: Main is ToolBoundary around a next/dynamic import with a Skeleton fallback (src/apps/bioreactor-simulator/workspace.tsx).
  • Use the primitives and recipes (Field, NumberField, Select, Switch, Banner, EmptyState, buttonPrimary, buttonSmall, metaLine) rather than new class strings, and only semantic tokens. Charts read useChartTheme() or chartTheme() at draw time and use chart-geometry.ts.

4. Announce, do not add live regions

Send state changes through host.announce(text) (or announce()), once per completed result, not per keystroke. The announcer (src/components/suite/a11y/announce.ts) coalesces within 400 ms, but a progress message never displaces a waiting status: the status is queued at once, ahead of the count (DS §9.2, REL-246). Bioreactor announces once per completed run, guarded by a ref holding the last announced result key. Visible status lines use InlineStatus or LoadingStatus, which are not live regions themselves.

5. Register exports and provenance

Build the view's ExportItem[] in a useMemo and pass it to useExports(items) (an empty list in preview). Bioreactor's exportItems(…) in src/apps/bioreactor-simulator/exports.ts produces the run and pump-sheet CSVs, the chart as SVG or PNG, the printed bench sheet, the method paragraph and (from the call site) the scenario JSON; each carries a ProvenanceBlock from the app's provenance.ts, and an item the view cannot provide sets disabledReason. A print report is a PrintReport rendered in place and printNow().

6. Expose actions to the shell

Commands and shortcuts are defined in definition.ts, outside the workspace tree. Bioreactor publishes its current view and actions to src/apps/bioreactor-simulator/store.ts (publishBioreactor in an effect, null on unmount), and the definition's commands call readBioreactor()?.actions.setView(id, { focus: true }) with when: () => readBioreactor() !== null. Shortcuts use keys 1 to 5 in group "Navigation"; never a reserved key. Commands registered only while a component is mounted can use useCommands(list) instead.

7. Add an inspector if the app needs one

For fixed content, set inspector: { title, kicker, Content } in the definition with Content loaded through next/dynamic (Calc: MethodInspector from ./inspector), and leave the inspector hidden by default when it should wait for an explicit pick (D-0060). Calc and Sequence explicitly opt into { defaultState: "docked", startHidden: true } for method content when the selected workbench has inputs; their blank starts and floating layouts remain hidden until explicitly opened. For "what is picked", set inspector: "selection" and call shell.inspector.open(node, { title, focus: true }) or host.openInspector on an explicit Inspect. Give dockMinMain when the main view needs more than 40rem.

8. Saving, sharing and objects

  • Saved work and recorded results go through WorkCaptureControls with the app's payload schema and a captureRecorded closure; see Storage and state.
  • Share: set share.schema so the share dialog lists exactly what the link carries, and share.link/contents where the link differs from the address. If the view holds something a link cannot carry, give notInLink, fileKind and onShareFile together.
  • Objects: use ObjectActions for an object on screen, useLearn to refresh facts, and add a type to objectTabs and src/apps/<slug>/objects.ts TAB_TYPES (and the registry's objects.tabs) for an object-page tab. src/apps/objects.test.ts checks the three agree.

9. Check it

Run the commands in CLAUDE.md: npm run lint (brand colours, registry imports), npm test (tokens, recipes, uppercase, registry and the app's own tests), npx tsc --noEmit, npm run build and npm run test:e2e. Check the view at 375 px, with keyboard only, in both themes and with reduced motion (DS §11.8), and in the dev kit if you added a primitive.

Known gaps and divergences

These are recorded for the Known issues page.

  • Exported $schema URLs do not resolve. json-export.ts writes /schemas/<slug>/<kind>-v<n>.json URLs, but no build step generates the files and public/schemas/ does not exist; store/schema.ts (lines 112 to 115) describes a prebuild that does not exist and a different path.
  • The shell's local project saving is unused by the apps. No app definition sets storage or session; the autosave, ProjectChooser, SaveStatus, ConflictDialog and ⌘S save paths are exercised only by the dev kit. pages, TopBarActions, paperMain, palettes and onSave are also unused, and every app's PAGE_IDS is empty.
  • Shared code depends on app trees. engines/rdkit/rdkit.worker.ts (lines 23 to 25) imports Molecule's chem.ts, engine.ts and library.ts, and engines/rdkit/protocol.ts and client.ts import Molecule's result types; objects/study-export.ts (line 32) imports @/apps/assay-analyser/example-export. A change in those app modules can break the shared engine or Study export.
  • Stale breakpoint comments. shell/use-shell-layout.ts (lines 11 and 14) and tokens.ts (line 84) describe xl as 1,280 px and lg as 1,024 to 1,279 px; the code uses 70.4rem and 88rem (1,126.4 and 1,408 CSS px). The same header (line 12) says the rail choice is kept in suite:<slug>:rail; in practice only the preview uses per-app layout and it persists nothing, while full workspaces use suite:suite:rail.
  • Stale route paths in comments. src/apps/page-ids.ts (line 3) names src/app/(apps)/apps/[slug]/app/[page]/page.tsx, which no longer exists; the route is src/app/(workspace)/[app]/app/[page]/.
  • Copy in definitions. src/apps/lab-calculators/definition.ts (line 78) and src/apps/sequence-toolkit/definition.ts (line 89) type share.contents sentences inline, against the rule that copy lives in content/.
  • Design system body out of date in places. DS §6.5's status-bar recipe and several §6 recipes still contain uppercase and wide tracking, which the code and no-uppercase-labels.test.ts forbid (principle 7). DS §11.6 and §11.7 still describe /apps/_kit with a NODE_ENV guard, and §11.7 lists a tiers.test.ts that does not exist in the repository. D-0036's "panel headings use an accent rule and divider" is superseded by principle 5; Panel follows the principle.
  • No separate shell size budgets. The shell-core, dialog-group, DataTable, motion and Floating UI budgets of DS §7.3 and §11.7 are not entries in scripts/size-budgets/.
  • Education marks are outside the mark grammar test. education-mark.tsx draws with strokeWidth={1.8}, and some of its marks have no specimen point or two points in one path; marks.test.ts checks only mark.tsx and icons.tsx.
  • The chart palette's validation is recorded, not repeated. The colour-vision figures in chart-palette.ts come from an external validator run; no test recomputes them.

Bench and Study dashboard composition (11 October 2026)

The CEO authorised a bounded dashboard pass using existing storage. Bench leads with a compact full-width selected Study summary, followed by a full-width three-item, Study-prioritised resume queue. Studies read as research questions; pinned records occupy a separate shelf. Study cards show actual updated dates rather than object/view counts that omit owned work. The guarded Bench note stays mounted below those sections; workflow guidance, full library and tool shelf remain reachable. Empty areas use short explanations without seeding records or repeating starter cards. Study context precedes Resume and notes at every width. Studies and pinned records each use the full width; comparable entries share responsive card grids. Two pins appear initially with an explicit expansion control. This corrects the unequal paired sections that left large empty columns throughout the page.

Bench rows show concise saved times and short Resume/Results actions with full accessible names. Work details retains mounted note editors and secondary facts. Recorded outcomes have their status and label in the disclosure summary, with numbers, original timestamps and qualifications together inside. Changed-input, loading and unavailable warnings stay visible. Dirty or saving notes reveal their enclosing disclosures when an action is blocked; filtering or collapsing the library first flushes pending work, and live reordering retains edited rows.

Study keeps its five sections and mounted editors. Overview distinguishes the research brief, planning checklist and recent saved inputs. Management is a compact disclosure with feedback visible when closed. Materials explains linked records; Evidence explains literature findings and links to Work for explicitly recorded results. Resume still uses existing newer-input consent and pending-save barriers; result history and qualifications remain separate from editable inputs.

The object library labels saved-input owners separately from explicit record membership. Both layouts and Study/name filters use the same read-only projection; all manifests per input reference are retained. Unresolved owner IDs are not unassigned, archived owners remain resolvable, and unavailable/failed reads cannot establish negative ownership. The compatibility Resume map chooses the most recent updated manifest with a deterministic ID tie-break. Multiple manifests have a visible explanation of that Resume choice. No scientific engine is loaded to compute dashboard summaries, and no storage records are created or moved by these presentation relations. See the 11 October coordinator CPO §6 and CTO continuity memos for the acceptance contract and scope.

The Bench (13 October 2026, D-0083 and D-0084)

The CEO's Bench screenshot of 13 October 2026 is applied on D-0082's shell (object-list.tsx, workspace/bench-*.tsx, bench.module.css; D-0084 item 4, which decides where the morning's mock-up, D-0083, and the screenshot differ). The page (as changed by the CEO's feedback of 14 October 2026): the serif greeting behind the accent bar ("Good morning", "Good afternoon" or "Good evening" by the browser's hour, before noon, before six and after, with the name, benchText.greetingName, a placeholder "Drybench scientist" until accounts carry one; BenchGreeting renders the standing title "Your research, connected." on the server and swaps in the greeting once the page runs, so the hour never mismatches hydration) with the lede "Explore. Analyse. Record. All in one dry-lab workspace."; at the right, Import (the Study recovery controls at #recovery) and New study (the page's primary key). The hero card and the working Study's brief beside the headline are gone: Recent studies, most recent first, carry the Studies. Then the notices; then two columns from a 58 rem main container (the left minmax(0, 1fr), a column container for its own grids, the right 21 rem), with the guide and Recovery and imports full width below; one column under that width and on phones. There is no section tab row: the full lists are views of the same page named by the hash (below). Section and panel titles are the serif face at 1.25rem (panelHeadingDense).

  • Counts (BenchStats, heading the left column with Recent activity beside them at the top of the right, the Bench's own <dl> markup rather than ReadoutStrip, whose tile sits at the top right with the label above the number; each card here holds the glyph in a tinted tile at the left and the number above its label): Studies (not archived; "n archived" as the detail), Pinned objects (the number in the working Study as the detail), Saved results (useResultCount; the newest result's app and time as the detail) and Saved work (every live manifest from useSavedWorkState; "n with results" from latestResultId). A count the store has not answered yet says "Counting…", never a dash (D-0084 item 5); when all four are a confirmed zero the strip is not drawn (D-0086). Each card's tile is coloured by an accent key through data-accent.
  • Recent studies lead the left column (the CEO, 14 October 2026). The saved-work queue, Resume saved work (WorkItemsPanel compact bench, the task cards in a row, quiet Resume keys and Search all saved work), is no longer on the overview: it heads the #objects view above the library, hidden there once the manifests are known to be none. Recent saved work stays in Recent activity.
  • Recent studies (StudyCards; four, then "View all →" opens #studies): each card carries a cover across its top (RecordCover, objects/record-preview.tsx), drawn from the Study's own records and never fetched: the 2D depiction of its first compound member with a saved structure (the RDKit drawing the record preview makes, through the Bench's one lazy worker lease, started when the first such cover mounts), else the plate grid of its first assay member with a saved dose-response layout (PlateGrid, roles and exclusions only), else a generated figure in the owning app's accent seeded by the Study's id (studyCoverRecord chooses; Figure draws): a protein ribbon-like curve for a target, a strand for a sequence, a circle for a construct, a drawn molecule otherwise. The figure stands until a depiction arrives and when drawing fails. A "Pinned" chip sits on the cover of a pinned Study (StudyData.pinned, set by setStudyPinned). Beneath: the Study's number (studyNumbers: its place by start date among the account's Studies, display only), the ⋯ menu (Open Study, Work in this Study, Pin to the top or Unpin, Open notes, Export study, Archive), the name, "n objects · n saved work", a "Current Study" chip and the person's status chip (StudyStatusChip), the contributing apps' marks and "Edited x ago". Most recently opened or changed first (prioritiseBenchStudies without a working Study: archived ones after the rest, examples after real Studies), the CEO's feedback of 14 October 2026. Four to a row where the column is 37 rem or wider, fewer beneath; the Start a new study tile ends the row. Empty: D-0039's starter (BenchStarter) with the walk offer, never a seeded record (left-aligned, the walk's link on the primary key's row and its source line beneath in small text, the CEO's feedback of 14 October 2026); the quiet "Load example Study" key stays in the section's head.
  • Your tools: the bench apps as cards, then Literature (to /literature, the text glyph, "Papers & evidence") in Cell's place (the CEO, 14 October 2026; Cell stays under Learn), with the mark in a tinted tile, the short name, one line and an arrow, five to a row where the column is 47 rem or wider (four from 37 rem), then a quiet quote card on a tinted ground filling the last row ("A faster path from data to discovery." with the suite's name, from benchText); "Browse tools" goes to the landing page's directory.
  • Pinned objects (PinFilter, PinnedTable, PinnedMenu; between Recent studies and Your tools since the CEO's feedback of 14 October 2026): a panel whose head holds the title, type chips with counts, "View all →" (to #pinned) and a ⋯ to the library; the table is the shared RecordTable (bench-pins.tsx): the mark and name, the type word with its mark in the app's colour (saved work as "Saved inputs · App", D-0045), the Studies (the owning Study and the Studies that include it, each a link whose visible text is the Study's name and whose accessible name is the relation sentence, "Work saved in X" or "Included as a record in X"; the unknown-read words as the library's), the record's own note or its tags (muted, truncated), when it changed, and the row's keys (Resume for saved work; ObjectActions variant="row": Pin and the ⋯ holding Send to, Add to Study and Open its page, as the record's page offers them). Five rows; the table scrolls inside its own labelled region. Empty: the placeholder sentence with "Choose a tool" (BenchPlaceholder, dashed), never a header band without rows.
  • Right column: Recent activity first, then Quick start (Tabs kind="segment": Templates opens the New study dialog or an app; Example data lists the example Study, the guided walk and each app's labelled example from content/apps/app-examples.generated.ts; Import points at #recovery, which ManagementDisclosure opens by its id, and at the apps that read files); Recent activity is ActivityPanel ( results, numbers, saved work, objects added to a Study, versions saved and Studies changed, each as a verb line with the subject and "x ago" under a tinted tile joined by a hairline; five rows, "See all →" in the head and "View all activity" beneath open #activity; the full sentence stays the link's name for screen readers and the specs; empty, the sentence with "Open a tool"), the example card, the Bench note (BenchNote compact, the panel's card chrome and serif title; without a working Study, a "Bench note" panel holding the sentence and "Create a Study for your notes", which opens the New study dialog) and Continue where you left off.
  • Views of the page (viewOf): #studies lists every Study as cards (with Include archived), #pinned the pinned records in the library's shape, #activity the whole timeline and #objects (or an object type, #target) the saved-work queue and the saved-object library; each replaces the overview under a "Back to the Bench" link (BenchBack, to #bench, the page's root) and keeps the headline and the keys; the hero and the counts belong to the overview. A view opened from a link comes into view with focus on its heading. The library loads on demand and moves focus to its own heading when it mounts only if focus has not moved since the view was asked for, or has fallen to the page (focusOnShow, a guard the Bench passes; REL-237): a key pressed in the rail while the module loads stays in the rail. The "Find DNA fragment" search sits with the library in the #objects view. The count cards open #studies, #pinned, #activity and #objects (the sidebar's rows for them were removed on 14 October 2026); without read access the overview shows the library gate under #studies and #objects whatever the hash, and Recover browser work opens #objects.
  • Relative times (relativeParts, ago) say "x minutes/hours/days ago" for a week and the date beyond it, with the absolute time in the <time> title; they render after the stores load, never on the server.

23 October Bench hierarchy refinement. The CEO removed the overview's counts strip and kept the suite's 110% desktop scale while tightening Bench content. When a browser-local recent location exists, Continue heads the main column: one compact featured destination, two smaller visible destinations and a disclosure for the rest. Its app and relative time stay visible, and the absolute time is in the <time> title. Recent studies follows; one pinned Study spans the main column and the other Cover cards use two columns from a 34 rem column width. The #studies view uses the same two-column card grid. Cards keep their real objective and object/saved-work counts, with an explicit Open Study key in the footer. No result count is inferred from a work manifest's latestResultId.

Study cards share cover and body tracks within each grid row, so a wrapping title aligns the neighbouring body without clipping either title. All titles use the same treatment, including the pinned card. Small cards reserve one objective line when none has been written; status/app marks and the footer form one bottom-aligned group, retaining the compact status-row height even without badges. The desktop minimum remains 19 rem and the pinned card spans the width. Include archived sits below the cards in both Recent studies and All Studies, using the existing checkbox and filter state.

Recent activity keeps five chronological events in the overview and now displays each event's full verb-and-subject sentence; the quieter second line gives Study context where it is known, app and relative time. #activity groups the same events by local day. Quick start keeps Start, Example data and Import as separate modes: Start shows three task rows plus Browse tools; Example data shows its Study and walk entries, two app examples and a disclosure for the remaining app examples. These changes alter presentation only, without changing saved data, destinations or scientific values. The CDO, CPO and CSO rulings are filed in the 23 October Bench hierarchy memos.

The generated figures (workspace/bench-figures.ts, pure, with bench-figures.test.ts; drawn by bench-figure.tsx): a drawn molecule (benzene-like or five-membered rings at one bond length, joined from their facing atoms by short zig-zag chains, one outward substituent each, so no bond crosses a ring; the CEO's feedback of 14 October 2026), a protein ribbon with a helical stretch, a double strand with rungs, and a plasmid circle with feature arcs, each from a seeded generator (FNV-1a over the seed, then mulberry32) so the same seed gives the same picture everywhere. Decoration only: aria-hidden, in the current accent, never a fetched image and never a depiction of a real record.

The saved-object library (objects/object-library.tsx, the redesign atlas's frames 174 and 175): a view of the Bench (#objects; #pinned for the pinned records) with the serif title, a labelled List/Cards segmented control with icons and words (List is the default; suite:library:layout remembers a choice) and the filters (Search, Type, Study, Sort) beside it; List is RecordTable's rows, Cards the rich RecordCard; 24 per page with Show more, which moves focus to the first new row's name link (or card's name). The library states stand: Study filters wait for membership information, unknown membership is not "unassigned", a read failure has Retry, no matches has Clear filters (solid), and no objects at all has the sentence with "Choose a tool" (dashed).

The saved-object record page (objects/object-page.tsx, the atlas's frame 176): the header (the type or owner eyebrow, the serif name, the ref and tags) with the record's cover at its right where the preview model has one (the compound's depiction through its own RecordPreviewProvider, the plate grid, the protein figure); then two columns from 58 rem: the record's tabs and content at the left (the type's tab registry: Overview, the apps' tabs, Literature and Evidence where the type has them, Mine with notes, tags, numbers and files, Used in), and "Object actions" in a panel at the right: Resume saved work (saved inputs), Pin, Send to and Add to Study (ObjectActions variant="bar", each only where the type supports it), and the Studies the record is in (its in-study links) or the sentence that it is in none.

The example Study (workspace/example-study.ts, copy in content/apps/example-study.ts): the "Load example Study" key in Recent studies' head and the Quick start's Example data row create a populated Study through the store's ordinary verbs under a run's write context (objects/write-context.ts, walk example-study@1), so the Study, its two records (EGFR, UniProt P00533; erlotinib, ChEMBL CHEMBL553, both pinned and carrying no facts until the apps retrieve them), their links and its Experiment all carry the run's mark and "Remove the example" takes them away in one action. The Study gets a worked objective, next action, notes, two next steps with due dates, the status "In progress" and the pin; no number, result or fact is written. Its banner is its own (ExampleStudyBanner chooses by walk). Never auto-loaded; a quiet key, never the page's primary. "Remove the example", from a record's or the Study's page, leaves for the library view (/o#objects), where what is left is listed.

The Study dashboard (13 and 14 October 2026, D-0083 and D-0084)

The CEO's Study mock-up (D-0083) and then the CEO's Study screenshot (D-0084, the redesign atlas round) are applied in objects/study-page.tsx with workspace/study-*.tsx, objects/study-material-card.tsx, objects/study-material-figure.tsx, objects/study-depiction.tsx, objects/study-figure.ts and study.module.css. The page: the serif name behind the accent bar with one meta line in one text node (the Study's number, "On this device" behind a lock glyph, when it started behind a clock glyph, the counts of saved tasks and recorded results, and the save state as "Saved x ago", divided by hairlines that read as " · "); at the right, in one row as drawn, the working-Study toggle, the status chip (StudyStatusMenu: the person's own word from StudyData.status, chosen from a menu, "Set status" until chosen), the primary key (StudyTaskKey: the linked next task's label, resumed with the consent for changed inputs; "Choose your next task" opens the brief's editor at its task select; Retry when the tasks could not be read) then a cog key, Manage Study (the CEO's feedback of 14 October 2026: ManagementDisclosure headless, so the operations have no summary row of their own; the cog toggles them as a titled card under the notices, aria-expanded, and its heading takes the focus), the primary key and a ⋯ holding Manage Study (opens the same card), Rename, Save version, Export study, Archive and Delete. The notices and the example banner follow, then the Manage Study card while it is open.

Under them a grid with an 18 rem right rail from 58 rem (21 rem under D-0083; narrowed so three material cards sit in a row as drawn): the main column holds the research brief as a callout (StudyBrief: a target tile, the "Study objective" eyebrow, the objective in serifLead, the next action and the linked task beneath, Edit at the right opening the same editor; the region keeps its name and the editor its labels), the progress line (StudyProgress: Question, Materials, Methods, Results and Conclusion, each done when the Study holds an objective, an object, saved work, a recorded result or a conclusion, the first not done current; each line the fact behind it, never a claimed stage) and the five section tabs.

  • Overview shows Linked materials (three materials; the action placeholder with "Link a record" when there are none), Evidence beside Notes, and Recent saved work.
  • Cards or List (the CEO, 14 October 2026): a Segmented View control, Cards or List, sits in the head of Linked materials and of the Materials tab's Linked records panel whenever the Study has materials. Cards (StudyMaterialCard, below) is the default; List (StudyMaterialRow, study.module.css .materialRows) shows each material as one ruled row: the app's tile, the name linking to the record, the type word and the same materialFacts on one line, when and from where it was added, the member's note, up to two app tabs from 640 px and the ⋯ with Remove. The choice is a presentation preference for this browser, kept in localStorage under suite:study:materials-layout (objects/library-view.ts, the same store shape as the library's suite:library:layout), shared by both places and across tabs, held in memory when the write is refused, and never part of a research export.
  • A material card: the app's mark in a tinted tile, the type word and a ⋯ with Remove; the record's name in the serif face; the record's thumbnail at the left beside its facts in two columns; Open record and up to two app tabs beneath. The facts (materialFacts) are the record's own, at most four in the drawing's order: up to three saved facts (libraryFacts, formatFactValue, each still carrying its source), then the record's public identifier unless a fact holds the same value, then a saved plate layout's format, plates and dataset and a file's name; when fewer than two remain, when and from where the record was added fills the list; never a dash. The thumbnail (StudyMaterialFigure) is the record's own picture where one is real and a design where none is: a compound's RDKit depiction drawn from its saved SMILES when the card comes into view, through one client for the page (StudyDepictionProvider, the same lazy record-depiction-client chunk and budget as the Bench's previews; restarted when the page or the account scope goes); an assay's saved plate layout as a grid of well roles (roles only, never readings); for a protein, a sequence or a plasmid a flat figure in the app's accent seeded by the record's identifier (study-figure.ts: a cartoon-like backbone with a helix and a strand arrow over faint surface blobs, two strands with rungs, a circle with feature arcs; the same record always draws the same figure), aria-hidden and titled as a design, never a fetched image; the type's glyph or the app's mark otherwise.
  • Evidence (a panel): "+ Add data" is a menu of the paths that already add data to a Study (each app that records results, to open it, and "Link a record") and a ⋯ with View all results and Literature evidence; line tabs Results and Literature with their counts (the recorded results; the Study's evidence notes and its papers without one, the evidence table's rows); the Results tab is StudyResultsTable (the first seven of useStudyWorkspace's results with their app, saved task, time, reporting status and whether the conclusion cites them, "Not cited" in words, each opening the recorded result; a header band and hairline rows), or the action placeholder with the same Add data menu when there are none, or the failed read with Retry; Literature is the existing evidence table.
  • Notes (StudyNotesPanel): tabs Research notes and Meeting notes. Research notes is the editor (StudyNotesEditor): an editing surface (role="textbox", aria-multiline, aria-readonly without write access) showing the notes as rich text and storing them as Markdown in StudyData.description (workspace/study-notes-markdown.ts: the notes allowlist's dialect plus ##/### headings, underline stored as _emphasis_ since the allowlist admits no underline, drawn underlined in the editor and read as emphasis elsewhere, and links to the suite's own /o/… and /study/… pages beside http(s) addresses), so the Bench note, exports, the Study report packet and search keep reading the field as plain text. The surface is built from the parsed blocks with DOM calls and serialised back on every change; pasted content arrives as plain text, so no image or foreign markup is stored; typed markers are escaped so a plain paragraph is stored exactly as typed. The toolbar: a Text style select (Normal text, Heading, Subheading), Bold, Italic, Underline, bullet and numbered lists and Link (an http(s) address), each labelled with aria-pressed for the states, arrow keys moving along it; an image key is left out because a note has no real image to hold. The footer: the real saved time ("Saved x ago", "Saving…", "Not saved yet" or the failure) with the character count from 90 % of the 20,000-character limit (an insert past it is refused, a paste cut to fit), "@ Mention" (a menu of the Study's records that inserts a link to the chosen record's page) and Save (flushes the autosave now). Autosave, draft protection, the revision check and the "changed in another tab" states are the page's, unchanged. Meeting notes lists the Study's frozen meeting outputs; Prepare meeting view stays in the rail, at its foot, so it is on every tab and the rail's title row stays as drawn. One editor instance moves between Overview and the Notes tab, where it is full width.
  • Work, Materials and Evidence keep their contents (Materials: all cards by type, then Saved app views; Evidence: the literature table, the Assay panels across the Study's assays and the Study report).
  • The rail (StudyContextPanel): Provenance (the Study's start, objects added, work saved, results recorded, versions and the change history's edits, newest first, six then Show all; "View graph" opens StudyGraphDialog, the same relations drawn as a small SVG with the Study at the centre and its records, saved views, saved work and results around it, each joined to what it belongs to, inside a scrolling region at its own width, with the relations as a list beneath for keyboards and screen readers); Linked items (the members, each with the mark of the app that owns its type in that app's tinted 30 px tile, as Provenance draws its rows, and a pin for a type no app owns; the generated thumbnails read as broken glyphs at that size, the CEO's feedback of 14 October 2026; and the saved views, as rows to their pages; "+ Link" opens StudyLinkDialog, the person's saved records not yet in the Study with a Link key each, written through the store's addToStudy); Related studies (the Studies sharing an object, with their number, name and status; "View all" opens the Bench's Studies tab; hidden when none); and Outcomes (StudyNextSteps panels="rail": the to-dos as a checklist with their due dates and "+ Add" in its head; the editor gained an optional Due date; completing one still records no scientific result).

useStudyWorkspace reads the Study's saved work, results, Experiments and change history once for the page (the Experiments panel's effect, plus studyStore().history). StudyData gained the optional status, pinned and todos[].due under D-0083 (studySchema admits them; no schema version change); D-0084 adds no field.

The 11 October section above is superseded where it describes the Bench's full-width single column, "Your studies" and "Pinned records", and the Study page's header, overview and panels. bench.spec.ts, bench-dashboard.spec.ts and rich-records.spec.ts were amended citing D-0083; the Study specs (workspace-study, study-dashboard, bench-note, bench, objects) citing D-0084 for the editing surface and the Outcomes section.

Rich saved-record cards (11 October 2026 candidate)

Library Cards use objects/object-library.tsx’s RecordCard; the pinned table and the library's List share RecordTable (workspace/bench-pins.tsx), and the Study cards and the record page draw RecordCover (D-0084, above). record-preview-model.ts projects existing saved data: UniProt identity, exact compound or validated Molecule-workspace SMILES, and supported dose-response Assay input layouts. Cached facts retain their source and date; RDKit facts are saved calculations with their cache timestamp, separate from the record’s updated date. Protein records do not carry structure images. The plate thumbnail shows categorical well roles and first-plate exclusions, never readings or fitted responses; it preserves synthetic/published/edited qualifications. Missing or unsupported payloads retain the card’s ordinary facts or fallback copy.

RecordPreviewProvider spans pinned and library cards without remounting editors. Show structure dynamically imports record-depiction-client.ts, which statically imports the existing shared RDKit client. One lazily created client serves the whole Bench. No worker starts on a card's render, focus or scrolling; a Study cover with a compound member (D-0084) asks the same lease when it mounts, so one Bench visit starts at most one worker and only when such a Study is shown. A pending import is shared; scope and caller-generation checks prevent obsolete requests from starting a depiction. Already submitted work can finish when one card is removed, but its result is ignored; individual cards never terminate a worker another card needs. Scope change or provider unmount invalidates the lease and calls restart() to terminate the shared worker. Returned light/dark SVG strings are rendered as image data URLs; a theme change selects the matching string without recomputation. The shared depiction operation may also initialise engine patterns and compute an InChIKey; the card does not use that key or call descriptor analysis. No preview writes a record or fetches a drug library.

The explicit client download has its own budget in scripts/size-budgets/record-previews.json under the CTO’s 11 October ruling. Worker and vendored WASM downloads are separate from that client budget. The 225 KB /o initial-route ceiling remains unchanged. Browser checks cover the no-start-before-click boundary, one worker across pinned/library cards, failure and keyboard retry, theme, scope cleanup, plate captions, List mode and layout.

October 16 usability changes

Numeric controls reserve six characters plus padding even beside a unit selector. Assay explicitly opts into stacked phone example buttons on DiscoveryStart; other starts retain their existing example layout. Calc's own amber accent is generated through the registry, preserving Bioreactor indigo, Pipeline violet and all chart/status tokens. PubChem palette results use the registered singular Molecule name.

The guided walk initially collapses step descriptions after step 1 below 768 px, with a disclosure to reopen them. Active changed-input warnings remain expanded and the existing paused/run state and Leave flow remain.

Stock editing and explicit-draft navigation (16 October 2026)

SuiteFrame provides one on-demand stock creation dialog above its keyed library rows, so changes of scope hide rather than discard a draft. Saved compound and private construct Object actions can preselect their exact saved revision; no material fields populate physical quantities. The stock overview keeps visited tabs mounted and asks the explicit-draft barrier before changing tabs. research-draft-navigation.ts registers the same save/discard guard used by useResearchDraftGuard for imperative same-policy routes such as palette results. It does not introduce stock autosave or change named-work autosave timing.

Explicit unsaved research forms now also register a synchronous history guard in research-draft-navigation.ts. The workspace layout installs history tracking synchronously through ResearchDraftHistoryBoundary before editors are available. Public pages retain only the global cross-policy interceptor. Cancelable same-document Navigation API traversals are refused before commit; otherwise a capture-phase popstate listener returns by the actual native entry index (or a known, href-stamped legacy ordinal) before Next restores another tree. Compensation preserves Forward history, Next's state fields and scroll, and never calls go(0). Native hash push/replace events update the accepted entry separately from traversals. The legacy fallback cannot infer a delta after an unknown writer removes an entry's marker: it retains the draft and suppresses routing without pretending that the address was restored.

Bench recovery and Study management mount their optional operations on first opening. Once opened, controls stay mounted while closed, retaining active work, conflict/recovery state and the existing visible feedback. Readable report readers and PDF fonts load only after an explicit export request.

Dismissing the phone Menu for a presentation

The shell context exposes optional closeMenu() (CPO ruling, 16 October 2026). It idempotently closes only AppMenuSheet; it does not change scientific inputs, save work, flush drafts or navigate. Calc’s plate-print action calls it after synchronously capturing the current inputs, so the parent modal cannot cover the new in-place preview. Other export actions and the drawer’s existing close/focus behavior are unchanged. The phone plate-print browser test verifies that no dialog blocks the preview and that Back preserves the source inputs.

Methods from a selected recorded result (16 October 2026)

NotebookCopyControl adds a Methods text section before the existing notebook iframe. Prepare loads recorded-methods.ts and only the selected app's historical reader. A visible paragraph, all qualifications and a compact historical receipt precede the explicit copy/download controls ("Copy methods text": it copies the methods text alone, which holds no reference checks; the methods dialog's "Copy methods and reference checks" is the control that carries them, audit of 20 October 2026, M4); expanded evidence includes the text template, exact hashes, both revision labels and both method-pin layers. Existing notebook HTML/text output is unchanged. Unsupported coverage has an explanation and no methods copy action; hash/integrity and lazy-load errors remain distinct.

The session is keyed by scope, access epoch, result ID and result hash. Layout cleanup invalidates unmounted sessions; a dialog generation invalidates closed preparations. All async operations check ownership around awaits and immediately before clipboard/download. Closing a pending methods preparation is allowed; clipboard operations keep the existing busy-dialog behaviour. One existing InlineStatus announces errors/success. Long ordinary text wraps at 375 px.

SendOptions.isCurrent is an optional caller-owned lifetime/input check around asynchronous Send to operations, including the lazy loader, record reads and handoff creation. The Molecule palette and walk use it with their result identity and workspace epoch; existing callers retain their prior behaviour.

Public reference workflows (16 October 2026)

ReferenceWorkspace composes the normal shell with a compact dataset/reset bar above the tool and a server article below it. Reference tools use H2 headings under one main landmark; the article owns the sole H1. Passive object learning is suppressed for public examples. See Public reference workflows for the shared route, state, source and export contracts.

Repeat setup in the Study's Experiments panel (contract 06, 17 October 2026)

experiments-panel.tsx puts one quiet Repeat setup button beside Add experiment, for a writable, unarchived Study, even when no pair is eligible. repeat-setup-action.tsx downloads repeat-setup-dialog.tsx (and through it the Calc and Assay adapters and src/lib/workspace/repeat-setup.ts) only when the button is pressed; a failed download says so. The dialog is one Dialog (size lg, one scrollable body, stacked fields below sm) with three stages, Saved pair → Inputs → Review, then the outcome in place:

  • Saved pair: two selects that start empty, listing eligible saved tasks with their revision and Experiment; every ineligible Calc or Assay task in the Study is listed in a disclosure with its specific reason and Open source task. Each chosen task gets a stacked summary card (app, label, saved revision, Experiment). A selection pair is read fresh each time; a refusal names its reason with an Open action for each task it involves and Choose another pair; the list of unusable tasks opens by itself when no pair is eligible. When this tab holds a named editor for a selected task (hasNamedWorkspaceEditor), Next waits for Use saved revision or Open task to save changes; nothing is saved or flushed by the dialog.
  • Inputs: replacement material (saved compounds only), stock concentration, unit and basis, carrier and percentage (never prefilled from the source; the source values are shown beside them), the dose design prefilled from the source with "Not recorded" for a missing value and a required "I have reviewed the dose design", the optional plate sheet (intermediate and stock on hand cleared; with no source sheet, "Add a plate sheet" starts empty and its plate and direction are chosen), the analysis settings, the analysis-block choice and explicit keep/clear for context and free text. Problems from preparation are shown at their fields.
  • Review: editable names and the saved sources, always shown (a name edit marks the preview stale and re-prepares the command after 400 ms; a failed read stops and offers Try again), the saved sources, What changes, then Changed, Kept after review, Cleared and Still needed with the CSO's planning and control-role qualifications, the normalisation's needs by mode, each unentered allowance or limit by name, and the engine's flags with the thresholds applied; Create planned Experiment.
  • Outcome: Planned Experiment created (Open Calc, Open Assay, Done; the new Experiment becomes the tab's working Experiment), This setup was already created (listing what survives, "Task unavailable here" for the rest), Saved inputs changed (Review updated saved inputs keeps the replacement entries), Not created with Try again, and Creation not yet confirmed with Check creation (the same prepared command; no edit or second create meanwhile). An account change discards every entry and the preview, leaving only its message and Close.

The Experiment editor shows a schema-2 Experiment's receipt as organisational lineage (RepeatReceipt): the source Study, the source tasks' historical labels and revisions, and the two new tasks, each with Open Calc or Open Assay (through the ordinary resume) when it is among this Study's saved work, else marked "Task unavailable here". Copy is in content/apps/workspace-repeat.ts.

Public-page motion (18 October 2026)

MarketingMotion is a null client leaf enhancing complete server-rendered public HTML. The landing and About routes mount it once. An IntersectionObserver plays each marked scene once, with native SVG/WAAPI via the existing motion helper. Paths draw, decorative nodes arrive, and rings rotate slightly into their final position. Text and controls stay available; graphics use registry app marks and semantic palette tokens. Initially visible scenes settle immediately and the controller does not own the first-visit showreel.

OS/settings reduced motion, data saver, hidden documents, showreel state, width changes and interaction settle the owned animations. Pointer cancellation applies to interactive elements; focus settles the containing scene. Initial hash navigation and hashchange cancel running animation, safely decode and locate the destination, settle its containing section’s scenes plus visible/above-viewport scenes, and leave later offscreen scenes available for their first scroll entry. A malformed fragment has no destination and cannot throw. popstate, pagehide and persisted page restoration retain whole-page settlement. The controller restores only its owned inline properties. No animation dependency, scientific engine, tracker or state store is added.

The landing sections now use illustrated editorial compositions. About routes use a split subject hero, real preview, full-width editorial capabilities (two columns on desktop, one on phones) and reference starters before compact source disclosures. ProductVisual is server-rendered abstract identity geometry, not calculated evidence. SourcesBlock and SourceList accept an opt-in compact/disclosure presentation on About; docs retain their full presentation. The source directory starts closed, with individual source disclosures inside; each keeps its complete registry facts, credit and notice links. Limitations remain in the existing app documentation, with the concise research-use statement and full-policy link retained on About (CEO direction, 18 October 2026). Shared themes, source data, preview gates and workspace behaviour are unchanged.

The landing page's guided example (landing-workflow.marketing.tsx, server only; LandingWorkflow in landing-blocks.marketing.tsx puts it under the section's heading, above its one link, "Try the guided example", and the walk's source note) is three static steps in an ordered list: Molecule, Calc and Assay, in a row on desktop and stacked on a phone. It was rebuilt this way at the CEO's request of 20 October 2026, on Codex's approved design of 18 October (D-0103), replacing the Gemini scene of 18 to 20 October (a stage rail, an app-screen panel, a spotlight overlay and a one-pass player). Each step is laid out like the trust section's columns: a hairline rule, the app's mark and short name in its accent (data-app on the step), the step's heading and sentences (content/apps/landing.ts, workflow.steps), then one card of key figures, its note held at the card's foot under a rule. On desktop a CSS subgrid lines the three cards up, at equal height. Molecule's card shows the salt's and the parent's formula weights and the shortfall if the parent's weight were used (the walk's {shortfall} slot, in the CSO's wording of 17 October 2026). Calc's, labelled "Illustrative preparation plan · not measured results", shows the mass to weigh for the stock and the dilution series with its eight planned concentrations, plus the solvent line "In your plan, …". Assay's shows a small static plot of the 23 finite published readings (raw MFI against log dose, no normalisation, fit or curve), the missing well named from its own fixture row, the CSO's source line and "The Broad made these plates; this example did not.", and a closed native table of every series row with plate and well (the missing reading stays "Missing"), with a link to the dataset. Every word is content/apps/landing-workflow.ts's and every figure is filled in by landing-workflow-data.ts: the walk's values from j3Slots(), the atomic-weight table from formula-mass.json, the dilution ladder from dilution.json, and the readings and missing well from the published PRISM fixture (public/verification/assay-analyser/0.2.0/prism-erlotinib-hcc827.json). The same module exports the plot's frame (PLOT, doseX, mfiY), which the browser spec uses to recompute each point. The structure drawing is gone; the CSO allowed removing it.

Since D-0112 (21 October 2026) the section comes first under the hero, with the CSO's heading and lede (REL-441, closed) and his step fixes. The code is in three parts. landing-workflow.marketing.tsx is the scene: it renders whichever WorkflowExample it is given (its steps, one WorkflowCard per step, its walk link, action, label and source note) and holds no example's words or figures. landing-workflow-j3.marketing.tsx is this example's cards and entry, and landing-examples.marketing.ts lists the examples (one today), so a second or third example (D-0114 adds them, with a switcher, in a later change) is one more module and one more entry. At desktop width (lg) the ol is one grid of seven rows and one column per step (two or three; an example of two steps takes two columns, D-0114); each step is a subgrid spanning them (heading, sentences, the card's four rows, the hand-off line) and each card a nested subgrid of four: caption, key figures, figure and foot, so captions, key figures, figures and foot rules sit level and the figure row takes up the difference. A card with no key figures (Assay's) puts its figure in their row and the figure's own <figcaption> in the figure row, through a third subgrid level; Assay's notes (the missing well, the source line, "The Broad made these plates; this example did not.") are that caption, visible. Every step but the last ends with its handoff line from content/apps/landing.ts, static, with an arrow across at three columns and down when stacked. Stacked (below lg), the cards keep their natural height. Chart tick labels are HTML (data-chart-label, text-xs), not SVG text, so they never scale with the drawing; the plot and the ladder are capped at max-w-md. A key value and its unit stay on one line (whitespace-nowrap, one type step down at lg), and in the words every number is joined to its unit with a no-break space where the landing fills its slots (keepUnits, content/apps/landing.ts, applied by landing-workflow-data.ts and the step texts; the walk's own values keep plain spaces). Nested subgrid needs Chrome or Edge 117, Firefox 71 or Safari 16; an older browser shows the cards in a row at their natural heights.

The section has no controls, no timer and no motion of its own, and no client code: no element in it animates, with or without reduced motion (DS §7.1 rule 3). The shared MarketingMotion owns none of it. landing-workflow-data.test.ts checks the words and figures against the fixtures, refuses "fit", "curve", "IC50", "potency" and "result" in the heading and absolutes in the lede, checks the CSO's six step fixes, and refuses typed copy, a motion hook, a timer or a control in the scene, the example's cards and the list. e2e/marketing/landing-workflow.spec.ts checks the figures, the 23 points' coordinates, the table, the hand-off lines, the notes inside the figure's caption, the row on desktop with its cards and their rows aligned, the stack at 375 px at natural heights, chart text at 11 px or more and each value on one line at seven widths from 1,440 to 375 px, no number parted from its unit at a line end at five widths, and that nothing animates over a minute of Playwright's clock.

Since D-0133 (24 October 2026) the landing's other sections changed in one pass after the board's single sitting on all seven (docs/board/memos/2026-10-24-*-landing-sections.md): the hero's trust line says "Runs in your browser" and its secondary key "See the worked examples"; each example's walk key names its work and the app it starts in, so no two keys on the page share a label with different destinations; the hero tiles (family.marketing.tsx) open the About pages, labelled "About each research tool"; the methods band (LandingTrust) carries three claims in one row at lg, the browser claim third in the CSO's wording with a link to the security page, and the research-use statement full width beneath; the AI section (landing-ai.marketing.tsx) is a band with its benefit rows exported but not rendered (its three-step line was horizontal at lg until the figure below replaced it); Education is tinted with its chart on a surface plate; and the Bench section's figure is landing-bench.marketing.tsx, a server-rendered Study record (four rows: the compound as Work in Molecule, the plan and the readings as recorded Results in Calc and Assay, a Materials & sources row) whose every value is a workflowSlots() slot from the J3 example, with the example's hand-off lines on a traced spine, rows that rise once through MarketingMotion, and no IC50 (that value is in no slot). Its words are content/apps/landing-bench.ts, page-local and not an export field. The band's desktop ceiling in suite-pages.spec.ts is 420 px for the third claim.

Since the AI figure (24 October 2026, later the same day; the CEO: a graphic or animation with the correct text, not a picture and not a tool list) landing-ai.marketing.tsx renders its status, heading, lede and availabilityNote in one column and, beside them, one figure of three examples under tabs, one per app (the CEO, that night: "another two examples with tabs for different apps"): the Calc dilution plan from the J3 fixtures, a Sequence melting temperature from the oligo Tm reference set's CMV forward primer case (public/verification/sequence-toolkit/0.1.0/oligo-tm.json, Biopython's Tm_NN as the oracle, with its ΔH, ΔS and salt-correction intermediates on the engine lines) and an Assay four-parameter fit of the MTT worked example's Compound A (public/verification/assay-analyser/0.2.0/drc-reference.json, R nls and drc as the oracles; synthetic readings, marked so on the card). The Sequence and Assay slots are landing-ai-data.ts (tmSlots(), fitSlots(), AI_FIGURE_CASES naming the cases; landing-ai-data.test.ts fills every example's copy from its slots and refuses a typed digit). The tabs are the worked examples' own :target panels (landing-examples.module.css: the first panel by default, each panel carrying the nav with its own aria-current, so they work without JavaScript); each panel is its own data-marketing-scene and plays once when shown. Each example is three labelled stages on one centred spine ("You ask", "The tool runs", "Your assistant replies"; the CEO, later that evening: the single arrow was out of line and the output was missing): the assistant's question as a bubble, a traced connector, the Calc dilution tool's answer as a result card carrying data-app for Calc's accent, with the mass and the series as its readouts and an engine block beneath them (conversation.engineHeader and engineRows: Calc's registry version with the tool id from mcp-source for the series and the fixture case's mass-for-molarity method id (massMethod, a workflowSlots() slot) for the mass, then the mass as c × V × M(salt) with the fixture's formula weight, the series as top ÷ fold each step, and the references and reference checks in one line; the caption, in the CSO's words, says the series is the tool's and the stock mass is Calc's own method (SCI-650: no MCP tool returns a mass); the CEO, 24 October 2026: the middle stage is the engine the question is sent to), a second connector, and the assistant's reply in prose (conversation.reply, the same slots). Every value is a workflowSlots() slot or a registry value, filled through fill(); nothing is typed and no engine runs. The question and card rise and the connector draws once through MarketingMotion (data-marketing-node, data-marketing-trace); without JavaScript or under reduced motion it is the finished figure. The three-step line, the methods sentence and the tools list left the band; the developer disclosure stays. The Bench figure's rows carry data-app, so each row takes its app's accent rather than the suite's. The Bench section's heading is "The whole experiment, in one place." (the CEO, 24 October 2026, after "Your work stays connected." earlier that evening).

The CEO removed the bespoke homepage concentration explorer on 18 October 2026 because it represented a calculator interface absent from the actual app. Its component, copy and fixture adapter are removed. The connected workflow remains an explicitly labelled illustration of the existing guided example, not an embedded app interface.

Homepage planned assistant connections and local MCP preview (18 October 2026)

marketing/landing-ai.marketing.tsx renders LandingAi after Education and before the closing band (D-0112 item 8, 21 October 2026; from 18 October it sat after the guided workflow and before the research-tool directory), its "Planned" status line visible without opening anything. It leads with planned connections for assistants such as ChatGPT and Claude, explicitly intended future targets rather than tested or available integrations. Its server-rendered explanatory diagram shows Your question → Scientific tools → Results with context, with the engine boundary labelled Hosted MCP connection · Planned and a caption stating those connections are not available yet. Copy comes from content/apps/landing-ai.ts; responseFields supplies human-facing outcome labels, not a protocol schema. The section mounts no assistant, scientific engine, request transport, connection control or state store.

A closed native disclosure, For developers: local MCP preview, contains the implemented four-tool inventory, local-preview status, prerequisites and setup action. The action is a plain link styled with buttonLinkClass, so it does not prefetch the dynamic technical documentation. This local inventory is not a commitment to the future hosted launch inventory. The export preserves the distinction through aiIntegration.localPreview.

The diagram's connector paths reuse MarketingMotion through data-marketing-scene and data-marketing-trace. They remain complete static paths without JavaScript or with motion disabled; prose, controls and numerical content do not depend on animation. The section uses existing semantic tokens and shared marketing components.

marketing/mcp-source.ts extracts identifiers at build time from the existing tool source files without importing the MCP server or engines. The local tool list is checked against the server's registered inventory in its Node test. Export version 4 added aiIntegration and its section-order entry (the export is version 5 since D-0112); the complete contract is documented in Registry and content. The homepage setup link to /technical/mcp-server is the narrow exception authorized by the CEO's 18 October MCP marketing request; noindex and technical security headers remain. This marketing section does not close Q-17 or activate hosted MCP access.

Homepage catalogue selection and motion (18 October 2026)

marketing/landing-catalogue.marketing.tsx supplies LandingTools and landingExport() with one server-side catalogue selection: the registry's catalogueApps (content/apps.ts), the landing apps and any app whose entry sets landingCatalogue: true beside landing: false (Patent, D-0103; the CPO's ruling of 20 October 2026, §7), in registry order. The hero continues using landingApps. An app listed through landingCatalogue reads its About hero lede through APP_PAGES as its description, so the catalogue shows the integrated preview qualification instead of duplicating or broadening the app's claims. No slug is typed in the component or the specs.

The catalogue groups are Discovery, Bench & process, then Papers & patents last (landingCopy.tools.groups; the CEO's words of 21 October 2026, "put literature and patent at the bottom", D-0112 item 6). Patent is in Papers & patents by its registry workspaceGroup, so no registry field changed. Literature’s identity and destination come from SHARED_WORKSPACES; it renders before Patent in the last group. Its icon and promotional description, the last group label and the Patent subtitle are landing content. Since the same decision the cards are compact ("make app card tiles a bit smaller"), and since 24 October 2026 ("make the choose your tools cards a bit smaller", after the CDO's memo of that day asked for compact rows on the phone) more so: the 2.5 rem mark ring sits beside the words (p-4, a base name, an xs description, the Patent subtitle inline after the name, xs links with a 2.25 rem target), so a desktop card is under 11 rem tall where it was about 19 (landing-catalogue.spec.ts pins the ring and the height); before that p-5, a 3.5 rem ring above the name, an xl name and 1 rem gaps. Open carries → (an in-site link; ↗ stays for links that leave the site, DS §9.7). Literature has only an Open action; registered apps retain Open and About. Workspace links disable prefetch. The catalogue is server-rendered and does not mount workspace code.

Cards use up to three columns on desktop, two in Papers & patents, and stack on phones. Each desktop row is a data-marketing-scene with data-marketing-sequence; the controller detects stacked children and observes each separately. data-marketing-catalogue animates a card once from opacity 0.65 and 24 px below its resting position to its complete final state over MS.move (300 ms), with a 60 ms row stagger. Existing SVG trace/node/orbit hooks decorate its mark. Default HTML remains complete, and the normal interaction, focus and reduced-motion policies apply. The catalogue-specific selector lives in the shared controller, so both landing and About JavaScript budgets require measurement even though About markup does not use that selector. No budget ceiling is changed.

The export keeps registered records in apps.items and shared workspaces in apps.sharedTools, with a group on each shared record. See Registry and content for the full export contract (version 6 since D-0114), including the catalogue copy fields.

The product About hero’s AvailabilityLine renders the three productPage.availability benefits: Free to use, Runs in your browser, Transparent methods (CEO instruction, 18 October 2026). It takes no app/advice arguments; the marketing route and internal developer-kit example use the same component. Version/download-size/advice are no longer appended to this row. Detailed app guidance and the lower research-use/policy context remain in their existing surfaces.

About hero subject scenes (18 October 2026)

marketing/product-visual.marketing.tsx selects a distinct decorative SVG scene by registry slug. It is server-only, with no engine, result data, new dependency or client scene bundle. content/apps/suite.ts owns the visible illustration caption. The actual workspace previews remain separate. SVGs use semantic colours and existing data-marketing-node/data-marketing-trace hooks. data-marketing-hero opts into first-visit choreography only when entry is unblocked, at scroll position zero, without a hash, and is not history restoration. Later policy changes retain the normal settle behavior. The unused reveal and label sequences were removed from MarketingMotion; orbit remains used by the landing catalogue. No export-contract change. e2e/marketing/product-hero.spec.ts checks each app's scene in light and dark, one test per app and theme (REL-462), and writes each scene's capture to that test's output folder.

Unified save and Study destinations (18 October 2026)

This amendment supersedes the Save to Bench / visible Pin action descriptions above. Object-backed work uses one primary Save work action. First save opens naming and a Study destination (existing, new, explicitly selected archived, or Without a Study). Repeat save remains visible; Work details is secondary. Record result stays a distinct secondary action, preserving immutable evidence. Actual ownership and View in Study are available inside Work details; the latter reveals the exact saved work.

The destination picker handles failed reads with Retry, blocks saving until the destination is valid, and rechecks account scope after loading its runtime. The picker loads when Work details first opens, so the details' "Save work" (and Save as new's key) is disabled until it has loaded and validated the Studies; a disabled button takes no focus, so keyboard focus reaches it once it is enabled, and e2e/assay-analyser/keyboard-handoffs.spec.ts waits for that before focusing it (REL-461). Adding a material does not change the default Study for future work. Existing work keeps its owner; Save as new uses the chosen destination. Pin is in More, including on object pages.

On a first save into a Study with a primary subject, the sheet's description is workspaceWorkText.capture.primarySubjectSaved, keyed by the first subject's object type (the CPO's ruling of 20 October 2026): compound (Molecule) says the analysis is saved in the chosen Study and its molecule added as a material, target (Protein) says the same of its protein. A subject of any other type, or none, keeps the control's ordinary description.

Study Overview now starts with Study contents: materials and saved work together, labelled by type, newest first, searchable by display names and scientific identifiers. Add material remains available in an empty Study. Attachment receipts open Materials with the exact canonical member highlighted; saved-work receipts open Overview with the exact work highlighted. The fragment carries the tab and member/work identifier. A revealed work remains visible alongside search matches with an explanatory line. The separate Materials and Work tabs remain available. Molecule and Protein show material membership inside Work details.

Optional WorkCaptureControls subjects supplies explicit primary scientific objects; subjectRefs alone may be consumed input bindings and never implies Study material membership. See Storage and state for transaction and legacy behaviour.

The destination chooser and material membership readout load as separate controls. Download failures show Retry; the first-save confirmation stays disabled until the chooser has loaded and validated its destination. The ownership line appears when Work details opens. Destination-only copy lives in content/apps/save-destination.ts. These delivery boundaries do not alter scientific state or save semantics.

The secondary WorkNoteEditor also loads when Work details renders it (CTO review, 18 October). Its lazy component is stable at module scope; existing mounted draft state, navigation guard and close semantics stay with the editor. Import failure has Retry and never implies a saved note. New lazy chunks have explicit measured budgets; this is a delivery optimisation, not a claim that total feature code is free.

Work details opts into retaining opened Popover/Sheet content while closed, so unfinished next-step notes survive close/reopen. Other overlays retain their existing mount policy. Retained closed popovers explicitly hide even without the native Popover API; note navigation guards reopen Work details before exposing the pending draft. Account/work keys still isolate note state. Resizing across the Popover/Sheet breakpoint changes ancestry and is not covered by this retention guarantee.

UI audit refinement — 22 October 2026

The Bench overview uses a unified metric surface, a study starter with an adjacent source-information band, ten peer tool launchers and a separate footer. Bench note and scoped recent locations sit below tools; activity, quick starts and examples stay in the supporting rail. Container queries stack metrics and support panels before their text is cramped. Empty-note and quick-start paths reuse the existing Study starter. Full-list hash views carry their own H1; overview keeps the greeting.

ReadoutContents is the common label, value, unit and qualification presentation used by ReadoutStrip, KPI tiles, Pipeline metrics and Patent dates. All use the semantic surface treatment; long values and qualifications wrap. Calc's status extraction reads an explicit data-answer-unit alongside the existing displayed value, without numerical recomputation. StatusMessage routes visible ordinary Assay feedback through the shared polite announcer; hidden panels do not announce, and urgent alerts retain their separate semantics.

Validation and the user-supplied audit scope are recorded in docs/progress/2026-10-22-ui-audit-refinement.md.

23 October Study cards, populated example and initial loading

Bench waits behind the shared three-bar WorkspaceLoader until its initial scoped object, Study, work and result reads and Continue validation settle. The hidden subtree remains mounted for Continue's completion callback. Read failures reveal existing Retry states; later background refreshes keep the dashboard visible. The /o and individual Study route fallbacks use the existing lightweight ShellSkeleton with their own loading labels, preserving the same bars without downloading a second interactive SuiteFrame before the page. The full frame and initial scoped-data gate belong to the mounted page.

The shell reads only its three Methods-pane labels from content/apps/methods-pane-labels.ts; the full methods/verification copy remains with the lazy pane. The original methods content object reuses those same labels.

Study headers hold their title over a 22% registry accent tint with faint decorative record art. The one pinned card spans the main column without extra height. A small pin replaces any prior pin; current Study state uses its chip, with no green card edge. Study Manage offers the actual app accent groups. Export offers a PDF overview or full recovery ZIP, through the existing report and bundle paths.

Overview separates Saved work and views from Materials and references. Every manifest has its own card, even if two tasks share one editable input. Only an exact stateRef identity removes a repeated input-only material card; subject references and labels never merge records. Experiment containers stay in Work. Resume and Open record share a footer line. Materials retain their own record actions and membership.

material-summary.ts dispatches to eight lazy, validated app readers. Cards and rows show at most four retained input facts, specific to the saved operation: stock versus dilution versus serial or everyday preparation, sequence operation and alphabet, Assay mode/model/unit, and reference views. Missing units and fields are omitted; no current defaults, cached outputs, engine calculations or network facts are promoted into saved input details. Public Patent product facts use a narrow allowlist and preserve zero counts. The Objects page has no separate saved-work queue; its list stays unchanged and Cards uses these same summaries alongside ordinary record previews. Tasks remain available in their Study and through a real manifest's Resume action on its object.

The explicit Load example Study command now loads Erlotinib response in HCC827 cells (example-study@4), superseding the earlier T790M draft. It is a completed reanalysis of the unchanged published PRISM subset, not a performed confirmation experiment. Four ordinary reference materials plus a paper and its reviewed method finding, seven tasks across Protein, Molecule, Sequence, Pipeline, Calc and Assay, three actual captures, source notes, objective, conclusion with the captured Assay result ID, task notes, due dates, colour, pin and two Experiments populate the page. New explicit loads initialise the Study itself as In progress (SCI-250), while the reanalysis Experiment is complete and independent confirmation remains planned. The six unfinished next steps and all captured science remain unchanged. This seed-only correction keeps example-study@4; opening old examples or Studies does not migrate or reset their chosen status. A successfully written example still has receipt.complete: true and its own canonical graph baseline; seed completion is distinct from research completion. Patent is a future lookup, with no invented patent record. The parent identity and illustrative hydrochloride material remain separate; canonical EGFR is not a cell-line genotype claim. No unrelated Plasmid/Bioreactor filler is added.

example-study.sources.ts retains excerpts tested against repository UniProt, PubChem formula-weight and dated NSCLC captures. example-study-science.ts uses ordinary import, current fit, recipe and capture paths. Its Assay record carries the row decisions the app's own import writes with every accepted source (tableDecisions: { v: 1, roles: {}, reasons: {}, history: [] }, read through readTableDecisions), which the strict reader requires once sourceId is set; without them, before 24 October 2026, the example's Assay work opened as "No assay open" and its Repeat always refused (CW-01, REL-560). The field changes no fit input, curve, qualification or verification (example-study-science.test.ts, the CSO's checks), and src/lib/workspace/suite-assay-writers.test.ts holds every Assay object the suite writes itself (the example's, its repeat, Repeat setup's) to readAssayDataStrict. The current unresolved confidence bound and serial transfer warning remain. No authored numerical fit or fabricated outcome fills a missing field.

Demo writes carry explicit bounded per-write marks, never a tab-global write context. The internal scoped example-study-receipt.ts records exact seed identities and revisions before granting cleanup authority; the final Study update and vacant-pin write are revision guarded. Completion rejects unexpected personal tasks or edits. Preview and removal use one complete-graph transaction; embedded dependencies in retained inputs, versions, objects, work and results preserve the whole demo. Only last-opened recency is ignored. Untouched demo rows can be removed together. Edits, reuse or incomplete seeding adopt all evidence as personal work, advance object/link revisions and Study history, strip removal marks, and keep immutable snapshots unchanged. Receipts are scoped internal metadata, never exported deletion authority. Missing authority fails closed even when the Study was removed elsewhere.

Objects rows, Study task cards and selectable reports — 23 October 2026

The Objects List now uses surface-backed compact rows. Public identifiers sit under the record name; private UUIDs stay off the face. A small type chip, actual Study links and a quiet update time replace the empty Notes column. Notes/tags have a disclosure under identity. Resume and More stay together; records with multiple manifests open an explicit task chooser. This presentation is scoped to #objects; Bench's pinned rows retain their existing form. At narrow widths the same semantic record row wraps inside its container without page overflow. List/Cards, filters, sort, paging, return highlighting and ownership failures remain shared.

The Study Work tab, as well as Overview, has one card per manifest. Work cards use a neutral border and a 12 px gap, app mark, task title, saved date, one recorded outcome, researcher note and quiet footer actions. A lazy input reader supplies the operation only when the editable record revision equals the manifest's checkpoint. Newer editable inputs never describe an older saved checkpoint. Exact result history, newer-input consent, pending-note guards and organisation commands are unchanged.

Export Study → PDF opens an intent-loaded builder with eight section checkboxes and Recommended overview / All sections presets. Objective, Materials, Methods, Results and Conclusion are selected initially; Work, Notes and Expanded appendix are optional. An empty selection cannot generate. Selection only changes PDF report parts from one consistent bundle and never changes stored records, complete HTML or the full ZIP. Title/date, historical/example labels, recovery notice and compact captured source credits are mandatory. Selected results retain units, reporting state, qualifications, exact input context, omissions, source credit and figure captions. If Methods is unchecked, retained historical prose still accompanies its result. A conclusion without the Results section keeps exact cited identities and qualifications and says supporting details were excluded. Empty selected sections disclose missing content. Export guards recheck account epoch through capture, rendering and download.

study-report-overview.ts builds the readable PDF separately from the complete document. Repeated import issues are grouped only by exact captured input, recognised diagnostic reason and rejected/unresolved disposition, retaining counts and example coordinates. Other facts, exclusions and totals remain. Detailed diagnostic coordinates and method receipts remain in complete HTML/ZIP with a PDF notice. Identical provenance blocks attached to a result and its exact input print once in the appendix with every capture identity retained. Scientific projectors and the vector engine are unchanged: A4 tables have rules/repeated headers, headings stay with a first row, figures/captions remain together, and running heads name the section. Authored Markdown headings become headings; IDs/hashes move to an optional appendix.

Future example loads capture PRISM provenance through exportExample, blockSources and buildProvenanceBlock, using the declared source release, retrieval date, licence, modifications, example statement and tested-form qualification. This credit belongs only to the published Assay reanalysis, not the illustrative Calc plans. Old snapshots are never backfilled from the current registry; missing supported provenance remains an explicit warning.

Bench, Study and Save simplification — 23 October 2026

The CEO-approved workflow uses Bench, Studies and Library, and the four Study sections Overview / Work / Results / Materials & sources. Bench Continue working reads actual saved WorkManifests; All work includes standalone activities and current saved analysis batches. Browser last-address links move to Recently viewed. App backing records are carried/private by default, while declared primary Protein/Molecule subjects are kept as Library items by explicit Save, including outside a Study. Add to Study links the reusable item without a second saved-view checkbox. The current example is example-study@3, with Sequence schema v2 and no legacy views.

Continue working orders saved activities by most recently saved across all Studies, including work without a Study. The selected Study does not change this ordering; each activity names its own Study or Without a Study. The former selected-Study-first sentence was removed at the CEO's request on 23 October.

On Bench, Continue working and Recent activity use BenchSectionHead above their content so their desktop headings share a baseline. Neither removed helper sentence is rendered. Recent activity retains its white body but uses 32 px tiles, 44 px minimum link targets and reduced vertical row padding; wrapped descriptions and qualifications determine the height naturally. The full Activity page retains its original panel and timeline spacing.

The action cluster stays Save · Send to · More. The first Save opens name/destination choices. For existing work it saves directly; More contains Name & Study and Save a copy through WorkActionContext, scoped only to that cluster. Save calls the captured render's result adapter when eligible. Reference/incomplete work saves no result; reason-reviewed analyses keep their review; partial failures and limited storage remain visible. An explicitly allowed identical Calc/Sequence calculation may reuse its original checkpoint. Save another result deliberately appends. These are explicit user acts, never effects or draft timers.

Study Results headlines read the existing demand-loaded immutable-result projections and their exact input snapshots. Multiple curves show shown/withheld counts; a single endpoint retains its label, unit and qualifications. Refused values are withheld. The synchronous report/Node entry delegates to the same presentation core. Overview shows selected results and the conclusion rather than duplicating the entire Work/Materials list. Experiments are optional grouping under Work, and next steps are editable plans on Overview.

The PDF builder offers Business and Science reports, section checkboxes and a prominent Export action. The report projection omits private IDs/checksums and places the methods, source credit, licences and retained-input detail in the appendix, preserving interpretation-critical qualifications beside each finding. Complete ZIP remains a separate recovery/transfer option. The former content-digest/RO-Crate UI is removed; View graph is at the foot of Provenance. Saved-work cards have increased padding and plate previews use their card's full width.

Library retains the former Objects four-panel icon (LIBRARY_ICON, overview) consistently in the sidebar, command palette and Library menu links, as requested by the CEO on 23 October.

Study workflow follow-ups and Bench archive placement — 23 October 2026

Study Work adds Add existing work and a lazy selection dialog. It names the source and destination and lets the scientist choose exact dated results. Included cards say From [Study] or Started without a Study, and Continue original opens the source activity through existing consent/draft guards. All work's Study filter uses actual memberships. A Study outcome/history shows its included snapshots rather than the activity's global latest pointer.

The Study checklist has Make next / Clear next, with one Next marker. Header Choose next task reveals the existing checklist; it does not create a second editor or another task source. Header, brief and featured Bench card read the same selection. Completing that item clears it.

Materials & sources keeps Open and Use in an app together. Available destinations come from the existing registry rules, with each app named. Starting from a material chooses the current Study with no inherited Work or Experiment. Pending-draft and owner-epoch guards apply before navigation. Unsupported materials show no fabricated destination.

Results and saved Literature findings offer Use in conclusion. This reveals the existing Overview editor, preserves written draft text and checks/adds the chosen citation; an explicit Save is still required. Paper findings are separately labelled From a paper with limitations and source-status qualifications. Frozen passage/citation details expand inline; source licences and methods stay in report supporting sections. There are no additional top-level tabs or floating context boxes.

The residual Include archived control on Continue working now sits after its cards and feedback, matching the controls below Recent studies and All Studies. Both Bench column headings continue to use BenchSectionHead without offsets. Populated-state browser verification covers archived-work filtering, aligned desktop headings and wrapping at 375 px in both themes.

Landing finish, 23 October 2026

Integration retains the approved stronger shared Light fgMuted: #3d4e57 in CSS, TypeScript and print. The following dated landing checkpoint records its original #425159 value; that value is superseded on this branch.

The two worked examples use plain in-page links styled as line tabs, CSS :target selection, and a fixed J3 default. Each focusable, labelled target panel carries its own navigation with aria-current="location" on its selected link, so fragment navigation retains the switcher below the header. There is no ARIA tablist, selection script or animation. At 768 px through the lg boundary each step has text/card columns (2fr/3fr) with natural card heights; phones stack and desktop restores the existing aligned subgrid. Catalogue cards and the Education disclosure use rounded-card. Public Dark uses --color-sunken: #142a21; Light secondary text uses #425159, including matching paper/print tokens. The violet workspace Dark override remains separate. The AI response list includes refusals with reasons and continues to identify hosted connections as planned.

Calibration-derived bench-sheet export infrastructure (22 October 2026)

src/lib/export/calibration-bench-sheet.ts provides exportCalibrationBenchSheet, a standalone, script-free HTML writer for the existing normalisation method. It accepts an existing strict source block and an explicit full config, resolves selected source IDs, uses calculateNormalizationBatch unchanged, then detaches and freezes source metadata, intervals, qualifications, withheld counts, omitted IDs, settings and results together. Refused rows remain visible without transfer volumes. Unknown configuration fields are refused. No manual concentration can be associated with a source merely by supplying its name.

Calc’s normalisation results now offer Capture, Refresh after edits and Download captured sheet. The export module itself defines no stored/importable schema; editable inputs use the separate np v1 contract described in app-calc.md. The caller resolves explicit source origins from the current non-example plan. It does not authenticate a payload or extend the existing method with reducing agent, overage or well-capacity calculations. Copy lives in content/apps/calibration-bench-sheet.ts and reuses the existing normalisation copy. Tests in src/lib/export/calibration-bench-sheet.test.ts cover the existing independent volume fixture, fit-to-document composition, deep capture, omissions, refusals and escaping. The finishing plan is docs/progress/calibration-to-bench-sheet-acceptance.md; the actual browser journey now tests edits, stale captures, download failure/retry, reload and named work reopening. Real-file observation and remote queue acceptance remain open.

The chosen unfinished Study checklist item labels only its matching work card as “Next for [Study] · [task]”. Bench keeps its three globally recent cards; an outside-three task adds no card. Handover notes remain independent annotations. Eligible Assay cards expose Repeat with new readings beside Resume/results. The review names the invocation Study destination and source revision/date. Once Create has written the run (repeat-assay-action.tsx), the dialog drops its prepared source, so Create stays disabled and one press makes one run; it closes before the address moves to the new run's own #s=mine= address, which Assay opens in place when the repeat started there (CW-02, REL-561). Existing inclusion review is available as Review other results; later source results are never included automatically.

The Selected results panel also shows one latest explicitly associated uncited result, sorted by recorded time then stable ID, without preferring reportable outcomes. Its shared preview reads exact immutable inputs against the Study revision, association and scope epoch. Broken chosen inputs show unavailable and Retry, never an older fallback. Captured scientific context and qualifications are visible beside the finding. Use in conclusion prepares an unsaved draft, retaining dirty prose and selections. Every Study-contained result inspector passes its invocation Study explicitly to historical Repeat.

Material menus describe registry purpose and accepted Opens with parameters. Opening reads only; activation flushes and rechecks material revision, Study revision/membership, scope epoch and accepted parameters before transfer writes and navigation. It starts new work in the current Study without an Experiment.

The existing PDF section builder now presents factual Question, Finding, Limits, Planned next step, included result/paper/citation counts and omissions from the actual prepared report document. It captures after notes and registered work flush. Missing prose is advisory. Relevant receipt drift invalidates generation and offers Refresh captured Study while retaining audience/section choices. The final PDF renders that exact prepared document and checks the receipt again after rendering. Recovery remains the complete format-6 closure.

Fresh Bench launches carry their scope/lifetime/intent through the document policy's final save wait; old saving context is cleared immediately before the actual navigation only. Managed launch links still obey document/CSP policy. Material activation has a separate monotonic intent, so a later choice cancels a delayed older transfer/navigation without cancelling merely on menu close.

Workflow context and prepared export follow-up — 24 October 2026

Study findings keep typed captured facts until rendering. Declared preparation value/unit pairs share a wrapping definition group, with explicit missing units; scientific facts, qualifications and biological identities remain visible. No arbitrary fact limit or live material enrichment applies. Latest uncited metadata uses the existing local date/time formatter and registered short app name. The next-task key wraps its label inside the available header width while retaining its normal type and target size. Export preflight precedes section choices and is still the inventory of the exact prepared document.

Prepared PDF selection loads studyOverview.prepare directly. Full HTML remains on its existing document entry. Unchanged task copy, number formatting, refit version checks, parsed-source identities and strict notebook preparation have narrow leaves with existing re-exports; no method, scientific value or cap is changed by those import boundaries.

The final UI disclosure allowlist moves only recognised dose-response control assignment rows intact to More captured context, showing the recorded role labels and disclosure direction beside the finding. Missing reference fields disclose only when absent in that exact capture. Recorded references, qPCR context, critical missingness and qualifications remain visible. Report facts are unchanged. The implementation lives in the Study-only presentation helper, not the report selector. Notebook preparation and inert writers retain their public compatibility exports; their implementation bodies are separate so a formatter does not import strict readers through a writer.

The HTML recovery control loads only complete-report orchestration. Study PDF preflight/generation has a separate active-capture owner, sharing only the historical-method helper. The overview formatter and complete HTML formatter keep their existing markers; complete HTML does not load overview as a nested branch.

Both export owners load study-report-evidence.ts on export intent, passing the same validated bundle and current-interaction guard. The stage prepares the exact ordered input snapshots, notebook block, recorded projection, essential context, captured provenance with originating identities and historical Methods receipts. Its detached, deeply frozen data names the bundle hash and Study identity/revision; it has no database access, recapture or cache beyond the current export interaction. Section changes prepare again from that same capture. The overview sorts a copied entry array and assigns display numbers; neither formatter mutates the evidence. All narrative, grouping, section selection, omissions, inventory and HTML/SVG/PDF layout remain in the existing formatters. The final PDF still renders the prepared document and rechecks the original capture receipt and interaction guard.

SCI-252's captured preparation presentation uses one content-owned four-key map in content/apps/essential-context.ts: fw is Formula weight (g/mol), lw is Weight from the vial (g/mol), pur is Purity (%) and dmso is Solvent in stock (%). Only the recognised stock/dilution/serial recipe v1 selections apply these labels. The selector retains its exact String(value)/missing-value handling; zero stays zero and absent values gain no defaults. A nonempty explicitly captured unit on these fields is retained literally as Captured unit declaration: {unit} in the existing fact detail, even when it equals or contradicts the grammar unit. Trimming tests only presence. No declaration is inferred, converted, normalised or used to override calculation replay.

The unchanged Study compactor, complete report and both PDF overview styles consume the corrected facts. The complete report's separate exact recipe-input rows reuse their labels/details and retain the literal captured value; other rows keep their existing value/unit handling. Component purity remains a fraction, component formula-weight basis stays separate, and per-dose solvent fraction is unchanged. Rendering does not rewrite input/result snapshots, content hashes, source revisions, historical outputs or prior exported files. SCI-253's separate missing/withheld/inapplicable-state policy remains open.

Save controls and the existing More menu import a dependency-free save-wording leaf, content/apps/bench-workflow-save.ts, instead of Bench/list wording. The original benchWorkflowText object spreads those same nine strings and retains its public shape. Component bodies, save/record guards and chart rendering are unchanged; this is a static import boundary, with no deferred code or new island.