# elise-studio-components

## Custom button events

Existing `Button` nodes can opt into click tracking with `eventName`:

```json
{
  "type": "Button",
  "props": {
    "content": "Explore floor plans",
    "action": "navigate",
    "target": "/floorplans",
    "eventName": "floorplans_cta_click",
    "eventCategory": "engagement",
    "eventParameters": { "placement": "hero", "rank": 1, "featured": true }
  }
}
```

Event names use lowercase snake_case and may be shared by several buttons.
Omit `eventName` to disable custom tracking; retained category and parameter
settings do not block publishing while tracking is disabled. Category defaults to `engagement`;
parameters are optional string, finite number, or boolean values. The keys
`event`, `gtm`/`gtm.*`, `ecommerce`, `event_category`, `property_id`, `type`, `asset`,
and prototype keys are reserved. Studio treats `type` as a node marker and `asset`
as an asset reference; use names such as `button_type` and `asset_name` instead.
Property ID comes from the nearest property scope, falling
back to the site's property; it is omitted when unavailable.
Parameter dictionaries are literal data: binding-like keys such as `$content`
are preserved rather than resolved as Studio directives.

The shared analytics dispatcher sends the event before the button action,
including native external links and document downloads. This measures the click,
not a completed download or booking. Native disabled buttons do not track.
Existing link and download activation behavior is unchanged.
Phone and email links send only their existing automatic `phone_click` or
`email_click` event; custom event settings do not apply to these links. Downloads
use custom tracking, even when a phone or email target is also configured.
Configuration errors are reported during authoring and blocked by the compiler
before publishing. Click handlers trust validated configuration; a failing
customer tag does not interrupt the custom-tracked button action.
Analytics mode, destination routing, and consent handling use the existing site
configuration; this feature does not grant consent or install tags.

Schema version 41 adds Button tracking fields using the existing `object` kind
for event parameters. Release this package first,
then update the exact `@meetelise/studio-components` version and lockfiles in
both the CRM editor and Studio compiler. The corresponding editor adds tracking
controls; the compiler blocks invalid custom tracking at publish
and refuses tracked buttons when its installed package lacks this contract.

### Nav button links and Footer actions

Nav button links (including buttons in dropdowns) and Footer actions accept the
same `eventName`, `eventCategory`, and `eventParameters` and send the same event
when clicked. Plain Nav links do not send custom events. Schema version 43
declares the fields on Nav links and schema version 44 on Footer actions; the
compiler blocks their tracking errors at publish like a Button's.

## Apply Now and Schedule a Tour buttons

`ApplyNowButton` and `ScheduleTourButton` are Buttons with a conversion event
prefilled. They take Button presentation props, a `content` label ("Apply Now" /
"Schedule a Tour"), an optional `propertyId` that overrides the nearest property
scope for the event, and the custom tracking fields of a Button (`eventName`,
`eventCategory`, `eventParameters`). The schema defaults `eventName` to
`apply_click` / `tour_cta_click` and `eventCategory` to `conversion`, and the
editor writes them when the button is inserted; authors can rename the event, add
parameters, or turn tracking off by removing `eventName`, as on a Button. A
missing `eventCategory` means `conversion`.

- `ApplyNowButton`'s default `apply_click` matches the payload unit cards send
  (`event_category: "conversion"` and `property_id`). It opens its application
  link in a new tab unless `openInNewTab` is false. The link is the property's CRM
  `application_link`, read from the site properties request (attributes only,
  shared by every apply button on the site). An authored `target` (Application URL)
  overrides it.
- `ScheduleTourButton`'s event also carries `tour_type` from `tourType`
  (`guided`, `sgt`, or `virtual`, matching ScheduleTour; the schema labels them
  In-Person, Self-Guided, and Virtual). Without a `target` it opens ScheduleTour for its property in a modal,
  with `tourType` selected when the property offers it; with one it follows
  `action` and `target` like a Button (a tour page, a modal, or a section).
  `tour_booking` still means a completed ScheduleTour booking.

ApplyNowButton without a URL or CRM link, and ScheduleTourButton without a
destination or a property, render nothing on published sites and show a
placeholder in the editor. Nav button links and Footer actions can't hold these
components; give them the same events with custom tracking (`eventName:
"apply_click"` or `"tour_cta_click"` and `eventCategory: "conversion"`).

