diff --git a/MEDIA_CAPTURE.md b/MEDIA_CAPTURE.md new file mode 100644 index 0000000..2436c64 --- /dev/null +++ b/MEDIA_CAPTURE.md @@ -0,0 +1,80 @@ +# GroundControl marketing media capture plan + +The public site should only use real product captures. Do not add invented dashboards, fake metrics, synthetic terminal output, or decorative UI mockups. + +## Capture set + +### 1. Agent workflow hero +Use the real ChatGPT + GroundControl session showing the authenticated GroundControl terminal while ChatGPT is working through deployment/investigation tasks. + +Purpose: prove the core story that a user can authenticate once and let ChatGPT use GroundControl capabilities without pasting VPS credentials into the conversation. + +Recommended format: 16:10 or 3:2 screenshot, 1400–1800px wide, WebP. Crop browser chrome only if it does not remove important context. + +### 2. Templates +Route: `/templates` + +Capture: +- template browser / choose step +- `vps-caddy-existing-compose` +- Odoo Community template +- one production-shaped SaaS template such as Next.js + PostgreSQL + Redis +- review/deploy step with real fields and environment requirements visible + +Purpose: demonstrate that templates are deployment workflows, not boilerplate cards. + +### 3. Deployment progress +Route: `/deployments` + +Capture a real deployment moving through source/config/build/deploy/verify, preferably one that finishes successfully and shows the public route verification. + +Recommended format: short 8–15 second MP4/WebM plus a still frame. + +### 4. Runtime +Route: `/runtime` + +Capture a real running service with container state, ports, health and resource details visible. + +Purpose: show the connection between what was deployed and what is actually running. + +### 5. Terminal +Route: `/terminal` + +Capture the onboard terminal running a harmless real host command such as container listing, process inspection or checking a service. + +Do not expose secrets, SSH material, tokens, private environment values or sensitive host data. + +### 6. Intelligence +Route: `/intelligence` + +Capture a real investigation with: +- selected failing endpoint or service +- visible evidence gathering +- exact failure reason +- actionable next step / recovery action +- verification after the action where available + +Purpose: sell GroundControl as an operational system, not merely a dashboard. + +### 7. Domains / Cloudflare +Capture a real deployment showing domain configuration and external HTTPS verification. Hide account identifiers or zone details that should not be public. + +## Editing rules + +- Prefer real screenshots and short recordings over illustrations. +- Crop for focus, not to manufacture a different product state. +- Redact secrets rather than replacing them with fictional values. +- Never fabricate success states, counts or metrics for marketing. +- Keep typography and UI untouched inside screenshots so captures remain recognizable as GroundControl. +- Compress stills to WebP and recordings to MP4/WebM before committing to `public/product/`. + +## Suggested page order once media is available + +1. Hero copy + real ChatGPT/GroundControl session +2. Real deployment clip +3. Product capability grid +4. Templates capture strip +5. Runtime + terminal paired captures +6. Intelligence investigation clip +7. Ownership/security explanation +8. Installer + open-source CTA diff --git a/app/globals.css b/app/globals.css index a9903f9..63b3386 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,19 +1,18 @@ @import "tailwindcss"; :root { - --ink: #f2f3f0; - --muted: #8b918b; - --dim: #5d635e; - --line: #282d29; - --line-strong: #363d37; + --ink: #f5f7f5; + --muted: #9aa29b; + --dim: #6d756e; + --line: #252b27; + --line-strong: #343c36; --black: #090b0a; --deep: #0d100e; - --panel: #121613; - --panel-soft: #171c18; - --accent: #596ee8; - --accent-bright: #7185f5; - --success: #79d29c; - --warning: #e1b35d; + --panel: #111512; + --panel-soft: #151a16; + --accent: #6d7df0; + --accent-bright: #8794ff; + --success: #77d39c; } * { box-sizing: border-box; } @@ -29,192 +28,152 @@ a { color: inherit; text-decoration: none; } button, a { -webkit-tap-highlight-color: transparent; } button { font: inherit; } +.section-shell { width: min(1220px, calc(100% - 48px)); margin: 0 auto; } +.eyebrow { margin: 0; color: var(--dim); font: 600 10px/1.4 var(--font-geist-mono); letter-spacing: .14em; } + .site-header { - position: absolute; - z-index: 20; + position: sticky; + z-index: 30; top: 0; - left: 0; - right: 0; - height: 74px; + height: 70px; display: flex; align-items: center; justify-content: space-between; - padding: 0 clamp(24px, 5vw, 72px); - border-bottom: 1px solid rgba(255,255,255,.07); + padding: 0 clamp(24px, 4vw, 64px); + background: rgba(9, 11, 10, .9); + border-bottom: 1px solid var(--line); + backdrop-filter: blur(18px); } -.wordmark { display: inline-flex; align-items: center; gap: 11px; font: 600 11px/1 var(--font-geist-mono); letter-spacing: .16em; } -.brand-mark { position: relative; width: 20px; height: 20px; border: 1px solid rgba(255,255,255,.75); display: inline-block; } -.brand-mark::before { content: ""; position: absolute; width: 8px; height: 8px; right: -1px; top: -1px; background: var(--accent); } -.brand-mark::after { content: ""; position: absolute; width: 5px; height: 5px; left: 4px; bottom: 4px; border: 1px solid rgba(255,255,255,.55); } -.brand-mark--small { width: 15px; height: 15px; } -.site-header nav { display: flex; align-items: center; gap: clamp(18px, 3vw, 40px); } -.site-header nav a { font: 500 10px/1 var(--font-geist-mono); color: #aeb3ae; letter-spacing: .1em; transition: color .2s; } +.wordmark { display: inline-flex; align-items: center; gap: 10px; font: 650 12px/1 var(--font-geist-mono); letter-spacing: .04em; } +.brand-mark { width: 28px; height: 28px; display: inline-grid; place-items: center; border: 1px solid var(--line-strong); color: var(--accent-bright); font: 700 8px/1 var(--font-geist-mono); } +.site-header nav { display: flex; align-items: center; gap: clamp(16px, 3vw, 34px); } +.site-header nav a, footer a { color: #aeb5af; font: 550 10px/1 var(--font-geist-mono); letter-spacing: .04em; transition: color .2s; } .site-header nav a:hover, footer a:hover { color: white; } -.hero { position: relative; min-height: 820px; height: 100svh; overflow: hidden; display: flex; align-items: center; padding: 112px clamp(24px, 8vw, 128px) 84px; background: radial-gradient(circle at 75% 45%, #202d2b 0, #141a18 28%, #0b0e0c 67%); } -.hero::after { content: ""; position: absolute; inset: 0; background: linear-gradient(90deg, rgba(7,9,8,.96) 0%, rgba(7,9,8,.76) 43%, rgba(7,9,8,.06) 80%); } -.hero-grid, .final-grid { position: absolute; inset: 0; opacity: .18; background-image: linear-gradient(rgba(255,255,255,.08) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.08) 1px, transparent 1px); background-size: 96px 96px; mask-image: linear-gradient(to right, transparent, black 55%, transparent); } -.hero-mesh { position: absolute; width: min(700px, 64vw); aspect-ratio: 1; right: -5%; top: 7%; filter: blur(8px); } -.hero-mesh span { position: absolute; border-radius: 999px; mix-blend-mode: screen; filter: blur(45px); opacity: .42; animation: drift 12s ease-in-out infinite alternate; } -.hero-mesh span:nth-child(1) { width: 64%; height: 64%; right: 5%; top: 5%; background: #4f6a67; } -.hero-mesh span:nth-child(2) { width: 52%; height: 52%; left: 3%; bottom: 8%; background: #2c3d79; animation-delay: -4s; } -.hero-mesh span:nth-child(3) { width: 35%; height: 35%; right: 18%; bottom: 4%; background: #775464; animation-delay: -8s; } -@keyframes drift { to { transform: translate3d(-32px, 25px, 0) scale(1.08); } } -.hero-content { position: relative; z-index: 2; max-width: 760px; } -.eyebrow { margin: 0; font: 500 10px/1.4 var(--font-geist-mono); letter-spacing: .15em; color: var(--muted); } -.hero-eyebrow { display: flex; align-items: center; gap: 9px; color: #c7cbc7; } -.live-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--success); box-shadow: 0 0 12px rgba(121,210,156,.45); } -.hero h1 { margin: 30px 0 26px; font-size: clamp(54px, 7.1vw, 104px); line-height: .92; letter-spacing: -.065em; font-weight: 490; } -.hero h1 span { color: var(--accent-bright); } -.hero-copy { max-width: 610px; margin: 0 0 38px; color: #a3a9a3; font-size: clamp(16px, 1.5vw, 20px); line-height: 1.65; } -.hero-actions { display: flex; flex-wrap: wrap; gap: 10px; } -.button { min-height: 48px; display: inline-flex; align-items: center; justify-content: center; gap: 20px; padding: 0 20px; border: 1px solid var(--line-strong); font: 600 10px/1 var(--font-geist-mono); letter-spacing: .09em; transition: background .2s, border-color .2s, transform .2s; } +.hero { + min-height: calc(100svh - 70px); + padding-top: clamp(72px, 10vw, 140px); + padding-bottom: 90px; + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(420px, .88fr); + gap: clamp(42px, 7vw, 96px); + align-items: center; +} +.hero-copy-block { max-width: 720px; } +.hero h1 { margin: 23px 0 28px; font-size: clamp(48px, 6.1vw, 88px); line-height: .97; letter-spacing: -.06em; font-weight: 520; } +.hero-copy { max-width: 680px; margin: 0; color: var(--muted); font-size: clamp(17px, 1.6vw, 21px); line-height: 1.62; } +.hero-actions { margin-top: 36px; display: flex; flex-wrap: wrap; gap: 10px; } +.button { min-height: 50px; display: inline-flex; align-items: center; justify-content: center; gap: 16px; padding: 0 20px; border: 1px solid var(--line-strong); font: 650 10px/1 var(--font-geist-mono); letter-spacing: .045em; transition: transform .2s, background .2s, border-color .2s; } .button:hover { transform: translateY(-2px); } -.button--primary { color: white; border-color: var(--accent); background: var(--accent); } +.button--primary { background: var(--accent); border-color: var(--accent); color: white; } .button--primary:hover { background: var(--accent-bright); border-color: var(--accent-bright); } -.button--secondary { background: rgba(255,255,255,.02); color: #c6cac6; } -.button--secondary:hover { border-color: #59615a; background: rgba(255,255,255,.05); } -.hero-proof { position: absolute; z-index: 2; left: clamp(24px, 8vw, 128px); right: clamp(24px, 5vw, 72px); bottom: 34px; display: flex; gap: 30px; color: #697069; font: 500 9px/1 var(--font-geist-mono); letter-spacing: .11em; } - -.section-shell { width: min(1180px, calc(100% - 48px)); margin: 0 auto; } -.preview-section { padding: 140px 0 130px; } -.section-intro { max-width: 760px; margin-bottom: 62px; } -.section-intro h2 { margin: 20px 0 0; font-size: clamp(38px, 5vw, 66px); line-height: 1.02; letter-spacing: -.052em; font-weight: 480; } -.section-intro p:not(.eyebrow) { margin: 0; color: var(--muted); font-size: 16px; line-height: 1.7; max-width: 560px; } -.section-intro--split { max-width: none; display: grid; grid-template-columns: minmax(180px, .7fr) minmax(0, 1.55fr); gap: 50px; align-items: start; } -.section-intro--split h2 { margin-top: 0; margin-bottom: 24px; } - -.product-preview { border: 1px solid var(--line-strong); background: #0b0e0c; box-shadow: 0 40px 100px rgba(0,0,0,.4); overflow: hidden; } -.preview-topbar { height: 48px; display: flex; align-items: center; justify-content: space-between; padding: 0 18px; border-bottom: 1px solid var(--line); background: #0e110f; } -.preview-brand, .preview-topmeta { display: flex; align-items: center; gap: 9px; color: #838983; font: 600 8px/1 var(--font-geist-mono); letter-spacing: .12em; } -.preview-body { min-height: 590px; display: flex; } -.preview-rail { width: 58px; border-right: 1px solid var(--line); display: flex; flex-direction: column; align-items: center; gap: 24px; padding-top: 28px; } -.rail-line { width: 17px; height: 2px; background: #39403a; } -.rail-line--active { width: 23px; background: var(--accent-bright); } -.preview-main { flex: 1; min-width: 0; padding: clamp(20px, 3vw, 40px); } -.preview-heading { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 28px; } -.preview-heading h2 { margin: 7px 0 0; font-size: 27px; letter-spacing: -.04em; font-weight: 500; } -.preview-clock { color: #606760; font: 500 8px/1 var(--font-geist-mono); } -.signal-panel { display: grid; grid-template-columns: 1.25fr .75fr; border: 1px solid var(--line-strong); } -.signal-copy { min-height: 260px; padding: clamp(24px, 3vw, 38px); border-right: 1px solid var(--line); display: flex; flex-direction: column; justify-content: center; } -.status-pill { width: fit-content; display: flex; align-items: center; gap: 7px; padding: 6px 8px; color: var(--success); border: 1px solid rgba(121,210,156,.24); background: rgba(121,210,156,.05); font: 600 7px/1 var(--font-geist-mono); letter-spacing: .08em; } -.signal-copy h3 { margin: 22px 0 10px; font-size: clamp(24px, 3.2vw, 42px); line-height: 1; letter-spacing: -.05em; font-weight: 480; } -.signal-copy p { color: #707770; font-size: 12px; } -.signal-actions { display: flex; gap: 8px; margin-top: 26px; } -.signal-actions span { padding: 8px 10px; color: #9ea49e; border: 1px solid var(--line); font: 600 7px/1 var(--font-geist-mono); } -.metric-grid { display: grid; grid-template-columns: 1fr 1fr; } -.metric { min-height: 130px; padding: 22px; display: flex; flex-direction: column; border-bottom: 1px solid var(--line); border-right: 1px solid var(--line); } -.metric:nth-child(2n) { border-right: 0; } -.metric:nth-child(n+3) { border-bottom: 0; } -.metric span, .metric small, .card-label { color: #606760; font: 500 7px/1.4 var(--font-geist-mono); letter-spacing: .08em; } -.metric strong { margin: auto 0 5px; font-size: 26px; font-weight: 500; letter-spacing: -.04em; } -.preview-lower { display: grid; grid-template-columns: 1.4fr .6fr; gap: 14px; margin-top: 14px; } -.telemetry-card, .runtime-card { min-height: 180px; padding: 18px; border: 1px solid var(--line); background: #0d100e; } -.card-label { display: flex; justify-content: space-between; } -.healthy-text { color: var(--success); } -.chart { position: relative; height: 108px; margin-top: 23px; overflow: hidden; background-image: linear-gradient(rgba(255,255,255,.04) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.04) 1px, transparent 1px); background-size: 25% 33%; } -.chart-fill { position: absolute; inset: 32% -5% -20% -5%; background: linear-gradient(180deg, rgba(89,110,232,.26), transparent); clip-path: polygon(0 50%, 10% 42%, 20% 58%, 31% 35%, 42% 44%, 52% 22%, 62% 30%, 72% 18%, 81% 31%, 91% 15%, 100% 24%, 100% 100%, 0 100%); } -.chart-line { position: absolute; inset: 29% -5% 0; border-top: 1px solid var(--accent-bright); transform: skewY(-5deg); opacity: .8; } -.runtime-card { padding-bottom: 10px; } -.service-row { height: 31px; display: grid; grid-template-columns: 13px 1fr auto; align-items: center; border-bottom: 1px solid #1d221e; color: #b9bdb9; font: 500 9px/1 var(--font-geist-mono); } -.service-row small { font-size: 6px; color: #666d66; } - -.capabilities { padding: 50px 0 150px; } -.capability-grid { border-top: 1px solid var(--line-strong); } -.capability-grid article { min-height: 220px; display: grid; grid-template-columns: 80px 1fr auto; gap: 34px; align-items: center; border-bottom: 1px solid var(--line); transition: background .25s; } -.capability-grid article:hover { background: linear-gradient(90deg, transparent, rgba(255,255,255,.018), transparent); } -.capability-number { align-self: start; padding-top: 46px; color: var(--accent-bright); font: 500 10px/1 var(--font-geist-mono); } -.capability-grid h3 { margin: 0 0 13px; font-size: clamp(25px, 3vw, 38px); font-weight: 480; letter-spacing: -.04em; } -.capability-grid p { max-width: 640px; margin: 0; color: var(--muted); line-height: 1.65; } -.capability-grid small { color: #606660; font: 500 8px/1 var(--font-geist-mono); letter-spacing: .08em; text-transform: uppercase; } - -.principle-band { padding: 130px 0; background: var(--panel-soft); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } -.principle-inner blockquote { max-width: 1040px; margin: 30px 0 56px; font-size: clamp(42px, 6vw, 76px); line-height: 1.05; letter-spacing: -.055em; font-weight: 440; } -.principle-meta { display: flex; gap: 36px; color: #6d746e; font: 500 8px/1 var(--font-geist-mono); letter-spacing: .1em; } - -.install-section { padding: 150px 0 130px; } -.install-console { margin-top: 70px; border: 1px solid var(--line-strong); background: #0b0d0c; } +.button--secondary { background: transparent; color: #c9cfca; } +.button--secondary:hover { border-color: #59625b; background: rgba(255,255,255,.025); } +.hero-notes { margin-top: 34px; display: flex; flex-wrap: wrap; gap: 8px; } +.hero-notes span { padding: 7px 9px; border: 1px solid var(--line); color: var(--dim); font: 550 8px/1 var(--font-geist-mono); letter-spacing: .06em; text-transform: uppercase; } + +.hero-media { margin: 0; border: 1px solid var(--line-strong); background: var(--deep); overflow: hidden; box-shadow: 0 28px 80px rgba(0,0,0,.35); } +.hero-media img { width: 100%; height: auto; display: block; } +.hero-media figcaption { padding: 18px 20px 20px; border-top: 1px solid var(--line); color: var(--muted); font-size: 13px; line-height: 1.55; } +.hero-media figcaption strong { color: var(--ink); font-weight: 600; } + +.proof-band { padding: 100px 0; background: var(--panel-soft); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } +.proof-grid { display: grid; grid-template-columns: minmax(160px, .55fr) minmax(0, 1.45fr); gap: 48px; } +.proof-grid h2, .section-heading h2, .ownership-grid h2, .final-cta h2 { margin: 0; font-size: clamp(38px, 5vw, 68px); line-height: 1.01; letter-spacing: -.052em; font-weight: 500; } +.proof-grid p:not(.eyebrow), .section-heading > p, .section-heading--split > p, .workflow-copy > p:not(.eyebrow), .ownership-grid > p:not(.eyebrow) { color: var(--muted); font-size: 16px; line-height: 1.72; } +.proof-grid h2 + p { max-width: 800px; margin: 24px 0 0; } + +.features, .templates, .install-section { padding: 130px 0; } +.section-heading { max-width: 900px; margin-bottom: 58px; } +.section-heading h2 { margin-top: 18px; } +.section-heading--split { max-width: none; display: grid; grid-template-columns: minmax(0, 1.25fr) minmax(280px, .75fr); gap: 64px; align-items: end; } +.section-heading--split > p { margin: 0; max-width: 520px; } + +.feature-grid { display: grid; grid-template-columns: repeat(3, 1fr); border-top: 1px solid var(--line-strong); border-left: 1px solid var(--line-strong); } +.feature-grid article { min-height: 260px; padding: 30px; display: flex; flex-direction: column; border-right: 1px solid var(--line-strong); border-bottom: 1px solid var(--line-strong); background: var(--deep); } +.feature-grid h3 { margin: 0 0 16px; font-size: 25px; letter-spacing: -.035em; font-weight: 520; } +.feature-grid p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.68; } +.feature-grid small { margin-top: auto; padding-top: 28px; color: var(--dim); font: 550 8px/1.45 var(--font-geist-mono); text-transform: uppercase; letter-spacing: .065em; } + +.workflow-section { padding: 120px 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); background: #0b0e0c; } +.workflow-grid { display: grid; grid-template-columns: minmax(0, .9fr) minmax(420px, 1.1fr); gap: clamp(56px, 8vw, 110px); align-items: start; } +.workflow-copy { margin: 0; position: sticky; top: 112px; } +.workflow-copy > p:not(.eyebrow) { max-width: 600px; margin: 24px 0 0; } +.workflow-steps { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--line-strong); } +.workflow-steps li { min-height: 150px; display: grid; grid-template-columns: 54px 1fr; gap: 24px; padding: 28px 0; border-bottom: 1px solid var(--line); } +.workflow-steps li > span { color: var(--accent-bright); font: 600 9px/1.4 var(--font-geist-mono); } +.workflow-steps strong { display: block; margin-bottom: 9px; font-size: 22px; font-weight: 520; letter-spacing: -.025em; } +.workflow-steps p { margin: 0; color: var(--muted); font-size: 14px; line-height: 1.65; } + +.template-grid { display: grid; grid-template-columns: 1fr 1fr; border-top: 1px solid var(--line-strong); } +.template-grid article { padding: 30px 0; border-bottom: 1px solid var(--line); } +.template-grid article:nth-child(odd) { padding-right: 32px; border-right: 1px solid var(--line); } +.template-grid article:nth-child(even) { padding-left: 32px; } +.template-topline { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; } +.template-grid h3 { margin: 0; font-size: 22px; font-weight: 520; letter-spacing: -.03em; } +.template-grid code { color: var(--accent-bright); font: 500 8px/1.4 var(--font-geist-mono); text-align: right; } +.template-grid p { margin: 18px 0 0; color: var(--muted); font-size: 14px; line-height: 1.68; max-width: 530px; } + +.ownership-section { padding: 120px 0; background: var(--panel-soft); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); } +.ownership-grid { display: grid; grid-template-columns: .45fr 1.55fr; gap: 48px; } +.ownership-grid h2 { max-width: 920px; } +.ownership-grid > p:last-child { grid-column: 2; max-width: 720px; margin: 28px 0 0; } + +.install-console { margin-top: 58px; border: 1px solid var(--line-strong); background: #080a09; } .install-tabs { display: grid; grid-template-columns: repeat(3, 1fr); border-bottom: 1px solid var(--line); } -.install-tab { min-height: 88px; padding: 18px 22px; border: 0; border-right: 1px solid var(--line); color: #717771; background: transparent; text-align: left; cursor: pointer; transition: background .2s, color .2s; } +.install-tab { min-height: 78px; padding: 17px 20px; border: 0; border-right: 1px solid var(--line); background: transparent; color: var(--dim); text-align: left; cursor: pointer; } .install-tab:last-child { border-right: 0; } -.install-tab span { display: block; margin-bottom: 8px; font-size: 13px; font-weight: 550; } -.install-tab small { font: 500 8px/1 var(--font-geist-mono); letter-spacing: .07em; } -.install-tab--active { color: var(--ink); background: #141815; box-shadow: inset 0 -2px var(--accent-bright); } -.command-shell { padding: 22px; } -.command-header { display: flex; justify-content: space-between; color: #525852; font: 500 7px/1 var(--font-geist-mono); letter-spacing: .09em; } -.terminal-dot { display: inline-block; width: 7px; height: 7px; margin-right: 6px; border-radius: 50%; } -.terminal-dot--red { background: #9d5757; }.terminal-dot--gold { background: #9c824c; }.terminal-dot--green { background: #4f8963; } -.command-line { min-height: 122px; display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 13px; align-items: center; margin-top: 15px; padding: 20px; background: #080a09; border: 1px solid #20241f; } -.prompt { color: var(--accent-bright); font: 500 12px/1 var(--font-geist-mono); } -.command-line code { overflow-wrap: anywhere; color: #c2c7c2; font: 500 12px/1.7 var(--font-geist-mono); } -.command-line button { border: 1px solid #303630; background: #101410; color: #969c96; padding: 9px 10px; font: 600 8px/1 var(--font-geist-mono); cursor: pointer; } -.command-line button:hover { color: white; border-color: #525a52; } -.command-note { display: flex; justify-content: space-between; margin-top: 14px; color: #515751; font: 500 7px/1 var(--font-geist-mono); letter-spacing: .08em; } -.install-steps { display: grid; grid-template-columns: repeat(3, 1fr); margin-top: 70px; border-top: 1px solid var(--line); } -.install-steps article { padding: 30px 34px 10px 0; border-right: 1px solid var(--line); } -.install-steps article:not(:first-child) { padding-left: 34px; } -.install-steps article:last-child { border-right: 0; } -.install-steps span { color: var(--accent-bright); font: 500 9px/1 var(--font-geist-mono); } -.install-steps h3 { margin: 30px 0 12px; font-size: 18px; font-weight: 520; } -.install-steps p { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.7; } - -.final-cta { position: relative; min-height: 560px; display: grid; place-items: center; overflow: hidden; border-top: 1px solid var(--line); background: radial-gradient(circle at center, #1c2623, #0b0e0c 62%); text-align: center; } -.final-cta > div:not(.final-grid) { position: relative; z-index: 2; } -.final-cta h2 { margin: 22px 0 38px; font-size: clamp(50px, 8vw, 100px); line-height: .9; letter-spacing: -.065em; font-weight: 470; } -footer { min-height: 110px; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 30px; padding: 0 clamp(24px, 5vw, 72px); border-top: 1px solid var(--line); background: #080a09; } -footer p { margin: 0; color: #555b55; font-size: 11px; } -footer > div { justify-self: end; display: flex; gap: 24px; color: #6e746e; font: 500 8px/1 var(--font-geist-mono); letter-spacing: .09em; } - -@media (max-width: 800px) { - .site-header nav a:first-child { display: none; } - .hero { min-height: 760px; padding-left: 24px; padding-right: 24px; } - .hero h1 { font-size: clamp(48px, 14vw, 74px); } - .hero-proof { left: 24px; gap: 15px; flex-wrap: wrap; } - .section-shell { width: min(100% - 32px, 1180px); } - .section-intro--split { grid-template-columns: 1fr; gap: 24px; } - .preview-section, .install-section { padding-top: 100px; } - .preview-body { min-height: 0; } - .preview-rail { display: none; } - .signal-panel { grid-template-columns: 1fr; } - .signal-copy { border-right: 0; border-bottom: 1px solid var(--line); } - .preview-lower { grid-template-columns: 1fr; } - .runtime-card { display: none; } - .capability-grid article { grid-template-columns: 45px 1fr; gap: 16px; padding: 32px 0; } - .capability-number { padding-top: 5px; } - .capability-grid small { grid-column: 2; } - .principle-meta { flex-wrap: wrap; gap: 16px; } - .install-steps { grid-template-columns: 1fr; } - .install-steps article, .install-steps article:not(:first-child) { padding: 28px 0; border-right: 0; border-bottom: 1px solid var(--line); } - footer { grid-template-columns: 1fr; padding-top: 34px; padding-bottom: 34px; text-align: center; } - footer .wordmark, footer > div { justify-self: center; } +.install-tab span { display: block; color: inherit; font: 600 10px/1 var(--font-geist-mono); } +.install-tab small { display: block; margin-top: 8px; font: 500 9px/1 var(--font-geist-mono); } +.install-tab--active { background: var(--panel); color: var(--ink); box-shadow: inset 0 2px 0 var(--accent-bright); } +.command-shell { min-height: 110px; padding: 24px; display: grid; grid-template-columns: auto 1fr auto; gap: 14px; align-items: start; } +.prompt { color: var(--success); font: 600 12px/1.6 var(--font-geist-mono); } +.command-shell code { min-width: 0; color: #c8cec9; font: 500 12px/1.65 var(--font-geist-mono); overflow-wrap: anywhere; } +.command-shell button { border: 1px solid var(--line-strong); background: transparent; color: var(--muted); padding: 8px 10px; cursor: pointer; font: 600 8px/1 var(--font-geist-mono); } +.command-shell button:hover { color: white; border-color: #59625b; } + +.final-cta { padding: 110px 0 125px; text-align: center; } +.final-cta h2 { max-width: 900px; margin: 20px auto 0; } +.final-cta .hero-actions { justify-content: center; } + +footer { min-height: 110px; display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 30px; border-top: 1px solid var(--line); } +footer p { margin: 0; color: var(--dim); font-size: 11px; text-align: center; } +footer > div { justify-self: end; display: flex; gap: 24px; } + +@media (max-width: 980px) { + .hero { grid-template-columns: 1fr; min-height: auto; padding-top: 90px; } + .hero-copy-block { max-width: 820px; } + .hero-media { max-width: 820px; } + .feature-grid { grid-template-columns: 1fr 1fr; } + .workflow-grid { grid-template-columns: 1fr; } + .workflow-copy { position: static; } + .ownership-grid { grid-template-columns: 1fr; } + .ownership-grid > p:last-child { grid-column: 1; } } -@media (max-width: 560px) { - .site-header { height: 64px; padding: 0 18px; } - .site-header nav { gap: 16px; } - .site-header nav a { font-size: 8px; } - .hero { min-height: 700px; padding-top: 95px; } - .hero-copy { font-size: 15px; } - .hero-proof span:last-child { display: none; } - .preview-section { width: 100%; } - .preview-section > .section-intro { width: calc(100% - 32px); margin-left: auto; margin-right: auto; } - .product-preview { border-left: 0; border-right: 0; } - .preview-main { padding: 16px; } - .signal-copy { min-height: 230px; } - .metric { min-height: 104px; padding: 15px; } - .metric strong { font-size: 21px; } - .preview-lower { display: none; } +@media (max-width: 720px) { + .section-shell { width: min(100% - 32px, 1220px); } + .site-header { height: 64px; padding: 0 16px; } + .site-header nav a:nth-child(1), .site-header nav a:nth-child(2) { display: none; } + .hero { padding-top: 70px; padding-bottom: 70px; } + .hero h1 { font-size: clamp(44px, 13vw, 62px); } + .proof-grid, .section-heading--split { grid-template-columns: 1fr; gap: 24px; } + .features, .templates, .install-section { padding: 90px 0; } + .feature-grid, .template-grid { grid-template-columns: 1fr; } + .feature-grid article { min-height: 230px; } + .template-grid article:nth-child(odd), .template-grid article:nth-child(even) { padding: 26px 0; border-right: 0; } + .template-topline { display: block; } + .template-grid code { display: block; margin-top: 9px; text-align: left; } + .workflow-section, .ownership-section, .proof-band { padding: 86px 0; } .install-tabs { grid-template-columns: 1fr; } - .install-tab { min-height: 66px; border-right: 0; border-bottom: 1px solid var(--line); } - .command-line { grid-template-columns: auto 1fr; } - .command-line button { grid-column: 2; justify-self: start; } - .command-note { gap: 20px; line-height: 1.5; } - .principle-band { padding: 90px 0; } - .final-cta { min-height: 460px; } + .install-tab, .install-tab:last-child { border-right: 0; border-bottom: 1px solid var(--line); } + .command-shell { grid-template-columns: auto 1fr; } + .command-shell button { grid-column: 2; justify-self: start; } + footer { grid-template-columns: 1fr; padding: 34px 0; text-align: center; } + footer .wordmark, footer > div { justify-self: center; } } @media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } - .hero-mesh span { animation: none; } *, *::before, *::after { transition-duration: .01ms !important; } } diff --git a/app/layout.tsx b/app/layout.tsx index def2c8e..21239c9 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import "./session.css"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -13,9 +14,9 @@ const geistMono = Geist_Mono({ }); export const metadata: Metadata = { - title: "GroundControl — Self-hosted VPS operations", + title: "GroundControl — Build, deploy and operate your VPS", description: - "Install a self-hosted control plane for deployments, runtime evidence and infrastructure operations on your VPS.", + "Open-source, self-hosted VPS operations for deployments, runtime, terminal, domains, templates and AI-agent workflows.", other: { "codex-preview": "development", }, diff --git a/app/page.test.tsx b/app/page.test.tsx index 383d727..cdbcc85 100644 --- a/app/page.test.tsx +++ b/app/page.test.tsx @@ -3,70 +3,67 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import Home from "@/app/page"; const GITHUB_URL = "https://github.com/teckedd-code2save/groundcontrol"; +const PRODUCT_URL = "https://groundcontrol.serendepify.com"; describe("Home page", () => { - it("renders the hero heading and primary install CTA", () => { + it("renders the product-led hero and live product CTA", () => { render(); expect( - screen.getByRole("heading", { level: 1, name: /operational co-pilot/i }), + screen.getByRole("heading", { level: 1, name: /build, deploy and operate your vps/i }), ).toBeInTheDocument(); - expect( - screen.getByRole("link", { name: /install on your vps/i }), - ).toHaveAttribute("href", "#install"); + expect(screen.getAllByRole("link", { name: /open groundcontrol/i })[0]).toHaveAttribute( + "href", + PRODUCT_URL, + ); }); it("points every GitHub link at the GroundControl product repo", () => { render(); const githubLinks = screen.getAllByRole("link", { name: /github/i }); expect(githubLinks.length).toBeGreaterThan(0); - for (const link of githubLinks) { - expect(link).toHaveAttribute("href", GITHUB_URL); - } + for (const link of githubLinks) expect(link).toHaveAttribute("href", GITHUB_URL); + }); + + it("markets real GroundControl capabilities", () => { + render(); + expect(screen.getByText("Deployments")).toBeInTheDocument(); + expect(screen.getByText("Runtime")).toBeInTheDocument(); + expect(screen.getByText("Terminal")).toBeInTheDocument(); + expect(screen.getByText("Intelligence")).toBeInTheDocument(); + expect(screen.getByText("Domains & edge")).toBeInTheDocument(); }); - it("renders the three capability cards", () => { + it("shows production deployment templates", () => { render(); - expect(screen.getByText("See the whole host")).toBeInTheDocument(); - expect(screen.getByText("Deploy with context")).toBeInTheDocument(); - expect(screen.getByText("Investigate from evidence")).toBeInTheDocument(); + expect(screen.getByText("Existing Compose")).toBeInTheDocument(); + expect(screen.getByText("Odoo Community")).toBeInTheDocument(); + expect(screen.getByText("Next.js SaaS")).toBeInTheDocument(); + expect(screen.getByText("FastAPI + worker")).toBeInTheDocument(); }); it("shows the remote install command by default", () => { render(); - expect(screen.getByRole("tab", { name: /remote/i })).toHaveAttribute( - "aria-selected", - "true", - ); + expect(screen.getByRole("tab", { name: /remote/i })).toHaveAttribute("aria-selected", "true"); expect(screen.getByText(/root@YOUR_VPS_IP/)).toBeInTheDocument(); }); it("switches the install command when another tab is selected", () => { render(); fireEvent.click(screen.getByRole("tab", { name: /guided/i })); - expect(screen.getByRole("tab", { name: /guided/i })).toHaveAttribute( - "aria-selected", - "true", - ); + expect(screen.getByRole("tab", { name: /guided/i })).toHaveAttribute("aria-selected", "true"); expect(screen.getByText(/--interactive/)).toBeInTheDocument(); expect(screen.queryByText(/root@YOUR_VPS_IP/)).not.toBeInTheDocument(); }); it("copies the active command to the clipboard", async () => { const writeText = vi.fn().mockResolvedValue(undefined); - Object.defineProperty(navigator, "clipboard", { - value: { writeText }, - configurable: true, - }); + Object.defineProperty(navigator, "clipboard", { value: { writeText }, configurable: true }); render(); - fireEvent.click( - screen.getByRole("button", { name: "Copy installation command" }), - ); + fireEvent.click(screen.getByRole("button", { name: "Copy installation command" })); await waitFor(() => expect(writeText).toHaveBeenCalledTimes(1)); - expect(writeText).toHaveBeenCalledWith( - expect.stringContaining("root@YOUR_VPS_IP"), - ); + expect(writeText).toHaveBeenCalledWith(expect.stringContaining("root@YOUR_VPS_IP")); expect(screen.getByText("COPIED")).toBeInTheDocument(); }); }); diff --git a/app/page.tsx b/app/page.tsx index feb7146..adeed18 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -2,140 +2,36 @@ import { useState } from "react"; +const PRODUCT_URL = "https://groundcontrol.serendepify.com"; +const GITHUB_URL = "https://github.com/teckedd-code2save/groundcontrol"; const BOOTSTRAP_URL = "https://raw.githubusercontent.com/teckedd-code2save/groundcontrol/main/scripts/bootstrap"; const commands = [ - { - id: "remote", - label: "Remote", - hint: "From your computer", - command: `curl -fsSL ${BOOTSTRAP_URL} | bash -s -- -i ~/.ssh/id_ed25519 root@YOUR_VPS_IP`, - }, - { - id: "guided", - label: "Guided", - hint: "Interactive setup", - command: `curl -fsSL ${BOOTSTRAP_URL} | bash -s -- --interactive`, - }, - { - id: "local", - label: "Local", - hint: "Already on the VPS", - command: `curl -fsSL ${BOOTSTRAP_URL} | bash`, - }, + { id: "remote", label: "Remote", hint: "From your computer", command: `curl -fsSL ${BOOTSTRAP_URL} | bash -s -- -i ~/.ssh/id_ed25519 root@YOUR_VPS_IP` }, + { id: "guided", label: "Guided", hint: "Interactive setup", command: `curl -fsSL ${BOOTSTRAP_URL} | bash -s -- --interactive` }, + { id: "local", label: "Local", hint: "Already on the VPS", command: `curl -fsSL ${BOOTSTRAP_URL} | bash` }, ] as const; -const capabilities = [ - { - number: "01", - title: "See the whole host", - copy: "Containers, images, services, processes and host telemetry are presented as one operating picture.", - meta: "Inventory · Health · Runtime", - }, - { - number: "02", - title: "Deploy with context", - copy: "Connect repositories, manage environment values and move services forward without losing the path back.", - meta: "GitHub · Compose · Rollback", - }, - { - number: "03", - title: "Investigate from evidence", - copy: "Bring changes, alerts and runtime signals together before deciding what should happen next.", - meta: "Signals · Timeline · Actions", - }, -]; - -const installSteps = [ - ["Connect", "GroundControl checks the VPS, Docker and the safest available installation path."], - ["Install", "A self-contained control plane is created on infrastructure you already own."], - ["Operate", "Open your instance, change the generated password and begin from the actual host state."], -]; - -function Arrow() { - return ; -} - -function ProductPreview() { - return ( -
-
-
- - GROUND CONTROL -
-
- LIVE HOST -
-
- -
- - -
-
-
-

