# Aether AI Agency

Applied-AI studio marketing site. Static, multi-page, plain HTML + CSS + vanilla JavaScript — no build step, no npm, no framework.

Built with Hudby.

---

## Page map

| File | Purpose |
| --- | --- |
| `index.html` | Homepage — hero, service disciplines, stats, testimonials, closing CTA |
| `services.html` | Five service lines with scope, duration and deliverables |
| `work.html` | Selected engagements and outcomes |
| `approach.html` | How engagements run, week by week |
| `about.html` | Studio masthead, team, operating principles |
| `contact.html` | Direct contact details, written brief form, booking CTA |
| `styles.css` | Design system: tokens, typography, layout, components, motion |
| `main.js` | Nav, scroll reveal, counters, tabs, booking modal, Airtable submit |
| `README.md` | This file |

All pages share the same header/footer chrome but have distinct layouts, content and section order. Navigation uses real relative links (`href="services.html"`), not a hash router.

---

## Running locally

Any static file server works. The site uses only relative paths plus CDN assets (Tailwind CDN, Google Fonts).

**Python (built in):**

```bash
python3 -m http.server 8080
# open http://localhost:8080
```

**Node:**

```bash
npx serve .
```

**Direct file open:** double-click `index.html`. Tailwind CDN and Google Fonts need network access; everything else runs from `file://`. The booking modal's Airtable submit requires an `http://` or `https://` origin — CORS does not apply to `file://`, so use a local server when testing real submissions.

---

## Design system

Defined as CSS custom properties in `styles.css` under `:root`, and mirrored as Tailwind theme extensions in each page's inline `tailwind.config` block.

- **Palette** — `bg` `#0b0d10`, `panel` `#11141a`, `line` `#232833`, `ink` `#eef1f6`, `muted` `#8f97a8`, `accent` `#c9f259`, `ember` `#f2a259`, `cool` `#59c7f2`.
- **Type** — Inter for UI and body; Instrument Serif for editorial display lines; JetBrains Mono for labels, metadata and numerals.
- **Spacing / radii / shadows** — `--r-sm|md|lg|xl`, `--sh-sm|md|lg`, spacing scale on a 4px base.
- **Motion** — transitions use `--ease` and `--dur`; `prefers-reduced-motion: reduce` disables reveals, counters snap to final values, and the modal appears without transition.
- **Components** — `.btn` (`.btn-primary`, `.btn-ghost`, `.btn-sm`, `.btn-shimmer`), `.card`, `.stat`, `.testimonial`, `.cta-panel`, `.footer`, `.booking-*` modal classes.

---

## Interactions (`main.js`)

`main.js` is a single IIFE. Each behaviour degrades silently if its markup is absent, so every page can include the script safely.

- **Scroll reveal** — `.reveal` elements fade/translate in via `IntersectionObserver`; per-element delay from `data-delay` (ms), written to `--delay`.
- **Nav shrink** — `#site-header` gains `.is-scrolled` past 12px of scroll.
- **Mobile menu** — `#nav-toggle` toggles `#mobile-menu`, keeps `aria-expanded` in sync, closes on link click, `Escape`, outside click and breakpoint change.
- **Stat counters** — `[data-counter]` animates `0 → data-target` with `data-suffix` using `requestAnimationFrame` and easeOutCubic, triggered once on scroll into view.
- **Service tabs** — `#service-tabs` buttons toggle panels with `role="tab"` / `aria-selected` semantics.
- **Booking modal** — `[data-booking-open]` on any CTA opens the shared strategy-call modal: focus trap, focus restore on close, `Escape` and backdrop dismissal, `aria-modal`, scroll lock, inline validation, loading/success/error states, and a local intent handoff if Airtable is not configured.

---

## Booking submissions → Airtable

The "Start a project" / "Book a scoping call" buttons across all pages open one shared modal defined in `index.html` (`.booking-*`, `#booking-modal`). `main.js` handles everything else.

### Configuration

Configuration is resolved at runtime, in this order:

1. `window.AETHER_BOOKING` — set in a script tag before `main.js` loads.
2. `<meta name="aether-airtable-base">` + `<meta name="aether-airtable-table">` + `<meta name="aether-airtable-token">`.
3. `<meta name="airtable-base-id">` + `<meta name="airtable-bookings-table">` (legacy names).

The `<meta>` placeholders ship empty in every HTML file. Fill them in, or define the global object, to go live:

```html
<!-- before <script src="main.js" defer></script> -->
<script>
  window.AETHER_BOOKING = {
    baseId:  "appXXXXXXXXXXXXXX",
    table:   "Bookings",
    token:   "patXXXXXXXXXXXXXX.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    endpoint: "" // optional: full URL override, wins over baseId + table
  };
</script>
```

If only `endpoint` is set it is used verbatim. Otherwise the script builds:

```
https://api.airtable.com/v0/{baseId}/{encodeURIComponent(table)}
```

Fields written on each successful submit:

| Airtable field | Source |
| --- | --- |
| `Name` | form name input |
| `Email` | form email input |
| `Company` | form company input (optional) |
| `Project` | form project description textarea |
| `Budget` | select value (optional) |
| `Timeline` | select value (optional) |
| `Source` | `data-booking-open` value of the trigger (e.g. `nav`, `contact-hero`) |
| `Submitted At` | ISO 8601 timestamp |
| `Page` | `location.pathname` of the submitting page |