## Button shapes and icons

`Button.radius` (`none`, `sm`, `md`, `lg`, `xl`, `full`) rounds one button on
the theme's radius scale without changing `radius.base`, so pill-shaped calls to
action can sit beside cards, maps, and modals that keep the theme radius.
`Button.icon` names an `Icon` glyph drawn beside the label; `iconPosition`
places it at the `start` (default) or `end`. Unknown names render no icon, and
document buttons keep their download glyph. Nav button links, Footer actions,
ContactForm submit buttons, and FloorplanCatalog actions forward these fields
through the shared Button presentation contract. Omitting them preserves the
theme radius and text-only label.

`Icon` adds `kitchen`, `lounge`, `sun`, `window`, `bus`, `car`, `phone`,
`mail`, `map-pin`, and `arrow-right` glyphs. Its optional `color` accepts the
same validated colors and `{"$token": "color.primary"}` bindings as other color
props; omitted icons inherit the surrounding text color.

Schema version 42 adds these fields with the Nav, Gallery, SpecialPromotion, and
FloorplanExplorer options below. Release this package first, then update the exact
`@meetelise/studio-components` version and lockfiles in both the CRM editor and
Studio compiler before authoring them in production.

## Leasing office hours

`OfficeHours` reads `attributes.leasing_office_hours_schedule` from the public
property API. Each interval contains `day_of_week`, property-local `start_time`
and `end_time` (`HH:MM`), and `by_appointment_only`. For a non-empty structured
schedule, both layouts render Monday through Sunday, preserving multiple intervals
per day and displaying missing days as `Closed`, matching CRM's enabled-day storage.
Table and inline layouts format these records directly without
parsing human-readable schedules or shifting times to the visitor's timezone.

When structured hours are absent or empty, the existing `leasing_office_hours`
string takes precedence over authored `hours`; an empty schedule does not imply
that all seven days are closed. Both text sources remain intact as text,
including when table layout is selected. Deploy the additive API field before
updating component-package pins to enable structured tables in consumers.

## Thumbnail carousel galleries

Set `Gallery.layout` to `thumbnail-carousel` for a large photo with square
previous/next controls and a strip of five visible thumbnails below it. At desktop
widths (1024px and up), arrows appear on photo hover or keyboard focus when the
device has a fine, hover-capable pointer. Smaller tablet/mobile viewports and
touch-only devices hide the arrows; tappable thumbnails remain available alongside swipes. Swipe left/right on the main photo to navigate on touch screens;
vertical scrolling and pinch zoom remain available, and a tap opens the lightbox. Clicking
a thumbnail selects that photo; the active border and strip stay synchronized
with the arrows. Selecting a photo smoothly shifts its thumbnail to the left
edge of the strip, stopping at the end so the last five thumbnails stay filled.
Overflow supports touch scrolling; only the strip moves, not the surrounding
page. Reduced-motion preferences disable the smooth animation.
Focused thumbnails support Left/Right arrows and Home/End as well as native
Enter/Space activation.

This layout supports category filters and independent section galleries, static
images and live property media. `carouselAccentColor` controls the arrows and
selected border, defaulting to the site's primary theme color. Existing tile
height, image fit, radius, and responsive settings still apply; the default
stage aspect ratio is 4:3. Clicking the large photo opens the existing lightbox;
`lightboxThumbnails` controls only the overlay's optional thumbnail strip.

The editor and compiler receive the new layout through the generated component
schema (version 37). Release the package and update the consuming editor/compiler
versions before selecting this layout in production.

## Scrolling photo strips

Set `Gallery.layout` to `strip` for a single row of square photos that scrolls
horizontally with snap points. `tileHeight` (with `responsiveTileHeight`) sets
each tile's height and width, defaulting to 16rem; `columnGap`, `tileRadius`,
`tileFit`, and `edgeBleed` keep their existing meanings. Tiles open the
existing lightbox. Touch, trackpad, and keyboard focus scroll the row; on wide
screens with a fine, hover-capable pointer, previous/next buttons appear on
hover or focus whenever more photos are available in that direction. Category
filters and section grouping work as in the other layouts.