FLEET OVERVIEW

-

Operations

-
-

02:14:38 UTC

-
+const features = [ + { title: "Deployments", copy: "Build and deploy from connected repositories to Docker Compose, static sites, k3s, Cloud Run and Terraform-backed targets.", meta: "Build · deploy · verify · redeploy" }, + { title: "Runtime", copy: "Inspect containers, services, health, logs, processes and host telemetry from the same place you deploy them.", meta: "Docker · services · logs · health" }, + { title: "Terminal", copy: "Use a real browser terminal against the VPS, including host-level commands when GroundControl itself is running in Docker.", meta: "Shell · host operations · AI commands" }, + { title: "Intelligence", copy: "Investigate broken customer journeys from live evidence, connect changes to failures and prepare the next safe action.", meta: "Evidence · investigation · recovery" }, + { title: "Domains & edge", copy: "Connect Cloudflare, provision DNS, route through Caddy and verify the public HTTPS path as part of deployment.", meta: "Cloudflare · Caddy · DNS · TLS" }, + { title: "Your infrastructure", copy: "GroundControl is self-hosted and single-tenant. Your instance, data and operational credentials stay on infrastructure you control.", meta: "Open source · self-hosted · single tenant" }, +] as const; -
-
- VERIFIED OPERATIONAL STATE -