Rename fields in one place: the `FIELD_MAP` object at the top of the booking section in `main.js`. Adjust `buildRecord()` there if your column names differ.

### Airtable base setup

Create a base named **Aether — Bookings** (separate from any other project's base) with a table named **Bookings** and these fields:

| Field name | Type | Notes |
| --- | --- | --- |
| `Name` | Single line text | primary field |
| `Email` | Email | |
| `Company` | Single line text | |
| `Project` | Long text | |
| `Budget` | Single select | e.g. `Under €25k`, `€25k–75k`, `€75k–150k`, `€150k+` |
| `Timeline` | Single select | e.g. `ASAP`, `1–3 months`, `3–6 months`, `Exploring` |
| `Source` | Single line text | |
| `Submitted At` | Date — include time | |
| `Page` | Single line text | |

Then create a personal access token scoped to **only this base** with the `data.records:write` scope. Do not use a full-account token.

### Token exposure — read this before going live

The token lives in the browser by design, because this project has no server. Airtable tokens placed in client-side HTML are visible to anyone who views source. Mitigations, in order of preference:

1. **Proxy submissions through a serverless function.** Point `endpoint` at a function (Cloudflare Worker, Netlify/Vercel function) that holds the token as a server-side environment variable and forwards the record to Airtable. Nothing secret ships to the browser.
2. **Narrow the token.** If you must keep it client-side, scope it to the single bookings base and `data.records:write` only. Never grant `schema.bases:read` or access to other bases.
3. **Add a passphrase later.** If the record volume grows, add a captcha or rate limit in the proxy — Airtable's own rate limit (5 requests/sec per base) is not abuse protection.

Unconfigured, the modal does not fail: it validates, then offers a local handoff (a `mailto:` draft with the details prefilled) and states plainly that the online submission is not connected.

---

## Enabling email notifications for new bookings

Email notifications are an Airtable concern, not a site concern. The site only creates records; Airtable sends the mail. Two supported routes:

### Option A — Airtable automation (recommended)

1. Open the **Aether — Bookings** base → **Automations** → **Create automation**.
2. **Trigger:** *When record is created* → table `Bookings`.
3. **Action:** *Send email*.
   - **To:** your intake address (e.g. `studio@yourdomain.com`).
   - **Subject:** `New strategy call — {Name} ({Company})`
   - **Body:** include `Name`, `Email`, `Company`, `Project`, `Budget`, `Timeline`, `Source`, `Page`, `Submitted At`. Lead with email and project so it can be triaged from a phone lock screen.
4. **Test** the automation with a real form submission from the deployed site.
5. **Turn the automation on.** Airtable saves drafts; an automation that is not switched on sends nothing.

Add a second action if you want a copy in Slack, Teams or a shared inbox — chain it after the email step.

### Option B — Airtable form / view subscription

If you prefer to read bookings in a view rather than an inbox: create a Grid view on `Bookings` filtered to `Submitted At` in the last week, then use *Notifications* on that view to email a digest. Less immediate, but nothing to maintain.

### Deliverability notes

- Send from an address on a domain you own; Airtable sends from its own infrastructure on your behalf, so a custom `From` improves trust.
- Add the recipient to your allowlist if your mail provider filters aggressively.
- Keep the intake address a shared mailbox, not one person's account — the person who set up the base will eventually leave.

---

## Browser support

Evergreen Chrome, Safari, Firefox and Edge. Uses `IntersectionObserver`, `requestAnimationFrame`, `fetch`, `AbortController` and CSS custom properties — all baseline-supported. `dialog` is not used; the modal is a plain element with manual focus management, so there is no Safari version cliff.

---

## Accessibility

- Skip link to `#main` on every page.
- Semantic landmarks: `header`, `nav`, `main`, `footer`, with `aria-labelledby` on each major section.
- Mobile toggle maintains `aria-expanded` and `aria-controls`.
- Booking modal: `role="dialog"`, `aria-modal="true"`, labelled by its heading, focus trapped while open, focus returned to the triggering button on close, `Escape` closes, background scroll locked.
- Form errors render next to their field, the first invalid field receives focus, and the summary is announced via `aria-live="polite"`.
- Visible `:focus-visible` rings tuned to the dark surface; focus is never obscured by the sticky header.
- `prefers-reduced-motion: reduce` disables reveals, counters animate instantly and modal transitions are removed.
- Touch targets meet 44px on mobile; inputs use 16px text to prevent iOS zoom; browser zoom is never disabled.

---

## Notes on deployment

- The site is fully static — deploy the repository root to any static host (Netlify, Vercel, Cloudflare Pages, GitHub Pages, S3). No build command, no output directory. Publish root; `index.html` is the entry point.
- Every file parses independently. There is no import graph, so a truncated or missing optional file cannot break the rest of the site.
- If a deploy fails, check that all nine files are present and that `styles.css` and `main.js` are referenced with relative paths from each HTML file.
- No image hotlinks. The hero and section art is inline SVG plus CSS gradients, so there are no third-party image dependencies to break.