## Floor plan rent display

`FloorplanExplorer.rentDisplay: "startingAt"` shows the lowest rent with a
"Starting at" label on standard cards instead of the full rent range. The
default `range` keeps the existing output; marketplace cards already show their
starting rent and are unaffected.

## Curated floor plan catalogs

`FloorplanCatalog` is a grouped, selector-and-accordion presentation of live
Elise floor plans. Each authored plan is a reference to an Elise floor plan id;
references missing from the authenticated property API are not rendered.
Authored JSON controls group order, marketing copy, actions, optional image
fallbacks, and an optional square-footage marketing label. API records remain
the source of identity, bed/bath values, availability, rent, and preferred
media. The component follows all cursor pages so a curated id is not lost
beyond the first response page, and stops safely if the API repeats a cursor.

The catalog uses Studio's keyed `sm` through `2xl` responsive contract and a
semantic `collapseAt` breakpoint. Actions reuse the canonical Button contract,
navigation stays router-aware, analytics emit canonical API floor plan ids, and
images use the shared accessible Lightbox. `FloorplanExplorer` remains unchanged
for uncurated listing grids and lists. A validated `compactPresentation` object
controls accordion typography, rhythm, panel insets, and action alignment below
the collapse breakpoint without relying on arbitrary classes or site templates.

The opt-in `desktopPresentation: "workspace"` mode integrates a bounded,
scrollable grouped selector with a stable summary-and-drawing canvas for larger
catalogs. Selector headings, counts, and API-backed plan details are separately
configurable. Below `collapseAt`, `compactDisclosure: "single"` turns the same
catalog into a globally single-open inline accordion; responsive detail columns
and connected panel styling remain authored presentation choices. The default
`split`/`perGroup` behavior and API contract are unchanged.

## Motion and background media

`BackgroundMedia` is a generic frame for decorative image or muted video
backgrounds. It accepts normal Studio children, so headings, buttons, and data
components remain independently editable. The optional `parallax` motion moves
only the media layer; overlay content and document layout stay fixed. Video is
paused outside the viewport and replaced by its poster or image when the visitor
requests reduced motion. If no still is authored, a paused first video frame is
used instead.

```json
{
  "type": "BackgroundMedia",
  "props": {
    "video": { "$content": "hero.video" },
    "poster": { "$content": "hero.poster" },
    "minHeight": "90vh",
    "contentPosition": "bottom-left",
    "motion": "parallax"
  },
  "children": [
    {
      "type": "Text",
      "props": { "content": { "$content": "hero.headline" }, "variant": "display" }
    }
  ]
}
```

`Parallax` applies the same bounded, request-animation-frame scroll transform to
any child subtree. `StickyFrame` pins a subtree within its parent scroll range
and defaults to normal flow on mobile. Together with `Grid`, `Container`, and
`ScrollReveal`, these frames support sticky storytelling, independently moving
media, and layered editorial sections without site-specific JavaScript or CSS.
All motion is progressive enhancement: server output remains visible, unsupported
browsers retain a static layout, and operating-system reduced-motion preferences
disable continuous translation.

## Atlas authoring contract

The exported component schema includes optional `authoring` metadata for Atlas and
semantic validators. It describes component roles, breakpoint-aware props,
composition constraints, required context, relative performance cost, stable repair
hints, and recommended recipe IDs. Runtime rendering ignores this metadata.

The generated `dist/componentSchema.json` versions the component schema, authoring
contract, and diagnostic envelope independently. Diagnostics use a shared shape and
may include existing Studio design operations for Atlas to apply:

```json
{
  "code": "STICKY_MEDIA_OVERLAP",
  "severity": "error",
  "nodeId": "hero-media",
  "breakpoint": "sm",
  "message": "The transformed media can leave its sticky boundary.",
  "suggestedOperations": [
    {
      "type": "set_node_prop",
      "node_id": "hero-media",
      "prop_path": ["motion"],
      "value": "none"
    }
  ]
}
```

`validateAuthoringTree` is the shared, side-effect-free semantic validator used by
Atlas, preview, and compiler consumers. It checks required props, component nesting,
authoring contexts, incompatibilities, deprecations, and an optional normalized
performance budget without resolving or rendering the tree:

```ts
const { diagnostics, performanceCost } = validateAuthoringTree(page.tree, {
  availableContexts: ["property"],
  performanceBudget: 24,
  path: "pages[0].tree",
});
```

Errors represent invalid authoring decisions; warnings cover deprecations, unknown
props, and budget pressure. When a safe default exists, a diagnostic may include a
typed Studio design operation that Atlas can apply as a repair.

## Atlas composition recipes

The `@meetelise/studio-components/recipes` export and generated
`dist/authoringRecipes.json` provide versioned, validated section blueprints. Each
recipe declares its content fields, required site contexts, normalized performance
budget, and a normal Studio component tree using `$content` bindings. The initial
catalog includes editorial hero, split-media story, sticky-media story, and
availability/conversion compositions.

Recipes are build-validated against the canonical component schema and semantic
validator. A build fails if a recipe references an unknown component, an undeclared
content key, or a missing recipe ID. Atlas can therefore select a recipe, create its
declared content fields, and insert the tree through ordinary typed Studio operations.

## Amenity presentations

`AmenityList` keeps layout and ordering independent. Existing sites continue to
use the alphabetized icon-grid default, while `order="source"` preserves the
first occurrence of each amenity from either authored content or the Elise
property API. The opt-in `bulleted` layout emits semantic `ul`/`li` markup and
uses theme body typography. Its indentation, item spacing, and typography can
be adjusted through the validated `bulletPresentation` object without baking a
property's measurements into the shared component.

## Overlay navigation below preceding content

Use `Container display="grid" overlapChildren` to layer navigation over page
content while earlier content, such as a specials banner, stays in document flow:

```tsx
<Container maxWidth="none" paddingX="none" paddingY="none">
  <SpecialPromotion presentation="banner" />
  <Container display="grid" overlapChildren align="start"
    maxWidth="none" paddingX="none" paddingY="none">
    <Nav overlay positioning="layout" sticky />
    <Container maxWidth="none" paddingX="none" paddingY="none">
      <main>{/* Hero and all subsequent page sections */}</main>
      <footer>{/* Footer content */}</footer>
    </Container>
  </Container>
</Container>
```

`overlapChildren` applies only to grid containers. Keep all page content, including
the footer, in one child so sticky navigation follows the full page, and leave
ancestor overflow visible. Shared layouts can place their page `Slot` in that
content child. This composition is opt-in: `overlapChildren` defaults to `false`
and Nav's `positioning` defaults to `"auto"`.

## Navigation presentations

Nav keeps the inline, solid header and dropdown menu as its default. Sites may
opt into stacked or overlay layouts, compact links or equal-width tabs, separate
primary and utility actions, per-link desktop/mobile visibility, a scrolled logo,
an optional compact scroll effect, and a modal drawer. The existing `sticky`
flag controls viewport following while `overlay` controls whether the initial
header occupies document flow. `collapseAt` accepts a boolean or a shared
`sm`/`md`/`lg`/`xl`/`2xl` breakpoint (default: `md`). A breakpoint shows the
hamburger below that width and expands navigation at or above it. Set
`collapseAt: true` to use the hamburger at every width, or `collapseAt: false`
to keep navigation expanded at every width. Booleans select the layout mode;
users can still open and close the hamburger menu normally. Use JSON booleans,
not the strings `"true"` or `"false"`. Changing to `false` closes any open menu
and releases the drawer's scroll lock. Optional validated lengths control logo bounds,
link rhythm, content width, section padding, tab borders, and drawer width without
embedding a site template in the component. For sticky compact navigation, optional `topStyle`
and `scrolledStyle` objects can independently control validated logo height,
frame padding and gap, background color/opacity, divider, shadow, backdrop blur, and
foreground scheme. A bounded pixel threshold and whitelisted transition timing
provide a two-state CSS transition with an optional logo crossfade; reduced-motion
preferences disable that motion. Button-style links consume the canonical Button
presentation contract. Omitting the presentation fields preserves the existing
inline/dropdown behavior.

