# Lazy Vacations — State of Development

Authoritative snapshot of the **Palette Editor** and the design-token system it
drives. Covers the architecture, the "everything disappears" bug and its fix, the
full token reference, every structural element, and all logic.

> **Companion docs (all current, re-synced to the CSS):**
> - `TOKENS.md` — the full, authoritative token reference (colour · type · shape ·
>   editor chrome). Read it for every token name/value; §3 here is the architecture-
>   level summary.
> - `CONTENT-MODEL.md` — the canonical content/section model for the property.
> - `TODO.md` — roadmap (booking drawer, flow, sub-pages, gallery).

---

## 1 · What this is

The **Palette Editor** (`Palette Editor.dc.html`) is a live theming workbench. A
left icon-rail + settings panel drive five controls; the right pane renders one of
two real page designs (Hero / Split) that re-theme instantly:

| Control | What it changes | Mechanism |
|---|---|---|
| **Palette** | colours + hero photo | `data-skin` on the preview surface → `palette.css` cascade |
| **Type** | the 3 font roles | `--font-heading/body/label` custom properties |
| **Button radius** | corner radius of all CTAs | `--btn-radius` custom property |
| **Layout** | which design is shown | mounts `Hero Design` or `Split Design` |
| **Device** | preview frame width | inline width on the frame wrapper |
| (Theme) | editor chrome dark/light | `data-theme` on `.la-app` (chrome only — not the design) |

The preview designs are **the real artifacts** — `Hero Design.dc.html` and
`Split Design.dc.html` — each composed from ~12 child components. The editor only
sets tokens on a wrapper; the design system's cascade does the rest.

---

## 2 · The "everything disappears" bug — root cause & fix

**Symptom:** every time the file was edited, the whole preview (and its colours/
fonts) blanked.

**Root cause:** the theme was applied through **template value-holes** in `style`
attributes — e.g. `style="{{ selStyle }} {{ pairStyle }} …"` and
`data-theme="{{ theme }}"`. In this runtime a `{{ hole }}` only resolves **after a
render completes**. While the template re-streams (which happens on every edit /
hot-reload), those holes are empty — so `--brand`, `--accent`, `--font-*`,
`--btn-radius` and the device frame all evaporated at once, and the imported
designs (built entirely on `var(--brand)` etc.) collapsed to nothing.

**Fix — two rules, now load-bearing:**

1. **Never theme through holes.** `applyTheme()` writes theme values **directly to
   the live DOM node** (`setAttribute` / `style.setProperty`) from
   `componentDidMount` + `componentDidUpdate`. Imperative writes don't depend on a
   completed render, so they survive streaming.
2. **Static defaults on the surface.** `#lvpe-surf` carries inline default tokens
   (`data-skin="rustic"` + default fonts + `--btn-radius:6px`) so it is **fully
   painted before/while JS runs** and can never flash empty. These inline defaults
   **must equal the `state` defaults** in the logic class.

A dead-end worth recording: an intermediate attempt used React `ref`s on the
nodes (incl. the component **root**). A ref on the root element can break mounting
in this runtime — it rendered blank. The working approach uses **`getElementById`
on stable inner IDs** (`lvpe-surf`, `lvpe-frame`), never a ref on the root.

> **Invariant:** the preview surface must be styleable without any hole resolving.
> If you add a new themed property, give it a static inline default on `#lvpe-surf`
> **and** set it imperatively in `applyTheme()` — do **not** add a `{{ hole }}` in
> the surface's `style`.

---

## 3 · Token reference

Four stylesheets own all visual values. Components/pages reference tokens and role
classes only — never hard-coded fonts/sizes/colours.

| File | Owns | Loaded by |
|---|---|---|
| `palette.css` | colour skins (primitives) + derived semantic colours | `type.css` (`@import`) |
| `shape.css` | radius · elevation · spacing · glass/scrim | `type.css` (`@import`) |
| `type.css` | font families, font roles, type scale, text colours, `.t-*` classes, `.t-btn/.t-pill`, responsive | every design's `<helmet>` |
| `lazyapp.css` | the **editor chrome** UI kit (`.la-*`) — separate from the site palette | Palette Editor only |

### 3.1 Colour — `palette.css`

**Primitives (per skin).** Applied with `data-skin="…"` high in the tree. Five
values per skin + a default hero photo:

| Skin | `--brand` | `--accent` | `--neutral` | `--on-brand` | `--on-accent` | `--hero-img` |
|---|---|---|---|---|---|---|
| `rustic` *(default)* | `#2f5a4a` | `#cc6b4a` | `#5e6158` | `#ffffff` | `#ffffff` | hero-rustic.avif |
| `coastal` | `#1f6f8b` | `#e3a23c` | `#5c6670` | `#ffffff` | `#21303a` | hero-coastal.png |
| `urban` | `#ff2d9b` | `#c6ff36` | `#6e6e77` | `#15151a` | `#15150f` | hero-urban.avif |
| `classy` | `#1c2b46` | `#c2a14d` | `#5a5d66` | `#ffffff` | `#1c2b46` | hero-classy.jpg |
| `cute` | `#13b3bd` | `#ff5aa8` | `#7d7680` | `#ffffff` | `#ffffff` | hero-cute.avif |

**Derived semantic tokens.** Declared on `*` (not `:root`) so every element
re-derives against its inherited skin primitives — this is what lets a
`data-skin` on a *descendant* (like the editor's inspector preview) re-theme just
that subtree. Components read **these**, never the primitives:

| Token | Derivation | Role |
|---|---|---|
| `--brand-strong` | brand + black 18% | hover/pressed brand |
| `--accent-strong` | accent + black 16% | hover/pressed accent |
| `--background` | neutral + white 94% | page background |
| `--surface` | `#ffffff` | cards & panels |
| `--surface-sunken` | neutral + white 88% | insets & fills |
| `--surface-tint` | brand + white 90% | brand-tinted fills |
| `--border` | neutral + white 70% | hairlines, inner grids |
| `--border-strong` | neutral + white 55% | main outlines |
| `--text-muted` | neutral + white 30% | captions, labels |
| `--text` | neutral + black 55% | headings & body |

> Why `*` and not `:root`: a `:root` var() freezes at the root, so overriding the
> primitives on a `[data-skin]` descendant would **not** re-derive. Declaring the
> derived tokens on `*` makes each element recompute. Keep it that way.

### 3.2 Type — `type.css`

**Families → roles → scale.** Edit a family token (and load its webfont) to swap a
typeface everywhere; re-point a role to restyle one job.

| Family token | Default | Role token | Drives |
|---|---|---|---|
| `--family-serif` | `Source Serif 4` | `--font-heading` ← serif | display · section · card title · price |
| `--family-sans` | `Source Sans 3` | `--font-body` ← sans | lead · body · caption · link · chip · input |
| `--family-lato` | `Lato` | `--font-label` ← lato | eyebrow · button · field label |
| `--family-sketch` | `Kalam` | — | wireframe annotation chrome only |

> The Palette Editor overrides `--font-heading/body/label` per font-pairing (§5.2).
> It does **not** touch the family tokens — it points the three roles at whole
> stacks directly.

**Type scale** (size / leading / weight / tracking) — one set of `--t-*` tokens;
see the full table in `TOKENS.md §2.3`. Roles render via `.t-*` classes
(`.t-display`, `.t-section`, `.t-card-title`, `.t-lead`, `.t-body`, `.t-caption`,
`.t-link`, `.t-chip`, `.t-input`, `.t-eyebrow`, `.t-button`, `.t-field-label`,
`.t-price`, `.t-strong`). Text colours are `--t-color-*`, wired on `*` to follow
`--text` / `--text-muted` / `--brand`.

**Buttons & pills.** `.t-btn` / `.t-pill` take intent (`--brand`/`--accent`) ×
style (`filled`/`outline`/`ghost`/`inverse…`) × size (`md`/`sm`). Both read
`--btn-radius` (fallback `--radius-sm`) — this is the hook the editor's radius
control writes.

### 3.3 Structure — `shape.css`

Radius (`--radius-sm 6 · --radius 10 · --radius-lg 16 · --radius-pill 999`),
elevation (`--elev-1/2/3`), spacing scale (`--space-1…7`), semantic density
(`--gap 16 · --pad-card 22 · --pad-section 30`), glass/scrim
(`--glass-bg/border/blur/shadow`, `--scrim`), and fixed wireframe-annotation
colours (`--wf-violet/ink/badge-shadow`). The editor's radius control overrides
`--btn-radius` (consumed by `.t-btn`/`.t-pill`); the `--radius*` tokens are the
underlying scale.

### 3.4 Editor chrome — `lazyapp.css` (`.la-*`)

A **separate** brutalist UI kit for the tool shell — not part of the site palette,
so it never re-themes when the user switches skins. Tokens `--la-paper/bg/ink/
line/shadow/teal/coral/font/frame/radius`. Has a `[data-theme="dark"]` block that
remaps paper/bg/ink/line/shadow (this is what the editor's moon/sun toggle flips).
Classes: `.la-app .la-panel .la-title .la-label .la-btn (+--teal/--coral)
.la-swatch .la-iconbtn`.

---

## 4 · File map

**Theming controller**
- `Palette Editor.dc.html` — the workbench (this doc's subject).

**Preview designs** (the real artifacts the editor themes)
- `Hero Design.dc.html` — sticky hero + left rail + content column, ~18 sections.
- `Split Design.dc.html` — title row + photo mosaic split layout.
- `*-print.dc.html`, `* (offline).dc.html`, `*-offline.html` — print/standalone exports.
- `Sabattus Shalomus - *(wireframe).html` — earlier static wireframes.

**Child components** (consumed by the designs via `<dc-import name="…">`). Each is
a token-built, prop-driven DC; data flows **one way** — a design's `renderVals()`
passes a plain-object prop, the component renders it. Props with `editor:null` are
whole data objects (not in-panel editable); `$preview` sets the specimen size.

| Component | Prop(s) | Renders |
|---|---|---|
| `Menu` | `menu` `{num, logo?, lead?, links[]}` | sticky header: logo/contact button + nav (`variant:'button'` → outline btn, else ghost) + mobile hamburger drawer |
| `Logo` | `h` (height) | the LV wordmark lockup |
| `ContactButton` | `text` `href` | a single `.t-btn` brand CTA |
| `Reviews` | `reviews` `{num}` | review-bar strip |
| `GuideSection` | `sec` `{num, anchor, title, titleNote?, mapTop?, intro?, mapInline?, icons[]?, parasTop[]?, photos?, parasAfter[]?, prices[]?, bot}` | the workhorse content section — every field is an optional `<sc-if>` block; `bot:'full'\|'compact'` picks the recurring Ask-the-Bot footer |
| `Gallery` | `gal` `{num, title, intro?, variant, cols, photoHeight, cards[]}` | card grid; `variant:'top-image'\|'overlay'` |
| `CallOut` | `call` `{num, title, desc, iconText?, button{text,href}}` | single call-out row with CTA |
| `SocialFeed` | `feed` `{num, title, note, posts[]}` | masonry post grid |
| `Weather` | — | weather-widget placeholder |
| `Contact` | — | name/email/message contact form |
| `LodgifyBooking` | — | third-party booking-widget placeholder (stands in for `Booking`) |
| `Booking` | `{num,title,cta}` | native booking card *(currently unmounted; Lodgify used instead)* |
| `ShowAllPhotos` | — | "Show all photos" pill button |
| `ImagePlaceholder` | `label` | greybox photo slot (diagonal-cross fill) |
| `Icons` | — | amenity-icon specimen sheet |

> **Wireframe chrome.** Every component carries its `{{ num }}` label badge + a
> `⬡ Component` tag, gated by `--lbl` (the `showLabels` prop on the designs flips
> `--lbl` between `inline-flex`/`none`). The `sketchFont` prop swaps the Kalam
> annotation face for a system face. These are scaffolding, not site UI.

**Token specimens / docs**
- `Type Sheet.dc.html`, `Color Sheet.dc.html`, `Usage Map.dc.html`, `Atoms.dc.html`
  — live token specimens (open to see/edit the system).
- `TOKENS.md` (full token reference — now current), `CONTENT-MODEL.md`,
  `TODO.md`, **this file**.

**Stylesheets** — `palette.css · shape.css · type.css · lazyapp.css`
**Assets** — `images/hero-*.{avif,png,jpg}` (one per skin), `icons/`.
**Runtime** — `support.js` (DC runtime; do not edit).

---

## 5 · Logic — `Palette Editor.dc.html`

A single `class Component extends DCLogic`. No external state; resets to defaults
on reload.

### 5.1 State (each field → one control)

```
skin:'rustic'  pair:'rustic'  btnRadius:'6px'  layout:'hero'
device:'desktop'  theme:'dark'  group:'layout'  panelOpen:true
```

> The four themed defaults (`skin·pair·btnRadius` + the device) **must mirror** the
> static inline defaults on `#lvpe-surf`. If you change one, change both.

### 5.2 Catalog constants (display + values)

- `SKINS[5]` — `{key,label,brand,accent}`. **Keys & hexes mirror `palette.css`'s
  `[data-skin]` blocks.** The hexes here drive only the panel's two swatch dots;
  the live preview is coloured by `palette.css` via `data-skin`, so these never
  need the full primitive set.
- `PAIRS[6]` — `{key,label,heading,body,label3}`; the three font-role stacks
  (elegant · modern · brutalist · simple · cute · rustic).
- `RADII[4]` — Sharp 0 · Soft 6 · Round 12 · Pill 999.
- `DEVICES[3]` — Desktop (null=100%) · Tablet 768 · Phone 390.
- `TOKEN_ROWS[9]` — the semantic tokens listed in the expanded palette inspector.
- `TITLES` — panel header per group.

### 5.3 `applyTheme()` — the theming contract

Called from `componentDidMount` and `componentDidUpdate`. Writes **only to live
nodes**, never holes:

```
#lvpe-surf:
  setAttribute('data-skin', skin)            → palette + hero photo (palette.css)
  style --font-heading / --font-body / --font-label  (from PAIRS)
  style --btn-radius
.la-app (closest):
  setAttribute('data-theme', theme)          → editor chrome only
#lvpe-frame:
  style.cssText = width + framing            → device frame (Desktop = 100%)
```

One attribute (`data-skin`) re-themes the entire imported design because
`palette.css` derives every semantic token from the skin primitives. The editor
therefore stores **zero** colour values at runtime beyond the panel-swatch hints.

### 5.4 `renderVals()` — view model

Pure derivation from `state`; returns:
- `panelOpen, groupTitle, themeIcon, themeToggle, closePanel`
- group flags `isLayout/isType/isRadius/isPalette` + togglers
  `selLayout/selType/selRadius/selPalette` (via `groupSelect`)
- control lists `layouts, pairs, radiusOpts, palettes (+chips), deviceOpts` — each
  item `{label, on, select}` (palette items also carry `key/brand/accent/chips`)
- preview summary `skinLabel, layoutLabel, isHero, isSplit`

`groupSelect(k)` opens group `k`, or **collapses** the panel if `k` is already the
open group (clicking an active rail icon toggles the panel shut).

### 5.5 Template anatomy

```
.la-app[data-theme]              flex row, full height
├─ icon rail (86px)              LV mark + 4 .la-iconbtn group toggles
├─ <sc-if panelOpen> settings panel (330px)
│   ├─ header: groupTitle + theme toggle + collapse
│   └─ <sc-if isLayout|isType|isRadius|isPalette> → the matching control list
│        (palette items expand to a data-skin'd inspector of TOKEN_ROWS chips)
└─ preview column (flex:1)
    ├─ header: "{layoutLabel} · {skinLabel}" + device buttons
    └─ #lvpe-surf  (static default theme; themed by applyTheme)
        └─ #lvpe-frame  (device width)
            ├─ <sc-if isHero>  <dc-import name="Hero Design">
            └─ <sc-if isSplit> <dc-import name="Split Design">
```

The palette inspector preview uses `data-skin="{{ p.key }}"` so each expanded skin
shows **its own** derived colours regardless of the active skin — leveraging the
same `*`-derivation as the live preview.

---

## 6 · Invariants (don't regress these)

1. **No theming through `{{ holes }}`** on `#lvpe-surf` — imperative writes only.
2. **Static inline defaults on `#lvpe-surf` == `state` defaults.**
3. **`getElementById`, never a `ref` on the component root.**
4. **`SKINS` keys/hexes mirror `palette.css`.** Add a skin → add it in *both*, plus
   an `images/hero-<key>.*` and the `--hero-img` line in `palette.css`.
5. **Derived colour tokens stay declared on `*`** (not `:root`).
6. **Editor chrome (`.la-*`) is separate from the site palette** — never wire one
   to the other, or the tool would re-theme when the user switches skins.

## 7 · How to extend

- **Add a palette:** new `[data-skin="x"]` block in `palette.css` (5 primitives +
  `--hero-img`) + `images/hero-x.*` + a `SKINS` row in the editor.
- **Add a font pairing:** load the webfonts (helmet `<link>`) + a `PAIRS` row.
- **Add a radius / device:** a `RADII` / `DEVICES` row — no other change needed.
- **Add a third layout:** a `<sc-if isFoo>` + `<dc-import>` in the preview, a
  `layouts` entry, and an `isFoo`/`layoutLabel` branch.
