Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 25 additions & 23 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,45 +1,45 @@
# AGENTS.md Ember
# AGENTS.md: Ember

Project-specific notes for the Ember study. Read [`README.md`](./README.md)
first for the traced motion references and the client decisions.
Project-specific notes for the Ember study. Read this file and task-relevant
product documents before changing the project.

## Repository checks

The checks under `scripts/` and `.github/workflows/` were borrowed by hand from
the `kiaquila/web-design` template and trimmed to this project. They are this
repository's own files edit them here when they are wrong. Nothing is synced
from upstream, so do not reintroduce a baseline lock file, a release manifest,
a managed-file list or an updater; see the **Repository baseline** section of
[`README.md`](./README.md). Ordinary npm lock files are a different thing and
belong here: the root pins the guard's YAML parser, `website/` pins wrangler.
repository's own files, so edit them here when they are wrong. Nothing is
synced from upstream. Do not reintroduce a baseline lock file, a release
manifest, a managed-file list, or an updater. Ordinary npm lock files are a
different thing and belong here: the root pins the guard's YAML parser, while
`website/` pins wrangler.

`.gitattributes` and `.github/dependabot.yml` were borrowed the same way and
are required by `npm run check`. Keep `linguist-vendored` on the borrowed
harness only — never on the page, its build scripts or the Worker — and keep
the Dependabot entries pointed at directories that really hold a `package.json`
and a lockfile. `semver-*-days` stays off the GitHub Actions ecosystem: action
tags are not guaranteed to be semantic versions.
harness only. Never apply it to the page, its build scripts, or the Worker.
Keep the Dependabot entries pointed at directories that really hold a
`package.json` and a lockfile. `semver-*-days` stays off the GitHub Actions
ecosystem because action tags are not guaranteed to be semantic versions.

## Project rules

- This is a self-contained lab piece, not a client business site. The page
lives in [`website/src/index.html`](./website/src/index.html) — keep it a
lives in [`website/src/index.html`](./website/src/index.html). Keep it a
single dependency-free file (inline CSS/JS, canvas 2D, Web Audio synthesis,
system fonts, zero network requests). Its only siblings are the two baked
favicon PNGs for Safari and the baked `og.png` social card (regenerated by
`npm run og`, seeded and reproducible); the build publishes exactly those
four files and fails on a fifth, so a new asset is a deliberate decision.
Exactly two absolute URLs are allowed, and only as the complete `content`
value of an `og:url`/`og:image` meta tag: the canonical page and its
`og.png` (enumerated in `build.mjs`). Every other absolute URL — any other
host, path, query, or attributestill fails the build; do not widen that
`og.png` (enumerated in `build.mjs`). Every other absolute URL, regardless
of host, path, query, or attribute, still fails the build. Do not widen that
allowance. The card is pinned to the page's figure code by a fingerprint
(`scripts/og-fingerprint.mjs`): the tests compare the hash baked into
`og.png` against the shipped page, and `make-og.mjs` refuses to render
while its hand-ported geometry disagrees with the page
(`PORTED_FIGURE_FINGERPRINT`). On a mismatch, update the port in
`make-og.mjs` to match the page, bump that constant, rerun `npm run og`,
and commit the regenerated card — never bump the constant alone.
and commit the regenerated card. Never bump the constant alone.
- The stage is a Cloudflare Worker named `ember`, configured as documented in
[`docs/stage-hosting.md`](./docs/stage-hosting.md). Its Content-Security-
Policy allows inline script and style because the page is one file by
Expand All @@ -49,16 +49,17 @@ tags are not guaranteed to be semantic versions.
cutover, so `main` here deploys the stage and pull requests get a Cloudflare
preview build. The `ember/website` path in `kiaquila/web-design` no longer
builds this Worker and is kept only as the rollback route.
- The motion concept is traced in [`README.md`](./README.md). Do not copy
assets or code from the referenced Pinterest pin or reactive-dots site;
only the documented motion idea is reproduced.
- The motion references are the Pinterest pin <https://pin.it/163xR16nq> and
<https://reactive-dots.vercel.app>. Treat them only as references for the
smoldering particle motion and centered interaction layout. Do not copy
assets, code, or text from either source.
- The tuning-fork sound is synthesized (136.1 Hz fundamental). Do not add
audio files or third-party libraries for it.
- Preserve the approved interaction contract: hover = local burn + recovery;
Play = one full burn → rebirth cycle with strikes — the figure burns down
Play = one full burn → rebirth cycle with strikes. The figure burns down
completely, reassembles as a new silhouette, and the control resets from
Stop back to Play when the cycle completes; the ash rebirth is the point
of no return Stop before it aborts the cycle and recovers the original
of no return. Stop before it aborts the cycle and recovers the original
figure, Stop after it lets the new silhouette finish reassembling (the
shape-shifting rebirth was rolled back on 2026-08-19 and explicitly
requested back on 2026-08-26 as this single-cycle ending; do not bring
Expand All @@ -69,12 +70,13 @@ tags are not guaranteed to be semantic versions.
gradient dot on the baseline, hugging the KS with a wide gap before DESIGN
(`--brand-dot`, indigo `#818cf8` into cyan `#22d3ee` at 135°, same pair as
the ks portfolio's `--brand-dot`; the client swapped it in for the flat
brand-gold `#e8a038` on 2026-08-28) header only,
brand-gold `#e8a038` on 2026-08-28), used in the header only,
while the footer credit stays plain "ks-design" text; the centered footer
credit links to <https://ks-design.art>.
- Audio must remain gesture-gated (autoplay policy) and fully silenced by
mute; keep `prefers-reduced-motion` support working.
- Before pushing: exercise hover, the full play cycle, stop, mute, and a
narrow viewport; run `npm --prefix website run check`, `npm run check` and
`npm test`.
Sound needs a real gesture — a scripted `click()` grants no user activation.
Sound needs a real gesture because a scripted `click()` grants no user
activation.
7 changes: 3 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# CLAUDE.md Ember
# CLAUDE.md: Ember