`utilityCollapseAt` takes the same values as `collapseAt` and lets utility links
(such as a phone number or tour and apply buttons) stay in the bar after primary
links move into the menu; for example, `collapseAt: true` with
`utilityCollapseAt: "lg"` keeps those actions visible from 1024px up. While they
are in the bar, the menu omits them. A value that would collapse utility links
later than `collapseAt` is ignored so every link stays reachable.
`menuButtonPosition` (`start` or `end`) renders the menu toggle inline in the bar,
next to the logo or after the utility links, instead of the default placement.
`drawerShowLogo` repeats the logo beside the drawer's close button. A dropdown
item with `triggerStyle: "button"` renders its desktop trigger with that item's
Button presentation (variant, colors, radius, sizing) and aligns its panel to the
trigger's end; mobile menus keep the standard disclosure row.

## Specials announcement bar

`SpecialPromotion.presentation: "bar"` renders a slim, full-width announcement
on the primary color: the heading (when set), the special's name and description
on one wrapping line, a text call to action with an arrow, and the dismiss
control. It uses the same live or manual content, frequency, delay, and
analytics as the other presentations. Set `heading` to an empty string to show
only the special itself.

## Footer presentations

Footer keeps its columns-and-copyright output as the default. The optional
`property` presentation composes structured brand, address, contact, action,
badge, and legal content inside one semantic footer. Sections and responsive
grids follow the content that is present; actions reuse Button's canonical
presentation and routing behavior. `propertyLayout` and keyed
`responsivePropertyLayout` overrides use Studio's shared breakpoints and
validated CSS lengths, while colors come from the active theme.

## Responsive layout overrides

Container and Stack keep their token spacing as the default design language. For
layouts that need exact authored geometry, Container also accepts optional base
edge padding values and keyed `responsivePadding` overrides. Studio uses the same
mobile-first scale as Tailwind: `sm` (40rem), `md` (48rem), `lg` (64rem), `xl`
(80rem), and `2xl` (96rem). Stack and Grid use the same duplicate-free keyed
contract for authored gaps. Values are limited to simple CSS lengths with finite,
unit-specific bounds; malformed and extreme declarations are ignored. Container
bottom margin is signed so layout composition can intentionally overlap sections.

Button supports the same opt-in pattern for base/responsive font size, weight,
line-height, tracking, horizontal/vertical padding, border width, inset outline,
and bounded shadow presets. `responsiveTypography` can override font size, line
height, or signed letter spacing at the shared breakpoints. Focus presentation is
site-wide rather than per button: token themes may define `focus.ring.width`,
`focus.ring.offset`, and `focus.ring.color`. Unsafe values are ignored, and the
native focus indicator remains the fallback. Omitting presentation fields
preserves the existing variant/size output.

Background-image sections can opt into `bgAttachment` and
`responsiveBgAttachment` (`scroll` or `fixed`). Omitting both preserves the
browser default while allowing authored fixed-background behavior at any shared
breakpoint without runtime-authored CSS classes. Background position supports
edges and all four corners. `bgOverlayOpacity` controls the overlay layer
independently from its color and is bounded to the CSS opacity range.

## Coordinate-aware property maps

`PropertyMap` renders a property coordinate on the same map runtime used by
Studio's unit marketplace. Its default MapLibre style is served by Elise; an
installation may instead configure Mapbox or an alternate style through the
existing Studio runtime configuration. Provider credentials are platform
configuration and are never authored into site JSON.

Authors control center, zoom, responsive height, the property marker, and an
optional list of nearby-place markers. Every marker can use an image or safe
custom/category color, a structured information card, and a generated or custom
Google Maps view/directions action. When nearby places are present the map fits
the property and visible places by default; authors may opt back into manual
center and zoom. Only one card opens at a time and it remains attached to its
coordinate while the visitor pans or zooms, flips and clamps inside the map
viewport, supports phone, email, and Google Maps actions, and can be dismissed
and reopened. Responsive values use Studio's keyed `sm` through `2xl` contract.
`MapEmbed` remains the lightweight iframe option and retains its existing output
and TypeScript `markers` input.

## Contact form presentations

`ContactForm` keeps its existing comfortable, stacked lead form as the default
and continues to submit through Elise's authenticated `/v1/prospect` endpoint.
Opt-in grid layout, one-to-three columns, compact density, grouped names,
configurable fields, labels, and requiredness are independent authoring choices.
The native date input remains the default; `usText` accepts editable numeric or
punctuated input, formats it on blur, validates the calendar date, and sends the
existing ISO `move_in_date` payload.

