# 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 `