All systems operational

-

No current customer-impacting signals across this host.

-
- OPEN INTELLIGENCE - REVIEW ALERTS -
-
-
- {[ - ["MEMORY", "42%", "1.7 / 4 GB"], - ["DISK", "31%", "24 / 80 GB"], - ["LOAD", "0.24", "4 cores"], - ["CONTAINERS", "8/8", "all healthy"], - ].map(([label, value, detail]) => ( -
- {label} - {value} - {detail} -
- ))} -
-
+const templates = [ + ["Existing Compose", "vps-caddy-existing-compose", "Bring a production docker-compose.yml from Git. GroundControl discovers the service graph, env requirements, ports and health checks, then publishes the selected service."], + ["Odoo Community", "vps-caddy-odoo-community", "Deploy Odoo Community with PostgreSQL, persistent data, generated credentials, a public domain and local recovery archives."], + ["Next.js SaaS", "nextjs-saas-postgres-redis", "Next.js with PostgreSQL and Redis for auth, queues, sessions and application state."], + ["FastAPI + worker", "fastapi-worker-postgres-redis", "FastAPI/Uvicorn, worker, PostgreSQL and Redis with a production-shaped service layout."], + ["Monorepo", "monorepo-web-api-worker", "Separate web and API domains with a worker, PostgreSQL and Redis behind the same deployment workflow."], + ["Microservices + ops", "caddy-secure-microservices-observability", "Web, API, worker, PostgreSQL, Redis, object storage, logs and uptime checks in one Caddy-first stack."], +] as const; -
-
-
HOST TELEMETRYLAST HOUR
- -
-
-
RUNTIME8 RUNNING
- {["api", "web", "postgres", "caddy"].map((service) => ( -
- - {service} - RUNNING -
- ))} -
-
-
-
-
- ); -} +function Arrow() { return ; } function InstallConsole() { const [selected, setSelected] = useState<(typeof commands)[number]["id"]>("remote"); @@ -143,9 +39,8 @@ function InstallConsole() { const active = commands.find((command) => command.id === selected) ?? commands[0]; async function copyCommand() { - if (navigator.clipboard?.writeText) { - await navigator.clipboard.writeText(active.command); - } else { + if (navigator.clipboard?.writeText) await navigator.clipboard.writeText(active.command); + else { const textArea = document.createElement("textarea"); textArea.value = active.command; textArea.setAttribute("readonly", ""); @@ -164,38 +59,16 @@ function InstallConsole() {
{commands.map((command) => ( - ))}
-
- - GROUND CONTROL INSTALLER -
-
- $ - {active.command} - -
-
- YOUR VPS · YOUR DATA · YOUR CONTROL PLANE - Docker required -
+ ${active.command} +
); @@ -203,119 +76,72 @@ function InstallConsole() { export default function Home() { return ( -
+
- - - GROUND CONTROL - + GroundControl
-
- -
); } diff --git a/app/session.css b/app/session.css new file mode 100644 index 0000000..0b151b5 --- /dev/null +++ b/app/session.css @@ -0,0 +1,41 @@ +.hero-session { + padding: clamp(28px, 4vw, 42px); + border: 1px solid var(--line-strong); + background: var(--deep); + box-shadow: 0 28px 80px rgba(0,0,0,.35); +} +.hero-session h2 { + margin: 18px 0 28px; + font-size: clamp(28px, 3.1vw, 44px); + line-height: 1.03; + letter-spacing: -.045em; + font-weight: 520; +} +.session-steps { border-top: 1px solid var(--line); } +.session-steps p { + margin: 0; + padding: 17px 0; + display: grid; + grid-template-columns: 36px 1fr; + gap: 14px; + border-bottom: 1px solid var(--line); + color: var(--muted); + font-size: 13px; + line-height: 1.55; +} +.session-steps span { + color: var(--accent-bright); + font: 600 9px/1.5 var(--font-geist-mono); +} +.hero-session > a { + display: inline-flex; + gap: 10px; + margin-top: 24px; + color: #d5d9d5; + font: 600 10px/1 var(--font-geist-mono); +} +.hero-session > a:hover { color: white; } + +@media (max-width: 980px) { + .hero-session { max-width: 820px; } +}