Base and keyed responsive values can control row, column, and label-to-control
gaps. A bounded control-presentation object covers minimum heights, padding,
border, radius, and label typography without embedding a site template. Submit
buttons consume the canonical Button presentation contract and add independent
base/responsive alignment. Invalid authored CSS values are ignored, fields keep
theme colors and visible focus, and validation errors identify and focus the
affected control.

## Property SEO template tokens

A `building_template: true` page may use the following compile-time tokens in
plain string fields. Resolution is recursive, so the same tokens work in the
page path, search/social metadata, and nested `seo.structuredData` strings.

| Token | Merged content source |
| --- | --- |
| `{{property.name}}` | `building.name` |
| `{{property.slug}}` | `vars.slug` |
| `{{property.city}}` | `building.city` |
| `{{property.state}}` | `building.state` |
| `{{property.neighborhood}}` | `building.neighborhood` |
| `{{property.address}}` | `building.address` |
| `{{property.shortDescription}}` | `building.short_description`, then `building.tagline` |
| `{{property.amenitySummary}}` | `building.amenity_summary` |
| `{{property.bedroomRange}}` | `building.bedroom_range` |
| `{{site.name}}` | `site.name`, then merged `meta.name` / `meta.title`, then raw site name metadata |

The existing `{{slug}}` syntax remains an alias for `vars.slug`. A token whose
source is missing stays visibly unresolved so publish-time SEO checks can report
the missing data. Runtime `{$data: "..."}` bindings are never interpolated.

Property hero images are intentionally not string tokens. Author an image or
social-image field with the building-scoped object binding
`{$content: "building.image"}`; the resolver converts its `AssetValue` to that
building's CDN URL.

## Typography theme tokens

Typography is configured by role so a bounded token set can cover every Text
variant. All tokens are optional. Without an authored role override, each
variant retains its built-in responsive size, weight, line-height, and tracking.

| Role | Family | Scale | Weight | Line height | Tracking |
| --- | --- | --- | --- | --- | --- |
| Heading (`display`, `h1`–`h3`) | `font.heading` | `font.heading.scale` | `font.heading.weight` | `font.heading.line-height` | `font.heading.tracking` |
| Body (`subtitle`, `body`, `body-lg`, `caption`, `overline`) | `font.body` | `font.body.scale` | `font.body.weight` | `font.body.line-height` | `font.body.tracking` |

Scale is a unitless multiplier such as `"1.1"`. Weight and line-height accept
CSS values such as `"600"` and `"1.4"`; tracking accepts a CSS letter-spacing
value such as `"-0.02em"`. All values may use the existing `{$token}` alias
shape, and building-mode overrides inherit through `ThemeScope`.

Individual `Text` nodes can override base font size, weight, line height,
letter spacing, transform, and alignment. The keyed `responsiveTypography`
object uses the same `sm` through `2xl` contract as layout and Button, with
optional font-size, line-height, and signed letter-spacing values at each
minimum viewport. Values are validated and bounded before rendering; invalid
authored values are ignored without removing the variant defaults.

Body Google fonts include the regular italic face in addition to the upright
weight range, allowing semantic emphasis to use the selected family without
requesting every italic weight. Duplicate stylesheet requests are suppressed.

## Centralized accessibility presentation

Built-in site components use semantic CSS colors for content, surfaces, actions,
control borders, and status messages. Themes can set `color.surface`,
`color.surface-muted`, `color.surface-inverse`, `color.on-inverse`, `color.on-dark`,
`color.on-primary`, `color.on-secondary`, `color.on-accent`, `color.overlay`,
`color.on-overlay`, `color.border`,
`color.border-strong`, `color.link`, and paired `color.success`, `color.warning`,
`color.danger`, and `color.info` / `color.*-surface` tokens. Keep foreground and
background pairs readable in every state; an arbitrary authored palette is not
a guarantee of WCAG contrast. Existing configurations retain their published color
bindings, individual Tailwind shades, opacity, hover states, and text metrics.
New author role overrides are opt-in: missing roles do not replace distinct legacy
surfaces or status shades with one shared value. The internal `--studio-fg-*`,
`--studio-bg-*` and `--studio-border-*` palette variables preserve those values
while allowing visitor transforms and component overrides. General component
color, background, and border overrides leave status palettes intact; customize
status colors through their paired theme roles.
Transparent components with `colorScheme="dark"` use `color.on-dark` (white by
default) for an externally supplied dark surface; this role follows visitor
palette changes. `color.on-inverse` belongs on a painted inverse surface.
Photographs, videos, and authored photographic overlays retain their pixels.
With inversion enabled, overlaid content uses a local light contrast palette and
a backing surface so text and controls remain paired against unchanged media.
An explicit visitor contrast choice takes precedence over that local palette.
An explicit component text color overrides its scheme; a background override
alone does not select a different foreground.