@AGENTS.md

Use `AGENTS.md` as the instruction source of truth. Also read `README.md` for
the traced motion references, the client decisions, and how this repository's
checks were borrowed from the `kiaquila/web-design` template.
Use `AGENTS.md` as the instruction source of truth. Read task-relevant product
documents before changing the project.
187 changes: 26 additions & 161 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,166 +1,31 @@
# Ember — interactive burn study
# Ember

A single-page interactive study for the **ks·design lab**: a dark tangled
wireframe figure smolders with golden embers, burns down completely, and
reassembles — scored by a deep meditative tuning-fork tone (136.1 Hz,
synthesized with the Web Audio API, no audio files).
A dark, tangled wireframe figure smolders with golden embers, burns down
completely, and reassembles as a new silhouette to a synthesized 136.1 Hz
tuning-fork tone.

## Source provenance

- Motion reference: Pinterest pin <https://pin.it/163xR16nq> — a hero
animation by `@skvortsov.design` (shared by NewForm's "Designer of the
Week"). Only the motion idea is reproduced (smoldering particle figure with
golden embers, smoke, and flying gold shards on a warm light-gray stage);
no assets, code, or copy were taken from it.
- Interaction/layout reference: <https://reactive-dots.vercel.app> — a
centered figure with a minimal control row at the bottom.
- Client decisions (Kristina, 2026-08-19; Play revised 2026-08-26): hover
makes the figure burn locally and then recover instead of burning away;
Play runs the burn-and-reassemble cycle with the tuning-fork strike exactly
once — the figure burns down completely, reassembles as a new silhouette,
and the control resets from Stop back to Play; the ash rebirth is the
point of no return — Stop before it recovers the original figure, Stop
after it lets the new silhouette finish reassembling (the shape-shifting rebirth
was tried and rolled back on 2026-08-19, then explicitly requested again on
2026-08-26 as the ending of this single cycle; the endless repeat of the
cycle was dropped the same day); controls are
limited to play/stop and mute; the favicon is the wireframe ball with an
inverted palette for dark color schemes plus PNG fallbacks for Safari; the
wordmark is "ks·design" — tracked caps with the cornflower gradient dot on
the baseline, hugging the KS with a wide gap before DESIGN (indigo `#818cf8`
into cyan `#22d3ee` at 135°, shared with the ks portfolio's `--brand-dot`;
it replaced the flat brand-gold `#e8a038` on 2026-08-28) — used in the
header only; the footer credit stays plain
text: "Designed by ks-design · Built with AI workflows", linking to
<https://ks-design.art>.
View the live work at [ember.ks-design.art](https://ember.ks-design.art).

## Implementation

- [`website/src/index.html`](./website/src/index.html) — the whole page:
inline CSS and vanilla JS, canvas 2D rendering, Web Audio synthesis. No
dependencies, no network requests, system fonts only. The only sibling files
are the two baked favicon PNGs and the baked social card.
- [`website/scripts/build.mjs`](./website/scripts/build.mjs) — copies those
four files into `dist/` and refuses to build a page that gained an
off-origin reference or lost a favicon. The `og:` meta tags are the one
place the page may name its own origin in full (scrapers ignore relative
URLs); any other absolute URL still fails the build.
- [`website/scripts/make-og.mjs`](./website/scripts/make-og.mjs) — renders
[`website/src/og.png`](./website/src/og.png), the 1200×630 social card, in
pure seeded Node (`npm run og`). It ports the page's own figure builder and
draw pass — with the lump deformation flattened to a clean sphere — frozen
at the approved hover moment: a local smolder with gold shards, plus the
favicon's wireframe ball as a corner mark. No text is baked in; wording
stays in `og:title`/`og:description`. Composition is a client decision
(Kristina, 2026-08-20): sphere with the smolder at the upper right, chosen
as variant 1 from ten seeded candidates. The constants in the script pin
that choice; rerun `npm run og` to reproduce it byte for byte — the script
carries its own fixed-Huffman deflate, so the bytes do not depend on the
runtime's zlib build. A fingerprint of the page's figure-geometry section
is baked into the PNG and re-checked by the tests
([`website/scripts/og-fingerprint.mjs`](./website/scripts/og-fingerprint.mjs)),
so reshaping the page's figure without regenerating the card fails the
build instead of leaving a stale card in other people's feeds.
- [`website/worker/index.ts`](./website/worker/index.ts) — the Cloudflare
Worker that serves `dist/` and attaches the security headers.
- Audio starts only after a user gesture (browser autoplay policy). The page
follows the same pattern as the reactive-dots reference: a best-effort
`AudioContext` resume on the first pointer move (works on returning visits
where the browser already trusts the site), a guaranteed unlock on any
click/tap/key (with a one-sample silent buffer for iOS), and the mute
button silences everything.
- The favicon is an inline SVG data URI with a `prefers-color-scheme: dark`
media query inside the SVG (supported by Chrome and Firefox). Safari
ignores SVG favicons, so [`website/favicon-32.png`](./website/src/favicon-32.png)
and [`website/apple-touch-icon.png`](./website/src/apple-touch-icon.png) carry
the same ball baked onto the page's warm-gray ground.
- `prefers-reduced-motion` disables rotation, sparks, and smoke while keeping
the play cycle functional.

## Stage

The study is published as its own Cloudflare Worker, `ember`, at
[ember.ks-design.workers.dev](https://ember.ks-design.workers.dev) and at the
custom domain [ember.ks-design.art](https://ember.ks-design.art) the portfolio
links. Its dashboard settings live in
[`docs/stage-hosting.md`](./docs/stage-hosting.md); the deploy itself is run
by Cloudflare, not by this repository.

**Cloudflare builds the Worker from this repository.** The Git connection was
moved here from `kiaquila/web-design` after this repository was created, so
`main` here is what updates the stage and pull requests get a Cloudflare
preview build. The cutover, its verification and its rollback are written down
in [`docs/stage-hosting.md`](./docs/stage-hosting.md). The `ember/` path in the
old repository is kept in place as the rollback route.

## Repository baseline

The checks in this repository were **borrowed by hand** from the
`kiaquila/web-design` template — `scripts/check-repository.mjs` and the Codex
review gate started as copies of that template's own, at commit
`ea8501fdb90236fcb891e97b15f7a42a62f76ff1`, and were then cut down to what one
static page actually needs.

That is the whole relationship. There is no baseline lock file, no release
manifest, no managed-file list and no updater: nothing here is synced, and
nothing upstream can change this repository. (`package-lock.json` at the root
and in `website/` is ordinary npm dependency pinning and unrelated to that
machinery.) Taking a later improvement means reading the
template again and porting the part that is worth porting, in a normal pull
request. These files are this project's own and may be edited freely.

### Dependency and language policy

Two more files were borrowed from the same template (`kiaquila/web-design#49`)
and trimmed the same way:

- [`.github/dependabot.yml`](./.github/dependabot.yml) — weekly updates for
GitHub Actions and for the two directories that hold a `package.json` and a
lockfile (the root's YAML parser and `website/`'s build tooling). Minor and
patch releases arrive as one grouped pull request per ecosystem, majors stay
separate, and a cooldown holds a freshly published version back: 7 days by
default, and 14/7/3 by semver level for npm. Action tags are not guaranteed
to be semantic versions, so the GitHub Actions entry carries only the
default. This file schedules routine version updates and nothing else —
Dependabot alerts and security updates are repository settings and are
enabled there.
- [`.gitattributes`](./.gitattributes) — the four borrowed harness scripts and
their two test files are `linguist-vendored`, so the language bar describes
the page rather than its guardrails. The page, its build scripts and the
Worker are product code and stay counted.

The `osv-scan` job in [`ci.yml`](./.github/workflows/ci.yml) follows the
template's two-step form: the scanner writes JSON under `continue-on-error`,
then `osv-reporter-action` turns it into pull-request annotations and fails the
job on a vulnerability. Both steps are pinned to the same SHA and the workflow's
permissions are unchanged (`contents: read`).

How this repository was extracted from the monorepository, and the proofs taken
at the time, are recorded in
[`docs/migration/source-provenance.md`](./docs/migration/source-provenance.md).

## Checks

- `npm --prefix website run check` — the build plus its tests, including the
size budget for the four published files. This is the project's real check.
- `npm run check` — the repository guard: tracked generated output, committed
secrets, symbolic links, workflow permissions and action pinning, and the
presence of the two policy files below. It parses the workflows with the same
YAML the runner uses, which is the root's one dependency; `npm ci` installs
it.
- `npm test` — the guard's own tests and the Codex review gate's rules.
- `npm --prefix website run dev` — build and serve `dist/` on port 4660.
- Verify by hand: hover ignition and recovery, the single full Play cycle
(burn → gone → a new silhouette reassembles → the control resets to Play),
Stop both before and after the rebirth, mute, the footer link, the
favicon in light
and dark browser themes, and a narrow-viewport layout. Sound needs a real
gesture — a scripted `click()` grants no user activation, and a browser
profile that has already earned media engagement resumes the context
immediately, which hides exactly the bug a fresh profile reveals.

## License

Released under the [MIT License](./LICENSE). © 2026 Kristina Aquila. The
borrowed guard and review-gate scripts carry their own upstream notice in
[`third-party-notices.md`](./third-party-notices.md).
- The artwork is one dependency-free page in `website/src/index.html`, with
inline CSS and JavaScript, Canvas 2D, Web Audio, and system fonts.
- Hover creates a local burn and recovery. Play runs one complete burn and
rebirth cycle. Sound starts only after a user gesture and can be fully muted.
- The build publishes exactly four allowlisted files and rejects unexpected
files and off-origin references. The Cloudflare Worker serves them with
security headers.
- User-facing motion honors `prefers-reduced-motion`, and the controls use
native accessible elements.

## Structure

```text
website/
├── src/index.html # page, rendering, interaction, and sound
├── src/*.png # two favicons and the social card
├── scripts/ # build, social card, and local preview
├── tests/ # shipped-output and policy tests
├── worker/index.ts # security headers for static assets
└── wrangler.json # deployable Worker configuration
```
Loading