Deploy to Cloudflare Workers and rebrand to Diagrav
Adds Workers + static-assets configs for the app (app.diagrav.com) and a separate marketing/landing site (diagrav.com), plus the deployment runbook used to stand up Supabase Cloud, Google OAuth, Resend SMTP, and Cloudflare DNS/custom domains. Renames the app from "AV Planner" to "Diagrav" to match the new domain. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
# AV Planner
|
||||
# Diagrav
|
||||
|
||||
Plan out AV/network installs: define devices and their ports, wire them together
|
||||
on a canvas, and get a bill of materials (devices + cables, with lengths) for
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
# Deployment & CI/CD planning notes
|
||||
|
||||
Discussion notes only — nothing here has been implemented yet. Written so a
|
||||
different Claude Code conversation on this repo can pick up where this one
|
||||
left off without re-deriving it. Nothing below is committed to as final; it's
|
||||
a record of what was discussed and concluded, plus open decisions.
|
||||
|
||||
## Hosting shape (discussed, not yet implemented)
|
||||
|
||||
The app is a static frontend (Vite build, no server of its own) + Supabase
|
||||
backend (Postgres, Auth, RLS, one Edge Function). [organized-ideas.md](organized-ideas.md)
|
||||
already commits to this being provider-agnostic (Supabase Cloud, self-hosted
|
||||
Supabase Docker stack, or export the Postgres DB elsewhere — no app code
|
||||
changes needed either way).
|
||||
|
||||
To go live:
|
||||
1. **Backend**: create a Supabase Cloud project, `supabase link` + `supabase db push`
|
||||
to apply the 15 existing migrations, `supabase functions deploy admin-user-action`.
|
||||
Set real Google OAuth credentials for the production redirect URL — right
|
||||
now `supabase/config.toml` and `vite.config.ts` are hardcoded to
|
||||
`127.0.0.1` with `skip_nonce_check = true` for local dev; this needs a
|
||||
production config pass. Needs a real SMTP provider for auth emails
|
||||
(Resend suggested in organized-ideas.md as the default) since Inbucket
|
||||
(local fake mail catcher) doesn't exist in the cloud.
|
||||
2. **Frontend**: `npm run build` → static `dist/`, deploy to any static host
|
||||
(Vercel/Netlify/Cloudflare Pages, or self-hosted). Set
|
||||
`VITE_SUPABASE_URL` / `VITE_SUPABASE_ANON_KEY` as build-time env vars
|
||||
pointing at the real Supabase project (Vite bakes these in at build
|
||||
time — not runtime-configurable without a refactor).
|
||||
|
||||
## Marketing site (`site/`)
|
||||
|
||||
A separate, informational "what is this thing" page lives in `site/` at the
|
||||
repo root — plain hand-written HTML/CSS (`index.html` + `style.css` +
|
||||
`images/`), no build step, not part of the Vite app or its `src/` tree at
|
||||
all. Screenshots under `images/` are real captures from the app (a demo
|
||||
DAWless rig wired up for the purpose), referenced with relative paths, so
|
||||
the folder is fully self-contained.
|
||||
|
||||
Deploys the same way the app's frontend would, just simpler — no env vars to
|
||||
bake in, no build command, just serve the static files as-is. Same free-tier
|
||||
host options apply (Vercel/Netlify/Cloudflare Pages, or self-hosted).
|
||||
|
||||
**Domain purchased: `diagrav.com`.** The app was renamed from "AV Planner" to
|
||||
**Diagrav** to match. The site's nav ("Log in" / "Sign up") and its closing
|
||||
CTA ("Sign up free") now link to `https://app.diagrav.com/` — so the
|
||||
subdomain-split option below is the working assumption baked into the site
|
||||
as of now, even though neither `diagrav.com` nor `app.diagrav.com` is
|
||||
actually deployed anywhere yet. Those links will 404 until the app is
|
||||
actually live at that subdomain; treat this as a leaning, not a final
|
||||
decision — swapping to the path-based option only means changing the few
|
||||
`href`s in `site/index.html`, nothing structural.
|
||||
- **Subdomain split (current assumption)**: bare `diagrav.com` for the
|
||||
marketing site, `app.diagrav.com` for the actual tool — cleanest
|
||||
separation, two hosting targets instead of one.
|
||||
- **Same domain, different path** (marketing at `/`, app at `/app` or
|
||||
similar) — one hosting target, but means the static host needs to route
|
||||
`/` to `site/` and everything else to the Vite `dist/` output, which the
|
||||
simple "just deploy `dist/`" flow above doesn't currently account for.
|
||||
|
||||
Whichever is chosen, this needs its own line in the CI/CD plan below
|
||||
(currently only the app's frontend/backend deploy steps are scoped) — it's a
|
||||
third deployable alongside "frontend" and "backend," not an afterthought
|
||||
folded into the frontend step.
|
||||
|
||||
## Cost / free-tier findings
|
||||
|
||||
Provider: **Supabase**, since the app already codes directly against its
|
||||
SDK/Auth/RLS model (`src/data/Supabase*.ts`) — switching providers would mean
|
||||
rewriting those repositories.
|
||||
|
||||
Current Supabase Free plan limits (confirmed against supabase.com/pricing,
|
||||
not from training data — re-verify if this doc is read much later):
|
||||
|
||||
| Resource | Free limit |
|
||||
|---|---|
|
||||
| Database size | 500 MB |
|
||||
| Egress | 10 GB/mo (5 GB cached + 5 GB uncached) |
|
||||
| Monthly active users | 50,000 |
|
||||
| Edge Function invocations | 500,000/mo |
|
||||
| File storage | 1 GB |
|
||||
| Realtime concurrent connections | 200 |
|
||||
| Inactivity | paused after 1 week idle, max 2 active projects |
|
||||
|
||||
**Conclusion: this app is storage-bound, not bandwidth-bound.** File storage,
|
||||
realtime, and edge function limits are all non-issues (no file uploads, no
|
||||
`supabase.channel(...)` usage anywhere in `src/`, and the one edge function
|
||||
is admin-only/rare). Egress is generous relative to what this app transfers
|
||||
per session. The one limit likely to actually bite is **database size**
|
||||
(500 MB) — driven less by user-count/traffic than by long-term accumulation
|
||||
in `diagrams` + `diagram_snapshots`.
|
||||
|
||||
**Specific risk found**: `SupabaseDiagramRepository.ts` (`maybeWriteSnapshot`,
|
||||
~line 70-93) writes a new version-history snapshot row every 5 minutes of
|
||||
active editing, with **no retention/expiry** — they accumulate indefinitely.
|
||||
A user who edits diagrams regularly for months will grow the DB more than a
|
||||
burst of new signups would. **Suggested follow-up (not yet done): add
|
||||
snapshot retention** — e.g. keep the last N snapshots per diagram, or prune
|
||||
anything older than ~90 days. This is the highest-leverage lever for keeping
|
||||
DB size (and therefore cost/free-tier headroom) under control.
|
||||
|
||||
Rough scale ballpark discussed: solo/small-team use won't come close to any
|
||||
limit for years. Somewhere in the ~300–1,500 regular-active-user range with
|
||||
several diagrams each is roughly where the 500MB DB cap would start to bite,
|
||||
well before the 10GB egress cap or 50k MAU cap would matter. Treat this as a
|
||||
rough estimate, not a measured number — worth checking real diagram JSON
|
||||
size in Studio once there's real usage.
|
||||
|
||||
Estimated cost: **$0/mo** to start (Supabase Free + Vercel/Netlify/Cloudflare
|
||||
Pages free tier + Resend free tier for auth email), **$25/mo** once you want
|
||||
Supabase Pro (no project auto-pause, higher limits across the board).
|
||||
|
||||
## CI/CD plan: Gitea + Woodpecker, pushbutton staging/prod
|
||||
|
||||
Repo is self-hosted on Gitea (`git.halfbinary.net/aarbit/av-planner`) with
|
||||
Woodpecker CI already in use elsewhere. No `.woodpecker.yml` exists yet in
|
||||
this repo.
|
||||
|
||||
**Desired flow**: PR merges to `main` → pipeline runs lint/typecheck/build
|
||||
and stops → a human clicks a "Deploy" button to push to staging, and
|
||||
separately clicks it again to promote to production. Not fully automatic on
|
||||
merge — deploys to both environments are manual/pushbutton.
|
||||
|
||||
**Mechanism (confirmed against current Woodpecker docs)**: Woodpecker has a
|
||||
built-in **deploy event** for exactly this. Enable **"Allow deployments"** in
|
||||
the repo's Woodpecker project settings, and any successful pipeline run gets
|
||||
a **Deploy** button in the UI. Clicking it re-runs that pipeline with
|
||||
`event: deploy` and a `deploy_to` string you type (e.g. `staging` or
|
||||
`production`), exposed to steps as `$CI_PIPELINE_DEPLOY_TARGET`. Steps gate
|
||||
on it, e.g.:
|
||||
|
||||
```yaml
|
||||
steps:
|
||||
- name: deploy-staging
|
||||
when:
|
||||
- event: deploy
|
||||
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "staging"'
|
||||
- name: deploy-prod
|
||||
when:
|
||||
- event: deploy
|
||||
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"'
|
||||
```
|
||||
|
||||
**What needs to exist**:
|
||||
1. **Two Supabase projects** — staging and prod. This exactly fills the free
|
||||
tier's "2 active projects" cap (see above) — no free-tier headroom left
|
||||
for a third project if one is ever wanted later.
|
||||
2. **Two frontend hosting targets** — two sites/deployments (SaaS host or
|
||||
self-hosted alongside Gitea/Woodpecker — not yet decided which).
|
||||
3. **Known wrinkle**: because Vite bakes `VITE_SUPABASE_URL`/`VITE_SUPABASE_ANON_KEY`
|
||||
in at build time, you can't build once and promote the identical artifact
|
||||
to both environments — each deploy target needs its own build with its
|
||||
own env vars baked in. Not hard, just means "deploy" reruns the build
|
||||
rather than reusing one artifact. A non-issue at this app's scale.
|
||||
4. **Migrations run per-target** via Supabase CLI, scripted per environment:
|
||||
```bash
|
||||
supabase link --project-ref $SUPABASE_PROJECT_REF
|
||||
supabase db push
|
||||
supabase functions deploy admin-user-action
|
||||
```
|
||||
5. **Secrets needed in Woodpecker** (scoped per target): `SUPABASE_ACCESS_TOKEN`,
|
||||
staging/prod `SUPABASE_PROJECT_REF`, staging/prod `VITE_SUPABASE_URL` /
|
||||
`VITE_SUPABASE_ANON_KEY`, plus whatever the frontend host needs (API
|
||||
token, or SSH key if self-hosted).
|
||||
6. **Google OAuth**: needs its own authorized redirect URI added in Google
|
||||
Cloud console for each of the staging and prod domains — one-time setup,
|
||||
easy to forget.
|
||||
|
||||
**Difficulty estimate discussed**: roughly a focused afternoon — nothing here
|
||||
fights the tooling (no containers to orchestrate, no server process to run).
|
||||
Breakdown: `.woodpecker.yml` (~1–2 hrs), second Supabase project + migration
|
||||
check (~30 min), frontend hosting target wiring (~30–60 min), secrets +
|
||||
"Allow deployments" toggle (~15 min), end-to-end test/debug (~1–2 hrs).
|
||||
|
||||
## Open decisions (not yet made)
|
||||
|
||||
- Which frontend host: SaaS (Vercel/Netlify/Cloudflare Pages) vs self-hosted
|
||||
alongside the existing Gitea/Woodpecker infra.
|
||||
- Where the marketing site (`site/`, see above) lives relative to the app —
|
||||
leaning subdomain (`diagrav.com` + `app.diagrav.com`, per the links already
|
||||
in `site/index.html`) over same-domain/different-path, but not finalized —
|
||||
and whether it gets its own line in the eventual `.woodpecker.yml` or
|
||||
piggybacks on the frontend deploy step.
|
||||
- Whether/when to add diagram-snapshot retention (the storage-risk item
|
||||
above) — not done yet, just identified.
|
||||
- Actual `.woodpecker.yml` has not been written yet — this doc captures the
|
||||
plan/shape only, per the user's request to keep this conversation
|
||||
discussion-only and not make repo changes here.
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AV Planner</title>
|
||||
<title>Diagrav</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
+174
@@ -0,0 +1,174 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Diagrav — Plan your wiring before you buy a single cable</title>
|
||||
<meta name="description" content="Diagrav is a visual tool for planning AV and DAWless synth setups: drag devices onto a canvas, wire them up with real connector compatibility checking, and get an automatic shopping list." />
|
||||
<link rel="canonical" href="https://diagrav.com/" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content="https://diagrav.com/" />
|
||||
<meta property="og:title" content="Diagrav" />
|
||||
<meta property="og:description" content="Plan your AV or DAWless synth wiring visually, catch incompatible connections before you buy anything, and get an automatic shopping list." />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="nav">
|
||||
<div class="wrap">
|
||||
<a class="brand" href="#top"><span class="dot"></span>Diagrav</a>
|
||||
<div class="nav-right">
|
||||
<div class="nav-links">
|
||||
<a href="#features">Features</a>
|
||||
<a href="#canvas">How it works</a>
|
||||
<a href="#library">Library</a>
|
||||
</div>
|
||||
<div class="nav-auth">
|
||||
<a class="nav-login" href="https://app.diagrav.com/">Log in</a>
|
||||
<a class="btn btn-primary btn-sm" href="https://app.diagrav.com/">Sign up</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<header class="hero" id="top">
|
||||
<div class="wrap">
|
||||
<span class="eyebrow">🔌 Plan the whole rig before you buy anything</span>
|
||||
<h1>Wire up your gear <span class="accent">on screen</span><br>before you touch a cable</h1>
|
||||
<p class="lede">
|
||||
Diagrav is a visual planning tool for anyone wiring together audio/video gear or a
|
||||
DAWless synth setup. Drag devices onto a canvas, connect their ports, and let the app
|
||||
catch incompatible connections and total up your shopping list — before you've spent a dollar.
|
||||
</p>
|
||||
<div class="hero-shot">
|
||||
<img src="images/canvas.jpg" alt="A wired synth rig diagram in Diagrav, showing a MIDI keyboard, drum machine, synth, mixer, and studio monitors connected with labeled cables" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section id="features">
|
||||
<div class="wrap">
|
||||
<div class="section-head">
|
||||
<h2>Everything you need to plan a rig, nothing you don't</h2>
|
||||
<p>Built for the moment before a purchase — when you're still figuring out what actually connects to what.</p>
|
||||
</div>
|
||||
<div class="features">
|
||||
<div class="feature">
|
||||
<div class="icon">🖱️</div>
|
||||
<h3>Drag-and-drop canvas</h3>
|
||||
<p>Place devices from a library of ports and connectors, then draw cables between them exactly like you would on a whiteboard — except it remembers everything.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="icon">✅</div>
|
||||
<h3>Real compatibility checking</h3>
|
||||
<p>Every connector type knows what it physically mates with. Try to wire an XLR into a 1/4" jack and the app tells you why not, instead of finding out at the store.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="icon">🧾</div>
|
||||
<h3>Automatic shopping list</h3>
|
||||
<p>Every cable and device on the canvas rolls up into a bill of materials — grouped by cable type and length, with running costs, so you know exactly what to order.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="icon">🎚️</div>
|
||||
<h3>Multi-cable bundles</h3>
|
||||
<p>A stereo pair or an 8-channel snake is really one purchase, not several. Bundle the individual runs together so the shopping list counts it the way you'd actually buy it.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="icon">📚</div>
|
||||
<h3>A growing shared library</h3>
|
||||
<p>Connector types, cable types, and device templates are shared across everyone using the app — with your own private additions for anything niche or proprietary.</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="icon">🤝</div>
|
||||
<h3>Share & collaborate</h3>
|
||||
<p>Share a diagram with view or edit access, keep a version history as it evolves, and pick up right where you left off.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="canvas">
|
||||
<div class="wrap">
|
||||
|
||||
<div class="showcase-row">
|
||||
<div class="showcase-copy">
|
||||
<div class="showcase-tag">The canvas</div>
|
||||
<h3>See your whole signal path at a glance</h3>
|
||||
<p>
|
||||
Drop a device onto the canvas, expand it to see its individual ports, and drag a
|
||||
connection from one port to another. Every wire is a real object — it knows which
|
||||
two ports it joins, what cable type it needs, and how long a run it is.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Devices collapse to a compact view when you don't need the detail</li>
|
||||
<li>MIDI, audio, video, network, and power connectors, side by side</li>
|
||||
<li>Bundle related runs (like a stereo pair) into one visually distinct cable</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="showcase-shot">
|
||||
<img src="images/canvas.jpg" alt="Wired device diagram on the Diagrav canvas" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="showcase-row reverse">
|
||||
<div class="showcase-copy">
|
||||
<div class="showcase-tag">The shopping list</div>
|
||||
<h3>A bill of materials that updates itself</h3>
|
||||
<p>
|
||||
As you wire things up, Diagrav keeps a running bill of materials in the side
|
||||
panel — every cable type you'll need, grouped by length, with a running cost total.
|
||||
Bundled cables count as the single item you'll actually order.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Cables grouped by type, then by length, so you know exactly what to buy and how many</li>
|
||||
<li>Device and cable costs roll up into one grand total</li>
|
||||
<li>Anything missing a cost is flagged, never silently treated as free</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="showcase-shot">
|
||||
<img src="images/bom.jpg" alt="Bill of materials panel showing cables grouped by type and length with a running cost total" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="showcase-row" id="library">
|
||||
<div class="showcase-copy">
|
||||
<div class="showcase-tag">The library</div>
|
||||
<h3>A device catalog that grows with you</h3>
|
||||
<p>
|
||||
Browse built-in devices and connectors by category or manufacturer, search across
|
||||
everything at once, and drop in your own custom gear when something isn't in the
|
||||
catalog yet — synths, mixers, adapters, whatever your rack actually has.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Organize by category or by manufacturer, whichever matches how you think</li>
|
||||
<li>Add your own devices and connector types in minutes</li>
|
||||
<li>Submit your additions back to the shared catalog for everyone else to use</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="showcase-shot">
|
||||
<img src="images/palette.jpg" alt="Device library panel organized by category, showing built-in and custom devices" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="wrap">
|
||||
<div class="strip">
|
||||
<h2>Stop guessing which cable you need</h2>
|
||||
<p>Plan the wiring first. Buy the right cables once.</p>
|
||||
<a class="btn btn-primary btn-lg strip-cta" href="https://app.diagrav.com/">Sign up</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<div class="wrap" style="display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:12px;">
|
||||
<span>Diagrav</span>
|
||||
<span>Built for planning AV and DAWless synth setups.</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
+412
@@ -0,0 +1,412 @@
|
||||
:root {
|
||||
--ink: #1e1b2e;
|
||||
--ink-soft: #4b4763;
|
||||
--bg: #fafafa;
|
||||
--panel: #ffffff;
|
||||
--border: #e7e5f0;
|
||||
--indigo: #4f46e5;
|
||||
--indigo-dark: #4338ca;
|
||||
--indigo-soft: #eef2ff;
|
||||
--amber: #d97706;
|
||||
--teal: #059669;
|
||||
--shadow: 0 1px 2px rgba(30, 27, 46, 0.04), 0 8px 24px rgba(30, 27, 46, 0.06);
|
||||
--max: 1080px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Inter, Roboto, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.55;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--indigo);
|
||||
}
|
||||
|
||||
.wrap {
|
||||
max-width: var(--max);
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
/* ---------- Nav ---------- */
|
||||
|
||||
.nav {
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: rgba(250, 250, 250, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.nav .wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-weight: 700;
|
||||
font-size: 17px;
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.brand .dot {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 999px;
|
||||
background: var(--indigo);
|
||||
box-shadow: 0 0 0 3px var(--indigo-soft);
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
display: flex;
|
||||
gap: 28px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
color: var(--ink-soft);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.nav-auth {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.nav-login {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--ink-soft);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-login:hover {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.nav-links {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Buttons ---------- */
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--indigo);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--indigo-dark);
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
font-size: 13px;
|
||||
padding: 7px 16px;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
font-size: 15px;
|
||||
padding: 13px 30px;
|
||||
}
|
||||
|
||||
/* ---------- Hero ---------- */
|
||||
|
||||
.hero {
|
||||
padding: 88px 0 64px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--indigo-dark);
|
||||
background: var(--indigo-soft);
|
||||
border-radius: 999px;
|
||||
padding: 6px 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: clamp(32px, 5vw, 52px);
|
||||
line-height: 1.12;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
|
||||
h1 .accent {
|
||||
color: var(--indigo);
|
||||
}
|
||||
|
||||
.lede {
|
||||
font-size: clamp(16px, 2vw, 19px);
|
||||
color: var(--ink-soft);
|
||||
max-width: 640px;
|
||||
margin: 0 auto 36px;
|
||||
}
|
||||
|
||||
.hero-shot {
|
||||
margin-top: 8px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
max-width: 960px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.hero-shot img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ---------- Section scaffolding ---------- */
|
||||
|
||||
section {
|
||||
padding: 72px 0;
|
||||
}
|
||||
|
||||
.section-head {
|
||||
max-width: 620px;
|
||||
margin: 0 auto 48px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-head h2 {
|
||||
font-size: clamp(24px, 3.2vw, 32px);
|
||||
letter-spacing: -0.01em;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.section-head p {
|
||||
color: var(--ink-soft);
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* ---------- Feature grid ---------- */
|
||||
|
||||
.features {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 860px) {
|
||||
.features {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.features {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.feature {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.feature .icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
margin-bottom: 16px;
|
||||
background: var(--indigo-soft);
|
||||
}
|
||||
|
||||
.feature h3 {
|
||||
font-size: 16px;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.feature p {
|
||||
font-size: 14px;
|
||||
color: var(--ink-soft);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* ---------- Showcase (alternating screenshot rows) ---------- */
|
||||
|
||||
.showcase-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 48px;
|
||||
align-items: center;
|
||||
padding: 48px 0;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.showcase-row:first-of-type {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.showcase-row.reverse .showcase-copy {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.showcase-row.reverse .showcase-shot {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.showcase-row,
|
||||
.showcase-row.reverse .showcase-copy,
|
||||
.showcase-row.reverse .showcase-shot {
|
||||
order: initial;
|
||||
}
|
||||
.showcase-row {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.showcase-tag {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--indigo);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.showcase-copy h3 {
|
||||
font-size: 22px;
|
||||
margin: 0 0 12px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
.showcase-copy p {
|
||||
color: var(--ink-soft);
|
||||
font-size: 15px;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
.showcase-copy ul {
|
||||
margin: 16px 0 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 14px;
|
||||
color: var(--ink-soft);
|
||||
}
|
||||
|
||||
.showcase-copy li {
|
||||
padding-left: 22px;
|
||||
position: relative;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.showcase-copy li::before {
|
||||
content: "✓";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--teal);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.showcase-shot {
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ---------- Callout strip ---------- */
|
||||
|
||||
.strip {
|
||||
background: var(--ink);
|
||||
color: #fff;
|
||||
border-radius: 20px;
|
||||
padding: 48px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.strip h2 {
|
||||
margin: 0 0 12px;
|
||||
font-size: clamp(22px, 3vw, 28px);
|
||||
}
|
||||
|
||||
.strip p {
|
||||
margin: 0 auto;
|
||||
color: #c7c4d9;
|
||||
max-width: 520px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.strip-cta {
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
/* ---------- Footer ---------- */
|
||||
|
||||
footer {
|
||||
border-top: 1px solid var(--border);
|
||||
padding: 32px 0;
|
||||
font-size: 13px;
|
||||
color: var(--ink-soft);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Deploys the built Vite app (dist/) as an assets-only Cloudflare Worker.
|
||||
// No server-side code — this is purely static hosting for app.diagrav.com.
|
||||
// Build first (with production VITE_* env vars baked in), then:
|
||||
// npx wrangler deploy --config wrangler.app.jsonc
|
||||
{
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"name": "diagrav-app",
|
||||
"compatibility_date": "2026-09-25",
|
||||
"assets": {
|
||||
"directory": "./dist",
|
||||
"not_found_handling": "single-page-application"
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"pattern": "app.diagrav.com",
|
||||
"custom_domain": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Deploys the hand-written marketing site (site/) as an assets-only
|
||||
// Cloudflare Worker for the bare diagrav.com domain. No build step —
|
||||
// serves the folder as-is:
|
||||
// npx wrangler deploy --config wrangler.site.jsonc
|
||||
{
|
||||
"$schema": "./node_modules/wrangler/config-schema.json",
|
||||
"name": "diagrav-site",
|
||||
"compatibility_date": "2026-09-25",
|
||||
"assets": {
|
||||
"directory": "./site",
|
||||
"not_found_handling": "404-page"
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"pattern": "diagrav.com",
|
||||
"custom_domain": true
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user