`ThemeProvider` accepts runtime `accessibility` preferences independently of
saved author themes: `textScale` (1–2), `contrast` (`none`, `light`, `dark`),
`inverted`, `monochrome`, `saturation` (0–2), `textSpacing`, and `reducedMotion`.
Nested building themes and their overlays retain those preferences. This is the
presentation API for a future visitor widget; the package does not add that widget
or persist a visitor's choices. Reduced motion also stops slideshow timers and
autoplaying videos and removes map camera animation. VideoHero exposes an opt-in `showPlaybackControl` author setting; its default
keeps the existing video chrome. Native browser zoom, forced colors, and
reduced motion continue to work.

Button, Link, Accordion, AmenityList, ApartmentAmenity, ContactForm, UnitExplorer, and
FloorplanExplorer expose optional `color`, `backgroundColor`, `borderColor`, and
`fontSize` in the shared authoring schema. Omitting a property restores its theme
default. Nested navigation/footer actions and ContactForm's `submitPresentation`
use the same Button color fields. Complex component font sizes set a base while
preserving the relative sizes of internal labels; `em`/`%` bases resolve against
the theme body size once. Text, Button, gallery headings, amenity bullets, navigation
labels, form labels, and floorplan catalog typography include visitor enlargement
when the author supplies an explicit or responsive size. Fixed authored line heights
enlarge with their text; unitless line heights remain proportional. Absolute
sizes retain their authored units, including pixels at non-16px document roots.
Visitor scaling does not expand the set of components controlled by preexisting
`font.body.*` tokens. Font-only nested scopes preserve inherited CSS colors and
typography, and focus styling is activated by explicit focus tokens.

When adding a component, use semantic variables for built-in styles and
`useStudioPresentation` for author-provided solid colors and sizes. Validate solid
colors with `safeCssColor` before transforming them. Use `color(value, role)` for
CSS color/background properties and `background(value)` for `background-image`.
Theme palette transforms support hex, named colors, numeric RGB/HSL (including
alpha and HSL angle units), and `var(--token)` aliases. Known color stops in
gradients and `color-mix()` follow the same transforms. Inversion, monochrome,
and saturation also support numeric `oklch()` colors used by the published Tailwind
palette; unresolved custom properties and other advanced functions are preserved.
Do not use a page-wide CSS filter: palette changes should preserve photographs
and videos. Media overlay/background pairs remain together through palette
changes using `color.overlay` and `color.on-overlay`. Use minimum heights for text
controls and allow text to wrap while preserving default dimensions. The existing
Accordion height animation is retained; collapsed panels are inert and hidden
from assistive technology. Mount fixed overlays outside
transformed or clipped ancestors and carry the hook's effective `variables` and
`preferences` onto a dedicated portal theme root. SpecialPromotion demonstrates
a body-level portal that preserves the nearest building theme without adding a
full-page canvas.

Use structured author controls or semantic utilities for colors and typography.
Arbitrary `className` literals and third-party embed or WebGL basemap styling are
outside the shared color contract; custom integrations must apply equivalent
preferences through their own APIs. Studio-owned map controls, markers, and
information cards use the shared presentation contract. Media itself is preserved.

Release verification should include browser checks for 200% text enlargement,
320 CSS-pixel reflow, WCAG text-spacing overrides, keyboard focus, forced colors,
and open overlays with both a root theme and a nested building theme. Unit tests
verify presentation/state contracts but cannot establish visual reflow or contrast.
