Complete authored HTML, CSS, JavaScript, build tools, tests, and local asset references. Expand a file to read it, or fetch the complete text bundle to recreate the project.
Source files
.vercelignore
Read raw text
captures/
reports/
node_modules/
dist/
.vercel/
.DS_Store
ASSETS.md
Read raw text
# Cosmos background sources
Selected from the user-supplied [Blue Glass collection](https://www.cosmos.so/sarveshsea/blue-glass), September 8, 2026. These are actual collection images, not generated replacements. Images are served locally. Cropping, blur, blending, and overlays happen in CSS. The new hero references were supplied directly by the user and encoded as WebP at quality 86 without resizing.
| Local file | Cosmos element | Cosmos upload credit | Dimensions | Placement |
| --- | --- | --- | --- | --- |
| `cosmos-folded-glass.webp` | [Folded glass](https://www.cosmos.so/e/2081991458) | 三日一物 SUNRIU | 1080 × 1080 | Agent backdrop |
| `cosmos-ribbed-glass.webp` | [Ribbed glass](https://www.cosmos.so/e/1100722503) | Ponti Design Studio | 1080 × 1346 | Prepared section and quiet closing crop |
Credits reflect the collection's upload labels. Full-resolution WebP assets are served locally, with no Cosmos request required at runtime. Product interfaces remain opaque white. The prepared section uses DualEntry brand-100 for the copy field and brand-800 beneath the glass field.
The former generated PNG plates are preserved in the previous source archive in the task workspace, and are no longer included in this build.
## V5 splash references
| Local file | Source | Dimensions | Placement |
| --- | --- | --- | --- |
| `cosmos-blue-loop.webp` | User-supplied blue-glass loop from the linked collection; exact element and creator not established | 1256 × 2760 | Right hero field |
| `cosmos-blue-wave.webp` | User-supplied blue-glass wave from the linked collection; exact element and creator not established | 1200 × 2133 | Left hero field |
`splash.css` applies 2px blur to the photographs and a deep navy-to-white overlay. The translucent screen rim uses 8px backdrop blur; the product surface remains opaque and sharp.
CAPTURES.md
Read raw text
# Current captures
41 PNGs cover eleven sections at desktop 1440px, tablet 768px, phone 390px, plus seven animation phases and a 320px splash. Two WebM recordings show the mobile loop and agent preparation.
|Section|Desktop|Tablet|Phone|
|---|---|---|---|
|Hero|[Desktop](captures/current/desktop-hero.png)|[Tablet](captures/current/tablet-hero.png)|[Phone](captures/current/mobile-hero.png)|
|Workflow|[Desktop](captures/current/desktop-workflow.png)|[Tablet](captures/current/tablet-workflow.png)|[Phone](captures/current/mobile-workflow.png)|
|Video|[Desktop](captures/current/desktop-video.png)|[Tablet](captures/current/tablet-video.png)|[Phone](captures/current/mobile-video.png)|
|Selector|[Desktop](captures/current/desktop-selector.png)|[Tablet](captures/current/tablet-selector.png)|[Phone](captures/current/mobile-selector.png)|
|Agent|[Desktop](captures/current/desktop-agent.png)|[Tablet](captures/current/tablet-agent.png)|[Phone](captures/current/mobile-agent.png)|
|Prepared|[Desktop](captures/current/desktop-prepared.png)|[Tablet](captures/current/tablet-prepared.png)|[Phone](captures/current/mobile-prepared.png)|
|Context|[Desktop](captures/current/desktop-context.png)|[Tablet](captures/current/tablet-context.png)|[Phone](captures/current/mobile-context.png)|
|Progression|[Desktop](captures/current/desktop-progression.png)|[Tablet](captures/current/tablet-progression.png)|[Phone](captures/current/mobile-progression.png)|
|Stats|[Desktop](captures/current/desktop-stats.png)|[Tablet](captures/current/tablet-stats.png)|[Phone](captures/current/mobile-stats.png)|
|Modules|[Desktop](captures/current/desktop-modules.png)|[Tablet](captures/current/tablet-modules.png)|[Phone](captures/current/mobile-modules.png)|
|Closing|[Desktop](captures/current/desktop-closing.png)|[Tablet](captures/current/tablet-closing.png)|[Phone](captures/current/mobile-closing.png)|
Regenerate with `npm run capture` and `npm run record`. Capture URLs use `/?capture=1§ion=stats`. Agent phases: queued,assigned,filling,review. Hero phases: queued,moving,review. Unsupported phases resolve to review.
CONCEPT-MAP.md
Read raw text
# Reference composition
Hero: centered copy and actions above a glass-framed animated board.
Workflow: three aligned cards, preserving Scattered / One board / Self-resolving wording.
Video: neutral 16:7 desktop placeholder, responsive on phones.
Selector: category rail and task preview.
Agent: fixed journal form fills once on entering view.
Prepared: full-width pale/deep teal split with journal summary.
Context: task card with source/work/action controls.
Progression: three aligned status stages.
Statistics: three attributed customer result cards, without product screenshots.
Modules: centered One board. Live state. heading and two orderly desktop rows of icons.
Closing: dark teal field and translucent CTA panel, followed by a dark navigation footer.
Mobile cards and diagrams stack; product text stays readable. All artwork, fonts, and editable product interfaces are local. `/source/` exposes complete readable source and asset hashes.
DEPLOYMENT.md
Read raw text
# Deploying the reference landing page
Production URL: https://dualentry-landing.vercel.app/
Project: `sarveshseas-projects/dualentry-landing`.
Build: `npm run build`. Output: `dist/`.
Local editing, building, testing, and packaging need no Vercel credentials. To deploy, an authorized account can link the existing project, then run:
```sh
vercel deploy --prod --yes --scope sarveshseas-projects
```
Only generated output is served. The public `/source/` export contains authored source, safe configuration, documentation, and asset references; it excludes account linkage, dependencies, captures, and reports.
Verification commands:
```sh
npm test
npm run test:e2e
BASE_URL=https://dualentry-landing.vercel.app/ npm run test:e2e
```
The browser suite checks 14 widths between 320 and 1440 pixels, the reference section sequence, hero card alignment, replay, controls, visibility pauses, reduced motion, and public source retrieval. `npm run capture` and `npm run record` refresh the optional media package. `npm run package` also works in a source-only copy before captures have been generated.
MOTION.md
Read raw text
# Ramp Stack reference observations
Source: https://ramp.com/stack, observed September 8, 2026. Browser frames captured with CDP timestamps; MP4s retain observed spacing and are encoded at 24fps. These are sampling-based recordings, not extracted source animation timelines.
Hero: three neutral board columns; center has greater contrast. A task lifts from its source queue, crosses the center, then settles into the destination. Ambient ivory/yellow sits behind the board, never as the main task-card fill. The sequence advances without clicking while the hero is visible. Heading computed weight 400, size 64px on observed desktop viewport.
Feature: clicking a process heading changes the expanded explanatory text and corresponding right-hand product scene. The demo progresses through its own internal steps. Backdrop remains behind a white product window. Feature heading computed weight 400, size 48px.
Interpretation applied to DualEntry: neutral product layers; lower-contrast sage/teal atmosphere instead of Ramp yellow; a short agent preparation sequence alongside the separate continuous splash loop; fixed form geometry and regular heading weights. Planned 5-second story and 160–220ms transitions are authored timings, not claimed measurements of Ramp source code.
## Implemented agent sequence
- 0ms: queued task and fixed skeletal journal-entry form.
- 800ms: journal agent assigned; task lifts 3px with a 200ms transition.
- 1700ms: entity and date appear as a group.
- 2800ms: two account rows and balanced $1,200 debit/credit amounts appear.
- 3800ms: memo appears.
- 5000ms: Ready for review; the draft stops before any approval or posting.
Visibility eligibility is half the stage, capped to half the viewport for tall phone layouts. Offscreen time and hidden-tab time do not advance the clock. Replay resets the state; finished demonstrations stay finished on re-entry. Capture and reduced-motion modes are static. No character-by-character typing or geometry shifts.
## Current background treatment
V4 replaces the generated plates with two actual images selected from the user's Cosmos collection. See [source credits](ASSETS.md). The files are unmodified WebP images. CSS supplies restrained 10–24px blur, oversized clipped edges, and luminosity blending over DualEntry teal. Only decorative pseudo-elements are filtered; product cards and text remain sharp. The new prepared section uses a single 180ms link-arrow hover response, disabled by capture and reduced-motion rules.
Reference recordings and the previous sampled agent recording remain in the task workspace. The current portable package includes fresh still captures, a mobile task-loop recording, and an agent-preparation recording. Agent timing and behavior are unchanged.
## Current splash loop
The glass imagery stays still at 2px blur. Three real cards share an overlay and move between measured slots. Six adjacent exchanges make a seamless 36-second cycle. Each six-second interval holds for 3.8 seconds, moves over 1.4 seconds with smooth acceleration and deceleration, then settles. No reset, caption, Replay button, syncing label, or placeholder text is shown.
Desktop movement uses a small 6px lift; phone movement uses vertical slots and a 6px sideways passing offset. Slots reserve the largest card height. Styles update only when a value changes. An icon-only pause/resume control is available. Offscreen and hidden-tab time do not advance the loop. Reduced motion and capture mode are static; changes to the preference take effect live.
## Family reference
Reference: https://family.co/, inspected September 8, 2026. The “Effortless onboarding. Masterful management.” group uses phone previews, with the active Drag and Drop preview visibly lifted and enlarged from about 209 × 428 to 230 × 471 CSS pixels. Moving the pointer over that preview started its video; the neighboring videos were paused. The page exposes short 100ms icon-color transitions. A local observation recording is stored in work/family-v6. These observations informed the treatment; DualEntry's timing and paths are independently authored.
Selector and progression controls retain short pointer feedback. Keyboard updates remain immediate. No image/background layer moves.
## Refactor contract
`src/scripts/main.js` initializes capture layout first, then each independent controller. Initializers return cleanup functions. Shared UI bindings cancel transient animations on reduced-motion changes and disposal. Agent Replay is an accessible icon in the form header; the external playback strip and explanatory form footer were removed. Phase announcements remain available to screen readers.
README.md
Read raw text
# DualEntry Tasks landing page
Eleven sections matching the selected reference built with plain HTML, CSS, SVG, and native JavaScript modules. No browser runtime dependencies, framework, backend, or account connections. Product examples use illustrative financial data. Customer statistics are attributed separately.
Live: https://dualentry-landing.vercel.app/
## Open on another computer
Install Node.js 22.22 or newer, extract the ZIP, and open a terminal in `dualentry-landing`:
```sh
npm run preview
```
Open the printed local URL (default http://127.0.0.1:4174). This builds and serves the site using Node alone; no dependency installation, Python, Vercel account, or original computer is needed. Stop with Ctrl+C. Set `PORT` to use another port. Native modules require this local server rather than double-clicking HTML.
For editing, use `npm run dev`. It rebuilds when files in `src/` change; refresh your browser to see changes. Keep assets at their existing paths or update references with your replacement.
## Editing map
| Folder | What to edit |
| --- | --- |
| `src/page.html` | Navigation, footer, metadata, shared SVG icons |
| `src/sections/` | One HTML fragment per demonstration; copy and product UI |
| `src/styles/tokens.css` | DualEntry colors, fonts, semantic aliases |
| `src/styles/` | Readable shared and section styles; filenames match demonstrations |
| `src/scripts/` | One entry point, independent controllers, immutable state helpers |
| `src/scripts/section-order.mjs` | Page order and accepted capture section IDs |
| `assets/` | Local fonts, logo, and glass background WebPs |
| `scripts/` | Build, preview, tests, capture, recording, and packaging commands |
| `tests/` | State, interaction, lifecycle, and isolated build regression tests |
| `dist/` | Generated deployment output; do not edit |
The build uses an explicit ordered manifest in `scripts/manifest.mjs`. It joins source styles into one `app.css`, assembles section HTML into one page, and copies only named runtime modules and local assets. Builds never write into authored source. CSS asset paths are relative to the generated `dist/app.css`.
To reuse a section, take its HTML, corresponding stylesheet/controller, shared tokens/product styles, used SVG symbols, and referenced assets. Keep its `data-*` hooks if retaining interactions. Controllers return cleanup functions. `main.js` applies capture layout before initializing them; import controllers individually when embedding in another app.
## Optional development tools
```sh
npm ci
npx playwright install chromium
npm test
npm run test:e2e
npm run capture
npm run record
npm run package
```
Only tests that use a browser, recording, and formatting require installed dev dependencies. Linux may additionally need `npx playwright install --with-deps chromium`. `npm test` uses Node's built-in runner and enforces 80% production JavaScript coverage for lines, branches, and functions. Test builds use separate temporary directories.
Browser verification starts and stops its own local server, tests 14 viewport widths, interactions, mobile targets, replay, and animation pauses. Set `BASE_URL` to verify a deployed site. Capture/record commands write to `captures/current`; reports go to ignored `reports/`. `npm run format` formats authored code.
`npm run package` rebuilds and creates `../dualentry-landing.zip` with source, assets, production output, tests, docs, and current captures when present. It excludes dependencies, reports, old captures, and `.vercel` account linkage. Run capture/record first to refresh images after changing the design.
## Capture URLs
- Full page still: `/?capture=1`
- Single section: `/?capture=1§ion=video`
- Agent phase: `/?capture=1§ion=agent&phase=filling`
- Splash phase: `/?capture=1§ion=hero&phase=moving`
Section IDs: hero, workflow, video, selector, agent, prepared, context, progression, stats, modules, closing.
Agent phases: queued, assigned, filling, review. Splash phases: queued, moving, review. Missing or unsupported phases settle at review. Reduced motion shows the completed presentation; capture mode suppresses motion and hides playback buttons without changing layout.
## Visual and motion contract
Preserve Lausanne regular typography, white product surfaces, DualEntry teal actions, and the existing Cosmos backgrounds. Main headings and essential product information remain visible; repeated caption strips and decorative explanation have been removed.
The splash continuously exchanges three cards between anchored slots, returning seamlessly after 36 seconds. Phones use three vertical queues. The loop pauses offscreen, in hidden tabs, on reduced-motion preference, and with its accessible pause button. The agent prepares grouped form fields over five seconds, stops before approval, and offers an icon Replay control in its form header.
See [asset credits](ASSETS.md), [motion details](MOTION.md), [captures](CAPTURES.md), and [deployment notes](DEPLOYMENT.md).
## Current page and public source
The reference sequence is hero → workflow → video placeholder → task selector → agent preparation → prepared entry → task context → progression → customer statistics → module index → closing CTA. The hero actions appear before its glass board. The video slot is an intentional placeholder, not a playable video.
Every build publishes `/source/`: readable code, individual raw text files, a SHA-256 manifest, and `/source/bundle.json` containing all authored text with paths. Assets are listed with URLs and hashes. Another agent can download the bundle, write each text file at its `path`, fetch each asset to its `path`, then run `npm run preview`. There are no private deployment credentials in this export. A source-only copy can package without generated captures; run capture/record first if those are wanted.
The statistics section uses attributed published customer results: [Slash](https://www.dualentry.com/customers/slash) reports 90% of manual tasks automated; [Graphite](https://www.dualentry.com/customers/graphite) reports close reduced from 20 days to one and $0 implementation costs. The displayed 95% is calculated as `(20 - 1) / 20 × 100`; it describes Graphite's close duration, not a universal time-saving promise.
package-lock.json
Read raw text
{
"name": "dualentry-landing",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "dualentry-landing",
"version": "1.0.0",
"devDependencies": {
"playwright": "1.62.1",
"prettier": "3.6.2"
},
"engines": {
"node": ">=22.22.0"
}
},
"node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/playwright": {
"version": "1.62.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.62.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.62.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/prettier": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
}
}
}
package.json
Read raw text
{
"name": "dualentry-landing",
"version": "1.0.0",
"private": true,
"type": "module",
"engines": {
"node": ">=22.22.0"
},
"scripts": {
"build": "node scripts/build.mjs",
"preview": "node scripts/preview.mjs",
"dev": "node scripts/preview.mjs --watch",
"test": "node scripts/test.mjs",
"test:e2e": "node scripts/verify.mjs",
"capture": "node scripts/verify.mjs --capture",
"record": "node scripts/record.mjs",
"package": "node scripts/package.mjs",
"format": "prettier --write src scripts tests package.json"
},
"devDependencies": {
"playwright": "1.62.1",
"prettier": "3.6.2"
}
}
scripts/build.mjs
Read raw text
import { readFile, writeFile, mkdir, cp, rm } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { resolve } from "node:path";
import { styles, modules } from "./manifest.mjs";
import { sectionOrder } from "../src/scripts/section-order.mjs";
import { publishSource } from "./publish-source.mjs";
export const root = fileURLToPath(new URL("../", import.meta.url));
export async function build(destination = resolve(root, "dist")) {
const source = resolve(root, "src");
const shell = await readFile(resolve(source, "page.html"), "utf8");
if (shell.split("<!-- SECTIONS -->").length !== 2)
throw new Error(
"Page template must have exactly one section insertion point.",
);
const sections = await Promise.all(
sectionOrder.map((name) =>
readFile(resolve(source, "sections", `${name}.html`), "utf8"),
),
);
const css = await Promise.all(
styles.map(
async (name) =>
`/* ${name} */\n${await readFile(resolve(source, "styles", name), "utf8")}`,
),
);
// Read all inputs before replacing generated output; authored source is never written.
await rm(destination, { recursive: true, force: true });
await mkdir(resolve(destination, "scripts"), { recursive: true });
await Promise.all(
modules.map((name) =>
cp(
resolve(source, "scripts", name),
resolve(destination, "scripts", name),
),
),
);
await cp(resolve(root, "assets"), resolve(destination, "assets"), {
recursive: true,
});
await writeFile(
resolve(destination, "index.html"),
shell
.replace("<!-- SECTIONS -->", sections.join("\n"))
.replace(
"</head>",
' <link rel="describedby" type="application/json" href="source/manifest.json">\n</head>',
),
);
await writeFile(resolve(destination, "app.css"), css.join("\n"));
await publishSource(root, destination);
return destination;
}
if (
process.argv[1] &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
build()
.then(() =>
console.log(
`Built ${sectionOrder.length} sections and readable source to dist/`,
),
)
.catch((error) => {
console.error(error);
process.exitCode = 1;
});
}
scripts/manifest.mjs
Read raw text
/** Explicit ordered inputs: tooling can never be copied into the deployed site. */
export const styles = Object.freeze([
"tokens.css",
"styles.css",
"product.css",
"scenes-flow.css",
"scenes-focus.css",
"agent.css",
"atmosphere.css",
"splash.css",
"splash-motion.css",
"microfeedback.css",
"workflow-cards.css",
"page-ending.css",
"reference-layout.css",
]);
export const modules = Object.freeze([
"main.js",
"ui-runtime.js",
"interactions.js",
"state.mjs",
"section-order.mjs",
"agent.js",
"agent-state.mjs",
"splash-motion.js",
"splash-motion-state.mjs",
"scenes-flow.js",
"scenes-focus.js",
]);
scripts/package.mjs
Read raw text
/** Portable ZIP using Node's built-in compression. No system zip command required. */
import { readFile, readdir, writeFile } from "node:fs/promises";
import { deflateRawSync } from "node:zlib";
import { resolve, relative } from "node:path";
import { build, root } from "./build.mjs";
const roots = [
"src",
"scripts",
"assets",
"tests",
"dist",
"captures/current",
"package.json",
"package-lock.json",
"vercel.json",
".vercelignore",
".gitignore",
"README.md",
"ASSETS.md",
"CAPTURES.md",
"MOTION.md",
"CONCEPT-MAP.md",
"DEPLOYMENT.md",
];
function crc32(bytes) {
let crc = 0xffffffff;
for (const byte of bytes) {
crc ^= byte;
for (let bit = 0; bit < 8; bit++)
crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0);
}
return (crc ^ 0xffffffff) >>> 0;
}
async function filesAt(path) {
const entries = await readdir(path, { withFileTypes: true });
const groups = await Promise.all(
entries
.sort((a, b) => a.name.localeCompare(b.name))
.filter((x) => x.name !== ".DS_Store")
.map((x) =>
x.isDirectory()
? filesAt(resolve(path, x.name))
: [resolve(path, x.name)],
),
);
return groups.flat();
}
await build();
const files = [];
for (const name of roots) {
const path = resolve(root, name);
if (name.includes(".") && !name.includes("/")) {
try {
await readFile(path);
files.push(path);
} catch (error) {
if (name !== ".gitignore" || error.code !== "ENOENT") throw error;
}
} else {
try {
files.push(...(await filesAt(path)));
} catch (error) {
if (name !== "captures/current" || error.code !== "ENOENT") throw error;
}
}
}
let offset = 0;
const records = [],
directory = [];
for (const path of files) {
const body = await readFile(path),
data = deflateRawSync(body),
name = Buffer.from(
"dualentry-landing/" + relative(root, path).split("\\").join("/"),
);
const crc = crc32(body),
header = Buffer.alloc(30);
header.writeUInt32LE(0x04034b50, 0);
header.writeUInt16LE(20, 4);
header.writeUInt16LE(0x800, 6);
header.writeUInt16LE(8, 8);
header.writeUInt16LE(33, 12);
header.writeUInt32LE(crc, 14);
header.writeUInt32LE(data.length, 18);
header.writeUInt32LE(body.length, 22);
header.writeUInt16LE(name.length, 26);
const central = Buffer.alloc(46);
central.writeUInt32LE(0x02014b50, 0);
central.writeUInt16LE(20, 4);
central.writeUInt16LE(20, 6);
central.writeUInt16LE(0x800, 8);
central.writeUInt16LE(8, 10);
central.writeUInt16LE(33, 14);
central.writeUInt32LE(crc, 16);
central.writeUInt32LE(data.length, 20);
central.writeUInt32LE(body.length, 24);
central.writeUInt16LE(name.length, 28);
central.writeUInt32LE(offset, 42);
records.push(header, name, data);
directory.push(central, name);
offset += header.length + name.length + data.length;
}
const central = Buffer.concat(directory),
end = Buffer.alloc(22);
end.writeUInt32LE(0x06054b50, 0);
end.writeUInt16LE(files.length, 8);
end.writeUInt16LE(files.length, 10);
end.writeUInt32LE(central.length, 12);
end.writeUInt32LE(offset, 16);
const output = resolve(root, "../dualentry-landing.zip");
await writeFile(output, Buffer.concat([...records, central, end]));
console.log(`Packaged ${files.length} files: ${output}`);
scripts/preview.mjs
Read raw text
import { createServer } from "node:http";
import { readFile, stat } from "node:fs/promises";
import { watch } from "node:fs";
import { resolve, extname, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { build, root } from "./build.mjs";
const mime = {
".json": "application/json; charset=utf-8",
".txt": "text/plain; charset=utf-8",
".html": "text/html; charset=utf-8",
".css": "text/css",
".js": "text/javascript",
".mjs": "text/javascript",
".svg": "image/svg+xml",
".webp": "image/webp",
".woff2": "font/woff2",
".png": "image/png",
};
export async function startPreview({ port = 4174, rebuild = true } = {}) {
if (rebuild) await build();
const directory = resolve(root, "dist");
const server = createServer(async (req, res) => {
try {
const path = decodeURIComponent(
new URL(req.url, "http://localhost").pathname,
);
let file = resolve(directory, "." + path);
if (file !== directory && !file.startsWith(directory + sep)) {
res.writeHead(403).end();
return;
}
if ((await stat(file)).isDirectory()) file = resolve(file, "index.html");
const body = await readFile(file);
res
.writeHead(200, {
"Content-Type": mime[extname(file)] || "application/octet-stream",
"Cache-Control": "no-store",
})
.end(body);
} catch (error) {
const status =
error.code === "ENOENT" ? 404 : error instanceof URIError ? 400 : 500;
if (status === 500) console.error(error);
res
.writeHead(status)
.end(status === 404 ? "Not found" : "Unable to serve this file");
}
});
await new Promise((done, reject) => {
server.once("error", reject);
server.listen(port, "127.0.0.1", done);
});
return {
url: `http://127.0.0.1:${server.address().port}/`,
close: () =>
new Promise((done, reject) =>
server.close((error) => (error ? reject(error) : done())),
),
};
}
if (
process.argv[1] &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
const app = await startPreview({ port: Number(process.env.PORT || 4174) });
console.log(`Preview: ${app.url}`);
let watcher,
timer,
building = Promise.resolve();
if (process.argv.includes("--watch"))
watcher = watch(resolve(root, "src"), { recursive: true }, () => {
clearTimeout(timer);
timer = setTimeout(() => {
building = building
.then(() => build())
.then(() => console.log("Rebuilt. Refresh the browser."))
.catch(console.error);
}, 100);
});
async function close() {
clearTimeout(timer);
watcher?.close();
await building;
await app.close();
}
process.once("SIGINT", close);
process.once("SIGTERM", close);
}
scripts/publish-source.mjs
Read raw text
import { createHash } from "node:crypto";
import { lstat, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
import { dirname, extname, resolve } from "node:path";
// Only these authored inputs are public. Build output, private deployment state,
// hidden files, dependencies, and symlinks are never traversed.
const textRoots = ["src", "scripts", "tests"];
const textExtensions = new Set([
".html",
".css",
".js",
".mjs",
".json",
".md",
]);
const assetExtensions = new Set([
".svg",
".png",
".jpg",
".jpeg",
".webp",
".avif",
".woff",
".woff2",
".mp4",
".webm",
]);
const documents = [
".vercelignore",
"package.json",
"package-lock.json",
"vercel.json",
"README.md",
"ASSETS.md",
"MOTION.md",
"CAPTURES.md",
"CONCEPT-MAP.md",
"DEPLOYMENT.md",
];
const excludedDirectories = new Set([
"node_modules",
"dist",
"reports",
"captures",
]);
function escapeHtml(value) {
return value.replace(
/[&<>"']/g,
(character) =>
({
"&": "&",
"<": "<",
">": ">",
'"': """,
"'": "'",
})[character],
);
}
async function listFiles(root, directory, extensions) {
let entries;
try {
if (!(await lstat(resolve(root, directory))).isDirectory()) return [];
entries = await readdir(resolve(root, directory), { withFileTypes: true });
} catch (error) {
if (error.code === "ENOENT") return [];
throw error;
}
const paths = await Promise.all(
entries.map(async (entry) => {
if (entry.name.startsWith(".") || entry.isSymbolicLink()) return [];
const path = `${directory}/${entry.name}`;
if (entry.isDirectory()) {
return excludedDirectories.has(entry.name)
? []
: listFiles(root, path, extensions);
}
return entry.isFile() && extensions.has(extname(entry.name))
? [path]
: [];
}),
);
return paths.flat();
}
async function existingDocuments(root) {
const paths = await Promise.all(
documents.map(async (path) => {
try {
return (await lstat(resolve(root, path))).isFile() ? [path] : [];
} catch (error) {
if (error.code === "ENOENT") return [];
throw error;
}
}),
);
return paths.flat();
}
function metadata(path, url, content) {
return {
path,
url,
bytes: content.length,
sha256: createHash("sha256").update(content).digest("hex"),
};
}
function indexPage(files, assets) {
const source = files
.map(
({ path, url, content }) => `
<details id="${escapeHtml(path)}">
<summary>${escapeHtml(path)}</summary>
<p><a href="../${escapeHtml(url)}">Read raw text</a></p>
<pre><code>${escapeHtml(content)}</code></pre>
</details>`,
)
.join("\n");
const assetLinks = assets
.map(
({ path, url }) =>
`<li><a href="../${escapeHtml(url)}">${escapeHtml(path)}</a></li>`,
)
.join("\n");
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DualEntry landing page — readable source</title>
<link rel="describedby" type="application/json" href="manifest.json">
<style>body{max-width:1000px;margin:48px auto;padding:0 24px;font:16px/1.6 system-ui;color:#1d1f20;background:#fff}h1{font-weight:400;line-height:1.2}a{color:#004f5c}details{border-top:1px solid #dfe9eb;padding:16px 0}summary{cursor:pointer;overflow-wrap:anywhere}pre{overflow:auto;background:#f4f6f6;padding:20px;font-size:13px;line-height:1.55}nav{display:flex;flex-wrap:wrap;gap:20px;margin:24px 0}li{overflow-wrap:anywhere}</style>
</head>
<body>
<h1>DualEntry landing page source</h1>
<p>Complete authored HTML, CSS, JavaScript, build tools, tests, and local asset references. Expand a file to read it, or fetch the complete text bundle to recreate the project.</p>
<nav aria-label="Source downloads"><a href="../">Landing page</a><a href="manifest.json">File manifest and SHA-256 hashes</a><a href="bundle.json">All source text as JSON</a></nav>
<main><h2>Source files</h2>${source}<h2>Assets</h2><ul>${assetLinks}</ul></main>
</body>
</html>\n`;
}
export async function publishSource(root, destination) {
const paths = (
await Promise.all([
...textRoots.map((directory) =>
listFiles(root, directory, textExtensions),
),
existingDocuments(root),
])
)
.flat()
.sort();
const files = await Promise.all(
paths.map(async (path) => {
const raw = await readFile(resolve(root, path));
const content = raw.toString("utf8");
// Fail visibly rather than expose machine-specific source or silently redact code.
const localPath = new RegExp(
"/(?:Users|Volumes|home)/[^\\s]+|[A-Za-z]:\\\\Users\\\\",
);
if (localPath.test(content))
throw new Error(`Machine-specific path in public source: ${path}`);
const url = `source/files/${path.split("/").map(encodeURIComponent).join("/")}.txt`;
return { ...metadata(path, url, raw), content };
}),
);
const assetPaths = (await listFiles(root, "assets", assetExtensions)).sort();
const assets = await Promise.all(
assetPaths.map(async (path) =>
metadata(
path,
path.split("/").map(encodeURIComponent).join("/"),
await readFile(resolve(root, path)),
),
),
);
const manifest = {
version: 1,
files: files.map(({ content, ...entry }) => entry),
assets,
};
const directory = resolve(destination, "source");
await mkdir(directory, { recursive: true });
await Promise.all(
files.map(async ({ url, content }) => {
const output = resolve(destination, decodeURIComponent(url));
await mkdir(dirname(output), { recursive: true });
await writeFile(output, content);
}),
);
await Promise.all([
writeFile(
resolve(directory, "manifest.json"),
JSON.stringify(manifest, null, 2) + "\n",
),
writeFile(
resolve(directory, "bundle.json"),
JSON.stringify({ version: 1, files, assets }, null, 2) + "\n",
),
writeFile(resolve(directory, "index.html"), indexPage(files, assets)),
]);
return manifest;
}
scripts/record.mjs
Read raw text
import { chromium } from "playwright";
import { startPreview } from "./preview.mjs";
import { root } from "./build.mjs";
import { mkdir } from "node:fs/promises";
process.chdir(root);
await mkdir("captures/current", { recursive: true });
const preview = await startPreview({ port: 0 });
(async () => {
let browser;
try {
browser = await chromium.launch();
const context = await browser.newContext({
viewport: { width: 390, height: 1000 },
recordVideo: { dir: "reports/video", size: { width: 390, height: 1000 } },
});
const page = await context.newPage();
await page.goto(preview.url);
await page.evaluate(() => document.fonts.ready);
await page.waitForTimeout(19000);
const video = page.video();
await context.close();
await video.saveAs("captures/current/mobile-task-loop.webm");
const agentContext = await browser.newContext({
viewport: { width: 1440, height: 1000 },
recordVideo: {
dir: "reports/video",
size: { width: 1440, height: 1000 },
},
});
const agentPage = await agentContext.newPage();
await agentPage.goto(preview.url + "#agent");
await agentPage.locator("[data-agent-demo]").scrollIntoViewIfNeeded();
await agentPage.waitForTimeout(7000);
const agentVideo = agentPage.video();
await agentContext.close();
await agentVideo.saveAs("captures/current/agent-preparation.webm");
console.log("Mobile loop and agent preparation recorded");
} finally {
await browser?.close();
await preview.close();
}
})().catch((e) => {
console.error(e);
process.exitCode = 1;
});
scripts/test.mjs
Read raw text
import { readdir } from "node:fs/promises";
import { spawnSync } from "node:child_process";
import { root } from "./build.mjs";
import { resolve } from "node:path";
const tests = (await readdir(resolve(root, "tests")))
.filter((name) => name.endsWith(".test.mjs"))
.sort();
const result = spawnSync(
process.execPath,
[
"--test",
"--experimental-test-coverage",
"--test-coverage-include=src/scripts/**",
"--test-coverage-lines=80",
"--test-coverage-branches=80",
"--test-coverage-functions=80",
...tests.map((name) => `tests/${name}`),
],
{ cwd: root, stdio: "inherit" },
);
if (result.error) throw result.error;
process.exitCode = result.status ?? 1;
scripts/verify.mjs
Read raw text
import { chromium } from "playwright";
import { startPreview } from "./preview.mjs";
import { root } from "./build.mjs";
import { resolve } from "node:path";
import assert from "node:assert/strict";
import * as fs from "node:fs/promises";
process.chdir(root);
const preview = process.env.BASE_URL ? null : await startPreview({ port: 0 });
const base = process.env.BASE_URL || preview.url;
(async () => {
let browser;
try {
browser = await chromium.launch();
const page = await browser.newPage({
viewport: { width: 390, height: 1000 },
deviceScaleFactor: 1,
});
const errors = [];
page.on("pageerror", (e) => errors.push(e.message));
const sections = [
"hero",
"workflow",
"video",
"selector",
"agent",
"prepared",
"context",
"progression",
"stats",
"modules",
"closing",
];
if (process.argv.includes("--capture")) {
await fs.mkdir("captures/current", { recursive: true });
for (const [label, width] of [
["desktop", 1440],
["tablet", 768],
["mobile", 390],
]) {
await page.setViewportSize({ width, height: 960 });
for (const section of sections) {
await page.goto(base + "?capture=1§ion=" + section);
await page.evaluate(() => document.fonts.ready);
await page
.locator('[data-capture-section="' + section + '"]')
.screenshot({
path: "captures/current/" + label + "-" + section + ".png",
});
}
}
for (const [section, phases] of [
["hero", ["queued", "moving", "review"]],
["agent", ["queued", "assigned", "filling", "review"]],
]) {
await page.setViewportSize({ width: 1440, height: 1300 });
for (const phase of phases) {
await page.goto(
base + "?capture=1§ion=" + section + "&phase=" + phase,
);
await page.evaluate(() => document.fonts.ready);
await page
.locator('[data-capture-section="' + section + '"]')
.screenshot({
path: "captures/current/" + section + "-phase-" + phase + ".png",
});
}
}
await page.setViewportSize({ width: 320, height: 1100 });
await page.goto(base + "?capture=1§ion=hero");
await page.evaluate(() => document.fonts.ready);
await page
.locator("[data-capture-section=hero]")
.screenshot({ path: "captures/current/small-hero.png" });
}
const removed = [
".flow-caption",
".progress-footnote",
".story-product-footer",
".prepared-entry-footer",
".context-caption",
".sel-stage-bottom",
".control-kicker",
".control-example-label",
".editorial-focus-bottom",
".agent-form-footer",
".agent-playback",
];
await page.goto(base + "?capture=1");
for (const selector of removed)
assert.equal(await page.locator(selector).count(), 0, selector);
assert.equal(await page.locator("#video-title").count(), 1);
assert.equal(
await page.locator(".agent-form-header [data-agent-replay]").count(),
1,
);
// Real controls still work after surrounding narration has been removed.
await page.goto(base);
await page.locator("#agent").scrollIntoViewIfNeeded();
await page.waitForFunction(
() =>
document.querySelector("[data-agent-demo]").dataset.agentPhase ===
"review",
);
await page.locator("[data-agent-replay]").click();
await page.waitForFunction(
() =>
document.querySelector("[data-agent-demo]").dataset.agentPhase ===
"queued",
);
await page.waitForFunction(
() =>
document.querySelector("[data-agent-demo]").dataset.agentPhase ===
"review",
);
await page.locator('[data-sel-choice="connections"]').click();
assert.equal(
await page.locator("#selector-stage").getAttribute("data-focus"),
"connections",
);
await page.locator('[data-context-choice="work"]').click();
assert.equal(
await page.locator("#context-demo").getAttribute("data-context"),
"work",
);
await page.locator('[data-progress-choice="move"]').click();
assert.equal(
await page
.locator("[data-progress-stage]")
.getAttribute("data-progress-stage"),
"move",
);
const sourceManifest = await (
await page.request.get(base + "source/manifest.json")
).json();
assert.ok(sourceManifest.files.length > 25);
const fullSource = await (
await page.request.get(base + "source/bundle.json")
).json();
assert.ok(JSON.stringify(fullSource).includes("Your next move."));
assert.ok(
await page.evaluate(
() =>
document.querySelector(".splash-actions").getBoundingClientRect()
.top <
document.querySelector(".splash-screen").getBoundingClientRect().top,
),
);
const widths = [
320, 360, 375, 390, 430, 540, 600, 680, 681, 768, 800, 801, 1024, 1440,
];
for (const width of widths) {
await page.setViewportSize({ width, height: 1000 });
await page.goto(base + "?capture=1");
await page.evaluate(() => document.fonts.ready);
await page.waitForTimeout(50);
const result = await page.evaluate(() => ({
width: innerWidth,
scroll: document.documentElement.scrollWidth,
cards: [...document.querySelectorAll("[data-splash-card]")].map((n) =>
n.getBoundingClientRect().toJSON(),
),
slots: [...document.querySelectorAll("[data-splash-slot]")].map((n) =>
n.getBoundingClientRect().toJSON(),
),
}));
assert.ok(result.scroll <= width, "overflow " + width);
assert.equal(result.cards.length, 3);
for (const [i, slot] of [1, 0, 2].entries()) {
assert.ok(Math.abs(result.cards[i].x - result.slots[slot].x) < 1);
assert.ok(Math.abs(result.cards[i].y - result.slots[slot].y) < 1);
}
if (width <= 800) {
for (const selector of [
".menu-toggle",
".header-cta",
".stat-source",
".site-footer nav a",
"[data-agent-replay]",
]) {
const height = await page
.locator(selector)
.first()
.evaluate((n) => n.getBoundingClientRect().height);
assert.ok(height >= 44, selector + " height " + height);
}
}
}
await page.setViewportSize({ width: 390, height: 1000 });
await page.goto(base);
await page.evaluate(() => document.fonts.ready);
assert.equal(await page.locator("[data-splash-replay]").count(), 0);
assert.equal(await page.locator("[data-splash-announcement]").count(), 0);
assert.ok(
!(await page.locator(".splash").textContent()).includes(
"Keeping your books in sync",
),
);
const poses = () =>
page
.locator("[data-splash-card]")
.evaluateAll((nodes) => nodes.map((n) => n.style.transform));
const initial = await poses();
await page.waitForFunction(
() =>
document.querySelector("[data-splash-demo]").dataset.splashPhase ===
"moving",
);
await page.locator("[data-splash-toggle]").click();
const frozen = await poses();
await page.waitForTimeout(450);
assert.deepEqual(await poses(), frozen);
assert.equal(
await page.locator("[data-splash-toggle]").getAttribute("aria-label"),
"Resume task animation",
);
await page.locator("[data-splash-toggle]").click();
await page.waitForTimeout(1700);
assert.notDeepEqual(await poses(), initial);
await page.locator("#stats").scrollIntoViewIfNeeded();
await page.waitForTimeout(100);
const offscreen = await poses();
await page.waitForTimeout(500);
assert.deepEqual(await poses(), offscreen);
await page.locator(".splash-screen").scrollIntoViewIfNeeded();
await page.emulateMedia({ reducedMotion: "reduce" });
await page.waitForTimeout(100);
const reduced = await poses();
await page.waitForTimeout(300);
assert.deepEqual(await poses(), reduced);
assert.ok(await page.locator("[data-splash-toggle]").isDisabled());
await page.emulateMedia({ reducedMotion: "no-preference" });
await page.waitForTimeout(100);
assert.ok(await page.locator("[data-splash-toggle]").isEnabled());
await page.waitForFunction(
(previous) =>
JSON.stringify(
[...document.querySelectorAll("[data-splash-card]")].map(
(n) => n.style.transform,
),
) !== JSON.stringify(previous),
reduced,
);
assert.notDeepEqual(await poses(), reduced);
assert.deepEqual(errors, []);
const report = {
base,
widths,
cards: 3,
overflow: false,
removedLabels: true,
pauseResume: true,
offscreenPause: true,
reducedMotionBothDirections: true,
errors,
};
await fs.mkdir("reports", { recursive: true });
await fs.writeFile(
"reports/" + (process.env.BASE_URL ? "live" : "local") + "-browser.json",
JSON.stringify(report, null, 2),
);
console.log(JSON.stringify(report));
} finally {
await browser?.close();
await preview?.close();
}
})().catch((e) => {
console.error(e);
process.exitCode = 1;
});
src/page.html
Read raw text
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="light" />
<meta
name="description"
content="Bring approvals, suggested matches, and connection issues into one shared task board with DualEntry Tasks."
/>
<link
rel="alternate"
type="application/json"
href="source/bundle.json"
title="Complete page source"
/>
<title>Tasks — A clear view of what’s next | DualEntry</title>
<link
rel="preload"
href="assets/lausanne-regular.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link rel="preload" as="image" href="assets/cosmos-blue-loop.webp" />
<link rel="preload" as="image" href="assets/cosmos-blue-wave.webp" />
<link rel="stylesheet" href="app.css" />
<script type="module" src="scripts/main.js"></script>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<svg class="sprite" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<defs>
<symbol id="i-arrow" viewBox="0 0 24 24">
<path d="M4 12h15m-6-6 6 6-6 6" />
</symbol>
<symbol id="i-search" viewBox="0 0 24 24">
<circle cx="10.5" cy="10.5" r="6.5" />
<path d="m16 16 4 4" />
</symbol>
<symbol id="i-filter" viewBox="0 0 24 24">
<path d="M3 4h18l-7 8v7l-4-2v-5L3 4Z" />
</symbol>
<symbol id="i-calendar" viewBox="0 0 24 24">
<rect x="4" y="5" width="16" height="16" rx="2" />
<path d="M8 3v4m8-4v4M4 10h16" />
</symbol>
<symbol id="i-layout" viewBox="0 0 24 24">
<rect x="3" y="4" width="18" height="16" rx="2" />
<path d="M3 9h18m-12 0v11m6-11v11" />
</symbol>
<symbol id="i-list" viewBox="0 0 24 24">
<path d="M8 6h12M8 12h12M8 18h12M4 6h.01M4 12h.01M4 18h.01" />
</symbol>
<symbol id="i-inbox" viewBox="0 0 24 24">
<rect x="4" y="5" width="16" height="14" rx="3" />
<path d="m5 7 7 6 7-6" />
</symbol>
<symbol id="i-spark" viewBox="0 0 24 24">
<path
d="m12 3 2.5 6.5L21 12l-6.5 2.5L12 21l-2.5-6.5L3 12l6.5-2.5L12 3Z"
/>
</symbol>
<symbol id="i-clock" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="9" />
<path d="M12 7v5l3 2" />
</symbol>
<symbol id="i-more" viewBox="0 0 24 24">
<circle cx="12" cy="5" r="1" />
<circle cx="12" cy="12" r="1" />
<circle cx="12" cy="19" r="1" />
</symbol>
<symbol id="i-database" viewBox="0 0 24 24">
<ellipse cx="12" cy="5" rx="8" ry="3" />
<path d="M4 5v14c0 4 16 4 16 0V5M4 12c0 4 16 4 16 0" />
</symbol>
<symbol id="i-check" viewBox="0 0 24 24">
<path d="m5 12 4 4L19 6" />
</symbol>
<symbol id="i-sync" viewBox="0 0 24 24">
<path
d="M20 9a8 8 0 0 0-14-3L3 9m0-6v6h6M4 15a8 8 0 0 0 14 3l3-3m0 6v-6h-6"
/>
</symbol>
<symbol id="i-play" viewBox="0 0 24 24">
<path d="m7 4 13 8-13 8V4Z" />
</symbol>
<symbol id="i-chevron" viewBox="0 0 24 24">
<path d="m7 10 5 5 5-5" />
</symbol>
</defs>
</svg>
<header class="site-header">
<div class="container header-row">
<a
class="logo"
href="https://www.dualentry.com/"
aria-label="DualEntry home"
><img src="assets/logo.svg" width="140" height="25" alt="DualEntry"
/></a>
<button
class="menu-toggle"
data-menu-toggle
aria-expanded="false"
aria-controls="site-nav"
type="button"
>
Menu
<svg class="icon" aria-hidden="true"><use href="#i-chevron" /></svg>
</button>
<nav id="site-nav" aria-label="Main navigation">
<a href="#workflow">How it works</a><a href="#selector">Your tasks</a
><a href="#progression">Stay in control</a
><a href="https://www.dualentry.com/customers">Customers</a>
</nav>
<a
class="button button-small header-cta"
href="https://www.dualentry.com/demo"
>Schedule demo
<svg class="icon" aria-hidden="true"><use href="#i-arrow" /></svg
></a>
</div>
</header>
<main id="main">
<!-- SECTIONS -->
</main>
<footer class="site-footer reference-footer">
<div class="container footer-grid">
<a
class="logo"
href="https://www.dualentry.com/"
aria-label="DualEntry home"
><img src="assets/logo.svg" width="140" height="25" alt="DualEntry"
/></a>
<nav aria-label="Product">
<h2>Product</h2>
<a
href="https://www.dualentry.com/core-financials/general-ledger-software"
>General Ledger</a
><a
href="https://www.dualentry.com/core-financials/close-management-software"
>Close Management</a
><a href="#agent">AI Accounting</a
><a href="https://www.dualentry.com/pricing">Pricing</a>
</nav>
<nav aria-label="Platform">
<h2>Platform</h2>
<a href="#selector">Tasks</a><a href="#progression">Workflows</a
><a href="#stats">AI Agents</a><a href="#modules">Integrations</a>
</nav>
<nav aria-label="Company">
<h2>Company</h2>
<a href="https://www.dualentry.com/customers">Customers</a
><a href="https://www.dualentry.com/about-us">About</a
><a href="https://www.dualentry.com/careers">Careers</a
><a href="https://www.dualentry.com/contact">Contact</a>
</nav>
<nav aria-label="Legal and source">
<h2>Legal</h2>
<a href="https://www.dualentry.com/legal/privacy-policy"
>Privacy Policy</a
><a href="https://www.dualentry.com/legal/website-terms-of-use"
>Website Terms</a
><a href="https://trust.dualentry.com/">Trust Center</a
><a href="source/">Page source</a>
</nav>
<small class="footer-copyright">© 2026 Entry Inc.</small>
</div>
</footer>
</body>
</html>
src/scripts/agent-state.mjs
Read raw text
const phaseTimes = Object.freeze({
queued: 0,
assigned: 800,
filling: 2800,
review: 5000,
});
export function createAgentState(search = "", reduced = false) {
const params = new URLSearchParams(search);
const capture = params.get("capture") === "1";
const requested = params.get("phase");
const phase = Object.hasOwn(phaseTimes, requested) ? requested : "review";
return {
elapsed: reduced ? 5000 : capture ? phaseTimes[phase] : 0,
locked: capture || reduced,
};
}
export function advanceAgent(state, milliseconds, active) {
if (
state.locked ||
!active ||
!Number.isFinite(milliseconds) ||
milliseconds < 0
)
return { ...state };
return { ...state, elapsed: Math.min(5000, state.elapsed + milliseconds) };
}
export function replayAgent(state) {
return state.locked ? { ...state } : { ...state, elapsed: 0 };
}
export function describeAgent({ elapsed }) {
const phase =
elapsed >= 5000
? "review"
: elapsed >= 1700
? "filling"
: elapsed >= 800
? "assigned"
: "queued";
const groups =
elapsed >= 3800 ? 3 : elapsed >= 2800 ? 2 : elapsed >= 1700 ? 1 : 0;
return { phase, groups };
}
export function eligibleAgentVisibility(
height,
intersectionHeight,
viewportHeight,
) {
return (
height > 0 && intersectionHeight >= Math.min(height, viewportHeight) / 2
);
}
src/scripts/agent.js
Read raw text
import {
createAgentState,
advanceAgent,
replayAgent,
describeAgent,
eligibleAgentVisibility,
} from "./agent-state.mjs";
const labels = Object.freeze({
queued: ["Waiting for an agent", "Ready to prepare", "Not started", "Queued"],
assigned: [
"Journal agent assigned",
"Preparing this task",
"Agent assigned",
"Agent assigned",
],
filling: [
"Journal agent preparing",
"Adding entry details",
"Preparing",
"Preparing entry",
],
review: [
"Prepared by Journal agent",
"Waiting for your review",
"Ready for review",
"Ready for review",
],
});
export function initializeAgent(root, env = window) {
const media = env.matchMedia("(prefers-reduced-motion: reduce)");
let state = createAgentState(env.location.search, media.matches);
let visible = false;
let frame = null;
let previousTime = null;
let lastRendered = "";
const replay = root.querySelector("[data-agent-replay]");
const announcement = root.parentElement.querySelector(
"[data-agent-announcement]",
);
const fields = [...root.querySelectorAll("[data-agent-field]")];
const slots = ["assignment", "queue-note", "status"].map((name) =>
root.querySelector(`[data-agent-${name}]`),
);
function render() {
const { phase, groups } = describeAgent(state);
const signature = `${phase}:${groups}:${state.locked}`;
if (signature === lastRendered) return;
lastRendered = signature;
root.dataset.agentPhase = phase;
root.dataset.agentGroups = String(groups);
slots.forEach((node, index) => {
node.textContent = labels[phase][index];
});
fields.forEach((node) => {
node.dataset.filled = String(Number(node.dataset.agentField) <= groups);
});
announcement.textContent = labels[phase][3];
replay.disabled = state.locked || phase !== "review";
}
function active() {
return (
visible && !env.document.hidden && !state.locked && state.elapsed < 5000
);
}
function tick(time) {
frame = null;
if (!active()) {
previousTime = null;
return;
}
state = advanceAgent(
state,
previousTime === null ? 0 : time - previousTime,
true,
);
previousTime = time;
render();
if (active()) frame = env.requestAnimationFrame(tick);
}
function syncPlayback() {
if (!active()) {
if (frame !== null) env.cancelAnimationFrame(frame);
frame = null;
previousTime = null;
} else if (frame === null) frame = env.requestAnimationFrame(tick);
}
const observer = new env.IntersectionObserver(
(entries) => {
const entry = entries[entries.length - 1];
visible =
entry.isIntersecting &&
eligibleAgentVisibility(
entry.boundingClientRect.height,
entry.intersectionRect.height,
env.innerHeight,
);
syncPlayback();
},
{ threshold: Array.from({ length: 101 }, (_, index) => index / 100) },
);
observer.observe(root);
function handleReplay() {
state = replayAgent(state);
previousTime = null;
render();
syncPlayback();
}
function handleMotionChange(event) {
if (event.matches) {
state = createAgentState(env.location.search, true);
render();
syncPlayback();
}
}
replay.addEventListener("click", handleReplay);
env.document.addEventListener("visibilitychange", syncPlayback);
media.addEventListener("change", handleMotionChange);
render();
return () => {
observer.disconnect();
if (frame !== null) env.cancelAnimationFrame(frame);
replay.removeEventListener("click", handleReplay);
env.document.removeEventListener("visibilitychange", syncPlayback);
media.removeEventListener("change", handleMotionChange);
};
}
src/scripts/interactions.js
Read raw text
import { createState, selectState } from "./state.mjs";
import { createUiBindings } from "./ui-runtime.js";
function closeFilterMenu(document) {
const menu = document.getElementById("status-menu");
if (!menu) return;
menu.open = false;
menu.querySelector("summary").focus({ preventScroll: true });
}
function renderFilter(document, element, filter) {
element.querySelectorAll("[data-task-kind]").forEach((row) => {
row.classList.toggle(
"is-muted",
filter !== "all" && row.dataset.taskKind !== filter,
);
});
const labels = {
all: "All tasks",
waiting: "Waiting on you",
connections: "Connections",
};
document.getElementById("filter-current-label").textContent = labels[filter];
const count = element.querySelectorAll(
"[data-task-kind]:not(.is-muted)",
).length;
document.getElementById("filter-result-count").textContent =
`${count} tasks in focus`;
}
function bindSelections(document, ui) {
let state = createState();
const bind = (key, attribute, targetId) => {
const element = document.getElementById(targetId);
const buttons = [...document.querySelectorAll(`[${attribute}]`)];
const render = () => {
buttons.forEach((button) =>
button.setAttribute(
"aria-pressed",
String(button.getAttribute(attribute) === state[key]),
),
);
if (!element) return;
element.dataset[key] = state[key];
if (key === "filter") renderFilter(document, element, state.filter);
};
render();
buttons.forEach((button) =>
ui.listen(button, "click", (event) => {
const next = selectState(state, key, button.getAttribute(attribute));
if (next[key] !== state[key]) {
state = next;
render();
ui.animate(
element,
event,
[
{ opacity: 0.82, transform: "translateY(2px)" },
{ opacity: 1, transform: "translateY(0)" },
],
{ duration: 160, easing: "ease-out" },
);
}
if (key === "filter") closeFilterMenu(document);
}),
);
};
bind("filter", "data-filter-choice", "filter-demo");
bind("context", "data-context-choice", "context-demo");
}
function bindNavigation(document, ui) {
const toggle = document.querySelector("[data-menu-toggle]");
const navigation = document.getElementById("site-nav");
if (!toggle || !navigation) return;
const setOpen = (open) => {
toggle.setAttribute("aria-expanded", String(open));
toggle.setAttribute(
"aria-label",
open ? "Close navigation" : "Open navigation",
);
navigation.classList.toggle("is-open", open);
};
setOpen(false);
ui.listen(toggle, "click", () =>
setOpen(toggle.getAttribute("aria-expanded") !== "true"),
);
ui.listen(navigation, "click", (event) => {
if (event.target.closest("a")) setOpen(false);
});
ui.listen(document, "keydown", (event) => {
if (
event.key !== "Escape" ||
toggle.getAttribute("aria-expanded") !== "true"
)
return;
setOpen(false);
toggle.focus();
});
}
export function initializeInteractions(document, env = window) {
const ui = createUiBindings(document, env);
bindNavigation(document, ui);
bindSelections(document, ui);
ui.listen(document, "keydown", (event) => {
if (event.key === "Escape" && document.getElementById("status-menu")?.open)
closeFilterMenu(document);
});
return () => ui.dispose();
}
src/scripts/main.js
Read raw text
import { isCaptureMode, getCaptureSection } from "./state.mjs";
import { initializeInteractions } from "./interactions.js";
import { initializeFlowScenes } from "./scenes-flow.js";
import { initializeFocusScenes } from "./scenes-focus.js";
import { initializeAgent } from "./agent.js";
import { initializeSplash } from "./splash-motion.js";
/** Capture layout is settled before any controller reads geometry or starts motion. */
export function applyCapture(document, search) {
if (isCaptureMode(search)) document.documentElement.dataset.capture = "true";
const captureSection = getCaptureSection(search);
if (!captureSection) return;
document.documentElement.dataset.isolated = "true";
document.querySelectorAll("[data-capture-section]").forEach((section) => {
section.hidden = section.dataset.captureSection !== captureSection;
});
}
export function initializePage(document, env = window) {
applyCapture(document, env.location.search);
const dispose = [
initializeInteractions(document, env),
initializeFlowScenes(document, env),
initializeFocusScenes(document, env),
];
const agent = document.querySelector("[data-agent-demo]");
const splash = document.querySelector("[data-splash-demo]");
if (agent) dispose.push(initializeAgent(agent, env));
if (splash) dispose.push(initializeSplash(splash, env));
return () => dispose.forEach((cleanup) => cleanup());
}
if (typeof document !== "undefined") initializePage(document);
src/scripts/scenes-flow.js
Read raw text
import { createUiBindings } from "./ui-runtime.js";
function selectControl(root, selector, attribute, selected) {
root.querySelectorAll(selector).forEach((button) => {
button.setAttribute(
"aria-pressed",
String(button.dataset[attribute] === selected),
);
});
}
function revealColumn(root, name) {
const viewport = root.querySelector(".pano-scroll");
const column = root.querySelector(`.pano-col-${name}`);
if (!viewport || !column || viewport.scrollWidth <= viewport.clientWidth)
return;
const offset =
column.getBoundingClientRect().left - viewport.getBoundingClientRect().left;
viewport.scrollLeft += offset;
}
function initializeProgression(root, ui) {
root.querySelectorAll("[data-progress-choice]").forEach((button) => {
ui.listen(button, "click", (event) => {
const selected = button.dataset.progressChoice;
if (!["see", "review", "move"].includes(selected)) return;
root.dataset.flowMotion = String(ui.allowsMotion(event));
root.dataset.progressStage = selected;
selectControl(root, "[data-progress-choice]", "progressChoice", selected);
});
});
}
function setPanoramaFocus(root, selected, event, ui) {
root.dataset.flowMotion = String(ui.allowsMotion(event));
root.dataset.panoFocus = selected;
selectControl(root, "[data-pano-choice]", "panoChoice", selected);
revealColumn(root, selected === "waiting" ? "waiting" : "todo");
}
function moveExample(root, event, ui) {
const card = root.querySelector("#pano-moving-task");
const next = root.dataset.panoState === "waiting" ? "done" : "waiting";
const destination = root.querySelector(`[data-pano-destination="${next}"]`);
if (!card || !destination) return;
const animate = ui.allowsMotion(event);
card.getAnimations().forEach((animation) => animation.cancel());
const before = card.getBoundingClientRect();
root.dataset.panoState = next;
root.dataset.flowMotion = String(animate);
root.dataset.panoFocus = "all";
selectControl(root, "[data-pano-choice]", "panoChoice", "all");
destination.prepend(card);
updatePanoramaText(root, next === "done");
revealColumn(root, next);
if (animate) {
const after = card.getBoundingClientRect();
const distance = Math.max(-320, Math.min(320, before.left - after.left));
ui.animate(
card,
event,
[
{ transform: `translate(${distance}px, 4px)`, opacity: 0.65 },
{ transform: "translate(0, 0)", opacity: 1 },
],
{ duration: 220, easing: "cubic-bezier(.2,.7,.2,1)" },
);
}
}
function updatePanoramaText(root, done) {
const values = [
['[data-pano-count="waiting"]', done ? "1" : "2"],
['[data-pano-count="control"]', done ? "1" : "2"],
['[data-pano-count="done"]', done ? "2" : "1"],
["[data-pano-button-label]", done ? "Reset" : "Move to Done"],
[
"[data-pano-task-title]",
done ? "Review complete." : "Run ready for review.",
],
["[data-pano-task-badge]", done ? "Reviewed" : "Ready to review"],
[
"[data-pano-status]",
done
? "Task moved to Done. One task still needs review."
: "Task returned to review.",
],
];
values.forEach(([selector, text]) => {
const element = root.querySelector(selector);
if (element) element.textContent = text;
});
const emptyState = root.querySelector("[data-pano-empty]");
if (emptyState) emptyState.hidden = !done;
}
function initializePanorama(root, ui) {
root.querySelectorAll("[data-pano-choice]").forEach((button) => {
ui.listen(button, "click", (event) => {
const selected = button.dataset.panoChoice;
if (["all", "waiting"].includes(selected))
setPanoramaFocus(root, selected, event, ui);
});
});
ui.listen(root.querySelector("[data-pano-move]"), "click", (event) =>
moveExample(root, event, ui),
);
}
export function initializeFlowScenes(document, env = window) {
const ui = createUiBindings(document, env);
document
.querySelectorAll("[data-progress-stage]")
.forEach((root) => initializeProgression(root, ui));
document
.querySelectorAll("[data-pano-state]")
.forEach((root) => initializePanorama(root, ui));
return () => ui.dispose();
}
src/scripts/scenes-focus.js
Read raw text
import { createUiBindings } from "./ui-runtime.js";
/* Deliberate, user-triggered illustration states. No timers or product connections. */
const focusCards = Object.freeze({
all: {
source: "Your task queue",
icon: "i-layout",
label: "Everything, together",
count: "All tasks",
title: "A clear view of your next steps",
time: "Updated today",
action: "View tasks",
},
approvals: {
source: "Inbox",
icon: "i-inbox",
label: "Waiting on you",
count: "13 approvals",
title: "13 approvals ready for your review",
time: "2 hrs ago",
action: "View approvals",
},
connections: {
source: "USAA Bank",
icon: "i-sync",
label: "Connection needs attention",
count: "Connections",
title: "Reconnect to see recent transactions",
time: "Just now",
action: "View connection",
},
agents: {
source: "Bank Recommendations",
icon: "i-spark",
label: "Ready for review",
count: "Agent runs",
title: "Suggested matches, ready for a closer look",
time: "2 days ago",
action: "View run",
},
});
const examples = Object.freeze([
{
status: "Pending review",
source: "Bank Recommendations",
title: "Run ready for review",
action: "Review",
outcome: "Waiting on you",
},
{
status: "Reviewed",
source: "Bank Recommendations",
title: "Recommendations reviewed",
action: "Next",
outcome: "Review complete",
},
{
status: "Pending review",
source: "Daily activity",
title: "Activity summary ready",
action: "Review",
outcome: "Waiting on you",
},
{
status: "Reviewed",
source: "Daily activity",
title: "Activity summary reviewed",
action: "Restart",
outcome: "Review complete",
},
]);
export function chooseFocus(key) {
return { ...focusCards[Object.hasOwn(focusCards, key) ? key : "all"] };
}
export function advanceExample(step) {
const next =
Number.isInteger(step) && step >= 0 && step < examples.length
? (step + 1) % examples.length
: 0;
return { ...examples[next], step: next };
}
function fillText(root, selector, value) {
const element = root.querySelector(selector);
if (element) element.textContent = value;
}
function bindFocus(document, ui) {
const section = document.getElementById("selector");
if (!section) return;
const choices = [...section.querySelectorAll("[data-sel-choice]")];
const stage = section.querySelector("#selector-stage");
choices.forEach((button) =>
ui.listen(button, "click", (event) => {
if (button.getAttribute("aria-pressed") === "true") return;
const key = button.dataset.selChoice;
const card = chooseFocus(key);
choices.forEach((choice) =>
choice.setAttribute("aria-pressed", String(choice === button)),
);
stage.dataset.focus = key;
["source", "label", "count", "title", "time"].forEach((field) =>
fillText(stage, `[data-sel-${field}]`, card[field]),
);
const action = stage.querySelector("[data-sel-action]");
action.firstChild.textContent = `${card.action} `;
stage
.querySelector("[data-sel-icon]")
.setAttribute("href", `#${card.icon}`);
settleIntoPlace(ui, stage.querySelector(".sel-main-card"), event);
}),
);
}
function bindControl(document, ui) {
const section = document.getElementById("control");
const button = section?.querySelector("[data-control-update]");
if (!button) return;
let step = 0;
ui.listen(button, "click", (event) => {
const next = advanceExample(step);
step = next.step;
const frame = section.querySelector("#control-example");
frame.dataset.reviewState =
next.status === "Reviewed" ? "reviewed" : "pending";
["source", "status", "title", "outcome"].forEach((field) =>
fillText(section, `[data-control-${field}]`, next[field]),
);
button.firstChild.textContent = next.action;
settleIntoPlace(ui, section.querySelector(".control-approval-card"), event);
});
}
function settleIntoPlace(ui, element, event) {
ui.animate(
element,
event,
[
{ opacity: 0.72, transform: "translateY(4px)" },
{ opacity: 1, transform: "translateY(0)" },
],
{ duration: 200, easing: "cubic-bezier(.2,.7,.2,1)" },
);
}
export function initializeFocusScenes(document, env = window) {
const ui = createUiBindings(document, env);
bindFocus(document, ui);
bindControl(document, ui);
return () => ui.dispose();
}
src/scripts/section-order.mjs
Read raw text
export const sectionOrder = Object.freeze([
"hero",
"workflow",
"video",
"selector",
"agent",
"prepared",
"context",
"progression",
"stats",
"modules",
"closing",
]);
src/scripts/splash-motion-state.mjs
Read raw text
const LEG_DURATION = 6000;
const HOLD_DURATION = 3800;
const MOVE_DURATION = 1400;
const LOOP_DURATION = LEG_DURATION * 6;
const phaseTimes = Object.freeze({ queued: 0, moving: 4500, review: 5200 });
const clamp = (value) => Math.min(1, Math.max(0, value));
const ease = (value) => value * value * (3 - 2 * value);
const pairs = Object.freeze(
[
[0, 1],
[1, 2],
[0, 1],
[1, 2],
[0, 1],
[1, 2],
].map(Object.freeze),
);
const swap = (slots, pair) =>
slots.map((slot) =>
slot === pair[0] ? pair[1] : slot === pair[1] ? pair[0] : slot,
);
const arrangements = Object.freeze(
pairs.reduce(
(poses, pair) => [...poses, Object.freeze(swap(poses.at(-1), pair))],
[Object.freeze([0, 1, 2])],
),
);
export function createSplashState(search = "", reduced = false) {
const params = new URLSearchParams(search);
const capture = params.get("capture") === "1";
const requested = params.get("phase");
const phase = Object.hasOwn(phaseTimes, requested) ? requested : "review";
return {
elapsed: reduced ? phaseTimes.review : capture ? phaseTimes[phase] : 0,
capture,
locked: capture || reduced,
};
}
export function advanceSplash(state, milliseconds, active) {
if (
state.locked ||
!active ||
!Number.isFinite(milliseconds) ||
milliseconds < 0
)
return { ...state };
return { ...state, elapsed: (state.elapsed + milliseconds) % LOOP_DURATION };
}
export function setSplashReduced(state, reduced) {
return {
...state,
elapsed: reduced ? phaseTimes.review : state.elapsed,
locked: state.capture || reduced,
};
}
export function describeSplash({ elapsed }) {
const time = elapsed % LOOP_DURATION;
const leg = Math.floor(time / LEG_DURATION);
const local = time % LEG_DURATION;
const progress = ease(clamp((local - HOLD_DURATION) / MOVE_DURATION));
const from = arrangements[leg];
const to = arrangements[leg + 1];
const phase =
progress > 0 && progress < 1
? "moving"
: time < HOLD_DURATION
? "queued"
: "review";
return {
phase,
cards: from.map((slot, index) => ({
from: slot,
to: to[index],
progress: slot === to[index] ? 0 : progress,
slot: progress === 1 ? to[index] : slot,
})),
};
}
export function splashPosition(progress, from, to, compact, direction) {
// The passing offset returns to exactly zero at either anchor, including the loop seam.
const passing =
progress === 0 || progress === 1 ? 0 : Math.sin(Math.PI * progress) * 6;
return {
x:
from.x + (to.x - from.x) * progress + (compact ? passing * direction : 0),
y:
from.y + (to.y - from.y) * progress - (compact ? 0 : passing * direction),
};
}
export function splashVisible(height, intersectionHeight, viewportHeight) {
return (
height > 0 && intersectionHeight >= Math.min(height, viewportHeight) / 2
);
}
src/scripts/splash-motion.js
Read raw text
import {
createSplashState,
advanceSplash,
setSplashReduced,
describeSplash,
splashPosition,
splashVisible,
} from "./splash-motion-state.mjs";
const labels = Object.freeze(["To do", "In progress", "For review"]);
const setStyle = (element, property, value) => {
if (element.style[property] !== value) element.style[property] = value;
};
export function initializeSplash(root, env = window) {
const media = env.matchMedia("(prefers-reduced-motion: reduce)");
let state = createSplashState(env.location.search, media.matches);
let visible = false;
let userPaused = false;
let frame = null;
let previousTime = null;
let geometry = null;
let disposed = false;
const board = root.querySelector(".kanban");
const cards = Array.from(root.querySelectorAll("[data-splash-card]"));
const slots = Array.from(root.querySelectorAll("[data-splash-slot]"));
const statuses = cards.map((card) =>
card.querySelector("[data-splash-card-status]"),
);
const toggle = root.querySelector("[data-splash-toggle]");
function render() {
const view = describeSplash(state);
if (root.dataset.splashPhase !== view.phase)
root.dataset.splashPhase = view.phase;
view.cards.forEach((pose, index) => {
if (geometry) {
const position = splashPosition(
pose.progress,
geometry.anchors[pose.from],
geometry.anchors[pose.to],
geometry.compact,
Math.sign(pose.to - pose.from),
);
setStyle(
cards[index],
"transform",
`translate3d(${position.x}px, ${position.y}px, 0)`,
);
setStyle(
cards[index],
"zIndex",
pose.progress > 0 && pose.progress < 1
? String(pose.to > pose.from ? 3 : 2)
: "1",
);
}
if (statuses[index] && statuses[index].textContent !== labels[pose.slot])
statuses[index].textContent = labels[pose.slot];
});
}
function renderControl() {
root.dataset.splashPaused = String(userPaused || state.locked);
toggle.disabled = state.locked;
toggle.setAttribute(
"aria-label",
userPaused ? "Resume task animation" : "Pause task animation",
);
toggle.setAttribute("aria-pressed", String(userPaused));
}
function measure() {
if (disposed) return;
const width = Math.min(
...slots.map((slot) => slot.getBoundingClientRect().width),
);
if (!width) return;
cards.forEach((card) => setStyle(card, "width", `${width}px`));
const height = `${Math.ceil(Math.max(...cards.map((card) => card.getBoundingClientRect().height)))}px`;
slots.forEach((slot) => setStyle(slot, "height", height));
const boardRect = board.getBoundingClientRect();
const anchors = slots.map((slot) => {
const rect = slot.getBoundingClientRect();
return { x: rect.left - boardRect.left, y: rect.top - boardRect.top };
});
geometry = { anchors, compact: Math.abs(anchors[0].x - anchors[1].x) < 1 };
root.dataset.splashCompact = String(geometry.compact);
render();
}
function active() {
return (
!disposed &&
visible &&
!env.document.hidden &&
!state.locked &&
!userPaused
);
}
function tick(time) {
frame = null;
if (!active()) {
previousTime = null;
return;
}
state = advanceSplash(
state,
previousTime === null ? 0 : time - previousTime,
true,
);
previousTime = time;
render();
frame = env.requestAnimationFrame(tick);
}
function syncPlayback() {
if (!active()) {
if (frame !== null) env.cancelAnimationFrame(frame);
frame = null;
previousTime = null;
} else if (frame === null) frame = env.requestAnimationFrame(tick);
}
const observer = new env.IntersectionObserver(
(entries) => {
const entry = entries[entries.length - 1];
visible =
entry.isIntersecting &&
splashVisible(
entry.boundingClientRect.height,
entry.intersectionRect.height,
env.innerHeight,
);
syncPlayback();
},
{ threshold: Array.from({ length: 101 }, (_, index) => index / 100) },
);
const resize = new env.ResizeObserver(measure);
function handleToggle() {
if (state.locked) return;
userPaused = !userPaused;
renderControl();
syncPlayback();
}
function handleMotionChange(event) {
state = setSplashReduced(state, event.matches);
previousTime = null;
render();
renderControl();
syncPlayback();
}
// Three real cards share an overlay; the anchored slots reserve their full geometry.
board.append(...cards);
root.dataset.splashInitialized = "true";
measure();
render();
renderControl();
observer.observe(root.querySelector(".splash-screen"));
resize.observe(board);
env.document.fonts?.ready.then(measure);
toggle.addEventListener("click", handleToggle);
env.document.addEventListener("visibilitychange", syncPlayback);
media.addEventListener("change", handleMotionChange);
return () => {
disposed = true;
observer.disconnect();
resize.disconnect();
if (frame !== null) env.cancelAnimationFrame(frame);
toggle.removeEventListener("click", handleToggle);
env.document.removeEventListener("visibilitychange", syncPlayback);
media.removeEventListener("change", handleMotionChange);
};
}
src/scripts/state.mjs
Read raw text
import { sectionOrder } from "./section-order.mjs";
const choices = Object.freeze({
filter: Object.freeze(["all", "waiting", "connections"]),
context: Object.freeze(["source", "work", "action"]),
});
export function createState() {
return { filter: "waiting", context: "source" };
}
export function selectState(state, key, value) {
const allowed = Object.hasOwn(choices, key) && choices[key].includes(value);
return allowed ? { ...state, [key]: value } : { ...state };
}
export function isCaptureMode(search) {
return new URLSearchParams(search).get("capture") === "1";
}
export function getCaptureSection(search) {
const section = new URLSearchParams(search).get("section");
return isCaptureMode(search) && sectionOrder.includes(section)
? section
: null;
}
src/scripts/ui-runtime.js
Read raw text
/** Shared lifecycle for user-triggered UI feedback. */
export function createUiBindings(document, env) {
const media = env.matchMedia("(prefers-reduced-motion: reduce)");
const bindings = [];
const animations = new Set();
const cancel = () => {
animations.forEach((animation) => animation.cancel());
animations.clear();
};
const listen = (target, type, callback) => {
if (!target) return;
target.addEventListener(type, callback);
bindings.push(() => target.removeEventListener(type, callback));
};
const allowsMotion = (event) =>
event.detail > 0 &&
document.documentElement.dataset.capture !== "true" &&
!media.matches;
listen(media, "change", () => {
if (media.matches) cancel();
});
return {
listen,
allowsMotion,
animate(element, event, frames, options) {
if (!element) return;
element.getAnimations().forEach((animation) => animation.cancel());
if (!allowsMotion(event)) return;
const animation = element.animate(frames, options);
animations.add(animation);
// Finished animations need no retained references. Cancellation also settles this promise.
animation.finished?.then(
() => animations.delete(animation),
() => animations.delete(animation),
);
},
dispose() {
bindings.forEach((remove) => remove());
cancel();
},
};
}
src/sections/agent.html
Read raw text
<section
class="section container agent-section"
id="agent"
aria-labelledby="agent-title"
data-capture-section="agent"
>
<div class="agent-heading">
<h2 id="agent-title">From task to prepared entry.</h2>
</div>
<div
class="agent-stage"
data-agent-demo
data-agent-phase="queued"
data-agent-groups="0"
>
<aside class="agent-queue" aria-label="Example task queue">
<div class="agent-queue-header">
<span>Tasks</span><span class="agent-count">1</span>
</div>
<div class="agent-task">
<div class="agent-task-source">
<span class="agent-entity">A</span><span>Acme Inc</span>
</div>
<h3>Prepare software expense entry</h3>
<p>Software subscription · $1,200</p>
<div class="agent-assignment">
<span class="agent-assignment-icon" aria-hidden="true"
><svg viewBox="0 0 20 20" fill="none">
<path
d="m10 2 2.3 5.7L18 10l-5.7 2.3L10 18l-2.3-5.7L2 10l5.7-2.3L10 2Z"
stroke="currentColor"
stroke-width="1.3"
/></svg></span
><span data-agent-assignment>Waiting for an agent</span>
</div>
</div>
<div class="agent-queue-note">
<span class="agent-note-dot" aria-hidden="true"></span
><span data-agent-queue-note>Ready to prepare</span>
</div>
</aside>
<div class="agent-transfer" aria-hidden="true">
<span></span
><svg viewBox="0 0 16 16" fill="none">
<path
d="M3 8h10m-4-4 4 4-4 4"
stroke="currentColor"
stroke-width="1.3"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</div>
<div
class="agent-form"
role="group"
aria-label="Illustrative journal entry"
>
<div class="agent-form-header">
<span>Journal entry</span>
<div class="agent-form-actions">
<span class="agent-form-status" data-agent-status>Not started</span
><button
class="agent-replay"
type="button"
data-agent-replay
aria-label="Replay journal entry preparation"
title="Replay"
disabled
>
<svg viewBox="0 0 16 16" aria-hidden="true" fill="none">
<path
d="M3.2 5.5A5 5 0 1 1 3 10M3 2v4h4"
stroke="currentColor"
stroke-width="1.3"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
</div>
</div>
<div class="agent-form-body">
<div class="agent-fields">
<div class="agent-field">
<span class="agent-field-label">Entity</span>
<div class="agent-field-value" data-agent-field="1">
<span>Acme Inc</span>
</div>
</div>
<div class="agent-field">
<span class="agent-field-label">Date</span>
<div class="agent-field-value" data-agent-field="1">
<span>Sep 8, 2026</span>
</div>
</div>
</div>
<div
class="agent-entry-table"
role="table"
aria-label="Journal entry lines"
>
<div class="agent-entry-row agent-entry-head" role="row">
<span role="columnheader">Account</span
><span role="columnheader">Debit</span
><span role="columnheader">Credit</span>
</div>
<div class="agent-entry-row" role="row">
<div class="agent-field-value" data-agent-field="2" role="cell">
<span>Software expense</span>
</div>
<div class="agent-field-value" data-agent-field="2" role="cell">
<span>$1,200.00</span>
</div>
<div class="agent-field-value" data-agent-field="2" role="cell">
<span>—</span>
</div>
</div>
<div class="agent-entry-row" role="row">
<div class="agent-field-value" data-agent-field="2" role="cell">
<span>Accounts payable</span>
</div>
<div class="agent-field-value" data-agent-field="2" role="cell">
<span>—</span>
</div>
<div class="agent-field-value" data-agent-field="2" role="cell">
<span>$1,200.00</span>
</div>
</div>
</div>
<div class="agent-field agent-memo">
<span class="agent-field-label">Memo</span>
<div class="agent-field-value" data-agent-field="3">
<span>Software subscription</span>
</div>
</div>
</div>
</div>
</div>
<span
class="sr-only"
data-agent-announcement
aria-live="polite"
aria-atomic="true"
>Queued</span
>
</section>
src/sections/closing.html
Read raw text
<section
class="close-month-section"
id="demo"
aria-labelledby="closing-title"
data-capture-section="closing"
>
<div class="container">
<div class="close-month-panel">
<p class="close-month-label">Tasks · DualEntry</p>
<h2 id="closing-title">Close all month. Approve in minutes.</h2>
<p class="close-month-description">
One board from live ledger state. Nothing posts until a human does.
</p>
<div class="close-month-actions">
<a
class="button close-month-button"
href="https://www.dualentry.com/demo"
>
Schedule demo
<svg class="icon" aria-hidden="true"><use href="#i-arrow" /></svg>
</a>
<a class="text-link close-month-link" href="#selector">
See the board
<svg class="icon" aria-hidden="true"><use href="#i-arrow" /></svg>
</a>
</div>
</div>
</div>
</section>
src/sections/context.html
Read raw text
<section
class="section container context-section"
id="context"
aria-labelledby="context-title"
data-capture-section="context"
>
<div
class="context-panel feature-panel"
id="context-demo"
data-context="source"
role="img"
aria-label="Task card showing its source, work count, and next action"
>
<div class="context-card" aria-hidden="true">
<div class="task-source" data-part="source">
<span class="avatar avatar-acme">A</span><span>Acme Inc</span
><span class="badge badge-lilac">38</span
><svg class="icon more" aria-hidden="true"><use href="#i-more" /></svg>
</div>
<p class="task-title" data-part="work">
38 suggested matches across 12 accounts are ready to review.
</p>
<div class="task-footer" data-part="action">
<span class="timestamp"
><svg class="icon" aria-hidden="true"><use href="#i-clock" /></svg>4
hrs ago</span
><span class="mini-action">Review</span>
</div>
</div>
</div>
<div class="context-copy">
<h2 id="context-title">
Everything you need<br />
to review.
</h2>
<div class="context-notes" role="group" aria-label="Explore task details">
<button type="button" data-context-choice="source" aria-pressed="true">
<span class="note-icon"
><svg class="icon" aria-hidden="true">
<use href="#i-database" /></svg></span
><span><strong>Where it came from.</strong></span
><svg class="icon note-arrow" aria-hidden="true">
<use href="#i-arrow" />
</svg></button
><button type="button" data-context-choice="work" aria-pressed="false">
<span class="note-icon"
><svg class="icon" aria-hidden="true">
<use href="#i-list" /></svg></span
><span><strong>What needs review.</strong></span
><svg class="icon note-arrow" aria-hidden="true">
<use href="#i-arrow" />
</svg></button
><button type="button" data-context-choice="action" aria-pressed="false">
<span class="note-icon"
><svg class="icon" aria-hidden="true">
<use href="#i-play" /></svg></span
><span><strong>What happens next.</strong></span
><svg class="icon note-arrow" aria-hidden="true">
<use href="#i-arrow" />
</svg>
</button>
</div>
</div>
</section>
src/sections/hero.html
Read raw text
<section
class="splash"
id="overview"
aria-labelledby="hero-title"
data-capture-section="hero"
data-splash-demo
data-splash-phase="review"
>
<div class="splash-art" aria-hidden="true"></div>
<div class="splash-overlay" aria-hidden="true"></div>
<div class="splash-copy container">
<h1 id="hero-title">Your next move.<br />Right here.</h1>
<p class="lead">
Approvals to review. Matches to check. Connections to restore.<br />
One place to see what needs you.
</p>
</div>
<div class="splash-actions cta-row container">
<a class="button" href="https://www.dualentry.com/demo"
>See Tasks in a demo
<svg class="icon" aria-hidden="true"><use href="#i-arrow" /></svg
></a>
<a class="text-link" href="#video"
>Explore Tasks
<svg class="icon" aria-hidden="true"><use href="#i-arrow" /></svg
></a>
</div>
<figure
class="splash-screen"
aria-label="Illustrative Tasks screen showing work to do, work in progress, tasks waiting for review, and completed work. On phones, three readable task queues are arranged vertically."
>
<div class="splash-glass" aria-hidden="true">
<div class="product-window">
<div class="product-tabs">
<span>Mosaic</span><span class="selected">Tasks</span
><span>Agents</span><span class="plus">+</span>
</div>
<div class="product-toolbar">
<span class="search-art"
><svg class="icon" aria-hidden="true"><use href="#i-search" /></svg
><span>Search tasks…</span></span
><span class="tool-art"
><svg class="icon" aria-hidden="true">
<use href="#i-filter" /></svg></span
><span class="tool-art optional-tool"
><svg class="icon" aria-hidden="true">
<use href="#i-calendar" /></svg></span
><span class="tool-art optional-tool"
><svg class="icon" aria-hidden="true">
<use href="#i-layout" /></svg></span
><span class="new-task">New task</span>
</div>
<div class="kanban">
<div class="kanban-column splash-loop-column">
<div class="column-heading">
To do<span class="count"
><span class="splash-count-desktop">3</span
><span class="splash-count-mobile">1</span></span
>
</div>
<div class="splash-task-slot" data-splash-slot="0">
<article
class="task-card splash-moving-card"
data-splash-card="0"
>
<div class="task-source">
<span class="avatar avatar-inbox"
><svg class="icon" aria-hidden="true">
<use href="#i-inbox" /></svg></span
><span>Inbox</span
><svg class="icon" aria-hidden="true">
<use href="#i-more" />
</svg>
</div>
<p class="task-title">13 approvals to review.</p>
<div class="task-footer">
<span class="splash-task-status" data-splash-card-status
>To do</span
><span class="mini-action"
>View
<svg class="icon" aria-hidden="true">
<use href="#i-arrow" /></svg
></span>
</div>
</article>
</div>
<article class="task-card splash-static-card">
<div class="task-source">
<span class="avatar avatar-agent"
><svg class="icon" aria-hidden="true">
<use href="#i-spark" /></svg></span
><span>Anomaly Detector</span
><svg class="icon" aria-hidden="true">
<use href="#i-more" />
</svg>
</div>
<p class="task-title">3 unusual postings to check.</p>
<div class="task-footer">
<span class="splash-task-status">Needs attention</span
><span class="mini-action"
>View
<svg class="icon" aria-hidden="true">
<use href="#i-arrow" /></svg
></span>
</div>
</article>
<article class="task-card splash-static-card">
<div class="task-source">
<span class="avatar avatar-agent"
><svg class="icon" aria-hidden="true">
<use href="#i-spark" /></svg></span
><span>Contracts</span
><svg class="icon" aria-hidden="true">
<use href="#i-more" />
</svg>
</div>
<p class="task-title">A new contract to review.</p>
<div class="task-footer">
<span class="splash-task-status">Today</span
><span class="mini-action"
>View
<svg class="icon" aria-hidden="true">
<use href="#i-arrow" /></svg
></span>
</div>
</article>
</div>
<div class="kanban-column splash-loop-column">
<div class="column-heading">
In progress<span class="count"
><span class="splash-count-desktop">2</span
><span class="splash-count-mobile">1</span></span
>
</div>
<div class="splash-task-slot" data-splash-slot="1">
<article
class="task-card splash-moving-card"
data-splash-card="1"
>
<div class="task-source">
<span class="avatar avatar-bank"></span><span>USAA Bank</span
><svg class="icon" aria-hidden="true">
<use href="#i-more" />
</svg>
</div>
<p class="task-title">Reconnect your bank.</p>
<div class="task-footer">
<span class="splash-task-status" data-splash-card-status
>In progress</span
><span class="mini-action"
>View
<svg class="icon" aria-hidden="true">
<use href="#i-arrow" /></svg
></span>
</div>
</article>
</div>
<article class="task-card splash-static-card">
<div class="task-source">
<span class="avatar avatar-agent"
><svg class="icon" aria-hidden="true">
<use href="#i-spark" /></svg></span
><span>Bank Recommendations</span
><svg class="icon" aria-hidden="true">
<use href="#i-more" />
</svg>
</div>
<p class="task-title">
Matching transactions across your accounts.
</p>
<div class="task-footer">
<span class="splash-task-status">In progress</span
><span class="mini-action"
>View
<svg class="icon" aria-hidden="true">
<use href="#i-arrow" /></svg
></span>
</div>
</article>
</div>
<div class="kanban-column splash-loop-column">
<div class="column-heading">
For review<span class="count"
><span class="splash-count-desktop">3</span
><span class="splash-count-mobile">1</span></span
>
</div>
<div class="splash-task-slot" data-splash-slot="2">
<article
class="task-card splash-moving-card"
data-splash-card="2"
>
<div class="task-source">
<span class="avatar avatar-acme">A</span><span>Acme Inc</span
><svg class="icon" aria-hidden="true">
<use href="#i-more" />
</svg>
</div>
<p class="task-title">38 matches to check.</p>
<div class="task-footer">
<span class="splash-task-status" data-splash-card-status
>For review</span
><span class="mini-action"
>View
<svg class="icon" aria-hidden="true">
<use href="#i-arrow" /></svg
></span>
</div>
</article>
</div>
<article class="task-card splash-static-card">
<div class="task-source">
<span class="avatar avatar-agent"
><svg class="icon" aria-hidden="true">
<use href="#i-spark" /></svg></span
><span>Journal agent</span
><svg class="icon" aria-hidden="true">
<use href="#i-more" />
</svg>
</div>
<p class="task-title">Software expense entry prepared.</p>
<div class="task-footer">
<span class="splash-task-status">Ready for review</span
><span class="mini-action"
>View
<svg class="icon" aria-hidden="true">
<use href="#i-arrow" /></svg
></span>
</div>
</article>
<article class="task-card splash-static-card">
<div class="task-source">
<span class="avatar avatar-agent"
><svg class="icon" aria-hidden="true">
<use href="#i-spark" /></svg></span
><span>Daily activity</span
><svg class="icon" aria-hidden="true">
<use href="#i-more" />
</svg>
</div>
<p class="task-title">Your summary is ready.</p>
<div class="task-footer">
<span class="splash-task-status">For review</span
><span class="mini-action"
>View
<svg class="icon" aria-hidden="true">
<use href="#i-arrow" /></svg
></span>
</div>
</article>
</div>
<div class="kanban-column splash-done">
<div class="column-heading">Done <span class="count">2</span></div>
<article class="task-card splash-static-card">
<div class="task-source">
<span class="avatar avatar-agent"
><svg class="icon" aria-hidden="true">
<use href="#i-spark" /></svg></span
><span>Bank Recommendations</span
><svg class="icon" aria-hidden="true">
<use href="#i-more" />
</svg>
</div>
<p class="task-title">Review complete.</p>
<div class="task-footer">
<span class="splash-task-status">Reviewed</span
><span class="mini-action"
>View
<svg class="icon" aria-hidden="true">
<use href="#i-arrow" /></svg
></span>
</div>
</article>
<article class="task-card splash-static-card">
<div class="task-source">
<span class="avatar avatar-bank"></span><span>USAA Bank</span
><svg class="icon" aria-hidden="true">
<use href="#i-more" />
</svg>
</div>
<p class="task-title">Connection restored.</p>
<div class="task-footer">
<span class="splash-task-status">Connected</span
><span class="mini-action"
>View
<svg class="icon" aria-hidden="true">
<use href="#i-arrow" /></svg
></span>
</div>
</article>
</div>
</div>
</div>
</div>
<button
class="splash-motion-toggle"
type="button"
aria-label="Pause task animation"
aria-pressed="false"
data-splash-toggle
>
<svg class="icon" viewBox="0 0 20 20" aria-hidden="true">
<g class="splash-pause-icon">
<path
d="M7 5v10M13 5v10"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
/>
</g>
<path
class="splash-play-icon"
d="M7 4.5 15 10l-8 5.5Z"
fill="currentColor"
/>
</svg>
</button>
</figure>
</section>
src/sections/modules.html
Read raw text
<section
class="modules-section"
id="modules"
aria-labelledby="modules-title"
data-capture-section="modules"
>
<div class="container">
<header class="modules-heading">
<p class="modules-label">Everything it touches</p>
<h2 id="modules-title">One board. Live state.</h2>
</header>
<ul class="module-grid" aria-label="Connected accounting modules">
<li>
<svg class="icon" aria-hidden="true"><use href="#i-check" /></svg
>Approval workflows
</li>
<li>
<svg class="icon" aria-hidden="true"><use href="#i-calendar" /></svg
>Close management
</li>
<li>
<svg class="icon" aria-hidden="true"><use href="#i-sync" /></svg>Bank
match
</li>
<li>
<svg class="icon" aria-hidden="true"><use href="#i-spark" /></svg>Agents
</li>
<li>
<svg class="icon" aria-hidden="true"><use href="#i-list" /></svg>Audit
trail
</li>
<li>
<svg class="icon" aria-hidden="true"><use href="#i-inbox" /></svg
>Contracts
</li>
<li>
<svg class="icon" aria-hidden="true"><use href="#i-clock" /></svg>Aging
</li>
<li>
<svg class="icon" aria-hidden="true"><use href="#i-database" /></svg
>General ledger
</li>
<li>
<svg class="icon" aria-hidden="true"><use href="#i-layout" /></svg
>Accounts payable
</li>
<li>
<svg class="icon" aria-hidden="true"><use href="#i-inbox" /></svg
>Accounts receivable
</li>
<li>
<svg class="icon" aria-hidden="true"><use href="#i-calendar" /></svg
>Revenue recognition
</li>
<li>
<svg class="icon" aria-hidden="true"><use href="#i-database" /></svg
>Multi-entity
</li>
</ul>
<a class="text-link modules-demo" href="https://www.dualentry.com/demo">
Schedule demo
<svg class="icon" aria-hidden="true"><use href="#i-arrow" /></svg>
</a>
</div>
</section>
src/sections/prepared.html
Read raw text
<section
class="prepared-section"
id="prepared"
aria-labelledby="prepared-title"
data-capture-section="prepared"
>
<div class="prepared-copy">
<div class="prepared-copy-inner">
<h2 id="prepared-title">
Every detail.<br />Ready for<br />your review.
</h2>
<p>
A prepared entry. A clear source.<br />The final decision stays with
you.
</p>
<a class="text-link" href="#context"
>Explore the details
<svg class="icon" aria-hidden="true"><use href="#i-arrow" /></svg
></a>
</div>
</div>
<div class="prepared-scene">
<article
class="prepared-entry"
aria-label="Illustrative prepared journal entry for Acme Inc"
>
<header class="prepared-entry-header">
<span
><svg class="icon" aria-hidden="true"><use href="#i-database" /></svg>
Journal entry</span
>
<span class="prepared-status">Ready for review</span>
</header>
<div class="prepared-entry-body">
<div class="prepared-entity">
<span class="avatar avatar-acme">A</span><span>Acme Inc</span
><time datetime="2026-09-08">Sep 8, 2026</time>
</div>
<h3>Software subscription</h3>
<p class="prepared-amount">$1,200<span>.00</span></p>
<dl class="prepared-lines">
<div>
<dt>Software expense<small>Debit</small></dt>
<dd>$1,200.00</dd>
</div>
<div>
<dt>Accounts payable<small>Credit</small></dt>
<dd>$1,200.00</dd>
</div>
</dl>
<div class="prepared-source">
<svg class="icon" aria-hidden="true"><use href="#i-spark" /></svg
><span>Prepared by Journal agent</span><span>Draft</span>
</div>
</div>
</article>
</div>
</section>
src/sections/progression.html
Read raw text
<section
class="section container progress-section"
id="progression"
aria-labelledby="progress-title"
data-capture-section="progression"
data-progress-stage="review"
>
<div class="progress-heading">
<h2 id="progress-title">
See it. Review it.<br />
Keep moving.
</h2>
</div>
<div class="progress-staircase">
<article class="progress-step" data-progress-card="see">
<button
type="button"
class="progress-selector"
data-progress-choice="see"
aria-pressed="false"
aria-controls="progress-see-card"
>
<span><strong>See it.</strong></span
><svg class="icon"><use href="#i-arrow" /></svg>
</button>
<div class="progress-card-field">
<span class="progress-stage-label"
>To do <span class="flow-count">3</span></span
>
<div class="task-card" id="progress-see-card">
<div class="task-source">
<span class="avatar avatar-inbox"
><svg class="icon" aria-hidden="true">
<use href="#i-inbox" /></svg></span
><span>Inbox</span
><svg class="icon more" aria-hidden="true">
<use href="#i-more" />
</svg>
</div>
<p class="task-title">13 approvals ready for your review.</p>
<div class="task-footer">
<span class="timestamp"
><svg class="icon" aria-hidden="true"><use href="#i-clock" /></svg
>2 hrs ago</span
><span class="mini-action">View</span>
</div>
</div>
</div>
</article>
<article class="progress-step" data-progress-card="review">
<button
type="button"
class="progress-selector"
data-progress-choice="review"
aria-pressed="true"
aria-controls="progress-review-card"
>
<span><strong>Review it.</strong></span
><svg class="icon"><use href="#i-arrow" /></svg>
</button>
<div class="progress-card-field">
<span class="progress-stage-label"
>Waiting on you
<span class="flow-count flow-count-amber">3</span></span
>
<div class="task-card" id="progress-review-card">
<div class="task-source">
<span class="avatar avatar-acme">A</span><span>Acme Inc</span
><span class="badge badge-lilac">38</span
><svg class="icon more" aria-hidden="true">
<use href="#i-more" />
</svg>
</div>
<p class="task-title">38 suggested matches across 12 accounts.</p>
<div class="task-footer">
<span class="timestamp"
><svg class="icon" aria-hidden="true"><use href="#i-clock" /></svg
>4 hrs ago</span
><span class="mini-action mini-action-primary">Review</span>
</div>
</div>
</div>
</article>
<article class="progress-step" data-progress-card="move">
<button
type="button"
class="progress-selector"
data-progress-choice="move"
aria-pressed="false"
aria-controls="progress-move-card"
>
<span><strong>Keep moving.</strong></span
><svg class="icon"><use href="#i-arrow" /></svg>
</button>
<div class="progress-card-field">
<span class="progress-stage-label"
>Done <span class="flow-count progress-count-done">4</span></span
>
<div class="task-card" id="progress-move-card">
<div class="task-source">
<span class="avatar avatar-agent"
><svg class="icon" aria-hidden="true">
<use href="#i-spark" /></svg></span
><span>Bank Recommendations</span
><svg class="icon more" aria-hidden="true">
<use href="#i-more" />
</svg>
</div>
<p class="task-title">
Review complete.<br />
Ready for what’s next.
</p>
<div class="task-footer">
<span class="timestamp"
><svg class="icon" aria-hidden="true"><use href="#i-clock" /></svg
>Just now</span
><span class="progress-reviewed"
><svg class="icon" aria-hidden="true"><use href="#i-check" /></svg
>Reviewed</span
>
</div>
</div>
</div>
</article>
</div>
</section>
src/sections/selector.html
Read raw text
<section
class="section container sel-section"
id="selector"
aria-labelledby="selector-title"
data-capture-section="selector"
>
<div class="sel-copy">
<h2 id="selector-title">
Choose<br />
your focus.
</h2>
<p class="lead">
The whole picture when you need it.<br />Just the next task when you
don’t.
</p>
<div class="sel-choices" role="group" aria-label="Choose a task category">
<button
type="button"
data-sel-choice="all"
aria-pressed="false"
aria-controls="selector-stage"
>
<span class="sel-choice-icon"
><svg class="icon" aria-hidden="true">
<use href="#i-layout" /></svg></span
>All tasks<svg class="icon sel-choice-arrow" aria-hidden="true">
<use href="#i-arrow" />
</svg>
</button>
<button
type="button"
data-sel-choice="approvals"
aria-pressed="true"
aria-controls="selector-stage"
>
<span class="sel-choice-icon"
><svg class="icon" aria-hidden="true">
<use href="#i-check" /></svg></span
>Approvals<svg class="icon sel-choice-arrow" aria-hidden="true">
<use href="#i-arrow" />
</svg>
</button>
<button
type="button"
data-sel-choice="connections"
aria-pressed="false"
aria-controls="selector-stage"
>
<span class="sel-choice-icon"
><svg class="icon" aria-hidden="true">
<use href="#i-sync" /></svg></span
>Connections<svg class="icon sel-choice-arrow" aria-hidden="true">
<use href="#i-arrow" />
</svg>
</button>
<button
type="button"
data-sel-choice="agents"
aria-pressed="false"
aria-controls="selector-stage"
>
<span class="sel-choice-icon"
><svg class="icon" aria-hidden="true">
<use href="#i-spark" /></svg></span
>Agent runs<svg class="icon sel-choice-arrow" aria-hidden="true">
<use href="#i-arrow" />
</svg>
</button>
</div>
</div>
<div class="sel-stage" id="selector-stage" data-focus="approvals">
<div class="sel-stage-top">
<span>Tasks</span
><span class="sel-stage-count" data-sel-count>13 approvals</span>
</div>
<div class="sel-card-track">
<article class="sel-main-card" aria-live="polite" aria-atomic="true">
<div class="sel-card-source">
<span class="sel-source-icon"
><svg class="icon" aria-hidden="true">
<use data-sel-icon href="#i-inbox" /></svg></span
><span data-sel-source>Inbox</span
><svg class="icon sel-more" aria-hidden="true">
<use href="#i-more" />
</svg>
</div>
<span class="sel-card-label" data-sel-label>Waiting on you</span>
<h3 data-sel-title>13 approvals ready for your review</h3>
<div class="sel-card-footer">
<span
><svg class="icon" aria-hidden="true"><use href="#i-clock" /></svg
><span data-sel-time>2 hrs ago</span></span
><span class="sel-art-action" data-sel-action
>View approvals
<svg class="icon" aria-hidden="true"><use href="#i-arrow" /></svg
></span>
</div>
</article>
<div class="sel-support-card sel-support-one" aria-hidden="true">
<div class="sel-card-source">
<span class="sel-source-icon sel-agent-icon"
><svg class="icon"><use href="#i-spark" /></svg></span
>Bank Recommendations
</div>
<p>
Run ready<br />
for review
</p>
<div class="sel-card-footer">
<span>2 days ago</span><span class="sel-mini-action">Review</span>
</div>
</div>
<div class="sel-support-card sel-support-two" aria-hidden="true">
<div class="sel-card-source">
<span class="sel-bank-dot"></span>USAA Bank
</div>
<p>
Reconnect to see<br />
recent transactions
</p>
<div class="sel-card-footer">
<span>Just now</span><span class="sel-mini-action">Reconnect</span>
</div>
</div>
</div>
</div>
</section>
src/sections/stats.html
Read raw text
<section
class="section stats-section"
id="stats"
aria-labelledby="stats-title"
data-capture-section="stats"
>
<div class="container">
<div class="stats-heading">
<p class="reference-label">Why ledger-native</p>
<h2 id="stats-title">Agents that reason from the books</h2>
<p class="lead">
Close on autopilot. Continuous close, not a month-end crunch.
</p>
</div>
<div class="stats-grid" aria-label="Published DualEntry customer results">
<article class="stat-card stat-card-primary">
<span class="stat-icon" aria-hidden="true"
><svg class="icon"><use href="#i-spark" /></svg
></span>
<p class="stat-number">90<span>%</span></p>
<h3>Manual tasks automated</h3>
<div class="stat-meter" aria-hidden="true">
<span style="--stat-fill: 90%"></span>
</div>
<a class="stat-source" href="https://www.dualentry.com/customers/slash"
>Slash customer story
<svg class="icon" aria-hidden="true"><use href="#i-arrow" /></svg
></a>
</article>
<article class="stat-card">
<span class="stat-icon" aria-hidden="true"
><svg class="icon"><use href="#i-clock" /></svg
></span>
<p class="stat-number">95<span>%</span></p>
<h3>Less time to close</h3>
<p class="stat-context">
<span>20 days</span
><svg class="icon" aria-hidden="true"><use href="#i-arrow" /></svg
><span>1 day</span>
</p>
<a
class="stat-source"
href="https://www.dualentry.com/customers/graphite"
>Graphite customer story
<svg class="icon" aria-hidden="true"><use href="#i-arrow" /></svg
></a>
</article>
<article class="stat-card">
<span class="stat-icon" aria-hidden="true"
><svg class="icon"><use href="#i-database" /></svg
></span>
<p class="stat-number">$0</p>
<h3>Implementation costs</h3>
<p class="stat-context">Full historical data migrated</p>
<a
class="stat-source"
href="https://www.dualentry.com/customers/graphite"
>Graphite customer story
<svg class="icon" aria-hidden="true"><use href="#i-arrow" /></svg
></a>
</article>
</div>
<p class="stats-attribution">
Published customer results. The 95% reduction is calculated from
Graphite’s 20-day to 1-day close.
</p>
</div>
</section>
src/sections/video.html
Read raw text
<section
class="section container video-section"
id="video"
aria-labelledby="video-title"
data-capture-section="video"
>
<div
class="video-placeholder"
role="img"
aria-label="Video placeholder. No video has been added yet."
>
<span class="video-corner video-corner-top" aria-hidden="true"></span>
<span class="video-placeholder-icon" aria-hidden="true"
><svg class="icon"><use href="#i-play" /></svg
></span>
<h2 id="video-title">Insert video here</h2>
<span class="video-placeholder-meta">Product walkthrough</span>
<span class="video-corner video-corner-bottom" aria-hidden="true"></span>
</div>
</section>
src/sections/workflow.html
Read raw text
<section
class="section container workflow-section"
id="workflow"
aria-labelledby="workflow-title"
data-capture-section="workflow"
>
<header class="workflow-heading">
<p class="workflow-eyebrow">The close today</p>
<h2 id="workflow-title">A scavenger hunt across modules</h2>
<p>
Four modules, four queues, and no single place that says what is left.
</p>
</header>
<div class="workflow-cards">
<article class="workflow-card">
<header class="workflow-card-heading">
<span class="workflow-icon workflow-icon-teal" aria-hidden="true">
<svg class="icon"><use href="#i-filter" /></svg>
</span>
<h3>Scattered</h3>
<p>Every module keeps its own queue.</p>
</header>
<div
class="workflow-preview workflow-modules"
aria-label="Separate module queues"
>
<div class="workflow-module">
<svg class="icon" aria-hidden="true"><use href="#i-sync" /></svg>
<span>Bank match</span
><span class="workflow-count workflow-count-amber">38</span>
</div>
<div class="workflow-module">
<svg class="icon" aria-hidden="true"><use href="#i-inbox" /></svg>
<span>Approvals</span><span class="workflow-count">13</span>
</div>
<div class="workflow-module">
<svg class="icon" aria-hidden="true"><use href="#i-sync" /></svg>
<span>Syncs</span><span class="workflow-count">4</span>
</div>
<div class="workflow-module">
<svg class="icon" aria-hidden="true"><use href="#i-list" /></svg>
<span>AR aging</span
><span class="workflow-count workflow-count-lilac">7</span>
</div>
<div class="workflow-module">
<svg class="icon" aria-hidden="true"><use href="#i-calendar" /></svg>
<span>Close</span><span class="workflow-count">9</span>
</div>
<div class="workflow-module">
<svg class="icon" aria-hidden="true"><use href="#i-database" /></svg>
<span>Contracts</span
><span class="workflow-count workflow-count-lilac">3</span>
</div>
</div>
</article>
<article class="workflow-card">
<header class="workflow-card-heading">
<span class="workflow-icon workflow-icon-lilac" aria-hidden="true">
<svg class="icon"><use href="#i-layout" /></svg>
</span>
<h3>One board</h3>
<p>Work arrives on its own, in one place.</p>
</header>
<div class="workflow-preview" aria-label="Tasks collected in one board">
<div class="workflow-queue-heading">
To Do <span class="workflow-count">3</span>
</div>
<ul class="workflow-queue">
<li>
<span
class="workflow-source-dot workflow-source-acme"
aria-hidden="true"
>A</span
><span>Bank Match: 38 auto matches</span><small>Acme Inc</small>
</li>
<li>
<span class="workflow-source-dot" aria-hidden="true"
><svg class="icon"><use href="#i-inbox" /></svg></span
><span>13 approvals ready for review</span><small>Inbox</small>
</li>
<li>
<span
class="workflow-source-dot workflow-source-agent"
aria-hidden="true"
><svg class="icon"><use href="#i-spark" /></svg></span
><span>AR aging run ready for review</span><small>Agent</small>
</li>
</ul>
</div>
</article>
<article class="workflow-card">
<header class="workflow-card-heading">
<span class="workflow-icon workflow-icon-sage" aria-hidden="true">
<svg class="icon"><use href="#i-check" /></svg>
</span>
<h3>Self-resolving</h3>
<p>Cards leave when the state changes.</p>
</header>
<div
class="workflow-preview"
aria-label="Completed tasks with clear status"
>
<div class="workflow-queue-heading">
Done <span class="workflow-count workflow-count-sage">4</span>
</div>
<ul class="workflow-queue">
<li>
<span
class="workflow-source-dot workflow-source-acme"
aria-hidden="true"
>A</span
><span>Bank Match: 38 auto matches</span
><svg class="icon workflow-complete" aria-label="Complete">
<use href="#i-check" />
</svg>
</li>
<li>
<span
class="workflow-source-dot workflow-source-bank"
aria-hidden="true"
></span
><span>Finish setup for USAA Bank</span
><svg class="icon workflow-complete" aria-label="Complete">
<use href="#i-check" />
</svg>
</li>
<li>
<span class="workflow-source-dot" aria-hidden="true"
><svg class="icon"><use href="#i-inbox" /></svg></span
><span>6 approvals ready for review</span
><svg class="icon workflow-complete" aria-label="Complete">
<use href="#i-check" />
</svg>
</li>
<li>
<span
class="workflow-source-dot workflow-source-agent"
aria-hidden="true"
><svg class="icon"><use href="#i-spark" /></svg></span
><span>Daily activity sent to the CFO</span
><svg class="icon workflow-complete" aria-label="Complete">
<use href="#i-check" />
</svg>
</li>
</ul>
</div>
</article>
</div>
</section>
src/styles/agent.css
Read raw text
.agent-section {
padding-block: 96px;
}
.agent-heading {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 24px;
align-items: end;
margin-bottom: 40px;
}
.agent-heading h2 {
grid-column: span 7;
margin: 0;
max-width: 620px;
font-weight: 400;
}
.agent-stage {
display: grid;
grid-template-columns: minmax(220px, 4fr) 48px minmax(0, 7fr);
align-items: center;
padding: 64px 40px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--de-brand-100);
}
.agent-queue,
.agent-form {
background: var(--white);
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
}
.agent-queue-header,
.agent-form-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
min-height: 64px;
padding: 18px 24px;
font-size: 15px;
font-weight: 400;
border-bottom: 1px solid var(--line);
}
.agent-form-header {
padding-block: 9px;
}
.agent-form-actions {
display: flex;
align-items: center;
gap: 4px;
min-width: 0;
}
.agent-count {
display: grid;
place-items: center;
width: 23px;
height: 23px;
border-radius: 5px;
background: var(--surface);
color: var(--muted);
font-size: 12px;
}
.agent-task {
margin: 16px;
padding: 20px;
border: 1px solid var(--line);
border-radius: 6px;
transition:
border-color 200ms ease,
transform 200ms ease;
}
.agent-task-source {
display: flex;
align-items: center;
gap: 8px;
color: var(--muted);
font-size: 12px;
}
.agent-entity {
display: grid;
place-items: center;
width: 23px;
height: 23px;
border-radius: 50%;
color: white;
background: var(--ink);
font-size: 12px;
}
.agent-task h3 {
margin: 20px 0 10px;
font-size: 21px;
line-height: 1.3;
font-weight: 400;
letter-spacing: -0.025em;
}
.agent-task p {
margin: 0;
font-size: 12px;
color: var(--muted);
line-height: 1.5;
}
.agent-assignment {
display: flex;
align-items: center;
gap: 8px;
margin-top: 28px;
font-size: 12px;
color: var(--muted);
}
.agent-assignment-icon {
display: grid;
place-items: center;
width: 26px;
height: 26px;
background: var(--surface);
border-radius: 6px;
color: var(--teal);
}
.agent-assignment-icon svg {
width: 17px;
height: 17px;
}
.agent-queue-note {
display: flex;
align-items: center;
gap: 8px;
padding: 0 24px 20px;
color: var(--muted);
font-size: 11px;
}
.agent-note-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: #94a09d;
}
.agent-transfer {
display: flex;
align-items: center;
color: #8ba2a0;
}
.agent-transfer span {
flex: 1;
height: 1px;
background: currentColor;
}
.agent-transfer svg {
width: 16px;
height: 16px;
margin-left: -3px;
}
.agent-form-status {
font-size: 11px;
line-height: 1.4;
padding: 5px 8px;
border-radius: 4px;
background: var(--surface);
color: var(--muted);
white-space: nowrap;
}
.agent-form-body {
padding: 24px;
}
.agent-fields {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.agent-field-label {
display: block;
color: var(--muted);
font-size: 11px;
margin-bottom: 8px;
}
.agent-field-value {
position: relative;
display: flex;
align-items: center;
min-height: 40px;
font-size: 13px;
line-height: 1.3;
}
.agent-fields .agent-field-value,
.agent-memo .agent-field-value {
border: 1px solid var(--line);
border-radius: 5px;
padding: 10px 12px;
}
.agent-field-value::after {
content: "";
display: block;
position: absolute;
left: 12px;
top: calc(50% - 3px);
width: 58%;
height: 6px;
border-radius: 2px;
background: var(--surface);
transition: opacity 200ms ease;
}
.agent-field-value > span {
visibility: hidden;
opacity: 0;
transform: translateY(3px);
transition:
opacity 200ms ease,
transform 200ms ease;
}
.agent-field-value[data-filled="true"] > span {
visibility: visible;
opacity: 1;
transform: none;
}
.agent-field-value[data-filled="true"]::after {
opacity: 0;
}
.agent-entry-table {
margin: 24px 0;
}
.agent-entry-row {
display: grid;
grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr) minmax(0, 1fr);
column-gap: 10px;
border-bottom: 1px solid var(--line);
}
.agent-entry-row > :not(:first-child) {
justify-content: flex-end;
text-align: right;
font-variant-numeric: tabular-nums;
}
.agent-entry-row .agent-field-value {
min-height: 54px;
font-size: 12px;
}
.agent-entry-row .agent-field-value::after {
left: 0;
width: 65%;
}
.agent-entry-row > :not(:first-child)::after {
left: auto;
right: 0;
}
.agent-entry-head {
padding-bottom: 10px;
font-size: 11px;
color: var(--muted);
}
[data-agent-phase="assigned"] .agent-task,
[data-agent-phase="filling"] .agent-task {
border-color: var(--de-brand-200);
}
[data-agent-phase="assigned"] .agent-task {
transform: translateY(-3px);
}
html[data-capture="true"] .agent-task {
transform: none !important;
}
@media (prefers-reduced-motion: reduce) {
.agent-task {
transform: none !important;
}
}
[data-agent-phase="review"] .agent-form-status {
background: var(--de-brand-100);
color: var(--teal);
}
.agent-replay {
display: grid;
place-items: center;
flex: none;
width: 44px;
height: 44px;
padding: 0;
color: var(--teal);
background: transparent;
border: 1px solid transparent;
border-radius: 5px;
font: inherit;
cursor: pointer;
transition: background-color 160ms ease;
}
.agent-replay svg {
width: 16px;
height: 16px;
}
.agent-replay:disabled {
opacity: 0.4;
cursor: default;
}
.agent-replay:focus-visible {
outline: 2px solid var(--teal);
outline-offset: 3px;
}
@media (hover: hover) {
.agent-replay:not(:disabled):hover {
background: var(--surface);
}
}
@media (max-width: 1000px) {
.agent-stage {
padding: 40px 24px;
grid-template-columns: minmax(190px, 3fr) 24px minmax(0, 5fr);
}
.agent-task {
padding: 16px;
margin: 12px;
}
.agent-heading {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.agent-heading h2 {
grid-column: span 4;
}
.agent-form-body {
padding: 20px;
}
}
@media (max-width: 700px) {
.agent-section {
padding-block: 64px;
}
.agent-heading {
display: block;
margin-bottom: 28px;
}
.agent-stage {
grid-template-columns: minmax(0, 1fr);
padding: 24px 16px;
}
.agent-queue-header {
min-height: 48px;
padding: 12px 20px;
}
.agent-task {
margin: 12px;
padding: 16px;
}
.agent-task h3 {
margin: 14px 0 8px;
font-size: 20px;
}
.agent-assignment {
margin-top: 18px;
}
.agent-queue-note {
padding: 0 20px 16px;
}
.agent-transfer {
justify-self: center;
width: 1px;
height: 32px;
flex-direction: column;
}
.agent-transfer span {
width: 1px;
height: auto;
}
.agent-transfer svg {
flex: none;
margin: -3px 0 0;
transform: rotate(90deg);
}
.agent-form-header {
min-height: 58px;
padding: 7px 8px 7px 16px;
gap: 4px;
font-size: 13px;
}
.agent-form-status {
font-size: 10px;
padding: 5px 6px;
}
.agent-form-body {
padding: 20px 16px;
}
.agent-entry-row {
column-gap: 8px;
grid-template-columns: minmax(0, 1.25fr) minmax(0, 1fr) minmax(0, 1fr);
}
.agent-entry-row .agent-field-value {
font-size: 11px;
min-height: 56px;
}
.agent-field-value {
font-size: 12px;
}
.agent-fields {
gap: 10px;
}
.agent-fields .agent-field-value {
padding: 10px 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.agent-section *,
.agent-section *::after {
transition: none !important;
}
}
html[data-capture="true"] .agent-section *,
html[data-capture="true"] .agent-section *::after {
transition: none !important;
}
html[data-capture="true"] .agent-replay {
visibility: hidden;
}
src/styles/atmosphere.css
Read raw text
/* Cosmos source photographs remain intact; crops and blur use CSS. */
.agent-stage,
.ribbon-closing {
position: relative;
isolation: isolate;
overflow: hidden;
background: var(--de-brand-100);
}
.agent-stage::before,
.ribbon-closing::before {
content: "";
position: absolute;
z-index: -1;
inset: -32px;
background: url("assets/cosmos-folded-glass.webp") 50% 60% / cover;
filter: grayscale(1) blur(18px);
mix-blend-mode: luminosity;
opacity: 0.38;
pointer-events: none;
}
.ribbon-closing {
background: #edf2f2;
}
.ribbon-closing::before {
background-image: url("assets/cosmos-ribbed-glass.webp");
opacity: 0.2;
filter: grayscale(1) blur(24px);
}
/* Full-bleed editorial interlude: five columns of copy, seven of product evidence. */
.prepared-section {
display: grid;
grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
min-height: min(960px, 100svh);
scroll-margin-top: 0;
background: var(--de-brand-100);
}
.prepared-copy {
display: flex;
align-items: center;
padding: 88px 48px 88px max(var(--gutter), calc((100vw - 1320px) / 2));
}
.prepared-copy-inner {
max-width: 440px;
}
.prepared-copy h2 {
color: var(--teal);
font-size: clamp(44px, 4.45vw, 72px);
line-height: 1.05;
letter-spacing: -0.048em;
text-wrap: initial;
}
.prepared-copy p {
margin-top: 32px;
color: var(--ink);
font-size: 18px;
line-height: 1.6;
text-wrap: pretty;
}
.prepared-copy .text-link {
margin-top: 36px;
}
.prepared-scene {
position: relative;
isolation: isolate;
overflow: hidden;
display: grid;
place-items: center;
background: var(--teal);
padding: 80px clamp(32px, 5vw, 96px);
}
.prepared-scene::before {
content: "";
position: absolute;
inset: -28px;
z-index: -2;
background: url("assets/cosmos-ribbed-glass.webp") 48% 46% / cover;
filter: grayscale(1) blur(10px);
mix-blend-mode: luminosity;
opacity: 0.6;
pointer-events: none;
}
.prepared-scene::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background: linear-gradient(
155deg,
#004f5c0d 10%,
#004f5c40 60%,
#004f5cd9 100%
);
pointer-events: none;
}
.prepared-entry {
width: 100%;
max-width: 488px;
min-width: 0;
background: var(--white);
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
}
.prepared-entry-header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
padding: 22px 28px;
border-bottom: 1px solid var(--line);
font-size: 14px;
}
.prepared-entry-header > span:first-child {
display: inline-flex;
align-items: center;
gap: 10px;
}
.prepared-entry-header .icon {
width: 16px;
height: 16px;
color: var(--teal);
}
.prepared-status {
background: var(--de-brand-100);
color: var(--teal);
border-radius: 4px;
padding: 6px 8px;
font-size: 11px;
white-space: nowrap;
}
.prepared-entry-body {
padding: 28px;
}
.prepared-entity {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--muted);
}
.prepared-entity .avatar {
width: 22px;
height: 22px;
font-size: 12px;
}
.prepared-entity time {
margin-left: auto;
font-size: 11px;
}
.prepared-entry h3 {
font-size: 22px;
margin-top: 34px;
}
.prepared-amount {
margin-top: 14px;
font-size: clamp(42px, 4vw, 58px);
line-height: 1.1;
letter-spacing: -0.055em;
font-variant-numeric: tabular-nums;
}
.prepared-amount > span {
color: #758285;
}
.prepared-lines {
margin: 32px 0 0;
border-top: 1px solid var(--line);
}
.prepared-lines > div {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 16px;
padding: 18px 0;
border-bottom: 1px solid var(--line);
}
.prepared-lines dt,
.prepared-lines dd {
font-size: 13px;
margin: 0;
}
.prepared-lines small {
display: block;
margin-top: 5px;
color: var(--muted);
font-size: 11px;
}
.prepared-lines dd {
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.prepared-source {
display: flex;
gap: 8px;
align-items: center;
margin-top: 24px;
font-size: 11px;
color: var(--muted);
}
.prepared-source .icon {
width: 14px;
height: 14px;
color: var(--teal);
}
.prepared-source > span:last-child {
margin-left: auto;
}
html[data-capture="true"] .prepared-section {
min-height: 900px;
}
@media (max-width: 1100px) {
.prepared-copy {
padding-right: 32px;
}
.prepared-scene {
padding-inline: 32px;
}
.prepared-entry-header,
.prepared-entry-body {
padding: 22px;
}
}
@media (max-width: 800px) {
.prepared-section {
grid-template-columns: 1fr;
min-height: auto;
}
.prepared-copy {
padding: 64px var(--gutter);
}
.prepared-copy-inner {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 32px;
max-width: none;
width: 100%;
align-items: start;
}
.prepared-copy h2 {
font-size: 48px;
grid-row: span 2;
}
.prepared-copy p {
margin-top: 4px;
font-size: 17px;
}
.prepared-copy .text-link {
margin-top: 20px;
align-self: end;
}
.prepared-scene {
padding: 64px var(--gutter);
}
html[data-capture="true"] .prepared-section {
min-height: auto;
}
}
@media (max-width: 540px) {
.prepared-copy {
padding-block: 48px;
}
.prepared-copy-inner {
display: block;
}
.prepared-copy h2 {
font-size: 44px;
}
.prepared-copy p {
margin-top: 24px;
font-size: 16px;
}
.prepared-copy .text-link {
margin-top: 22px;
}
.prepared-scene {
padding-block: 40px;
}
.prepared-entry-header,
.prepared-entry-body {
padding: 20px;
}
.prepared-entry h3 {
font-size: 20px;
}
.prepared-amount {
font-size: 48px;
}
.prepared-lines > div {
gap: 8px;
}
.prepared-source {
flex-wrap: wrap;
}
}
src/styles/microfeedback.css
Read raw text
/* Feedback belongs to controls, while product illustrations keep their geometry. */
.button,
.control-update-button,
[data-agent-replay],
.menu-toggle {
transition:
transform 180ms cubic-bezier(0.22, 0.68, 0, 1),
background-color 180ms ease,
border-color 180ms ease;
}
.button:active,
.control-update-button:active,
[data-agent-replay]:not(:disabled):active,
.menu-toggle:active {
transform: scale(0.98);
}
.text-link .icon {
transition: transform 180ms cubic-bezier(0.22, 0.68, 0, 1);
}
.editorial-filter-menu summary > .icon:last-child {
transition: transform 180ms cubic-bezier(0.22, 0.68, 0, 1);
}
@media (hover: hover) and (pointer: fine) {
.text-link:hover .icon {
transform: translateX(4px);
}
.context-notes button .note-arrow {
transform: translateX(-3px);
transition:
transform 180ms ease,
opacity 180ms ease;
}
.context-notes button:hover .note-arrow,
.context-notes button[aria-pressed="true"] .note-arrow {
transform: translateX(0);
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.button,
.control-update-button,
[data-agent-replay],
.menu-toggle,
.text-link .icon,
.note-arrow {
transform: none !important;
}
}
html[data-capture="true"]
:is(
.button,
.control-update-button,
[data-agent-replay],
.menu-toggle,
.text-link .icon,
.note-arrow
) {
transform: none !important;
}
/* Comfortable touch targets for the actual landing-page controls. */
@media (max-width: 800px) {
.logo,
.site-footer nav a,
.text-link {
display: inline-flex;
align-items: center;
min-height: 44px;
}
.header-cta,
.menu-toggle,
.pano-focus-control button,
.pano-move-button,
.editorial-filter-menu summary,
.editorial-filter-menu .filter-toolbar button,
.match-options button {
min-height: 44px;
}
}
src/styles/page-ending.css
Read raw text
/* The page closes with a compact module index and one clear next step. */
.modules-section {
padding-block: 104px 72px;
text-align: center;
background: var(--white);
}
.modules-heading {
margin-bottom: 36px;
}
.modules-label,
.close-month-label {
margin-bottom: 14px;
font-size: 10px;
line-height: 1.5;
letter-spacing: 0.16em;
text-transform: uppercase;
}
.modules-label {
color: var(--muted);
}
.modules-heading h2 {
font-size: clamp(32px, 3.2vw, 46px);
}
.module-grid {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 12px;
padding: 0;
margin: 0;
list-style: none;
}
.module-grid li {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
min-height: 44px;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 24px;
background: var(--surface);
color: #526569;
font-size: 12px;
line-height: 1.35;
}
.module-grid .icon {
width: 16px;
height: 16px;
color: var(--_v3---brand--700);
}
.modules-demo {
margin-top: 26px;
font-size: 13px;
}
.close-month-section {
padding-block: 80px;
background: linear-gradient(
175deg,
#073640 0%,
var(--teal) 58%,
var(--_v3---brand--700) 100%
);
color: var(--white);
}
.close-month-panel {
padding: 60px 32px;
border: 1px solid #ffffff24;
border-radius: 10px;
background: #a6d0d326;
text-align: center;
}
.close-month-label {
color: #d8ebedde;
}
.close-month-panel h2 {
font-size: clamp(34px, 3.5vw, 50px);
line-height: 1.12;
}
.close-month-description {
margin-top: 18px;
color: #e3eff0;
font-size: 16px;
line-height: 1.5;
text-wrap: pretty;
}
.close-month-actions {
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 12px 24px;
margin-top: 28px;
}
.close-month-button {
border-color: var(--white);
background: var(--white);
color: var(--teal);
}
.close-month-link {
color: var(--white);
}
@media (hover: hover) and (pointer: fine) {
.close-month-button:hover {
border-color: var(--de-brand-100);
background: var(--de-brand-100);
}
}
@media (max-width: 1100px) {
.module-grid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (max-width: 800px) {
.modules-section {
padding-block: 72px 48px;
}
.module-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.close-month-section {
padding-block: 48px;
}
.close-month-panel {
padding: 48px 24px;
}
}
@media (max-width: 540px) {
.modules-heading {
margin-bottom: 28px;
}
.module-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 8px;
}
.module-grid li {
justify-content: flex-start;
gap: 6px;
padding-inline: 10px;
font-size: 11px;
}
.close-month-panel {
padding: 40px 20px;
}
.close-month-panel h2 {
font-size: clamp(30px, 8vw, 38px);
}
.close-month-description {
font-size: 15px;
}
.close-month-actions {
flex-direction: column;
gap: 8px;
}
}
src/styles/product.css
Read raw text
/* Flat product illustrations. Real text and SVG remain crisp at capture resolution. */
.product-window {
background: #fff;
border: 1px solid var(--line);
border-radius: 8px;
overflow: hidden;
font-size: 12px;
}
.product-tabs {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 14px 8px;
color: #667276;
font-size: 12px;
}
.product-tabs > span {
padding: 7px 8px;
white-space: nowrap;
}
.product-tabs .selected {
background: #edf1f2;
border-radius: 5px;
color: var(--ink);
}
.product-tabs .plus {
font-size: 18px;
padding: 3px;
}
.product-toolbar {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px 14px;
border-bottom: 1px solid var(--line);
}
.search-art {
min-width: 0;
display: flex;
align-items: center;
gap: 7px;
flex: 1;
border: 1px solid #dce4e6;
border-radius: 3px;
height: 30px;
padding: 0 8px;
color: #859095;
}
.search-art .icon {
width: 12px;
height: 12px;
}
.search-art span {
white-space: nowrap;
overflow: hidden;
}
.tool-art {
display: grid;
place-items: center;
width: 29px;
height: 30px;
border: 1px solid #dce4e6;
color: #718086;
border-radius: 3px;
}
.tool-art .icon {
width: 14px;
height: 14px;
}
.new-task {
display: flex;
align-items: center;
justify-content: center;
white-space: nowrap;
height: 30px;
padding-inline: 10px;
border-radius: 3px;
background: var(--teal);
color: white;
font-size: 11px;
}
.kanban {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
background: #fafcfc;
}
.kanban-column {
padding: 0 10px 14px;
border-right: 1px solid var(--line);
min-width: 0;
}
.kanban-column:last-child {
border-right: 0;
}
.column-heading {
display: flex;
align-items: center;
gap: 7px;
font-size: 12px;
white-space: nowrap;
padding: 14px 0;
}
.count,
.badge {
display: inline-flex;
align-items: center;
justify-content: center;
font-variant-numeric: tabular-nums;
font-size: 11px;
background: #e5ecee;
color: #386470;
padding: 3px 5px;
min-width: 19px;
border-radius: 4px;
}
.count-amber {
color: #946b3c;
background: #f9f0e5;
}
.badge-lilac {
background: #f0ecfa;
color: #655293;
}
.task-card {
background: white;
border: 1px solid #dce4e6;
border-radius: 7px;
padding: 12px;
margin-bottom: 10px;
min-width: 0;
}
.task-source {
display: flex;
align-items: center;
gap: 7px;
font-size: 10px;
color: #667276;
min-height: 20px;
}
.task-source > span:not(.avatar):not(.badge) {
min-width: 0;
}
.avatar {
display: inline-flex;
align-items: center;
justify-content: center;
width: 17px;
height: 17px;
border-radius: 50%;
flex-shrink: 0;
font-size: 10px;
}
.avatar-acme {
color: white;
background: #222626;
}
.avatar-bank {
background: #24428e;
}
.avatar-inbox {
background: #e7eff1;
color: #426f7a;
}
.avatar-agent {
background: #f0ecfc;
color: #675298;
}
.avatar .icon {
width: 10px;
height: 10px;
}
.more {
margin-left: auto;
width: 12px;
height: 14px;
color: #7c898d;
}
.task-title {
margin: 16px 0 20px;
font-size: 14px;
line-height: 1.4;
letter-spacing: -0.015em;
}
.task-footer {
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.timestamp {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
color: #839095;
white-space: nowrap;
}
.timestamp .icon {
width: 12px;
height: 12px;
}
.task-footer .timestamp {
margin-right: auto;
}
.mini-action {
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 10px;
white-space: nowrap;
padding: 5px 7px;
background: #edf0f1;
color: #566266;
border-radius: 4px;
}
.mini-action-primary {
background: var(--teal);
color: white;
}
.quiet-state {
display: flex;
align-items: center;
gap: 8px;
color: #8a9699;
font-size: 10px;
padding: 20px 6px;
line-height: 1.4;
}
.quiet-state .icon {
width: 14px;
}
.filter-panel {
padding: 20px;
}
.filter-toolbar {
display: flex;
gap: 6px;
padding: 0 0 20px;
align-items: center;
}
.filter-toolbar button {
display: flex;
align-items: center;
gap: 7px;
border: 1px solid transparent;
border-radius: 5px;
padding: 8px 12px;
min-height: 38px;
background: transparent;
color: #6e7b80;
font-size: 12px;
white-space: nowrap;
transition:
color 160ms,
border-color 160ms,
background 160ms;
}
.filter-toolbar button[aria-pressed="true"] {
background: white;
border-color: #cbd9dd;
color: var(--ink);
}
.focus-row {
display: grid;
grid-template-columns: 128px minmax(0, 1fr) 62px 62px;
gap: 10px;
align-items: center;
padding: 18px 12px;
border: 1px solid var(--line);
border-radius: 6px;
background: white;
margin-bottom: 8px;
transition: opacity 180ms;
}
.focus-source {
display: flex;
align-items: center;
gap: 7px;
color: #637075;
font-size: 10px;
}
.focus-title {
font-size: 12px;
line-height: 1.45;
}
.focus-row .timestamp {
font-size: 9px;
}
.focus-row .timestamp .icon {
display: none;
}
.focus-row.is-muted {
opacity: 0.34;
}
.panel-bottom {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
color: #7b888c;
font-size: 11px;
padding-top: 16px;
}
.panel-bottom .icon {
width: 12px;
height: 12px;
}
.queue-list {
padding: 8px;
border: 1px solid var(--line);
background: white;
border-radius: 8px;
width: 100%;
}
.queue-list > div {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 10px;
font-size: 14px;
border: 1px solid var(--line);
border-radius: 5px;
margin-bottom: 7px;
}
.queue-list > div:last-child {
margin: 0;
}
.queue-list .icon {
width: 16px;
height: 16px;
color: #748388;
}
.queue-list .count {
margin-left: auto;
}
.step-task {
width: 100%;
max-width: 290px;
}
.step-task .task-card {
padding: 18px;
}
.step-task .task-source {
font-size: 12px;
}
.step-task .task-title {
font-size: 18px;
}
.step-task .timestamp {
font-size: 11px;
}
.step-task .mini-action {
font-size: 11px;
}
.context-card {
background: white;
padding: 28px;
border: 1px solid #dce4e6;
border-radius: 9px;
position: relative;
}
.context-card .task-source {
font-size: 15px;
gap: 10px;
}
.context-card .avatar {
width: 24px;
height: 24px;
font-size: 14px;
}
.context-card .badge {
font-size: 14px;
}
.context-card .task-title {
font-size: 25px;
margin: 32px 0 36px;
line-height: 1.4;
letter-spacing: -0.025em;
}
.context-card .timestamp {
font-size: 13px;
}
.context-card .mini-action {
font-size: 14px;
padding: 8px 14px;
}
.context-card [data-part] {
position: relative;
}
.context-card [data-part]::before {
content: "";
position: absolute;
inset: -8px;
border-radius: 5px;
border: 1px solid #7cabb333;
opacity: 0;
transition: opacity 160ms;
pointer-events: none;
}
#context-demo[data-context="source"] [data-part="source"]::before,
#context-demo[data-context="work"] [data-part="work"]::before,
#context-demo[data-context="action"] [data-part="action"]::before {
opacity: 1;
}
@media (max-width: 1100px) {
.focus-row {
grid-template-columns: 100px minmax(0, 1fr) 55px;
padding-inline: 10px;
}
.focus-row > .timestamp {
display: none;
}
}
@media (max-width: 800px) {
.focus-row {
grid-template-columns: 120px minmax(0, 1fr) 65px;
}
.focus-title {
font-size: 13px;
}
.focus-source {
font-size: 11px;
}
.step-task .task-card {
padding: 12px;
}
.step-task .task-title {
font-size: 14px;
}
.step-task .task-source {
font-size: 10px;
}
.step-task .task-footer {
gap: 8px;
}
.step-task .timestamp {
font-size: 9px;
}
.queue-list > div {
font-size: 11px;
gap: 6px;
padding: 12px 6px;
}
.queue-list .icon {
width: 13px;
}
.context-card {
max-width: 460px;
width: 100%;
margin-inline: auto;
}
.context-card .task-title {
font-size: 25px;
}
}
@media (max-width: 540px) {
.filter-panel {
padding: 12px;
}
.filter-toolbar {
gap: 4px;
padding-bottom: 12px;
}
.filter-toolbar button {
padding: 7px 8px;
font-size: 10px;
}
.filter-toolbar .count {
min-width: 14px;
font-size: 9px;
padding: 2px 4px;
}
.focus-row {
grid-template-columns: 1fr 58px;
gap: 7px 10px;
padding: 13px 10px;
}
.focus-source {
grid-column: 1;
}
.focus-title {
grid-column: 1;
font-size: 12px;
}
.focus-row .mini-action {
grid-column: 2;
grid-row: 1 / 3;
justify-self: end;
}
.focus-row.is-muted {
opacity: 0.34;
}
.step-task .task-card {
padding: 20px;
}
.step-task .task-title {
font-size: 19px;
}
.step-task .task-source {
font-size: 12px;
}
.step-task .timestamp {
font-size: 11px;
}
.queue-list {
max-width: 290px;
}
.queue-list > div {
font-size: 15px;
padding: 13px 10px;
gap: 12px;
}
.queue-list .icon {
width: 16px;
}
.context-card {
padding: 22px;
}
.context-card .task-source {
font-size: 13px;
gap: 8px;
}
.context-card .task-title {
font-size: 21px;
margin-block: 28px;
}
.context-card .timestamp {
font-size: 11px;
}
.context-card .mini-action {
font-size: 12px;
}
}
src/styles/reference-layout.css
Read raw text
/* Reference page rhythm. Product sections keep their own scoped responsive rules. */
.container {
max-width: 1160px;
}
.splash {
padding-top: 64px;
padding-bottom: 64px;
}
.splash-screen {
max-width: 1080px;
margin-top: 38px;
}
.splash-actions {
margin-top: 24px;
}
.splash-actions .text-link {
color: #edf5f7;
}
.splash-actions .text-link:hover {
color: #fff;
}
.section {
padding-block: 88px;
}
.reference-label {
color: var(--muted);
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
margin-bottom: 14px;
}
.sel-copy > .lead {
font-size: 16px;
line-height: 1.55;
margin-top: 22px;
margin-bottom: 28px;
}
.prepared-copy {
padding-left: max(var(--gutter), calc((100vw - 1160px) / 2));
}
.video-section {
padding-top: 0;
padding-bottom: 48px;
}
.video-placeholder {
width: 100%;
min-width: 0;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
aspect-ratio: 16/7;
min-height: 240px;
border: 1px solid var(--line);
border-radius: 10px;
background: linear-gradient(135deg, #edf2f1, var(--de-brand-100));
color: var(--teal);
}
.video-placeholder-icon {
display: grid;
place-items: center;
width: 64px;
height: 64px;
border: 1px solid #004f5c26;
border-radius: 50%;
background: #ffffffad;
margin-bottom: 20px;
}
.video-placeholder-icon .icon {
width: 23px;
height: 23px;
margin-left: 3px;
}
.video-placeholder h2 {
font-size: 24px;
letter-spacing: -0.025em;
}
.video-placeholder-meta {
margin-top: 10px;
font-size: 13px;
color: var(--muted);
}
.video-corner {
position: absolute;
width: 24px;
height: 24px;
border-color: #004f5c30;
border-style: solid;
}
.video-corner-top {
left: 24px;
top: 24px;
border-width: 1px 0 0 1px;
}
.video-corner-bottom {
right: 24px;
bottom: 24px;
border-width: 0 1px 1px 0;
}
.stats-section {
background: linear-gradient(180deg, #f4f7f6, #fff);
border-block: 1px solid var(--line);
}
.stats-heading {
margin-bottom: 38px;
}
.stats-heading h2 {
max-width: 900px;
}
.stats-heading .lead {
margin-top: 16px;
font-size: 17px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 24px;
}
.stat-card {
min-width: 0;
padding: 32px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--white);
display: flex;
flex-direction: column;
align-items: flex-start;
}
.stat-card-primary {
background: var(--teal);
color: white;
border-color: var(--teal);
}
.stat-icon {
display: grid;
place-items: center;
width: 36px;
height: 36px;
background: var(--de-brand-100);
color: var(--teal);
border-radius: 7px;
}
.stat-card-primary .stat-icon {
color: white;
background: #ffffff1a;
}
.stat-number {
margin-top: 34px;
margin-bottom: 16px;
font-size: clamp(64px, 6.5vw, 92px);
font-weight: 400;
letter-spacing: -0.055em;
line-height: 1;
font-variant-numeric: tabular-nums;
}
.stat-number > span {
font-size: 0.6em;
margin-left: 3px;
}
.stat-card h3 {
font-size: 19px;
line-height: 1.35;
letter-spacing: -0.02em;
}
.stat-context {
display: flex;
align-items: center;
gap: 14px;
color: var(--muted);
font-size: 13px;
min-height: 34px;
margin-top: 16px;
}
.stat-context .icon {
width: 15px;
height: 15px;
color: var(--teal);
}
.stat-meter {
width: 100%;
height: 4px;
margin-top: 32px;
margin-bottom: 14px;
background: #ffffff25;
border-radius: 2px;
}
.stat-meter span {
display: block;
width: var(--stat-fill);
height: 100%;
background: #bfd3d6;
border-radius: inherit;
}
.stat-source {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
width: 100%;
margin-top: 26px;
padding-top: 22px;
border-top: 1px solid var(--line);
font-size: 12px;
color: var(--teal);
min-height: 44px;
}
.stat-source .icon {
width: 16px;
height: 16px;
transition: transform 180ms ease;
}
.stat-card-primary .stat-source {
color: white;
border-color: #ffffff26;
}
.stat-source:hover .icon {
transform: translateX(3px);
}
.stats-attribution {
margin-top: 18px;
font-size: 11px;
color: var(--muted);
line-height: 1.5;
}
.reference-footer {
margin: 0;
max-width: none;
width: 100%;
background: #171b1c;
color: #e8edec;
padding: 64px 0 36px;
}
.footer-grid {
display: grid;
grid-template-columns: 1.4fr repeat(4, 1fr);
gap: 40px;
align-items: start;
}
.reference-footer .logo img {
filter: brightness(0) invert(1);
}
.reference-footer nav {
margin: 0;
justify-self: start;
text-align: left;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.reference-footer nav h2 {
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 6px;
color: #a7b8ba;
}
.reference-footer nav a {
font-size: 12px;
line-height: 1.5;
color: #c5d0d1;
}
.reference-footer nav a:hover {
color: white;
}
.footer-copyright {
grid-column: 1/-1;
margin-top: 24px;
font-size: 11px;
color: #99a9aa;
}
@media (max-width: 1000px) {
.stat-card {
padding: 24px;
}
.stat-card h3 {
font-size: 17px;
}
.footer-grid {
gap: 24px;
}
}
@media (max-width: 760px) {
.section {
padding-block: 64px;
}
.splash {
padding-top: 36px;
padding-bottom: 36px;
}
.splash-screen {
margin-top: 28px;
}
.splash-actions {
gap: 12px;
margin-top: 24px;
}
.video-section {
padding-top: 0;
padding-bottom: 24px;
}
.video-placeholder {
aspect-ratio: 16/10;
}
.stats-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.stat-card {
padding: 28px;
}
.stat-number {
font-size: 76px;
margin-top: 24px;
}
.stat-source {
margin-top: 22px;
}
.footer-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 32px 24px;
}
.reference-footer .logo {
grid-column: 1/-1;
}
.reference-footer nav {
gap: 0;
}
.reference-footer nav a {
min-height: 44px;
display: flex;
align-items: center;
}
.reference-footer {
padding-top: 48px;
}
.prepared-copy {
padding-left: var(--gutter);
}
}
@media (prefers-reduced-motion: reduce) {
.stat-source .icon {
transition: none;
}
}
html[data-capture="true"] .stat-source .icon {
transition: none;
}
src/styles/scenes-flow.css
Read raw text
/* Source, task, and board occupy fixed grid tracks with edge-to-edge connectors. */
.flow-heading {
max-width: 850px;
}
.flow-bridge {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 24px;
align-items: center;
}
.flow-sources {
grid-column: span 4;
padding: 24px;
border: 1px solid var(--line);
border-radius: 8px;
background: white;
position: relative;
min-width: 0;
}
.flow-panel-kicker {
display: block;
font-size: 13px;
color: var(--muted);
margin-bottom: 24px;
}
.flow-source-list {
display: grid;
gap: 8px;
}
.flow-source-list > div {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 0;
border-bottom: 1px solid var(--line);
font-size: 12px;
}
.flow-source-list > div:last-child {
border: 0;
}
.flow-source-list > div > .icon {
width: 16px;
height: 16px;
color: var(--muted);
flex-shrink: 0;
}
.flow-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 23px;
padding: 3px 6px;
font-size: 12px;
font-weight: 400;
border-radius: 4px;
background: var(--de-brand-100);
color: var(--teal);
font-variant-numeric: tabular-nums;
}
.flow-source-list .flow-count {
margin-left: auto;
}
.flow-count-amber {
background: #f7eee1;
color: #966b36;
}
.flow-count-indigo {
background: var(--de-indigo-100);
color: #60548d;
}
.flow-count-neutral {
background: var(--surface);
color: var(--muted);
}
.flow-inbox-card {
grid-column: span 4;
padding: 24px;
margin: 0;
min-width: 0;
position: relative;
border-color: var(--line);
}
.flow-sources::after,
.flow-inbox-card::after {
content: "";
position: absolute;
left: 100%;
top: 50%;
width: 25px;
height: 1px;
background: #b9c9cc;
}
.flow-inbox-card .task-source {
font-size: 12px;
}
.flow-inbox-card .avatar {
width: 23px;
height: 23px;
}
.flow-inbox-card .task-title {
font-size: 23px;
font-weight: 400;
line-height: 1.35;
margin: 28px 0 32px;
}
.flow-inbox-card .timestamp {
font-size: 10px;
}
.flow-inbox-card .mini-action {
padding: 8px 10px;
gap: 8px;
font-size: 11px;
}
.flow-inbox-card .mini-action .icon {
width: 13px;
height: 13px;
}
.flow-board-field {
grid-column: span 4;
min-width: 0;
padding: 16px;
border-radius: 8px;
border: 1px solid var(--line);
background: var(--surface);
}
.flow-board {
background: white;
border: 1px solid var(--line);
border-radius: 6px;
overflow: hidden;
}
.flow-board-nav {
display: flex;
align-items: center;
gap: 14px;
padding: 16px;
font-size: 12px;
color: var(--muted);
}
.flow-board-nav strong {
font-weight: 400;
margin-right: auto;
color: var(--ink);
}
.flow-board-nav .icon {
width: 13px;
height: 13px;
}
.flow-board-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
border-block: 1px solid var(--line);
padding: 12px;
font-size: 11px;
}
.flow-board-columns {
display: grid;
gap: 10px;
padding: 12px;
background: var(--surface);
}
.flow-board-columns .task-card {
min-height: 152px;
padding: 14px;
margin: 0;
display: flex;
flex-direction: column;
}
.flow-board-columns .task-source {
font-size: 10px;
gap: 6px;
}
.flow-board-columns .avatar {
width: 18px;
height: 18px;
flex-shrink: 0;
}
.flow-board-columns .task-title {
font-size: 15px;
line-height: 1.4;
margin: 18px 0;
}
.flow-board-columns .task-footer {
margin-top: auto;
gap: 6px;
}
.flow-board-columns .timestamp,
.flow-board-columns .mini-action {
font-size: 10px;
}
/* The wide frame keeps the surrounding workflow visible, without 3D decoration. */
.pano-heading {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 48px;
margin-bottom: 44px;
}
.pano-controls {
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
margin-bottom: 20px;
}
.pano-focus-control {
display: inline-flex;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 6px;
padding: 4px;
gap: 4px;
}
.pano-focus-control button {
display: flex;
align-items: center;
gap: 8px;
min-height: 38px;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
color: var(--muted);
font-size: 12px;
padding: 7px 12px;
}
.pano-focus-control button[aria-pressed="true"] {
background: white;
border-color: var(--line);
color: var(--ink);
}
.pano-focus-control .flow-count {
font-size: 10px;
min-width: 18px;
}
.pano-move-button {
display: inline-flex;
align-items: center;
gap: 12px;
border: 0;
background: transparent;
color: var(--teal);
min-height: 44px;
font-size: 12px;
padding: 8px 0;
}
.pano-move-button .icon {
width: 16px;
height: 16px;
}
.pano-scroll {
overflow-x: auto;
overflow-y: hidden;
border: 1px solid var(--line);
border-radius: 9px;
background: var(--surface);
scrollbar-width: thin;
scrollbar-color: #bed0d4 transparent;
scroll-snap-type: x proximity;
}
.pano-scroll:focus-visible {
outline: 2px solid var(--teal);
outline-offset: 5px;
}
.pano-board {
display: grid;
grid-template-columns: 1fr 1fr 1.35fr 1fr;
min-width: 1040px;
min-height: 550px;
}
.pano-column {
min-width: 0;
padding: 22px 14px 32px;
border-right: 1px solid var(--line);
scroll-snap-align: start;
}
.pano-column:last-child {
border-right: 0;
}
.pano-column h3 {
display: flex;
align-items: center;
gap: 9px;
font-size: 15px;
letter-spacing: -0.015em;
padding: 0 2px 23px;
}
.pano-col-waiting {
background: var(--surface);
}
.pano-col-done {
background: var(--surface);
}
.pano-card-stack {
display: grid;
gap: 12px;
}
.pano-card-stack .task-card {
margin: 0;
padding: 17px 15px;
}
.pano-card-stack .task-source {
font-size: 11px;
}
.pano-card-stack .task-title {
font-size: 16px;
margin: 20px 0 22px;
}
.pano-card-stack .mini-action {
font-size: 11px;
}
.pano-card-stack .timestamp {
font-size: 10px;
}
.pano-moving-card {
border-color: #8bb9c3;
position: relative;
}
.pano-moving-card::before {
position: absolute;
content: "";
left: -1px;
top: 20px;
bottom: 20px;
width: 3px;
background: var(--teal);
border-radius: 0 3px 3px 0;
}
.pano-task-detail {
display: flex;
gap: 6px;
margin: -7px 0 25px;
flex-wrap: wrap;
}
.pano-task-detail > span {
background: var(--de-indigo-100);
color: #695990;
font-size: 10px;
padding: 4px 6px;
border-radius: 3px;
}
.pano-completed-card {
border-color: #d9e3dc;
}
.pano-empty-state {
display: flex;
align-items: center;
gap: 7px;
font-size: 11px;
padding: 22px 8px;
color: #57776f;
}
.pano-empty-state .icon {
width: 14px;
height: 14px;
}
[data-pano-focus="waiting"]
.pano-column:not(.pano-col-waiting)
.pano-card-stack {
opacity: 0.42;
}
[data-pano-state="done"] .pano-moving-card {
border-color: #8db5a7;
}
[data-pano-state="done"] .pano-moving-card::before {
background: #527e6e;
}
[data-pano-state="done"] [data-pano-task-badge] {
background: #e8f2ec;
color: #46715e;
}
/* Equal stages share card and footer baselines; selection does not move the grid. */
.progress-heading {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 48px;
align-items: end;
}
.progress-staircase {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 24px;
margin-top: 48px;
}
.progress-step {
min-width: 0;
display: flex;
flex-direction: column;
}
.progress-selector {
display: flex;
align-items: flex-start;
gap: 12px;
width: 100%;
text-align: left;
border: 0;
background: transparent;
padding: 0 0 20px;
color: var(--ink);
min-height: 48px;
}
.progress-selector strong {
display: block;
font-weight: 400;
font-size: 23px;
letter-spacing: -0.035em;
margin-bottom: 8px;
}
.progress-selector > .icon {
width: 15px;
height: 15px;
margin: 6px 0 0 auto;
opacity: 0.25;
flex-shrink: 0;
}
.progress-selector[aria-pressed="true"] > .icon {
opacity: 1;
color: var(--teal);
}
.progress-card-field {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
padding: 20px;
flex: 1;
display: flex;
flex-direction: column;
}
.progress-stage-label {
display: flex;
align-items: center;
gap: 9px;
font-size: 12px;
padding-bottom: 20px;
}
.progress-card-field .task-card {
padding: 20px;
margin: 0;
min-height: 230px;
display: flex;
flex-direction: column;
flex: 1;
}
.progress-card-field .task-source {
font-size: 11px;
}
.progress-card-field .task-title {
font-size: 20px;
font-weight: 400;
line-height: 1.35;
margin: 26px 0;
}
.progress-card-field .task-footer {
margin-top: auto;
}
.progress-card-field .mini-action {
font-size: 11px;
}
.progress-reviewed {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 7px;
border-radius: 4px;
background: #eaf3ec;
color: #496e59;
font-size: 10px;
}
.progress-reviewed .icon {
width: 12px;
height: 12px;
}
.progress-count-done {
background: #e8f2ec;
color: #46715e;
}
[data-progress-stage="see"] [data-progress-card="see"] .task-card,
[data-progress-stage="review"] [data-progress-card="review"] .task-card,
[data-progress-stage="move"] [data-progress-card="move"] .task-card {
border-color: var(--teal);
}
[data-flow-motion="true"] .pano-card-stack {
transition: opacity 180ms ease;
}
[data-flow-motion="true"] .progress-card-field .task-card {
transition: border-color 180ms ease;
}
[data-flow-motion="true"] .progress-selector > .icon {
transition: opacity 160ms ease;
}
@media (max-width: 1100px) {
.flow-sources,
.flow-inbox-card {
padding: 20px;
}
.flow-board-field {
padding: 12px;
}
.flow-inbox-card .task-title {
font-size: 21px;
}
.progress-card-field {
padding: 14px;
}
.progress-card-field .task-card {
padding: 16px;
}
.progress-card-field .task-title {
font-size: 18px;
}
}
@media (max-width: 800px) {
.flow-bridge {
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 32px 24px;
}
.flow-sources,
.flow-inbox-card,
.flow-board-field {
grid-column: 2 / 6;
}
.flow-sources::after,
.flow-inbox-card::after {
left: 50%;
top: 100%;
width: 1px;
height: 33px;
}
.flow-source-list > div {
font-size: 13px;
}
.flow-inbox-card .task-title {
font-size: 24px;
}
.flow-board-columns .task-source {
font-size: 12px;
}
.flow-board-columns .task-title {
font-size: 18px;
}
.pano-heading {
gap: 24px;
align-items: flex-start;
flex-direction: column;
}
.progress-heading {
grid-template-columns: 1fr;
gap: 24px;
}
.progress-staircase {
grid-template-columns: 1fr;
gap: 28px;
}
.progress-selector {
min-height: 0;
}
.progress-card-field .task-card {
min-height: 205px;
}
}
@media (max-width: 540px) {
.flow-bridge {
grid-template-columns: 1fr;
}
.flow-sources,
.flow-inbox-card,
.flow-board-field {
grid-column: 1;
}
.pano-heading {
margin-bottom: 28px;
}
.pano-controls {
align-items: flex-start;
flex-direction: column;
gap: 10px;
}
.pano-focus-control button {
min-height: 36px;
padding-inline: 10px;
}
.pano-move-button {
font-size: 12px;
padding-block: 4px;
min-height: 36px;
}
.pano-board {
min-width: 1110px;
grid-template-columns: 260px 260px 320px 270px;
min-height: 510px;
}
.pano-column {
padding: 20px 12px;
}
.progress-staircase {
margin-top: 32px;
}
.progress-card-field {
padding: 20px;
}
.progress-card-field .task-card {
padding: 20px;
}
.progress-card-field .task-title {
font-size: 21px;
}
}
src/styles/scenes-focus.css
Read raw text
/* Focus, handoff, and closing scenes. The figures are live HTML, never raster mockups. */
.sel-section {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 24px;
align-items: center;
}
.sel-copy {
grid-column: span 4;
}
.sel-choices {
display: grid;
gap: 7px;
margin-top: 34px;
}
.sel-choices button {
display: flex;
align-items: center;
gap: 14px;
width: 100%;
min-height: 66px;
padding: 10px 16px;
border: 0;
border-left: 3px solid transparent;
border-radius: 5px;
background: transparent;
color: var(--muted);
text-align: left;
font-size: 18px;
}
.sel-choices button[aria-pressed="true"] {
background: var(--de-brand-100, #dfe9eb);
border-left-color: var(--teal);
color: var(--teal);
}
.sel-choice-icon {
width: 34px;
height: 34px;
border-radius: 6px;
display: grid;
place-items: center;
background: var(--surface);
color: var(--teal);
flex-shrink: 0;
}
.sel-choices button[aria-pressed="true"] .sel-choice-icon {
background: white;
}
.sel-choice-arrow {
width: 17px;
margin-left: auto;
opacity: 0.55;
}
.sel-choices button[aria-pressed="true"] .sel-choice-arrow {
opacity: 1;
}
.sel-stage {
grid-column: span 8;
min-width: 0;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 8px;
padding: 24px;
display: flex;
flex-direction: column;
gap: 24px;
}
.sel-stage-top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
font-size: 12px;
color: var(--muted);
}
.sel-stage-count {
font-size: 11px;
padding: 6px 9px;
border: 1px solid var(--line);
border-radius: 4px;
white-space: nowrap;
}
.sel-card-track {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 24px;
}
.sel-main-card {
grid-column: 1 / -1;
min-width: 0;
min-height: 280px;
padding: 24px;
border: 1px solid var(--line);
border-radius: 6px;
background: white;
display: flex;
flex-direction: column;
}
.sel-card-source {
display: flex;
align-items: center;
gap: 10px;
font-size: 13px;
color: #596a70;
}
.sel-source-icon {
display: inline-grid;
place-items: center;
width: 28px;
height: 28px;
background: var(--de-brand-100, #dfe9eb);
border-radius: 7px;
color: var(--teal);
flex-shrink: 0;
}
.sel-source-icon .icon {
width: 16px;
height: 16px;
}
.sel-agent-icon {
background: var(--de-indigo-100, #f0ecfc);
color: #6856a0;
}
.sel-more {
margin-left: auto;
width: 14px;
height: 18px;
color: var(--muted);
}
.sel-card-label {
font-size: 10px;
letter-spacing: 0.04em;
color: var(--teal);
margin-top: 33px;
}
.sel-main-card h3 {
font-size: 28px;
font-weight: 400;
line-height: 1.25;
letter-spacing: -0.035em;
margin-top: 10px;
min-height: 70px;
text-wrap: pretty;
}
.sel-card-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
font-size: 10px;
color: var(--muted);
margin-top: auto;
padding-top: 28px;
}
.sel-card-footer > span:first-child {
display: flex;
align-items: center;
gap: 4px;
}
.sel-card-footer .icon {
width: 13px;
height: 13px;
}
.sel-art-action {
display: flex;
align-items: center;
gap: 7px;
background: var(--teal);
border-radius: 4px;
color: white;
padding: 9px 10px;
font-size: 10px;
white-space: nowrap;
}
.sel-support-card {
min-width: 0;
padding: 20px;
border: 1px solid var(--line);
border-radius: 6px;
background: white;
min-height: 220px;
display: flex;
flex-direction: column;
}
.sel-support-card .sel-card-source {
font-size: 11px;
gap: 7px;
}
.sel-support-card .sel-source-icon {
width: 23px;
height: 23px;
}
.sel-support-card p {
font-size: 18px;
line-height: 1.4;
letter-spacing: -0.02em;
margin-top: 24px;
font-weight: 400;
}
.sel-mini-action {
background: var(--de-brand-100, #dfe9eb);
color: var(--teal);
border-radius: 4px;
padding: 7px 9px;
}
.sel-bank-dot {
display: inline-block;
width: 21px;
height: 21px;
border-radius: 50%;
background: #294797;
flex-shrink: 0;
}
.control-heading {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 36px;
margin-bottom: 50px;
}
.control-composition {
display: grid;
grid-template-columns: minmax(0, 1fr) 32px minmax(0, 1.4fr) 32px minmax(
0,
0.9fr
);
align-items: center;
}
.control-source-block {
background: white;
padding: 24px;
border: 1px solid var(--line);
border-radius: 8px;
min-width: 0;
}
.control-connector {
height: 1px;
width: 100%;
background: #b9c9cc;
}
.control-source-item {
display: flex;
align-items: center;
gap: 10px;
padding: 18px 0;
border-bottom: 1px solid var(--line);
}
.control-source-item:last-child {
border-bottom: 0;
padding-bottom: 6px;
}
.control-letter {
width: 23px;
height: 23px;
display: inline-grid;
place-items: center;
background: var(--ink);
color: white;
border-radius: 50%;
font-size: 12px;
flex-shrink: 0;
}
.control-source-glyph {
display: grid;
place-items: center;
color: var(--teal);
width: 23px;
}
.control-source-glyph .icon {
width: 19px;
}
.control-source-item strong {
font-weight: 400;
font-size: 14px;
}
.control-source-item p {
font-size: 12px;
margin-top: 7px;
color: var(--muted);
line-height: 1.4;
}
.control-source-count {
margin-left: auto;
background: white;
border-radius: 4px;
padding: 5px;
font-size: 11px;
color: var(--teal);
}
.control-source-arrow {
margin-left: auto;
width: 14px;
color: var(--teal);
}
.control-decision-block {
background: var(--surface);
padding: 24px;
border: 1px solid var(--line);
border-radius: 8px;
min-width: 0;
}
.control-approval-card {
border: 1px solid var(--line);
padding: 24px;
border-radius: 6px;
background: white;
min-height: 260px;
display: flex;
flex-direction: column;
}
.control-review-status {
align-self: flex-start;
display: inline-block;
padding: 5px 7px;
border-radius: 4px;
font-size: 10px;
background: var(--de-indigo-100, #f0ecfc);
color: #655687;
margin-top: 25px;
}
.control-approval-card h3 {
font-size: 25px;
font-weight: 400;
line-height: 1.3;
margin-top: 11px;
min-height: 65px;
text-wrap: pretty;
}
.control-decision-footer {
display: flex;
align-items: center;
gap: 12px;
margin-top: auto;
padding-top: 25px;
}
.control-owner {
display: flex;
align-items: center;
gap: 9px;
font-size: 10px;
color: var(--muted);
}
.control-owner > span {
font-size: 10px;
color: var(--muted);
border-left: 1px solid var(--line);
padding-left: 9px;
}
.control-complete-icon {
opacity: 0;
width: 16px;
margin-left: auto;
color: var(--teal);
}
.control-update-row {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 16px;
margin-top: 20px;
}
.control-update-button {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-size: 12px;
color: var(--teal);
background: white;
border: 1px solid var(--line);
border-radius: 5px;
padding: 10px 12px;
min-height: 44px;
white-space: nowrap;
}
.control-update-button .icon {
width: 14px;
height: 14px;
}
.control-outcome-block {
padding: 24px;
background: white;
border: 1px solid var(--line);
border-radius: 8px;
min-width: 0;
}
.control-outcome-row {
display: flex;
gap: 11px;
align-items: center;
}
.control-outcome-row strong {
font-size: 15px;
font-weight: 400;
}
.control-outcome-dot {
display: block;
width: 7px;
height: 7px;
background: var(--teal);
border-radius: 50%;
flex-shrink: 0;
}
.control-outcome-row > .icon {
margin-left: auto;
color: var(--teal);
width: 16px;
opacity: 0.35;
}
.control-composition:has([data-review-state="reviewed"])
.control-outcome-row
> .icon,
[data-review-state="reviewed"] .control-complete-icon {
opacity: 1;
}
[data-review-state="reviewed"] .control-review-status {
background: var(--de-brand-100, #dfe9eb);
color: var(--teal);
}
.ribbon-closing {
text-align: center;
overflow: hidden;
margin-top: 24px;
border-top: 1px solid var(--line);
padding: 48px 24px;
background: var(--de-brand-100);
border-radius: 8px;
}
.ribbon-closing-copy {
padding: 72px 16px;
}
.ribbon-closing-copy h2 {
font-size: clamp(40px, 4.2vw, 62px);
}
.ribbon-closing-copy .button {
margin-top: 30px;
}
.ribbon-track {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 24px;
text-align: left;
align-items: stretch;
}
.ribbon-card {
min-width: 0;
min-height: 206px;
padding: 20px;
border: 1px solid var(--line);
background: white;
border-radius: 6px;
display: flex;
flex-direction: column;
}
.ribbon-source {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
color: var(--muted);
}
.ribbon-source .sel-source-icon {
width: 23px;
height: 23px;
border-radius: 5px;
}
.ribbon-source .sel-source-icon .icon {
width: 13px;
height: 13px;
}
.ribbon-source .control-letter,
.ribbon-source .sel-bank-dot {
width: 19px;
height: 19px;
font-size: 10px;
}
.ribbon-count {
background: var(--de-indigo-100, #f0ecfc);
color: #645387;
border-radius: 4px;
padding: 3px 5px;
font-size: 10px;
margin-left: 2px;
}
.ribbon-card p {
font-size: 17px;
font-weight: 400;
line-height: 1.4;
letter-spacing: -0.015em;
margin-top: 20px;
min-height: 47px;
}
.ribbon-foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: auto;
padding-top: 20px;
color: var(--muted);
font-size: 10px;
}
.ribbon-foot > span {
background: var(--surface);
color: #53686e;
padding: 5px 7px;
border-radius: 4px;
}
.ribbon-foot .icon {
width: 13px;
height: 13px;
color: var(--teal);
}
@media (hover: hover) and (pointer: fine) {
.sel-choices button:hover {
color: var(--teal);
}
.sel-choices button,
.control-update-button {
transition:
background 180ms ease,
color 180ms ease;
}
.control-update-button:hover {
background: var(--de-brand-100);
}
}
@media (max-width: 1100px) {
.sel-stage {
padding: 20px;
}
.sel-card-track {
gap: 20px;
}
.sel-support-card {
padding: 16px;
}
.sel-support-card .sel-card-source {
font-size: 10px;
}
.sel-support-card p {
font-size: 17px;
}
.control-composition {
grid-template-columns: minmax(0, 1fr) 24px minmax(0, 1.4fr) 24px minmax(
0,
0.9fr
);
}
.control-source-block,
.control-decision-block,
.control-outcome-block {
padding: 16px;
}
.control-approval-card {
padding: 18px;
}
.control-approval-card h3 {
font-size: 22px;
}
.control-source-count,
.control-source-arrow {
display: none;
}
.control-source-item {
gap: 8px;
}
.control-source-item strong {
font-size: 12px;
}
.control-source-item p {
font-size: 11px;
}
.control-outcome-row {
align-items: flex-start;
}
.control-outcome-dot {
margin-top: 7px;
}
.control-outcome-row > .icon {
display: none;
}
.ribbon-track {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.ribbon-track > .ribbon-card:nth-child(n + 3) {
display: none;
}
}
@media (max-width: 800px) {
.sel-section {
grid-template-columns: repeat(6, minmax(0, 1fr));
}
.sel-copy,
.sel-stage {
grid-column: 1 / -1;
}
.sel-copy h2 br {
display: none;
}
.sel-choices {
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 6px;
margin-top: 25px;
}
.sel-choices button {
min-height: 52px;
font-size: 12px;
justify-content: center;
padding: 8px;
gap: 6px;
border-left: 0;
border-bottom: 2px solid transparent;
}
.sel-choices button[aria-pressed="true"] {
border-bottom-color: var(--teal);
}
.sel-choice-icon {
width: 25px;
height: 25px;
background: transparent;
}
.sel-choice-icon .icon {
width: 16px;
height: 16px;
}
.sel-choice-arrow {
display: none;
}
.sel-support-card {
padding: 20px;
}
.sel-support-card .sel-card-source {
font-size: 11px;
}
.control-heading {
display: block;
margin-bottom: 32px;
}
.control-composition {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 0 24px;
}
.control-source-block,
.control-decision-block,
.control-outcome-block {
grid-column: 2 / 6;
padding: 24px;
}
.control-connector {
grid-column: 2 / 6;
justify-self: center;
height: 32px;
width: 1px;
}
.control-approval-card {
padding: 24px;
}
.control-source-item strong {
font-size: 14px;
}
.control-source-item p {
font-size: 12px;
}
.control-source-count,
.control-source-arrow {
display: inline-flex;
}
.control-approval-card h3 {
font-size: 25px;
}
.control-outcome-row > .icon {
display: block;
}
}
@media (max-width: 540px) {
.sel-section {
grid-template-columns: 1fr;
}
.sel-choices {
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 5px;
}
.sel-choices button {
justify-content: flex-start;
font-size: 13px;
padding-inline: 10px;
}
.sel-stage {
padding: 16px;
gap: 20px;
}
.sel-stage-top {
font-size: 11px;
gap: 8px;
}
.sel-stage-count {
font-size: 10px;
}
.sel-card-track {
grid-template-columns: 1fr;
gap: 16px;
}
.sel-main-card {
padding: 20px;
min-height: 320px;
}
.sel-main-card h3 {
font-size: 24px;
min-height: 120px;
}
.sel-card-source {
font-size: 11px;
}
.sel-card-label {
margin-top: 26px;
}
.sel-card-footer {
font-size: 10px;
}
.sel-support-card {
min-height: 190px;
}
.sel-support-card p br {
display: none;
}
.control-composition {
grid-template-columns: 1fr;
}
.control-source-block,
.control-decision-block,
.control-outcome-block,
.control-connector {
grid-column: 1;
}
.control-source-block,
.control-decision-block,
.control-outcome-block {
padding: 20px;
}
.control-approval-card {
padding: 20px;
min-height: 260px;
}
.control-update-button {
width: 100%;
}
.ribbon-closing {
padding: 24px 16px;
}
.ribbon-track {
grid-template-columns: 1fr;
}
.ribbon-track > .ribbon-card:nth-child(n + 2) {
display: none;
}
.ribbon-card {
min-height: 190px;
}
.ribbon-closing-copy {
padding: 48px 0;
}
.ribbon-closing-copy h2 {
font-size: 38px;
}
.ribbon-closing-copy h2 br {
display: none;
}
.ribbon-closing-copy .button {
font-size: 14px;
}
}
src/styles/splash-motion.css
Read raw text
/* Three stable slots keep the board still while its tasks exchange places. */
.splash-screen {
position: relative;
}
.splash .kanban {
position: relative;
}
.splash-task-slot {
min-height: 146px;
margin-bottom: 12px;
}
.splash-task-slot > .task-card {
margin: 0;
}
.splash[data-splash-initialized] .splash-moving-card {
position: absolute;
top: 0;
left: 0;
margin: 0;
z-index: 2;
}
.splash .splash-moving-card {
background: #fff;
}
.splash .task-source > .icon:last-child {
width: 12px;
height: 12px;
margin-left: auto;
color: var(--muted);
}
.splash .mini-action {
display: inline-flex;
align-items: center;
gap: 5px;
}
.splash .mini-action .icon {
width: 12px;
height: 12px;
}
.splash-task-status {
color: var(--muted);
}
.splash-count-mobile {
display: none;
}
.splash-motion-toggle {
position: absolute;
right: 26px;
top: 23px;
display: grid;
place-items: center;
width: 32px;
height: 32px;
padding: 0;
border: 1px solid var(--line);
border-radius: 5px;
background: #fff;
color: var(--teal);
cursor: pointer;
transition: background-color 180ms ease;
}
.splash-motion-toggle .icon {
width: 16px;
height: 16px;
}
.splash-motion-toggle:hover {
background: var(--de-brand-100);
}
.splash-motion-toggle:focus-visible {
outline: 2px solid var(--teal);
outline-offset: 3px;
}
.splash-motion-toggle:disabled {
opacity: 0.45;
cursor: default;
}
.splash-play-icon {
display: none;
}
.splash[data-splash-paused="true"] .splash-pause-icon {
display: none;
}
.splash[data-splash-paused="true"] .splash-play-icon {
display: block;
}
.splash .product-tabs {
padding-right: 64px;
}
@media (max-width: 800px) and (min-width: 681px) {
.splash .kanban-column {
padding-inline: 10px;
}
.splash .task-card {
padding: 12px;
}
.splash .task-title {
font-size: 14px;
}
.splash .task-source {
font-size: 10px;
}
.splash .task-footer {
flex-wrap: wrap;
gap: 10px;
}
.splash .column-heading {
font-size: 12px;
gap: 6px;
}
}
@media (max-width: 680px) {
.splash {
padding-top: 32px;
padding-bottom: 36px;
}
.splash h1 {
font-size: clamp(38px, 10.8vw, 48px);
}
.splash-copy .lead {
font-size: 15px;
line-height: 1.5;
margin-top: 18px;
max-width: 350px;
}
.splash-screen {
margin-top: 24px;
}
.splash .kanban {
grid-template-columns: 1fr;
gap: 0;
padding: 2px 10px 10px;
}
.splash .kanban-column.splash-loop-column {
display: block;
border: 0;
padding: 0;
}
.splash .splash-static-card,
.splash .splash-done {
display: none;
}
.splash .column-heading {
padding: 10px 4px 7px;
font-size: 11px;
line-height: 1.2;
min-height: 0;
gap: 6px;
}
.splash .count {
padding: 2px 5px;
font-size: 10px;
}
.splash-count-desktop {
display: none;
}
.splash-count-mobile {
display: inline;
}
.splash-task-slot {
min-height: 103px;
margin-bottom: 3px;
}
.splash .splash-moving-card {
padding: 11px 13px;
border-radius: 6px;
}
.splash .splash-moving-card .task-source {
font-size: 11px;
gap: 6px;
}
.splash .splash-moving-card .avatar {
width: 16px;
height: 16px;
font-size: 10px;
}
.splash .splash-moving-card .task-title {
font-size: 16px;
line-height: 1.3;
margin: 8px 0 10px;
}
.splash .splash-moving-card .task-footer {
font-size: 10px;
}
.splash .splash-moving-card .mini-action {
font-size: 10px;
padding: 3px 6px;
}
.splash-motion-toggle {
width: 44px;
height: 44px;
top: 9px;
right: 13px;
}
.splash .product-tabs {
padding-top: 10px;
padding-bottom: 4px;
}
.splash .product-toolbar {
padding-top: 8px;
padding-bottom: 10px;
}
.splash-actions {
margin-top: 22px;
gap: 12px;
}
.splash-actions .button {
min-height: 44px;
font-size: 13px;
gap: 10px;
padding: 10px 13px;
}
.splash-actions .text-link {
font-size: 13px;
gap: 8px;
}
}
@media (prefers-reduced-motion: reduce) {
.splash-motion-toggle {
transition: none;
}
}
html[data-capture="true"] .splash-motion-toggle {
visibility: hidden;
}
src/styles/splash.css
Read raw text
/* A dark blue editorial splash; the product and remaining page stay light. */
:root {
--splash-navy: #08283e;
--splash-top: #092b42;
}
.site-header {
position: relative;
z-index: 4;
background: var(--splash-top);
color: white;
border-color: #ffffff24;
}
.site-header .logo img {
filter: brightness(0) invert(1);
}
.site-header #site-nav {
color: #e0ebf0;
}
.site-header #site-nav a:hover {
color: white;
}
.site-header .header-cta {
background: white;
border-color: white;
color: var(--teal);
}
.site-header .header-cta:hover {
background: #e5eef1;
border-color: #e5eef1;
}
.site-header .menu-toggle {
color: white;
background: transparent;
}
.site-header :focus-visible {
outline-color: #b9e8f5;
}
.splash {
position: relative;
isolation: isolate;
overflow: hidden;
padding: 76px 0 72px;
background: var(--splash-navy);
}
.splash-art,
.splash-overlay {
position: absolute;
inset: 0;
z-index: -1;
pointer-events: none;
}
.splash-art {
overflow: hidden;
}
.splash-art::before,
.splash-art::after {
content: "";
position: absolute;
inset-block: -8px;
filter: blur(2px);
}
.splash-art::before {
width: 70%;
right: -9%;
background: url("assets/cosmos-blue-loop.webp") 50% 45% / cover no-repeat;
}
.splash-art::after {
width: 55%;
left: -7%;
background: url("assets/cosmos-blue-wave.webp") 52% 40% / cover no-repeat;
opacity: 0.82;
mask-image: linear-gradient(90deg, #000 70%, transparent);
}
.splash-overlay {
background: linear-gradient(
180deg,
#092b42f5 0%,
#092b42d9 13%,
#06344d94 32%,
#004f5c30 52%,
#b8d8e054 76%,
#ffffff 100%
);
}
.splash-copy {
text-align: center;
color: white;
}
.splash h1 {
font-size: clamp(48px, 5vw, 72px);
line-height: 1.04;
letter-spacing: -0.055em;
font-weight: 400;
}
.splash-copy .lead {
max-width: 660px;
margin: 26px auto 0;
color: #e4eef4;
font-size: 19px;
line-height: 1.55;
}
.splash-screen {
width: min(1200px, calc(100% - var(--gutter) * 2));
margin: 44px auto 0;
}
.splash-glass {
padding: 10px;
border: 1px solid #ffffff88;
border-radius: 17px;
background: #ffffff2e;
box-shadow:
inset 0 1px 0 #ffffff7a,
0 24px 64px #072b4333;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.splash .product-window {
border-radius: 9px;
border-color: #ffffffbb;
background: #f8fbfc;
}
.splash .product-tabs {
padding: 12px 18px 6px;
font-size: 13px;
}
.splash .product-toolbar {
padding: 10px 18px 14px;
}
.splash .search-art,
.splash .tool-art,
.splash .new-task {
height: 33px;
}
.splash .kanban {
grid-template-columns: repeat(4, minmax(0, 1fr));
background: #f7fafb;
}
.splash .kanban-column {
padding: 0 12px 6px;
}
.splash .column-heading {
font-size: 14px;
padding: 18px 0;
}
.splash .task-card {
padding: 14px;
margin-bottom: 12px;
}
.splash .task-source {
font-size: 11px;
gap: 6px;
}
.splash .task-title {
font-size: 15px;
line-height: 1.4;
margin: 18px 0 22px;
}
.splash .task-footer {
font-size: 11px;
justify-content: space-between;
}
.splash .timestamp {
font-size: 11px;
}
.splash .mini-action {
font-size: 11px;
padding: 6px 8px;
}
.splash-actions {
justify-content: center;
margin-top: 30px;
gap: 14px 28px;
}
.splash-actions .button {
background: white;
border-color: #d2e3e8;
color: var(--teal);
box-shadow: 0 3px 12px #08283e08;
}
.splash-actions .button:hover {
background: #f2f7f8;
}
.splash-actions .text-link {
color: var(--teal);
}
@media (max-width: 1100px) {
.splash .kanban {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.splash .splash-done {
display: none;
}
.splash .kanban-column:nth-child(3) {
border-right: 0;
}
}
@media (max-width: 800px) {
.site-header #site-nav.is-open {
background: var(--splash-top);
border-bottom-color: #ffffff24;
}
.splash {
padding-top: 56px;
padding-bottom: 56px;
}
.splash h1 {
font-size: 58px;
}
.splash-copy .lead {
font-size: 18px;
max-width: 550px;
}
.splash-copy .lead br {
display: none;
}
.splash-screen {
margin-top: 36px;
}
.splash-art::before {
width: 110%;
right: -35%;
}
.splash-art::after {
width: 85%;
left: -32%;
}
}
@media (max-width: 540px) {
.splash {
padding-top: 48px;
padding-bottom: 48px;
}
.splash h1 {
font-size: clamp(40px, 11vw, 52px);
}
.splash-copy .lead {
font-size: 17px;
margin-top: 22px;
}
.splash-screen {
width: calc(100% - 32px);
margin-top: 30px;
}
.splash-glass {
padding: 7px;
border-radius: 13px;
}
.splash .product-tabs {
padding-inline: 10px;
font-size: 12px;
gap: 3px;
}
.splash .product-toolbar {
padding-inline: 10px;
gap: 6px;
}
.splash .optional-tool {
display: none;
}
.splash .kanban-column {
padding-inline: 12px;
}
.splash .task-source {
font-size: 12px;
}
.splash .task-title {
font-size: 17px;
margin-block: 16px 20px;
}
.splash .task-footer,
.splash .timestamp,
.splash .mini-action {
font-size: 12px;
}
.splash-art::before {
width: 160%;
right: -66%;
}
.splash-art::after {
width: 120%;
left: -60%;
}
.splash-actions {
gap: 10px 18px;
margin-top: 26px;
}
.splash-actions .button {
font-size: 14px;
padding-inline: 16px;
}
}
src/styles/styles.css
Read raw text
@font-face {
font-family: Lausanne;
src: url("assets/lausanne-regular.woff2") format("woff2");
font-style: normal;
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: Lausanne;
src: url("assets/lausanne-medium.woff2") format("woff2");
font-style: normal;
font-weight: 500;
font-display: swap;
}
:root {
color-scheme: light;
--grid-gap: 24px;
--grid-columns: 12;
--gutter: clamp(24px, 4.45vw, 80px);
font-family: Lausanne, Arial, sans-serif;
color: var(--ink);
background: var(--white);
font-synthesis: none;
-webkit-font-smoothing: antialiased;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
}
[hidden] {
display: none !important;
}
button,
input {
font: inherit;
}
a {
color: inherit;
text-decoration: none;
}
button,
a {
-webkit-tap-highlight-color: transparent;
}
button {
cursor: pointer;
}
button:focus-visible,
a:focus-visible {
outline: 2px solid #206570;
outline-offset: 5px;
}
button:disabled {
cursor: default;
}
figure,
p {
margin: 0;
}
h1,
h2,
h3 {
margin: 0;
font-weight: 400;
text-wrap: balance;
}
h1 {
font-size: clamp(44px, 4.8vw, 68px);
line-height: 1.06;
letter-spacing: -0.05em;
}
h2 {
font-size: clamp(34px, 3.25vw, 48px);
line-height: 1.1;
letter-spacing: -0.045em;
}
h3 {
font-size: 22px;
line-height: 1.2;
letter-spacing: -0.025em;
}
.icon {
width: 20px;
height: 20px;
fill: none;
stroke: currentColor;
stroke-width: 1.6;
stroke-linecap: round;
stroke-linejoin: round;
flex-shrink: 0;
}
.sprite {
position: absolute;
width: 0;
height: 0;
overflow: hidden;
}
.container {
width: min(100% - var(--gutter) * 2, 1320px);
margin-inline: auto;
}
.section {
padding-block: 100px;
scroll-margin-top: 30px;
}
.lead {
font-size: clamp(18px, 1.5vw, 22px);
line-height: 1.5;
color: var(--muted);
text-wrap: pretty;
}
.button {
display: inline-flex;
min-height: 48px;
align-items: center;
justify-content: center;
gap: 16px;
border: 1px solid var(--teal);
background: var(--teal);
color: white;
border-radius: 5px;
font-size: 15px;
padding: 12px 20px;
white-space: nowrap;
transition:
background 160ms,
border-color 160ms;
}
.button:hover {
background: #206570;
border-color: #206570;
}
.button .icon,
.text-link .icon {
width: 18px;
height: 18px;
transition: transform 160ms;
}
@media (hover: hover) and (pointer: fine) {
.button:hover .icon,
.text-link:hover .icon {
transform: translateX(3px);
}
}
.button-small {
min-height: 38px;
padding: 8px 14px;
font-size: 13px;
}
.text-link {
display: inline-flex;
min-height: 44px;
align-items: center;
gap: 10px;
color: var(--teal);
font-size: 15px;
}
.cta-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 12px 24px;
}
.logo {
display: inline-flex;
flex-shrink: 0;
}
.logo img {
display: block;
width: 126px;
height: auto;
filter: brightness(0);
}
.site-header {
border-bottom: 1px solid var(--line);
}
.header-row {
display: flex;
align-items: center;
min-height: 80px;
gap: 48px;
}
#site-nav {
display: flex;
gap: 28px;
margin-left: auto;
font-size: 13px;
color: #4f595c;
}
#site-nav a {
padding: 12px 0;
}
#site-nav a:hover {
color: var(--teal);
}
.menu-toggle {
display: none;
}
.section-heading {
margin-bottom: 40px;
}
.section-heading.centered {
text-align: center;
margin-bottom: 48px;
}
.feature-panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
min-width: 0;
}
.split-section {
display: grid;
grid-template-columns: 0.85fr 1.65fr;
gap: 64px;
align-items: center;
}
.section-copy .lead {
margin-top: 24px;
max-width: 340px;
}
.section-copy .text-link {
margin-top: 24px;
}
.steps-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 24px;
}
.step-panel {
display: flex;
flex-direction: column;
padding: 24px;
}
.step-art {
min-height: 290px;
display: flex;
align-items: center;
justify-content: center;
}
.step-caption {
padding: 16px 4px 8px;
}
.step-number {
font-size: 11px;
color: #778285;
display: block;
margin-bottom: 16px;
}
.step-caption h3 {
font-size: 26px;
}
.step-caption p {
font-size: 17px;
color: var(--muted);
line-height: 1.45;
margin-top: 10px;
}
.context-section {
display: grid;
grid-template-columns: 1.05fr 1fr;
align-items: center;
gap: 72px;
}
.context-panel {
min-height: 550px;
padding: 48px;
display: flex;
flex-direction: column;
justify-content: center;
}
.context-copy h2 {
margin-bottom: 32px;
}
.context-notes {
display: grid;
gap: 10px;
}
.context-notes button {
display: flex;
align-items: center;
gap: 18px;
text-align: left;
border: 1px solid transparent;
background: white;
border-radius: 8px;
padding: 16px 12px;
color: var(--ink);
transition: background 160ms;
}
.context-notes button:hover {
background: #f6f8f8;
}
.note-icon {
height: 42px;
width: 42px;
display: grid;
place-items: center;
background: #f0f4f4;
border-radius: 8px;
flex-shrink: 0;
}
.note-icon .icon {
width: 22px;
height: 22px;
}
.context-notes strong {
display: block;
font-size: 20px;
font-weight: 400;
margin: 0;
letter-spacing: -0.025em;
}
.note-arrow {
margin: 0 0 0 auto;
opacity: 0;
width: 16px;
color: var(--teal);
transition: opacity 160ms;
}
.context-notes button[aria-pressed="true"] .note-arrow {
opacity: 1;
}
.context-notes button[aria-pressed="true"] .note-icon {
background: #e6eeee;
color: var(--teal);
}
.heading-split {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 32px;
}
.heading-split > p {
padding-bottom: 6px;
max-width: 320px;
}
.closing {
border-top: 1px solid var(--line);
margin-top: 64px;
text-align: center;
padding-block: 104px 112px;
}
.closing h2 {
font-size: clamp(40px, 4vw, 58px);
}
.closing .button {
margin-top: 32px;
}
.site-footer {
display: flex;
align-items: center;
border-top: 1px solid var(--line);
gap: 36px;
min-height: 112px;
font-size: 12px;
color: var(--muted);
}
.site-footer nav {
display: flex;
gap: 24px;
margin-left: auto;
}
.site-footer a:hover {
color: var(--teal);
}
.skip-link {
position: absolute;
left: 16px;
top: -80px;
padding: 12px;
background: white;
z-index: 10;
}
.skip-link:focus {
top: 12px;
}
@media (max-width: 1100px) {
.header-row {
gap: 24px;
}
#site-nav {
gap: 20px;
}
.split-section {
gap: 32px;
grid-template-columns: 0.85fr 1.5fr;
}
.context-section {
gap: 36px;
}
.context-panel {
padding: 32px;
}
.step-panel {
padding: 18px;
}
.step-art {
min-height: 250px;
}
}
@media (max-width: 800px) {
:root {
--grid-columns: 6;
}
.section {
padding-block: 64px;
}
.header-row {
min-height: 72px;
}
#site-nav {
display: none;
}
.header-cta {
margin-left: auto;
}
.menu-toggle {
display: inline-flex;
align-items: center;
gap: 4px;
border: 0;
padding: 10px 0;
background: white;
font-size: 13px;
order: 3;
}
.menu-toggle .icon {
width: 14px;
}
#site-nav.is-open {
position: absolute;
top: 72px;
left: 0;
right: 0;
display: grid;
gap: 0;
padding: 16px var(--gutter);
background: white;
border-bottom: 1px solid var(--line);
z-index: 5;
}
.split-section {
grid-template-columns: 1fr;
gap: 32px;
}
.section-copy .lead {
max-width: 520px;
}
.section-copy h2 br {
display: none;
}
.section-copy .text-link {
margin-top: 16px;
}
.steps-grid {
gap: 14px;
}
.step-panel {
padding: 16px;
}
.step-art {
min-height: 250px;
}
.step-caption h3 {
font-size: 21px;
}
.step-caption p {
font-size: 15px;
}
.context-section {
grid-template-columns: 1fr;
gap: 36px;
}
.context-panel {
min-height: 380px;
order: 2;
padding: 48px;
}
.context-copy h2 br {
display: none;
}
.context-notes {
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.context-notes button {
flex-direction: column;
padding: 12px 8px;
}
.context-notes strong {
font-size: 17px;
}
.note-arrow {
display: none;
}
.heading-split {
align-items: flex-start;
flex-direction: column;
gap: 8px;
}
.heading-split > p {
max-width: 450px;
}
.closing {
margin-top: 40px;
padding-block: 72px;
}
.site-footer {
flex-wrap: wrap;
gap: 24px;
padding-block: 32px;
}
}
@media (max-width: 540px) {
:root {
--gutter: 22px;
--grid-columns: 1;
}
h1 {
font-size: 44px;
}
h2 {
font-size: 34px;
}
.section {
padding-block: 48px;
}
.header-row {
gap: 16px;
min-height: 68px;
}
.logo img {
width: 112px;
}
.header-cta {
font-size: 11px;
padding: 8px 10px;
min-height: 34px;
}
.header-cta .icon {
display: none;
}
#site-nav.is-open {
top: 68px;
}
.section-heading {
margin-bottom: 28px;
}
.steps-grid {
grid-template-columns: 1fr;
gap: 16px;
}
.step-panel {
padding: 24px;
}
.step-art {
min-height: 230px;
}
.step-caption h3 {
font-size: 26px;
}
.step-caption p {
font-size: 17px;
}
.context-panel {
padding: 24px;
min-height: 350px;
}
.context-notes {
grid-template-columns: 1fr;
}
.context-notes button {
flex-direction: row;
gap: 16px;
}
.context-notes strong {
font-size: 19px;
}
.site-footer nav {
gap: 18px;
margin-left: auto;
}
.site-footer > span {
width: 100%;
}
.closing {
padding-block: 64px;
margin-top: 24px;
}
.closing h2 {
font-size: 40px;
}
}
@media (max-width: 360px) {
.header-row {
gap: 10px;
}
.header-row .logo img {
width: 96px;
}
.menu-toggle {
font-size: 12px;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation: none !important;
transition: none !important;
scroll-behavior: auto !important;
}
}
html[data-capture="true"] *,
html[data-capture="true"] *::before,
html[data-capture="true"] *::after {
animation: none !important;
transition: none !important;
caret-color: transparent !important;
}
html[data-isolated="true"] .site-header,
html[data-isolated="true"] .site-footer,
html[data-isolated="true"] .skip-link {
display: none;
}
html[data-isolated="true"] .closing,
html[data-isolated="true"] .ribbon-closing {
margin-top: 0;
border-top: 0;
}
@media print {
.section {
break-inside: avoid;
}
.site-header,
.site-footer {
position: static;
}
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}
src/styles/tokens.css
Read raw text
/* Public DualEntry marketing CSS, verified 2026-09-08.
Semantic aliases below preserve source values; this is not a private library export. */
:root {
--_v3---text-on-light--primary: #1d1f20;
--_v3---text-on-light--secondary: #1d1f20a6;
--_v3---background--secondary: #f5f6f6;
--_v3---background--tertiary: #f1f2f2;
--_v3---border-on-light--primary: #e4ebec;
--_v3---brand--100: #dfe9eb;
--_v3---brand--200: #bfd3d6;
--_v3---brand--700: #206570;
--_v3---brand--800: #004f5c;
--base-color-accent--cyan-100: #ebfcff;
--base-color-accent--cyan-300: #c0f2fa;
--base-color-accent--indigo-300: #bfa2ff;
--base-color-accent--indigo-300-30: #bfa2ff4d;
--de-brand-100: var(--_v3---brand--100);
--de-brand-200: var(--_v3---brand--200);
--de-cyan-100: var(--base-color-accent--cyan-100);
--de-cyan-300: var(--base-color-accent--cyan-300);
--de-indigo-100: #f0ecfc; /* Public Tasks prototype source token. */
--de-lavender-block: var(--base-color-accent--indigo-300-30);
--ink: var(--_v3---text-on-light--primary);
--muted: var(--_v3---text-on-light--secondary);
--teal: var(--_v3---brand--800);
--surface: var(--_v3---background--secondary);
--line: var(--_v3---border-on-light--primary);
--white: #fff;
--radius: 8px;
--motion-fast: 160ms;
--motion-scene: 240ms;
--ease-enter: cubic-bezier(0.32, 0.72, 0, 1);
}
src/styles/workflow-cards.css
Read raw text
/* The close today: three aligned, readable illustrations of the same workflow. */
.workflow-heading {
display: grid;
gap: 16px;
margin-bottom: 36px;
}
.workflow-heading h2 {
font-size: clamp(28px, 2.8vw, 40px);
}
.workflow-heading > p:last-child {
color: var(--muted);
font-size: 16px;
line-height: 1.5;
}
.workflow-eyebrow {
color: var(--muted);
font-size: 11px;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.workflow-cards {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 24px;
}
.workflow-card {
display: grid;
grid-template-rows: auto 1fr;
min-width: 0;
padding: 24px;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
}
.workflow-card-heading {
display: grid;
justify-items: start;
gap: 10px;
margin-bottom: 24px;
}
.workflow-card-heading h3 {
font-size: 20px;
line-height: 1.25;
}
.workflow-card-heading p {
min-height: 40px;
color: var(--muted);
font-size: 14px;
line-height: 1.45;
}
.workflow-icon {
display: grid;
place-items: center;
width: 36px;
height: 36px;
margin-bottom: 4px;
border-radius: 7px;
}
.workflow-icon .icon {
width: 19px;
height: 19px;
}
.workflow-icon-teal {
color: var(--teal);
background: var(--de-brand-100);
}
.workflow-icon-lilac {
color: #71629b;
background: var(--de-indigo-100);
}
.workflow-icon-sage {
color: #456c55;
background: #e8efe9;
}
.workflow-preview {
min-width: 0;
min-height: 188px;
padding: 14px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 6px;
}
.workflow-modules {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
align-content: start;
gap: 10px 8px;
padding-block: 20px;
}
.workflow-module {
display: flex;
align-items: center;
gap: 5px;
min-width: 0;
min-height: 36px;
padding: 6px;
border: 1px solid var(--line);
border-radius: 4px;
font-size: 11px;
}
.workflow-module > .icon {
width: 12px;
height: 12px;
color: #6b848b;
}
.workflow-module > span:first-of-type {
white-space: nowrap;
}
.workflow-module .workflow-count {
margin-left: auto;
}
.workflow-count {
display: inline-grid;
place-items: center;
min-width: 18px;
height: 18px;
padding: 0 4px;
background: var(--de-brand-100);
border-radius: 3px;
color: var(--teal);
font-size: 10px;
font-variant-numeric: tabular-nums;
}
.workflow-count-amber {
color: #8b6031;
background: #f7ede0;
}
.workflow-count-lilac {
color: #71629b;
background: var(--de-indigo-100);
}
.workflow-count-sage {
color: #456c55;
background: #e8efe9;
}
.workflow-queue-heading {
display: flex;
align-items: center;
gap: 8px;
padding-bottom: 12px;
font-size: 11px;
}
.workflow-queue {
display: grid;
gap: 0;
padding: 0;
margin: 0;
list-style: none;
}
.workflow-queue li {
display: flex;
align-items: center;
gap: 7px;
min-height: 32px;
border-top: 1px solid #f1f3f3;
font-size: 11px;
line-height: 1.35;
}
.workflow-queue li > span:nth-child(2) {
flex: 1;
}
.workflow-queue small {
color: #879395;
font-size: 9px;
white-space: nowrap;
}
.workflow-source-dot {
display: grid;
place-items: center;
flex: 0 0 15px;
width: 15px;
height: 15px;
background: var(--de-brand-100);
color: var(--teal);
border-radius: 50%;
font-size: 9px;
}
.workflow-source-dot .icon {
width: 10px;
height: 10px;
}
.workflow-source-acme {
background: #27302f;
color: white;
}
.workflow-source-agent {
background: var(--de-indigo-100);
color: #71629b;
}
.workflow-source-bank {
background: #2e4993;
}
.workflow-complete {
width: 12px;
height: 12px;
color: #63806d;
}
@media (max-width: 1100px) and (min-width: 761px) {
.workflow-cards {
gap: 16px;
}
.workflow-card {
padding: 16px;
}
.workflow-preview {
padding: 10px;
}
.workflow-modules {
grid-template-columns: 1fr;
gap: 5px;
}
.workflow-module {
min-height: 26px;
}
.workflow-queue small {
display: none;
}
}
@media (max-width: 760px) {
.workflow-cards {
grid-template-columns: 1fr;
gap: 16px;
}
.workflow-card {
padding: 24px;
}
.workflow-card-heading p {
min-height: 0;
}
.workflow-preview {
min-height: 180px;
}
.workflow-module,
.workflow-queue li {
font-size: 12px;
}
.workflow-queue small {
font-size: 10px;
}
}
@media (max-width: 380px) {
.workflow-card {
padding: 18px;
}
.workflow-preview {
padding: 10px;
}
.workflow-module {
gap: 4px;
font-size: 10px;
}
.workflow-queue li {
font-size: 11px;
}
.workflow-queue small {
display: none;
}
}
tests/agent-controller.test.mjs
Read raw text
import test from "node:test";
import assert from "node:assert/strict";
import { initializeAgent } from "../src/scripts/agent.js";
function eventTarget(extra = {}) {
const listeners = new Map();
return {
...extra,
addEventListener: (key, fn) => listeners.set(key, fn),
removeEventListener: (key) => listeners.delete(key),
emit: (key, event = {}) => listeners.get(key)?.(event),
};
}
function fixture(search = "", reduced = false) {
let intersection;
let serial = 0;
let disconnected = false;
const frames = new Map();
const replay = eventTarget({ disabled: false });
const announcement = { textContent: "" };
const fields = [1, 1, 2, 2, 2, 2, 2, 2, 3].map((group) => ({
dataset: { agentField: String(group) },
}));
const slots = new Map(
["assignment", "queue-note", "status"].map((name) => [
`[data-agent-${name}]`,
{ textContent: "" },
]),
);
const root = {
dataset: {},
parentElement: {
querySelector: (selector) =>
selector === "[data-agent-replay]" ? replay : announcement,
},
querySelector: (selector) =>
selector === "[data-agent-replay]" ? replay : slots.get(selector),
querySelectorAll: () => fields,
};
const media = eventTarget({ matches: reduced });
const document = eventTarget({ hidden: false });
const env = {
location: { search },
innerHeight: 800,
document,
matchMedia: () => media,
requestAnimationFrame: (callback) => {
frames.set(++serial, callback);
return serial;
},
cancelAnimationFrame: (id) => frames.delete(id),
IntersectionObserver: class {
constructor(callback) {
intersection = callback;
}
observe() {}
disconnect() {
disconnected = true;
}
},
};
const dispose = initializeAgent(root, env);
return {
root,
replay,
announcement,
fields,
slots,
media,
document,
dispose,
frames: () => frames.size,
disconnected: () => disconnected,
visible(height = 600, shown = 600) {
intersection([
{
isIntersecting: shown > 0,
boundingClientRect: { height },
intersectionRect: { height: shown },
},
]);
},
tick(time) {
const item = frames.entries().next().value;
if (item) {
frames.delete(item[0]);
item[1](time);
}
},
};
}
test("controller renders grouped fields once, pauses offscreen/hidden, and replays", () => {
const app = fixture();
assert.equal(app.root.dataset.agentPhase, "queued");
assert.equal(app.frames(), 0);
assert.ok(app.fields.every((field) => field.dataset.filled === "false"));
app.visible();
app.tick(0);
app.tick(800);
assert.equal(app.root.dataset.agentPhase, "assigned");
app.visible(600, 200);
assert.equal(app.frames(), 0);
app.tick(9000);
app.visible();
app.tick(10000);
app.tick(10900);
assert.equal(app.root.dataset.agentPhase, "filling");
assert.equal(
app.fields.filter((field) => field.dataset.filled === "true").length,
2,
);
app.document.hidden = true;
app.document.emit("visibilitychange");
assert.equal(app.frames(), 0);
app.document.hidden = false;
app.document.emit("visibilitychange");
app.tick(50000);
app.tick(51100);
assert.equal(
app.fields.filter((field) => field.dataset.filled === "true").length,
8,
);
app.tick(52100);
assert.ok(app.fields.every((field) => field.dataset.filled === "true"));
app.tick(53300);
assert.equal(app.root.dataset.agentPhase, "review");
assert.equal(app.frames(), 0);
assert.equal(app.replay.disabled, false);
app.visible(600, 0);
app.visible();
assert.equal(
app.frames(),
0,
"completed animation does not restart on reentry",
);
app.replay.emit("click");
assert.equal(app.root.dataset.agentPhase, "queued");
assert.equal(app.frames(), 1);
app.dispose();
assert.equal(app.frames(), 0);
assert.equal(app.disconnected(), true);
});
test("capture states remain fixed and replay cannot trigger animation", () => {
for (const phase of ["queued", "assigned", "filling", "review"]) {
const app = fixture(`?capture=1&phase=${phase}`);
app.visible();
app.replay.emit("click");
app.tick(10000);
assert.equal(app.root.dataset.agentPhase, phase);
assert.equal(app.frames(), 0);
assert.equal(app.replay.disabled, true);
app.dispose();
}
});
test("reduced motion is settled on load and when preference changes during playback", () => {
const reduced = fixture("", true);
assert.equal(reduced.root.dataset.agentPhase, "review");
reduced.visible();
assert.equal(reduced.frames(), 0);
reduced.dispose();
const app = fixture();
app.visible(1100, 399);
assert.equal(app.frames(), 0);
app.visible(1100, 400);
app.tick(0);
app.tick(1800);
app.media.emit("change", { matches: false });
assert.equal(app.root.dataset.agentPhase, "filling");
app.media.emit("change", { matches: true });
assert.equal(app.root.dataset.agentPhase, "review");
assert.equal(app.frames(), 0);
assert.equal(app.replay.disabled, true);
app.dispose();
});
tests/agent-state.test.mjs
Read raw text
import test from "node:test";
import assert from "node:assert/strict";
import {
createAgentState,
advanceAgent,
replayAgent,
describeAgent,
eligibleAgentVisibility,
} from "../src/scripts/agent-state.mjs";
test("agent progresses through grouped fields and stops at review", () => {
const queued = createAgentState("");
assert.equal(describeAgent(queued).phase, "queued");
const assigned = advanceAgent(queued, 800, true);
assert.equal(describeAgent(assigned).phase, "assigned");
const filling = advanceAgent(assigned, 900, true);
assert.equal(describeAgent(filling).phase, "filling");
assert.equal(describeAgent(filling).groups, 1);
assert.equal(describeAgent(advanceAgent(filling, 1100, true)).groups, 2);
assert.equal(describeAgent(advanceAgent(filling, 2100, true)).groups, 3);
const done = advanceAgent(filling, 9999, true);
assert.deepEqual(describeAgent(done), { phase: "review", groups: 3 });
assert.equal(done.elapsed, 5000);
assert.deepEqual(advanceAgent(done, 500, true), done);
assert.equal(queued.elapsed, 0);
});
test("offscreen and hidden states do not consume elapsed time; replay resets", () => {
const running = advanceAgent(createAgentState(""), 1700, true);
assert.deepEqual(advanceAgent(running, 3000, false), running);
assert.deepEqual(advanceAgent(running, -1, true), running);
assert.deepEqual(advanceAgent(running, NaN, true), running);
assert.deepEqual(advanceAgent(running, Infinity, true), running);
assert.equal(replayAgent(running).elapsed, 0);
});
test("capture accepts deterministic phases, ignores invalid phases and cannot replay", () => {
for (const phase of ["queued", "assigned", "filling", "review"]) {
const state = createAgentState("?capture=1&phase=" + phase);
assert.equal(describeAgent(state).phase, phase);
assert.deepEqual(advanceAgent(state, 9000, true), state);
assert.deepEqual(replayAgent(state), state);
}
assert.equal(
describeAgent(createAgentState("?capture=1&phase=bad")).phase,
"review",
);
assert.equal(describeAgent(createAgentState("?capture=1")).phase, "review");
assert.equal(
describeAgent(createAgentState("?phase=review")).phase,
"queued",
);
});
test("reduced motion is a settled review even when capture phase is specified", () => {
const state = createAgentState("?capture=1&phase=queued", true);
assert.equal(describeAgent(state).phase, "review");
assert.deepEqual(replayAgent(state), state);
});
test("visibility threshold is half the stage or viewport for tall mobile layouts", () => {
assert.equal(eligibleAgentVisibility(600, 299, 900), false);
assert.equal(eligibleAgentVisibility(600, 300, 900), true);
assert.equal(eligibleAgentVisibility(1100, 400, 800), true);
assert.equal(eligibleAgentVisibility(1100, 399, 800), false);
assert.equal(eligibleAgentVisibility(0, 0, 800), false);
});
tests/build.test.mjs
Read raw text
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { build } from "../scripts/build.mjs";
import test from "node:test";
import assert from "node:assert/strict";
import { readFile, readdir } from "node:fs/promises";
import { fileURLToPath } from "node:url";
const root = fileURLToPath(new URL("../", import.meta.url));
test("production output contains every composed section and only runtime assets", async (t) => {
const output = await mkdtemp(join(tmpdir(), "dualentry-build-"));
t.after(() => rm(output, { recursive: true, force: true }));
const generated = pathToFileURL(output + "/");
await build(output);
const html = await readFile(new URL("index.html", generated), "utf8");
const sections = [...html.matchAll(/data-capture-section="([^"]+)"/g)].map(
(x) => x[1],
);
assert.deepEqual(sections, [
"hero",
"workflow",
"video",
"selector",
"agent",
"prepared",
"context",
"progression",
"stats",
"modules",
"closing",
]);
assert.ok(!html.includes('class="eyebrow"'));
assert.ok(!html.includes("data-view-choice"));
assert.ok(!html.includes("data-step-choice"));
const ids = [...html.matchAll(/\sid="([^"]+)"/g)].map((x) => x[1]);
assert.equal(
new Set(ids).size,
ids.length,
"IDs must be unique after composition",
);
for (const match of html.matchAll(/(?:src|href)="([^"#]+)"/g)) {
if (!match[1].startsWith("http"))
await readFile(
new URL(
match[1].endsWith("/") ? match[1] + "index.html" : match[1],
generated,
),
);
}
const files = await readdir(generated);
for (const unwanted of [
"sections",
"tests",
"captures",
"README.md",
".vercel",
])
assert.ok(!files.includes(unwanted));
assert.ok(!html.includes("<!-- SECTIONS -->"));
for (const file of files.filter((name) => name.endsWith(".css"))) {
const css = await readFile(new URL(file, generated), "utf8");
assert.equal(
(css.match(/\/\*/g) || []).length,
(css.match(/\*\//g) || []).length,
`${file} has unbalanced comments`,
);
assert.ok(
!css.replace(/\/\*[\s\S]*?\*\//g, "").includes("/*"),
`${file} has an unterminated comment`,
);
}
});
tests/focus-scenes.test.mjs
Read raw text
import test from "node:test";
import assert from "node:assert/strict";
import {
chooseFocus,
advanceExample,
initializeFocusScenes,
} from "../src/scripts/scenes-focus.js";
test("focus has distinct meaningful task content for every supported selection", () => {
const cards = ["all", "approvals", "connections", "agents"].map(chooseFocus);
assert.equal(new Set(cards.map((card) => card.title)).size, 4);
assert.ok(cards.every((card) => card.title && card.source && card.action));
});
test("unknown focus safely falls back to all tasks without modifying existing data", () => {
const first = chooseFocus("all");
assert.deepEqual(chooseFocus("__proto__"), first);
assert.notEqual(chooseFocus("all"), first);
});
test("example explicitly cycles pending, reviewed, and a new pending example", () => {
const first = advanceExample(0);
assert.equal(first.step, 1);
assert.equal(first.status, "Reviewed");
const second = advanceExample(first.step);
assert.equal(second.step, 2);
assert.equal(second.status, "Pending review");
assert.notEqual(second.title, first.title);
assert.equal(advanceExample(3).step, 0);
});
test("invalid update state recovers to first example", () => {
assert.equal(advanceExample(NaN).step, 0);
assert.equal(advanceExample(-2).step, 0);
assert.equal(advanceExample("1").step, 0);
});
function element(queries = {}) {
return {
dataset: {},
attributes: {},
textContent: "",
firstChild: { textContent: "" },
events: {},
animations: [],
querySelector(selector) {
return queries[selector] ?? null;
},
querySelectorAll(selector) {
return queries[selector] ?? [];
},
setAttribute(key, value) {
this.attributes[key] = value;
},
getAttribute(key) {
return this.attributes[key];
},
addEventListener(key, callback) {
this.events[key] = callback;
},
removeEventListener(key) {
delete this.events[key];
},
animate(frames, options) {
this.animations.push({ frames, options });
return { cancel() {} };
},
getAnimations() {
return [{ cancel() {} }];
},
};
}
function illustrationFixture() {
const choices = ["all", "approvals", "connections", "agents"].map((key) => {
const button = element();
button.dataset.selChoice = key;
button.attributes["aria-pressed"] = String(key === "approvals");
return button;
});
const stageFields = Object.fromEntries(
["source", "label", "count", "title", "time", "action", "icon"].map(
(field) => [`[data-sel-${field}]`, element()],
),
);
const mainCard = element();
const stage = element({ ...stageFields, ".sel-main-card": mainCard });
const selector = element({
"[data-sel-choice]": choices,
"#selector-stage": stage,
});
const controlFields = Object.fromEntries(
["source", "status", "title", "outcome"].map((field) => [
`[data-control-${field}]`,
element(),
]),
);
const update = element();
const approvalCard = element();
const controlFrame = element();
const control = element({
...controlFields,
"[data-control-update]": update,
"#control-example": controlFrame,
".control-approval-card": approvalCard,
});
const document = {
documentElement: { dataset: {} },
getElementById: (id) => ({ selector, control })[id],
};
return {
document,
choices,
stage,
stageFields,
mainCard,
controlFrame,
controlFields,
update,
approvalCard,
};
}
test("bound selection updates accessible state, task content, and only pointer motion", async () => {
const fixture = illustrationFixture();
const originalDocument = globalThis.document;
const originalMatch = globalThis.matchMedia;
let reducedMotion = false;
globalThis.document = fixture.document;
globalThis.matchMedia = () => ({
get matches() {
return reducedMotion;
},
addEventListener() {},
removeEventListener() {},
});
try {
const dispose = initializeFocusScenes(fixture.document, {
matchMedia: globalThis.matchMedia,
});
fixture.choices[2].events.click({ detail: 1 });
assert.equal(fixture.choices[2].attributes["aria-pressed"], "true");
assert.equal(fixture.choices[1].attributes["aria-pressed"], "false");
assert.equal(
fixture.stageFields["[data-sel-title]"].textContent,
"Reconnect to see recent transactions",
);
assert.equal(
fixture.stageFields["[data-sel-icon]"].attributes.href,
"#i-sync",
);
assert.equal(fixture.mainCard.animations.length, 1);
fixture.choices[2].events.click({ detail: 1 });
assert.equal(
fixture.mainCard.animations.length,
1,
"selecting the same category does not replay motion",
);
fixture.choices[3].events.click({ detail: 0 });
assert.equal(fixture.stage.dataset.focus, "agents");
assert.equal(
fixture.mainCard.animations.length,
1,
"keyboard activation settles immediately",
);
fixture.document.documentElement.dataset.capture = "true";
fixture.choices[0].events.click({ detail: 1 });
assert.equal(
fixture.mainCard.animations.length,
1,
"capture mode suppresses motion",
);
fixture.document.documentElement.dataset.capture = "false";
reducedMotion = true;
fixture.choices[1].events.click({ detail: 1 });
assert.equal(
fixture.mainCard.animations.length,
1,
"reduced motion suppresses motion",
);
fixture.update.events.click({ detail: 0 });
assert.equal(fixture.controlFrame.dataset.reviewState, "reviewed");
assert.equal(
fixture.controlFields["[data-control-status]"].textContent,
"Reviewed",
);
assert.equal(fixture.update.firstChild.textContent, "Next");
fixture.update.events.click({ detail: 1 });
assert.equal(fixture.controlFrame.dataset.reviewState, "pending");
assert.equal(
fixture.controlFields["[data-control-source]"].textContent,
"Daily activity",
);
assert.equal(fixture.approvalCard.animations.length, 0);
reducedMotion = false;
fixture.update.events.click({ detail: 1 });
assert.equal(fixture.approvalCard.animations.length, 1);
fixture.update.events.click({ detail: 1 });
assert.equal(
fixture.controlFields["[data-control-source]"].textContent,
"Bank Recommendations",
);
dispose();
assert.equal(fixture.update.events.click, undefined);
assert.ok(fixture.choices.every((button) => !button.events.click));
} finally {
globalThis.document = originalDocument;
globalThis.matchMedia = originalMatch;
}
});
tests/portability.test.mjs
Read raw text
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { build } from "../scripts/build.mjs";
import test from "node:test";
import assert from "node:assert/strict";
import { readFile, readdir } from "node:fs/promises";
const root = new URL("../", import.meta.url);
test("portable build isolates runtime output and preserves authored source", async (t) => {
const before = await readFile(new URL("src/page.html", root), "utf8");
const output = await mkdtemp(join(tmpdir(), "dualentry-build-"));
t.after(() => rm(output, { recursive: true, force: true }));
const generated = pathToFileURL(output + "/");
await build(output);
assert.equal(await readFile(new URL("src/page.html", root), "utf8"), before);
const html = await readFile(new URL("index.html", generated), "utf8");
assert.equal((html.match(/rel="stylesheet"/g) || []).length, 1);
assert.equal((html.match(/<script /g) || []).length, 1);
assert.ok(html.includes("scripts/main.js"));
for (const text of [
"Less searching.",
"One connected workflow.",
"The source. The work. The next step.",
"Example workflow ·",
"Stay focused.",
"01 / Work comes together",
])
assert.ok(!html.includes(text), text);
for (const path of [
"src",
"tests",
"captures",
"node_modules",
"package.json",
".vercel",
"build.mjs",
])
assert.ok(!(await readdir(generated)).includes(path));
assert.ok(html.includes('data-capture-section="video"'));
assert.ok(html.includes('id="video-title"'));
});
tests/reference-layout.test.mjs
Read raw text
import test from "node:test";
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import { sectionOrder } from "../src/scripts/section-order.mjs";
const read = (name) =>
readFile(
new URL("../src/sections/" + name + ".html", import.meta.url),
"utf8",
);
test("reference layout keeps the requested sequence and puts hero actions above its board", async () => {
assert.deepEqual(sectionOrder, [
"hero",
"workflow",
"video",
"selector",
"agent",
"prepared",
"context",
"progression",
"stats",
"modules",
"closing",
]);
const hero = await read("hero");
assert.ok(hero.indexOf("splash-actions") < hero.indexOf("splash-screen"));
const video = await read("video");
assert.ok(video.includes("Insert video here"));
assert.ok(!video.includes("<iframe"));
const stats = await read("stats");
for (const value of [
"90",
"95",
"$0",
"/customers/slash",
"/customers/graphite",
])
assert.ok(stats.includes(value));
assert.ok(!stats.includes("task-card"));
const workflow = await read("workflow");
for (const text of [
"A scavenger hunt across modules",
"Scattered",
"Self-resolving",
])
assert.ok(workflow.includes(text));
});
tests/runtime-controllers.test.mjs
Read raw text
import test from "node:test";
import assert from "node:assert/strict";
import { initializeInteractions } from "../src/scripts/interactions.js";
import { initializeFlowScenes } from "../src/scripts/scenes-flow.js";
import { applyCapture, initializePage } from "../src/scripts/main.js";
function node(queries = {}) {
const listeners = new Map();
const classes = new Set();
return {
dataset: {},
attributes: {},
queries,
animations: [],
textContent: "",
focused: false,
classList: {
toggle(key, on) {
if (on) classes.add(key);
else classes.delete(key);
},
contains: (key) => classes.has(key),
},
querySelector(selector) {
return this.queries[selector] ?? null;
},
querySelectorAll(selector) {
return this.queries[selector] ?? [];
},
getElementById(id) {
return this.queries[id] ?? null;
},
getAttribute(key) {
return this.attributes[key];
},
setAttribute(key, value) {
this.attributes[key] = value;
},
addEventListener(key, fn) {
listeners.set(key, [...(listeners.get(key) ?? []), fn]);
},
removeEventListener(key, fn) {
listeners.set(
key,
(listeners.get(key) ?? []).filter((item) => item !== fn),
);
},
emit(key, event = {}) {
(listeners.get(key) ?? []).forEach((fn) => fn(event));
},
listenerCount() {
return [...listeners.values()].reduce(
(sum, list) => sum + list.length,
0,
);
},
focus() {
this.focused = true;
},
getAnimations() {
return this.animations;
},
animate() {
const item = {
cancelled: false,
cancel() {
this.cancelled = true;
},
finished: Promise.resolve(),
};
this.animations.push(item);
return item;
},
getBoundingClientRect() {
return { left: 0, top: 0, width: 200, height: 120 };
},
prepend(card) {
this.card = card;
},
};
}
function environment(document, search = "") {
const media = node();
media.matches = false;
document.documentElement = { dataset: {} };
return { document, location: { search }, matchMedia: () => media, media };
}
function button(attribute, value) {
const item = node();
item.setAttribute(attribute, value);
return item;
}
test("filter, context, and navigation retain keyboard state and detach listeners", () => {
const filterButtons = ["all", "waiting", "connections"].map((value) =>
button("data-filter-choice", value),
);
const contextButtons = ["source", "work", "action"].map((value) =>
button("data-context-choice", value),
);
const rows = ["waiting", "connections", "waiting"].map((kind) => {
const item = node();
item.dataset.taskKind = kind;
return item;
});
const filter = node({ "[data-task-kind]": rows });
filter.querySelectorAll = (selector) =>
selector.endsWith(":not(.is-muted)")
? rows.filter((row) => !row.classList.contains("is-muted"))
: rows;
const summary = node();
const menu = node({ summary });
const toggle = node();
const nav = node();
const context = node();
const label = node();
const count = node();
const document = node({
"[data-filter-choice]": filterButtons,
"[data-context-choice]": contextButtons,
"filter-demo": filter,
"context-demo": context,
"filter-current-label": label,
"filter-result-count": count,
"status-menu": menu,
"[data-menu-toggle]": toggle,
"site-nav": nav,
});
const env = environment(document);
const dispose = initializeInteractions(document, env);
assert.equal(filter.dataset.filter, "waiting");
filterButtons[0].emit("click", { detail: 0 });
filterButtons[1].emit("click", { detail: 1 });
assert.equal(count.textContent, "2 tasks in focus");
assert.equal(label.textContent, "Waiting on you");
assert.equal(filter.animations.length, 1);
assert.equal(summary.focused, true);
menu.open = true;
filterButtons[1].emit("click", { detail: 1 });
assert.equal(menu.open, false);
contextButtons[1].emit("click", { detail: 0 });
assert.equal(context.dataset.context, "work");
assert.equal(context.animations.length, 0);
toggle.emit("click");
assert.equal(toggle.getAttribute("aria-expanded"), "true");
document.emit("keydown", { key: "x" });
assert.equal(nav.classList.contains("is-open"), true);
document.emit("keydown", { key: "Escape" });
assert.equal(toggle.focused, true);
toggle.emit("click");
nav.emit("click", { target: { closest: () => null } });
assert.equal(toggle.getAttribute("aria-expanded"), "true");
nav.emit("click", { target: { closest: () => ({}) } });
assert.equal(toggle.getAttribute("aria-expanded"), "false");
menu.open = true;
document.emit("keydown", { key: "Escape" });
assert.equal(menu.open, false);
env.media.matches = true;
env.media.emit("change");
assert.equal(filter.animations[0].cancelled, true);
filterButtons[2].emit("click", { detail: 1 });
assert.equal(filter.animations.length, 1);
dispose();
assert.equal(document.listenerCount(), 0);
assert.ok(
[...filterButtons, ...contextButtons, toggle, nav].every(
(item) => item.listenerCount() === 0,
),
);
});
test("panorama moves, resets, announces status, and cleans up; progression stays selectable", () => {
const controls = ["all", "waiting", "invalid"].map((value) => {
const item = node();
item.dataset.panoChoice = value;
return item;
});
const progressButtons = ["see", "review", "move", "invalid"].map((value) => {
const item = node();
item.dataset.progressChoice = value;
return item;
});
const progress = node({ "[data-progress-choice]": progressButtons });
const move = node();
const card = node();
const done = node();
const waiting = node();
const viewport = node();
viewport.scrollWidth = 800;
viewport.clientWidth = 200;
viewport.scrollLeft = 0;
const column = node();
column.getBoundingClientRect = () => ({ left: 250 });
const fields = Object.fromEntries(
[
'[data-pano-count="waiting"]',
'[data-pano-count="control"]',
'[data-pano-count="done"]',
"[data-pano-button-label]",
"[data-pano-task-title]",
"[data-pano-task-badge]",
"[data-pano-status]",
"[data-pano-empty]",
].map((key) => [key, node()]),
);
const panorama = node({
...fields,
"[data-pano-choice]": controls,
"[data-pano-move]": move,
"#pano-moving-task": card,
'[data-pano-destination="done"]': done,
'[data-pano-destination="waiting"]': waiting,
".pano-scroll": viewport,
".pano-col-done": column,
".pano-col-waiting": column,
".pano-col-todo": column,
});
panorama.dataset.panoState = "waiting";
const document = node({
"[data-progress-stage]": [progress],
"[data-pano-state]": [panorama],
});
const env = environment(document);
const dispose = initializeFlowScenes(document, env);
controls[1].emit("click", { detail: 1 });
assert.equal(panorama.dataset.panoFocus, "waiting");
controls[2].emit("click", { detail: 1 });
assert.equal(panorama.dataset.panoFocus, "waiting");
move.emit("click", { detail: 1 });
assert.equal(done.card, card);
assert.equal(panorama.dataset.panoState, "done");
assert.equal(fields['[data-pano-count="done"]'].textContent, "2");
assert.match(fields["[data-pano-status]"].textContent, /moved to Done/);
assert.equal(card.animations.length, 1);
move.emit("click", { detail: 0 });
assert.equal(waiting.card, card);
assert.equal(fields["[data-pano-empty]"].hidden, true);
assert.equal(card.animations.length, 1);
progressButtons[1].emit("click", { detail: 0 });
assert.equal(progress.dataset.progressStage, "review");
assert.equal(progress.dataset.flowMotion, "false");
progressButtons[3].emit("click", { detail: 1 });
assert.equal(progress.dataset.progressStage, "review");
document.documentElement.dataset.capture = "true";
progressButtons[2].emit("click", { detail: 1 });
assert.equal(progress.dataset.flowMotion, "false");
viewport.scrollWidth = 100;
controls[0].emit("click", { detail: 1 });
panorama.queries["#pano-moving-task"] = null;
move.emit("click", { detail: 1 });
dispose();
assert.equal(move.listenerCount(), 0);
assert.ok(
[...controls, ...progressButtons].every(
(item) => item.listenerCount() === 0,
),
);
});
test("capture is applied before optional scenes initialize and an empty page disposes safely", () => {
const hero = node();
hero.dataset.captureSection = "hero";
const agent = node();
agent.dataset.captureSection = "agent";
const document = node({ "[data-capture-section]": [hero, agent] });
const env = environment(document, "?capture=1§ion=agent");
const dispose = initializePage(document, env);
assert.equal(document.documentElement.dataset.capture, "true");
assert.equal(hero.hidden, true);
assert.equal(agent.hidden, false);
dispose();
assert.equal(document.listenerCount(), 0);
assert.equal(env.media.listenerCount(), 0);
const ordinary = node();
environment(ordinary);
applyCapture(ordinary, "");
assert.deepEqual(ordinary.documentElement.dataset, {});
});
tests/source-export.test.mjs
Read raw text
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import {
mkdir,
mkdtemp,
readFile,
rm,
symlink,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { publishSource } from "../scripts/publish-source.mjs";
async function fixture(t) {
const root = await mkdtemp(join(tmpdir(), "source-export-"));
t.after(() => rm(root, { recursive: true, force: true }));
for (const folder of ["src", "scripts", "tests", "assets", ".vercel", "dist"])
await mkdir(join(root, folder));
await writeFile(
join(root, "src/page.html"),
'<main title="example">Hello & goodbye</main>',
);
await writeFile(
join(root, "scripts/build.mjs"),
"export const build = true;\n",
);
await writeFile(join(root, "tests/example.test.mjs"), "// Portable test\n");
await writeFile(join(root, "assets/logo.svg"), "<svg></svg>");
await writeFile(join(root, "package.json"), '{"private":true}');
await writeFile(join(root, "README.md"), "# Setup\n");
await writeFile(join(root, ".env"), "PRIVATE=never-export");
await writeFile(join(root, ".vercel/project.json"), "private-project");
await writeFile(join(root, "src/.env"), "PRIVATE=never-export");
await writeFile(join(root, "dist/old.js"), "old-output");
return root;
}
test("source publication exposes portable text and local asset hashes only", async (t) => {
const root = await fixture(t);
await symlink(join(root, ".env"), join(root, "src/linked.js"));
const destination = join(root, "dist");
await publishSource(root, destination);
const manifest = JSON.parse(
await readFile(join(destination, "source/manifest.json")),
);
const bundle = JSON.parse(
await readFile(join(destination, "source/bundle.json")),
);
assert.equal(manifest.version, 1);
assert.deepEqual(
manifest.files.map(({ path }) => path),
[
"README.md",
"package.json",
"scripts/build.mjs",
"src/page.html",
"tests/example.test.mjs",
],
);
for (const entry of manifest.files) {
const raw = await readFile(join(destination, entry.url));
assert.equal(entry.bytes, raw.length);
assert.equal(entry.sha256, createHash("sha256").update(raw).digest("hex"));
assert.equal(
bundle.files.find(({ path }) => path === entry.path).content,
raw.toString("utf8"),
);
}
assert.equal(manifest.assets[0].url, "assets/logo.svg");
assert.deepEqual(bundle.assets, manifest.assets);
const index = await readFile(join(destination, "source/index.html"), "utf8");
assert.ok(index.includes("<main title="example">"));
assert.ok(index.includes("Hello & goodbye"));
assert.ok(!index.includes("<main title="));
for (const secret of ["never-export", "private-project", "old-output", root])
assert.ok(!JSON.stringify(bundle).includes(secret));
});
test("source publication refuses machine-specific paths in publishable text", async (t) => {
const root = await fixture(t);
await writeFile(
join(root, "src/local.js"),
`const file = '${"/" + "Users/"}someone/private';`,
);
await assert.rejects(
publishSource(root, join(root, "dist")),
/machine-specific path.*src\/local.js/i,
);
});
test("allowlisted roots cannot follow a symlink into private files", async (t) => {
const root = await fixture(t);
await rm(join(root, "scripts"), { recursive: true });
await symlink(join(root, ".vercel"), join(root, "scripts"));
const manifest = await publishSource(root, join(root, "dist"));
assert.ok(!manifest.files.some(({ path }) => path.startsWith("scripts/")));
});
tests/splash-motion-controller.test.mjs
Read raw text
import test from "node:test";
import assert from "node:assert/strict";
import { initializeSplash } from "../src/scripts/splash-motion.js";
function target(extra = {}) {
const listeners = new Map();
return {
...extra,
addEventListener: (name, fn) => listeners.set(name, fn),
removeEventListener: (name) => listeners.delete(name),
emit: (name, event = {}) => listeners.get(name)?.(event),
};
}
function fixture(search = "", reduced = false, compact = false) {
let intersection, resize;
let serial = 0;
const frames = new Map();
const rect = (x, y, width, height) => ({
x,
y,
left: x,
top: y,
width,
height,
});
const node = (bounds) => ({
bounds,
style: {},
textContent: "",
getBoundingClientRect() {
return this.bounds;
},
});
const board = {
...node(rect(100, 100, 1000, 600)),
append(...children) {
this.children = children;
},
};
const slots = Array.from({ length: 3 }, (_, index) =>
node(
rect(
compact ? 112 : 120 + index * 300,
compact ? 124 + index * 180 : 154,
compact ? 290 : 250,
140,
),
),
);
const statuses = Array.from({ length: 3 }, () => node());
const cards = statuses.map((status, index) => ({
...node(rect(0, 0, 250, 140 + index * 5)),
querySelector: () => status,
}));
const attributes = new Map();
const toggle = target({
disabled: false,
setAttribute: (name, value) => attributes.set(name, value),
});
const nodes = new Map([
[".kanban", board],
[".splash-screen", node()],
["[data-splash-toggle]", toggle],
]);
const root = {
dataset: {},
querySelector: (selector) => nodes.get(selector),
querySelectorAll: (selector) =>
selector === "[data-splash-card]" ? cards : slots,
};
const media = target({ matches: reduced });
const document = target({ hidden: false });
const env = {
location: { search },
innerHeight: 800,
document,
matchMedia: () => media,
requestAnimationFrame: (callback) => {
frames.set(++serial, callback);
return serial;
},
cancelAnimationFrame: (id) => frames.delete(id),
IntersectionObserver: class {
constructor(callback) {
intersection = callback;
}
observe() {}
disconnect() {}
},
ResizeObserver: class {
constructor(callback) {
resize = callback;
}
observe() {}
disconnect() {}
},
};
const dispose = initializeSplash(root, env);
return {
root,
cards,
slots,
statuses,
toggle,
attributes,
media,
document,
dispose,
frames: () => frames.size,
visible(shown = 600) {
intersection([
{
isIntersecting: shown > 0,
boundingClientRect: { height: 600 },
intersectionRect: { height: shown },
},
]);
},
tick(time) {
const item = frames.entries().next().value;
if (item) {
frames.delete(item[0]);
item[1](time);
}
},
resize: () => resize(),
};
}
test("all cards loop continuously, pause offscreen or hidden, and resume without skipped time", () => {
const app = fixture();
assert.equal(app.root.dataset.splashPhase, "queued");
assert.equal(app.frames(), 0);
assert.equal(app.cards[0].style.transform, "translate3d(20px, 54px, 0)");
assert.deepEqual(
app.slots.map((slot) => slot.style.height),
["150px", "150px", "150px"],
);
app.visible();
app.tick(0);
app.tick(4500);
assert.equal(app.root.dataset.splashPhase, "moving");
const pose = app.cards.map((card) => card.style.transform);
app.visible(100);
app.tick(10000);
assert.equal(app.frames(), 0);
assert.deepEqual(
app.cards.map((card) => card.style.transform),
pose,
);
app.visible();
app.tick(11000);
assert.deepEqual(
app.cards.map((card) => card.style.transform),
pose,
);
app.document.hidden = true;
app.document.emit("visibilitychange");
assert.equal(app.frames(), 0);
app.document.hidden = false;
app.document.emit("visibilitychange");
app.tick(50000);
app.tick(50700);
assert.equal(app.root.dataset.splashPhase, "review");
assert.equal(app.cards[0].style.transform, "translate3d(320px, 54px, 0)");
assert.deepEqual(
app.statuses.map((status) => status.textContent),
["In progress", "To do", "For review"],
);
assert.equal(app.frames(), 1);
app.tick(81500);
assert.equal(app.cards[0].style.transform, "translate3d(20px, 54px, 0)");
assert.equal(app.frames(), 1);
app.dispose();
assert.equal(app.frames(), 0);
});
test("phone cards move between real vertical anchors and remeasure after width changes", () => {
const app = fixture("", false, true);
assert.equal(app.root.dataset.splashCompact, "true");
assert.equal(app.cards[0].style.transform, "translate3d(12px, 24px, 0)");
app.visible();
app.tick(0);
app.tick(4500);
assert.equal(app.cards[0].style.transform, "translate3d(18px, 114px, 0)");
app.slots.forEach((slot) => {
slot.bounds = { ...slot.bounds, width: 250 };
});
app.resize();
assert.equal(app.cards[0].style.width, "250px");
app.tick(5200);
assert.equal(app.cards[0].style.transform, "translate3d(12px, 204px, 0)");
assert.equal(app.cards[1].style.transform, "translate3d(12px, 24px, 0)");
app.dispose();
});
test("capture and reduced motion lock playback, and switching motion preference back resumes", () => {
for (const phase of ["queued", "moving", "review"]) {
const app = fixture(`?capture=1§ion=hero&phase=${phase}`);
app.visible();
app.toggle.emit("click");
app.tick(9999);
assert.equal(app.root.dataset.splashPhase, phase);
assert.equal(app.frames(), 0);
assert.equal(app.toggle.disabled, true);
app.media.emit("change", { matches: false });
assert.equal(app.frames(), 0);
app.dispose();
}
const app = fixture("", true);
assert.equal(app.root.dataset.splashPhase, "review");
app.visible();
assert.equal(app.frames(), 0);
app.media.emit("change", { matches: false });
assert.equal(app.frames(), 1);
assert.equal(app.toggle.disabled, false);
app.tick(0);
app.tick(5000);
app.media.emit("change", { matches: true });
assert.equal(app.root.dataset.splashPhase, "review");
assert.equal(app.frames(), 0);
assert.equal(app.toggle.disabled, true);
app.dispose();
});
test("accessible pause control freezes in place and preserves user pause across visibility changes", () => {
const app = fixture();
app.visible();
app.tick(0);
app.tick(4500);
const pose = app.cards[0].style.transform;
app.toggle.emit("click");
assert.equal(app.frames(), 0);
assert.equal(app.attributes.get("aria-label"), "Resume task animation");
assert.equal(app.attributes.get("aria-pressed"), "true");
app.visible(0);
app.visible();
assert.equal(app.frames(), 0);
app.toggle.emit("click");
assert.equal(app.attributes.get("aria-label"), "Pause task animation");
app.tick(20000);
assert.equal(app.cards[0].style.transform, pose);
app.tick(20700);
assert.notEqual(app.cards[0].style.transform, pose);
app.dispose();
});
tests/splash-motion-state.test.mjs
Read raw text
import test from "node:test";
import assert from "node:assert/strict";
import {
createSplashState,
advanceSplash,
setSplashReduced,
describeSplash,
splashPosition,
splashVisible,
} from "../src/scripts/splash-motion-state.mjs";
test("six adjacent swaps return every card to its initial slot without a loop boundary jump", () => {
const initial = Object.freeze(createSplashState());
for (let leg = 0; leg < 6; leg++) {
const held = describeSplash(
advanceSplash(initial, leg * 6000 + 3700, true),
);
const flight = describeSplash(
advanceSplash(initial, leg * 6000 + 4500, true),
);
const settled = describeSplash(
advanceSplash(initial, leg * 6000 + 5300, true),
);
assert.equal(held.cards.filter((card) => card.progress !== 0).length, 0);
assert.equal(
flight.cards.filter((card) => card.progress > 0 && card.progress < 1)
.length,
2,
);
assert.equal(flight.phase, "moving");
assert.equal(new Set(settled.cards.map((card) => card.slot)).size, 3);
const next = describeSplash(advanceSplash(initial, (leg + 1) * 6000, true));
assert.deepEqual(
settled.cards.map((card) => card.slot),
next.cards.map((card) => card.slot),
);
}
assert.deepEqual(
describeSplash(advanceSplash(initial, 36000, true)),
describeSplash(initial),
);
assert.equal(advanceSplash(initial, 36000 * 12 + 1234, true).elapsed, 1234);
assert.equal(initial.elapsed, 0);
});
test("capture phases freeze, unknown phase settles, and reduced motion can resume only outside capture", () => {
for (const phase of ["queued", "moving", "review"]) {
const state = createSplashState(`?capture=1&phase=${phase}`);
assert.equal(describeSplash(state).phase, phase);
assert.deepEqual(advanceSplash(state, 9999, true), state);
assert.equal(setSplashReduced(state, false).locked, true);
}
assert.equal(
describeSplash(createSplashState("?capture=1&phase=bad")).phase,
"review",
);
assert.equal(describeSplash(createSplashState("?capture=1")).phase, "review");
assert.equal(
describeSplash(createSplashState("?phase=review")).phase,
"queued",
);
const reduced = setSplashReduced(createSplashState(), true);
assert.equal(describeSplash(reduced).phase, "review");
assert.equal(reduced.locked, true);
assert.equal(setSplashReduced(reduced, false).locked, false);
});
test("hidden or invalid elapsed time cannot advance playback", () => {
const state = createSplashState();
for (const time of [-1, Infinity, NaN])
assert.deepEqual(advanceSplash(state, time, true), state);
assert.deepEqual(advanceSplash(state, 5000, false), state);
assert.equal(splashVisible(800, 300, 600), true);
assert.equal(splashVisible(800, 299, 600), false);
assert.equal(splashVisible(0, 0, 600), false);
});
test("actual desktop and vertical phone anchors share exact endpoints and bounded passing movement", () => {
for (const [from, to, compact] of [
[{ x: 20, y: 170 }, { x: 620, y: 54 }, false],
[{ x: 12, y: 24 }, { x: 12, y: 224 }, true],
]) {
assert.deepEqual(splashPosition(0, from, to, compact, 1), from);
assert.deepEqual(splashPosition(1, from, to, compact, -1), to);
const midway = splashPosition(0.5, from, to, compact, 1);
assert.equal(midway.x, (from.x + to.x) / 2 + (compact ? 6 : 0));
assert.equal(midway.y, (from.y + to.y) / 2 - (compact ? 0 : 6));
}
});
tests/state.test.mjs
Read raw text
import test from "node:test";
import assert from "node:assert/strict";
import {
createState,
selectState,
isCaptureMode,
getCaptureSection,
} from "../src/scripts/state.mjs";
test("initial product illustrations remain ready for image capture", () => {
assert.deepEqual(createState(), {
filter: "waiting",
context: "source",
});
assert.notEqual(createState(), createState());
});
test("selections return a new state without changing other illustrations", () => {
const original = Object.freeze(createState());
for (const [key, values] of Object.entries({
filter: ["all", "waiting", "connections"],
context: ["source", "work", "action"],
})) {
for (const value of values) {
const next = selectState(original, key, value);
assert.deepEqual(next, { ...original, [key]: value });
assert.notEqual(next, original);
}
}
assert.equal(original.filter, "waiting");
});
test("unknown keys and unsupported values cannot enter product state", () => {
const original = createState();
for (const [key, value] of [
["view", "grid"],
["filter", "review"],
["step", "3"],
["constructor", "bad"],
["__proto__", "bad"],
]) {
assert.deepEqual(selectState(original, key, value), original);
}
});
test("only the explicit capture flag enables a still presentation", () => {
assert.equal(isCaptureMode("?capture=1"), true);
for (const query of ["", "?capture=0", "?capture=true", "?other=1"]) {
assert.equal(isCaptureMode(query), false);
}
});
test("isolated capture only accepts known sections with explicit capture mode", () => {
for (const section of [
"hero",
"workflow",
"video",
"selector",
"agent",
"prepared",
"context",
"progression",
"stats",
"modules",
"closing",
]) {
assert.equal(getCaptureSection(`?capture=1§ion=${section}`), section);
}
for (const search of [
"",
"?section=hero",
"?capture=0§ion=hero",
"?capture=1",
"?capture=1§ion=unknown",
"?capture=1§ion=views",
"?capture=1§ion=recurring",
]) {
assert.equal(getCaptureSection(search), null);
}
});
vercel.json
Read raw text
{"framework":null,"buildCommand":"npm run build","outputDirectory":"dist","headers":[{"source":"/assets/(.*)","headers":[{"key":"Cache-Control","value":"public, max-age=86400"}]}],"name":"dualentry-landing","version":2}
Assets