# UI Interaction and Component Standards
**Subject:** Virtual Art Gallery — screen and interaction guidance
**Applies to:** Public visitor UI, 3D halls, curator tools
**Companion rule:** `.cursor/rules/ui-interaction-standards.mdc`
---
## 0. Introduction
The Gallery is a single React SPA (`HomePage.tsx` view union — no URL router except `?layout=`). Public visitors browse a museum-dark timeline and 3D halls; curators use the same shell for catalog tools (Checkup, Translations, Influences, Tours, Users, Activity). Work lands in the same header, palette, and back-stack, so a new screen that invents its own chrome, confirmations, or loading pattern fragments the experience.
This document is the shared contract for **how the UI behaves**. Architecture, APIs, and 3D hall construction live in [basics.md](basics.md), [API.md](API.md), and [data-and-images.md](data-and-images.md). Tree geometry lives in [movement-tree.md](movement-tree.md). Locale strings live in [i18n-russian.md](i18n-russian.md).
### 0.1 Purpose and audience
- **Purpose:** Define common UI interaction, navigation, and component standards for the Gallery client.
- **Primary audience:** Developers (and agents) adding or changing `client/src` UI; anyone writing curator-tool screens.
- **Secondary audience:** QA, product, copy/i18n.
### 0.2 Scope
- **In scope:**
- Navigation and back-stack behaviour
- Timeline / search / 3D / detail / curator-tool interaction patterns
- Loading, empty, error, and confirmation behaviour
- Permission-based UI (`can()` / RBAC)
- Shared component and visual-language rules
- **Out of scope:**
- REST contracts, retries, and image pipeline internals
- Three.js hall architecture, textures, and lighting (see [basics.md](basics.md))
- Pixel-perfect branding kit (no separate design-system package; follow existing CSS)
### 0.3 Requirement levels
- **MUST:** Mandatory for new work and for fixes that touch the same screen.
- **SHOULD:** Recommended; deviate only with a short note in the PR or screen section.
- **MAY:** Optional pattern when the screen specification calls for it.
### 0.4 Surface map
| Surface | Typical components | Visitors | Curators |
|---------|--------------------|----------|----------|
| Timeline home | `Timeline`, `VerticalTimeline`, `MovementBands`, `VerticalMovementBands`, `MovementTree`, `CatalogSearchBar` | yes | yes |
| 3D hall | `VirtualGallery`, wing navigator, exit overlays | yes | yes |
| Painting / bio | `PaintingDetail`, `PaintingLightbox`, `ArtistBio`, `PaintingAnnotations` | yes | + debug panel when `can('images')` |
| Overlays | `CuratorLoginModal`, `ArtistFilterModal`, `ToursPopup`, `DebugSearchResultsModal` | some | all |
| Curator tools | `CheckupPage`, `TranslationsPage`, `InfluencesPage`, `ToursPage`, `UsersPage`, `AuditPage` | no | permission-gated |
---
## 1. Design Goals and Principles
### 1.1 Problem statement
- Statement: Visitors and curators share one shell. Inconsistent back labels, ad-hoc modals, English-only captions, and one-off loading/error treatment make the museum feel like several apps glued together.
- Scope: All `client/src` screens and overlays.
- Rationale: The product is a gallery, not an admin console with a visitor skin. Predictable chrome is part of the exhibit.
- Verification: Compare a new screen’s header, back control, loading marker, and locale keys against this document.
### 1.2 Functional design principles
- Principle 1: One museum, two roles
- Statement: Public browse chrome MUST stay museum-dark (navy / gold / Georgia). Curator tools MAY be denser but MUST reuse the same header back pattern, gold accent, and permission hiding — they MUST NOT look like a separate product.
- Rationale: Curators enter from the same timeline; a visual cliff breaks trust.
- Verification: Side-by-side with timeline header and Checkup / Tours editor.
- Principle 2: Predictable drill-down and return
- Statement: Navigation MUST follow Timeline → (movement picker) → hall → painting/bio, with an explicit in-app Back that restores the intended session — not the browser history stack.
- Rationale: `HomePage` owns view state; the browser Back button is not wired.
- Verification: Walk the [basics.md navigation flow](basics.md#user-navigation-flow) and the Back table in §2.4.
- Principle 3: Clarity of actions and feedback
- Statement: Every user-initiated load, save, delete, or failed request MUST show a loading, success, or error state the user can see without opening the console.
- Rationale: 3D and image work is slow; silent failure looks like a broken hall.
- Verification: Trigger catalog load, hall open, form save, and a failed API call.
- Principle 4: Minimize cognitive load
- Statement: Timeline charts MUST keep zoom/pan/click hints visible. Curator tables MUST put filter/search above the grid. Destructive actions MUST be confirmed.
- Rationale: Dense history data and catalog tables are easy to mis-click.
- Verification: Hint captions present; filters above tables; delete paths show a confirm.
- Principle 5: Locale and permission are first-class
- Statement: New visitor-facing copy MUST go through `react-i18next` (`locales/{en,ru}`). Actions the user cannot perform MUST be hidden, not disabled-without-explanation.
- Rationale: EN/RU is a product requirement; exposing forbidden tools invites errors.
- Verification: Toggle EN|RU; log in as a curator without the relevant `can()` and confirm the control is absent.
---
## 2. Navigation Principles
### 2.1 Application chrome (no left module rail)
The Gallery has **no persistent left navigation menu**. Primary wayfinding is the **timeline header** on home, and an explicit **Back** control on every nested view.
- Statement: The timeline home MUST keep layout switch, catalog search, locale switcher, and role-appropriate tools in the site header.
- Scope: `HomePage` when `view` is `timeline` / `timeline-vertical` / `timeline-tree`.
- Rationale: Visitors need search and layout without hunting; curators need tools without leaving the museum frame.
- Verification: Header remains usable at 100vh; search dropdown stacks above movement bands (`z-index` on `.site-header`).
- Statement: Layout switch links MUST stay in the top-left (`.site-layout-switch`). The Tree of Art control SHOULD use `.site-layout-link-feature`.
- Scope: Timeline home.
- Rationale: Three layouts must stay discoverable; Tree is the featured alternative start page.
- Verification: Classic / Vertical / Tree links match [basics.md](basics.md#timeline-and-movement-flow).
- Statement: Nested views (hall, painting, bio, curator pages) MUST NOT reintroduce a second global nav. They MUST show a single primary Back control in the page header.
- Scope: All non-home views.
- Rationale: Avoid competing menus; the drill-down is the nav.
- Verification: No duplicate “home” plus “modules” rails on curator pages.
### 2.2 Page hierarchy
- Statement: Screens MUST stay within this hierarchy (max four levels):
1. Timeline home (classic / vertical / tree)
2. Overlay or picker (artist filter, tours popup, login) **or** curator tool page
3. 3D hall (artist / movement / tour)
4. Painting detail or artist bio (optional lightbox on top of detail)
- Scope: All visitor and curator flows.
- Rationale: Matches the existing drill-down; deeper stacks become unrecoverable without a router.
- Verification: New views are added to the `View` union in `HomePage.tsx` with a defined parent and Back handler.
- Statement: Curator tools MUST open as siblings of the timeline (replace the home canvas), not as a fifth level under a hall.
- Scope: Checkup, Translations, Influences, Tours editor, Users, Activity.
- Rationale: Tools operate on the catalog, not on a hall session.
- Verification: Opening Checkup from a hall is not required; from timeline header, Back returns to timeline home.
### 2.3 Location awareness (no breadcrumbs)
- Statement: The app MUST NOT add a breadcrumb trail unless a future router lands. Until then, the Back label MUST name the destination (`← Back to Timeline`, `← Back to Gallery`, `← Back`).
- Scope: All nested views.
- Rationale: There is no URL path to reflect; a fake breadcrumb would lie.
- Verification: Labels match §2.4; they are i18n keys (`backToTimeline`, `backToGallery`, …).
- Statement: Timeline layout MUST be deep-linkable via `?layout=classic|vertical|tree` (omit param for classic). Other views MUST NOT pretend to be bookmarkable until a router exists.
- Scope: Timeline home.
- Rationale: Layout is the one shareable start-page choice; halls and tools are session state.
- Verification: Load `?layout=tree`, switch layouts, confirm `history.replaceState` updates the query.
### 2.4 Back navigation
| Control | MUST return to |
|---------|----------------|
| **← Back to Timeline** (hall header, movement Exit to Timeline, painting opened from search) | Home timeline via `goToTimelineHome()` — unmount hall, clear session, reset year window to full catalog bounds |
| **← Back to Gallery** (painting from a hall) | Same hall session (camera / wing preserved) |
| **← Back** (artist bio) | `returnTo` view (usually the hall that opened bio) |
| Curator page Back | Timeline home |
- Statement: Back MUST be an in-app control. The browser Back button MUST NOT be relied on (it is not wired to `View` state).
- Scope: All nested views.
- Rationale: `setView` is the router.
- Verification: From painting detail, in-app Back restores the hall; browser Back does not need to.
- Statement: `goToTimelineHome()` MUST be the single implementation for “leave everything and show the timeline.” New exits MUST call it rather than duplicating reset logic.
- Scope: Halls, search-opened paintings, curator Back.
- Rationale: Year-range reset and session clear must stay consistent.
- Verification: After Exit to Timeline, `viewStart`/`viewEnd` equal catalog bounds.
### 2.5 State preservation
- Statement: Timeline pan/zoom (`viewStart` / `viewEnd`) MUST persist while the user stays on a timeline layout. Switching classic ↔ vertical ↔ tree MUST keep the same year window.
- Scope: Timeline home.
- Rationale: Layout is a lens, not a new dataset.
- Verification: Zoom, switch to Tree, confirm the year rail range is unchanged.
- Statement: Returning to timeline via `goToTimelineHome()` MUST reset the year window to full catalog bounds.
- Scope: Hall / search / curator exits that call `goToTimelineHome()`.
- Rationale: Documented in [basics.md](basics.md#back-navigation); visitors expect a fresh overview, not a leftover zoom.
- Verification: Zoom in, enter a hall, Back to Timeline → full span.
- Statement: Hall camera and wing MUST be preserved across **Back to Gallery** from painting detail. They MUST be discarded on **Back to Timeline**.
- Scope: Artist, movement, and tour halls.
- Rationale: Inspecting a painting is a detour; leaving the museum is not.
- Verification: Move in the hall, open a painting, Back to Gallery → same viewpoint.
- Statement: Catalog search input MAY clear when the dropdown closes. It MUST NOT change timeline zoom by itself.
- Scope: `CatalogSearchBar`.
- Rationale: Search is a jump, not a filter on the chart.
- Verification: Type a query, Escape; year window unchanged.
---
## 3. Common UI Interaction Patterns
### 3.1 Catalog search (visitor)
- Statement: Timeline search MUST live in the header, require **2** trimmed characters, debounce **300 ms**, and group results into Artists, Movements, Paintings.
- Scope: `CatalogSearchBar.tsx`.
- Rationale: Documented product behaviour; keeps `/api/search` load reasonable.
- Verification: 1 character shows no fetch; 2+ after debounce shows groups.
- Statement: Keyboard MUST support `↑`/`↓` highlight, `Enter` to open, `Escape` to close.
- Scope: Catalog search dropdown.
- Rationale: Timeline is pointer-heavy; search should still be keyboardable.
- Verification: Keyboard-only open of an artist, movement, and painting.
- Statement: Opening a painting from search MUST set `returnTo` timeline so Back is **← Back to Timeline**, not Gallery.
- Scope: Search → painting detail.
- Rationale: There is no hall session.
- Verification: Search a title, open, Back → home timeline.
### 3.2 Timeline charts (zoom, pan, click)
- Statement: Scroll MUST zoom, drag MUST pan, click on a stream/limb/label MUST open the movement (artist filter → hall). All three layouts MUST use `zoomTimelineView` / `panTimelineView`.
- Scope: `MovementBands`, `VerticalMovementBands`, `MovementTree`, era rails.
- Rationale: Shared year window; one mental model.
- Verification: Same wheel/drag behaviour on classic, vertical, and tree.
- Statement: Each chart MUST show a localised hint caption (`captionClassicTimeline`, `captionClassicFlow`, `captionVerticalTimeline`, `captionVerticalFlow`, `captionTreeFlow`).
- Scope: Timeline home.
- Rationale: First-time visitors cannot discover zoom/pan otherwise.
- Verification: EN and RU captions change with `LocaleSwitcher`.
- Statement: Timeline layout shifts (lane packing, tree fit scale) SHOULD animate rather than snap.
- Scope: Movement charts.
- Rationale: Unexplained jumps look like bugs.
- Verification: Zoom/pan does not teleport streams.
### 3.3 Filters and search (curator tables)
- Statement: Filter/search controls MUST sit in a toolbar **above** the table, not in a column header hack or a page footer.
- Scope: Checkup, Translations, Influences, Tours editor, Users, Activity.
- Rationale: Matches Checkup (`checkup-toolbar`) and keeps the grid scannable.
- Verification: Filters remain visible while the table scrolls.
- Statement: Simple text filters MAY apply as the user types. Expensive operations (image search, import, refetch) MUST require an explicit button (e.g. Checkup **Search visible**).
- Scope: Curator list screens.
- Rationale: Checkup search is rate-limited and slow; typing must not fire it.
- Verification: Typing in Checkup filter does not start image search.
- Statement: When a filter hides rows, the toolbar SHOULD show how many rows are visible (e.g. `N shown`).
- Scope: Filtered tables.
- Rationale: Empty-looking tables need an explanation.
- Verification: Filter to zero rows → empty state plus count.
### 3.4 Date selection
- Statement: Year fields in the catalog and timeline MUST use numeric years (negative = BCE). They MUST NOT switch to locale-specific calendar widgets for historical BCE dates.
- Scope: Timeline bounds, artist lifespan, painting years, curator year filters.
- Rationale: The catalog spans −800 to the present; HTML date inputs cannot represent BCE.
- Verification: Ancient era still filters correctly.
- Statement: If a future screen needs a civil date (e.g. audit log day), it SHOULD use ISO `YYYY-MM-DD` and validate start ≤ end for ranges.
- Scope: Activity / audit and any new timestamp filters.
- Rationale: Consistent with API timestamps; avoids DD/MM ambiguity.
- Verification: Invalid range shows a field-level message.
### 3.5 Tables and lists (curator)
- Statement: Structured curator datasets MUST use a labeled HTML table (or existing page table classes), one logical record per row.
- Scope: Checkup, Users, Influences worklists, Tours list, Translations worklist, Activity.
- Rationale: Comparison and row actions need columns, not cards.
- Verification: Column headers present; row click/action affects one record.
- Statement: Visitor-facing catalog MUST NOT be presented as a spreadsheet. Timeline streams, tree limbs, and 3D hangs are the list metaphor.
- Scope: Public home and halls.
- Rationale: The product is a gallery, not a DAM table.
- Verification: No “all paintings” data grid on the public home.
- Statement: Tables MAY omit pagination while the dataset is curator-sized and client-filtered. If a list grows past comfortable scrolling, it SHOULD paginate or virtualise rather than rendering thousands of DOM rows.
- Scope: Curator tools.
- Rationale: Checkup is already filter-then-scroll; unbounded paint is a future foot-gun.
- Verification: New tools with large lists have a documented paging or virtualisation plan.
- Statement: Row actions MUST sit in a dedicated column or overflow control, not as random icons in every cell.
- Scope: Interactive curator tables.
- Rationale: Scanability.
- Verification: Action column or consistent button set per row.
### 3.6 Multi-row and per-record actions
- Statement: Bulk actions MUST use a leading checkbox column, Select All for **visible** rows only, and a confirmation that includes the affected count for destructive work.
- Scope: Any new bulk-enabled table. (Today: ArtistFilterModal multi-select is a picker, not a bulk delete.)
- Rationale: Same as the Logistics template; prevent silent mass edits.
- Verification: Select All does not imply “all matching in the database” unless explicitly labelled.
- Statement: Artist filter before a movement hall MUST be a modal checklist with explicit proceed/cancel, not a bulk-edit of the catalog.
- Scope: `ArtistFilterModal`.
- Rationale: It only chooses who appears in the hall.
- Verification: Cancel leaves the user on the timeline; proceed opens the hall.
- Statement: Primary row/object action SHOULD be the name/title (open painting, open user, open tour). Secondary actions SHOULD stay in the row’s action controls.
- Scope: Curator tables and search results.
- Rationale: Matches search-result click-to-open.
- Verification: Clicking a Checkup title opens the painting when that handler exists.
- Statement: Unavailable-by-permission actions MUST be hidden. Unavailable-by-record-state SHOULD be disabled with a `title`/tooltip explaining why.
- Scope: All tools.
- Rationale: RBAC vs workflow are different signals.
- Verification: Non-admin does not see Users; a disabled Fix button states why.
### 3.7 Forms
- Statement: Short auth and picker flows MUST use a modal. Multi-section catalog editors (Users create/edit, Tours editor, Influences wizard, Translations worklist) MUST be full-page (or the existing page layout), not nested modals.
- Scope: All forms.
- Rationale: Halls and timeline need to stay the “place”; heavy edit needs space.
- Verification: Login is modal; Users is a page.
- Statement: Forms SHOULD be a single column. Related fields MAY group under a heading when there are more than five inputs.
- Scope: Curator forms.
- Rationale: Scanning beats dense multi-column on museum-width pages.
- Verification: Users create form remains vertically grouped.
- Statement: Required fields MUST use the native `required` attribute and/or a visible marker; validation errors MUST appear next to the field or as a form-level error the submit control does not obscure.
- Scope: Login, Users, Tours, Influences, Translations.
- Rationale: Silent submit-disable is not enough.
- Verification: Submit empty login → field or form error, not a blank modal.
- Statement: After successful save, the system MUST show an inline success message (or equivalent) and keep the user on the tool unless the spec says to return to timeline.
- Scope: Curator mutations.
- Rationale: Users page already uses `message` / `error` banners.
- Verification: Save permissions → success text; failed save → error text.
- Statement: Unsaved-change guards SHOULD be added when a form is long enough that accidental Back would lose work (Tours editor, Translations). Login and tiny pickers MAY skip this.
- Scope: Heavy editors.
- Rationale: `window.confirm` on delete already exists; abandon-edit is the remaining hole.
- Verification: Dirty Tours editor + Back prompts or discards explicitly.
### 3.8 Edit interaction pattern selection
| Pattern | Use when |
|---------|----------|
| **Modal** | Login, artist filter, tours list popup, debug image picker, lightbox, hall exit/wing overlays |
| **Inline** | Checkup flags, debug **Checked** / **Fix it** on painting detail — small, reversible |
| **Full page** | Curator tools, painting detail, artist bio, 3D hall |
| **Drawer** | MUST NOT be introduced unless a spec adds a shared drawer component |
- Statement: Nested modals MUST NOT be used (no modal opened from another modal). The debug “More” picker MAY stack on painting detail because detail is a full page, not a modal.
- Scope: All overlays.
- Rationale: Focus traps and Back labels break.
- Verification: Login does not open another dialog.
### 3.9 Modals and overlays
- Statement: Modals MUST use `role="dialog"` and `aria-modal="true"`, a visible close/cancel, and **Escape** to dismiss unless a submit is in flight.
- Scope: `CuratorLoginModal`, `ArtistFilterModal`, `ToursPopup`, `DebugSearchResultsModal`, `PaintingLightbox`, hall exit overlays.
- Rationale: Accessibility and parity with search.
- Verification: Esc closes lightbox and debug picker; backdrop click matches existing login behaviour.
- Statement: Backdrop click MAY close pickers and login. It MUST NOT close a modal that is applying a destructive or long-running action.
- Scope: Overlays.
- Rationale: Accidental dismiss during Fix/upload is costly.
- Verification: Click outside login closes; do not dismiss mid-upload.
### 3.10 Loading, empty, and error states
- Statement: Catalog, portrait, and hall loads MUST use `GalleryLoadingMarker` (overlay or banner), not an ad-hoc spinner per screen unless the marker cannot cover the region.
- Scope: Home, halls, painting/bio image work.
- Rationale: One recognisable “the museum is fetching” treatment.
- Verification: First visit shows “Loading art history…”; hall open uses the same marker family.
- Statement: Loading SHOULD be scoped to the affected region. Full-viewport overlay MUST be used only when the user cannot usefully interact (first catalog load, hall WebGL init).
- Scope: All loads.
- Rationale: Portrait banner vs full-page overlay already follows this.
- Verification: Timeline remains visible while “Loading portraits…” banners.
- Statement: Empty datasets MUST explain themselves (e.g. vertical flow: no movements in range; search: no matches; Checkup: no rows for filter).
- Scope: Charts, search, tables.
- Rationale: Blank gold-on-navy reads as a crash.
- Verification: Zoom to a year with no movements; search a nonsense string.
- Statement: Failed loads MUST set a visible error string (`error-banner`, form error, or page error) — never `console.error` alone.
- Scope: All data-fetching views.
- Rationale: Visitors have no console.
- Verification: Stop the API and confirm home shows a load failure message.
### 3.11 3D hall interaction
- Statement: Halls MUST keep **← Back to Timeline**, pointer-lock / click-to-move as already implemented, and **E** (or documented key) for exit/wing navigation. New hall UI MUST not steal those keys without updating this section.
- Scope: `VirtualGallery`.
- Rationale: Muscle memory across artist, movement, and tour halls.
- Verification: Same Back label and exit overlay pattern in all three hall kinds.
- Statement: Clicking a framed painting MUST open painting detail with `returnTo` the current hall. Missing images MUST show the draped-canvas placeholder, not a broken ``.
- Scope: Halls.
- Rationale: Documented in [basics.md](basics.md).
- Verification: Work without a file still shows a frame cover.
---
## 4. Behavioral Standards (Screen UX)
### 4.1 Permission-based rendering (RBAC)
- Statement: Header tools and debug controls MUST render only when `can('')` (and login for curator). Anonymous visitors MUST see browse + Tours popup + locale, not Checkup/Users/etc.
- Scope: `HomePage` header, painting/bio debug panel.
- Rationale: Security UX: hide, don’t tease.
- Verification: Logged-out home; curator without `users` cannot open Users.
- Statement: Route-like views that require a role MUST show the existing “Curator access required” panel with login and back-to-gallery actions — they MUST NOT render an empty privileged page.
- Scope: Checkup, Translations, Influences, Tours editor, Users, Activity.
- Rationale: Deep view state can still be set; the gate must hold.
- Verification: Set view to Users while logged out → curator required copy.
- Statement: The same permission MUST hide the same action on every surface (header, painting debug, API). Do not leave a visible button that 403s.
- Scope: All mutations.
- Rationale: Predictable roles.
- Verification: `can('images')` off → no debug panel and no Fix buttons.
### 4.2 Notifications and user feedback
- Statement: Curator mutations MUST show success or error text in the page’s existing banner/message area. The app has no global toast system; new screens MUST NOT invent a third notification widget without replacing this standard.
- Scope: Curator pages.
- Rationale: Users/Influences already use inline `message` / `error`.
- Verification: One visual treatment per page, consistent placement under the header.
- Statement: Visitor-facing destructive actions (painting **Remove entry** in debug) MUST confirm and then show failure inline if the API rejects.
- Scope: Debug painting tools.
- Rationale: Catalog deletes are irreversible.
- Verification: Cancel confirm → no delete.
- Statement: Copy MUST be concise and, for errors, actionable (“Is the server running?”, “Sign in as a curator…”).
- Scope: All user-visible strings.
- Rationale: Support load.
- Verification: Home catalog failure string remains understandable.
### 4.3 Confirmation and cancellation
- Statement: Destructive curator actions (delete influence, delete tour, remove painting, deactivate user if offered) MUST confirm before the request. `window.confirm` with an i18n string is the current standard (`confirmDelete`); a shared modal MAY replace it later but MUST stay one pattern.
- Scope: Influences, Tours, painting remove, Users.
- Rationale: Accidental clicks on dense tables.
- Verification: Delete tour → confirm; Cancel → no API call.
- Statement: Cancel on a confirm MUST leave filters, selection, and unsaved fields unchanged.
- Scope: All confirms.
- Rationale: Context preservation.
- Verification: Filtered Checkup, cancel a destructive action → filter still applied.
---
## 5. Component Consistency Rules
### 5.1 Shared interaction pattern usage
- Statement: New UI MUST reuse existing components before creating parallels: `GalleryLoadingMarker`, `CatalogSearchBar`, `LocaleSwitcher`, `CuratorLoginModal`, `ArtistFilterModal`, `PaintingLightbox`, `DebugSearchResultsModal`, `DebugUploadButton`.
- Scope: `client/src`.
- Rationale: Duplicate spinners and dialogs already caused drift.
- Verification: PR does not add a second login modal or loading overlay.
- Statement: Movement colours MUST go through `utils/movementColor.ts` (`vividMovementColor`, `shadeMovementColor`). Hex parsing MUST tolerate `#rgb` / `#rrggbb` and keep the first six digits of longer values.
- Scope: Timeline charts and any movement swatch.
- Rationale: Classic, vertical, and tree already share this util.
- Verification: No local `parseHexColor` copies in components.
- Statement: Timeline zoom/pan MUST use `utils/timelineView.ts`. Tree horizontal layout MUST use `utils/movementTree.ts` (view-independent structure).
- Scope: Timeline surfaces.
- Rationale: Layouts share one year window.
- Verification: No one-off wheel handlers that bypass the util.
### 5.2 Component and style selection
- Statement: There is **no Ant Design / MUI**. UI MUST be React + colocated CSS (`ComponentName.tsx` + `ComponentName.css`). Global museum tokens SHOULD reuse existing values:
| Token | Typical value | Use |
|-------|----------------|-----|
| Gold | `#c9a96e` / `rgba(201, 169, 110, …)` | Accents, borders, links |
| Ink / navy | `#0f0f1a`, `#1a1a2e`, `#16213e` | Page background |
| Type | Georgia, serif | Titles, captions, layout links |
| Viewport | `100vh`, `overflow: hidden` on home | No document scroll for the museum shell |
- Scope: All client UI.
- Rationale: The look *is* the design system.
- Verification: New CSS does not introduce a bright Bootstrap theme.
- Statement: Prefer semantic `