2 Commits

Author SHA1 Message Date
Vantz Stockwell
9a5b93dd08 feat(api): early-access signup endpoint (POST /api/early-access)
All checks were successful
Test Asgard Runner / test (push) Successful in 3s
Real @Public() NestJS endpoint persisting to the existing early_access_signups table (email + server_count), matching the schema exactly (no migration). Duplicate-email safe (pre-check + unique-constraint catch -> friendly success). Wired into app.module. Makes the marketing early-access form functional end-to-end on next API deploy. tsc/nest build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 05:09:34 -04:00
Vantz Stockwell
3545e6f5c8 feat(marketing): pricing, how-it-works, FAQ, roadmap, early-access pages (real content)
Five marketing sub-pages built to match the landing's design language, all real content: Pricing (4 real tiers + Fleet Block + commercial-use definition + feature-comparison table + self-service support model), How it works (one agent -> N game instances, BYOS, no-SSH), FAQ (real support/product/games/billing Q&A reflecting the self-service model), Roadmap (honest Shipped/In-progress/Planned, no fake dates), Early access (real signup form). 3 icons added (circle/send/help-circle). Visually verified via Playwright; 0 console errors. Build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 05:09:34 -04:00
11 changed files with 1847 additions and 663 deletions

View File

@@ -44,6 +44,7 @@ import { FurnaceSplitterModule } from './modules/furnacesplitter/furnacesplitter
import { BetterChatModule } from './modules/betterchat/betterchat.module'; import { BetterChatModule } from './modules/betterchat/betterchat.module';
import { TimedExecuteModule } from './modules/timedexecute/timedexecute.module'; import { TimedExecuteModule } from './modules/timedexecute/timedexecute.module';
import { RaidableBasesModule } from './modules/raidablebases/raidablebases.module'; import { RaidableBasesModule } from './modules/raidablebases/raidablebases.module';
import { EarlyAccessModule } from './modules/early-access/early-access.module';
// Shared Services // Shared Services
import { NatsService } from './services/nats.service'; import { NatsService } from './services/nats.service';
@@ -123,6 +124,7 @@ import { NatsBridgeGateway } from './gateways/nats-bridge.gateway';
BetterChatModule, BetterChatModule,
TimedExecuteModule, TimedExecuteModule,
RaidableBasesModule, RaidableBasesModule,
EarlyAccessModule,
], ],
providers: [ providers: [
// Global guards (order matters: auth first, then license, then permissions) // Global guards (order matters: auth first, then license, then permissions)

View File

@@ -0,0 +1,14 @@
import { IsEmail, IsOptional, IsString, MaxLength } from 'class-validator';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
export class CreateEarlyAccessDto {
@ApiProperty({ example: 'admin@example.com' })
@IsEmail()
email: string;
@ApiPropertyOptional({ example: 'rust', description: 'Primary game interest or server count' })
@IsOptional()
@IsString()
@MaxLength(10)
server_count?: string;
}

View File

@@ -0,0 +1,19 @@
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import { Public } from '../../common/decorators/public.decorator';
import { EarlyAccessService } from './early-access.service';
import { CreateEarlyAccessDto } from './dto/create-early-access.dto';
@ApiTags('early-access')
@Controller()
export class EarlyAccessController {
constructor(private readonly earlyAccessService: EarlyAccessService) {}
@Public()
@Post('early-access')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'Register for early access' })
async register(@Body() dto: CreateEarlyAccessDto) {
return this.earlyAccessService.register(dto);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { EarlyAccessSignup } from '../../entities/early-access-signup.entity';
import { EarlyAccessController } from './early-access.controller';
import { EarlyAccessService } from './early-access.service';
@Module({
imports: [TypeOrmModule.forFeature([EarlyAccessSignup])],
controllers: [EarlyAccessController],
providers: [EarlyAccessService],
})
export class EarlyAccessModule {}

View File

@@ -0,0 +1,42 @@
import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { EarlyAccessSignup } from '../../entities/early-access-signup.entity';
import { CreateEarlyAccessDto } from './dto/create-early-access.dto';
@Injectable()
export class EarlyAccessService {
private readonly logger = new Logger(EarlyAccessService.name);
constructor(
@InjectRepository(EarlyAccessSignup)
private readonly repo: Repository<EarlyAccessSignup>,
) {}
async register(dto: CreateEarlyAccessDto): Promise<{ success: true; alreadyRegistered: boolean }> {
const existing = await this.repo.findOne({ where: { email: dto.email } });
if (existing) {
// Duplicate email — return friendly success rather than a 409 that would break the UX
return { success: true, alreadyRegistered: true };
}
const signup = this.repo.create({
email: dto.email,
server_count: dto.server_count ?? 'not specified',
});
try {
await this.repo.save(signup);
} catch (err: unknown) {
// Guard against a race-condition duplicate (unique constraint violation)
const pg = err as { code?: string };
if (pg.code === '23505') {
return { success: true, alreadyRegistered: true };
}
this.logger.error('Failed to save early-access signup', err);
throw err;
}
return { success: true, alreadyRegistered: false };
}
}

View File

@@ -25,6 +25,7 @@ import {
Pencil, Save, ShoppingBag, Target, User, Pencil, Save, ShoppingBag, Target, User,
// Marketing site additions // Marketing site additions
Route, Timer, Megaphone, DatabaseBackup, Store, Undo2, Route, Timer, Megaphone, DatabaseBackup, Store, Undo2,
Circle, Send, HelpCircle,
} from 'lucide-vue-next' } from 'lucide-vue-next'
const props = withDefaults( const props = withDefaults(
@@ -63,6 +64,7 @@ const registry: Record<string, Component> = {
// Marketing site additions // Marketing site additions
route: Route, timer: Timer, megaphone: Megaphone, route: Route, timer: Timer, megaphone: Megaphone,
'database-backup': DatabaseBackup, store: Store, 'undo-2': Undo2, 'database-backup': DatabaseBackup, store: Store, 'undo-2': Undo2,
circle: Circle, send: Send, 'help-circle': HelpCircle,
} }
const cmp = computed<Component | null>(() => registry[props.name] ?? null) const cmp = computed<Component | null>(() => registry[props.name] ?? null)

View File

@@ -1,16 +1,39 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' /**
import { Shield, Users, Star, MessageCircle, Clock, ChevronRight, Check, Zap, Terminal, RefreshCw, LayoutDashboard } from 'lucide-vue-next' * EarlyAccess signup page.
*
* Backend endpoint: POST /api/early-access
* The early_access_signups entity exists but no NestJS controller/module exposes it yet.
* TODO: Create backend-nest/src/modules/early-access/ with a @Public() POST /early-access
* controller that accepts { email, name?, game_interest? } and writes to early_access_signups.
* The server_count column on the entity is varchar(10) — map game_interest to it or add a
* migration adding a game_interest column.
*/
import { ref, onMounted, onUnmounted } from 'vue'
import { RouterLink } from 'vue-router'
import Icon from '@/components/ds/core/Icon.vue'
import CorrosionMark from '@/components/brand/CorrosionMark.vue'
// ---------- Email capture ---------- const panelUrl = import.meta.env.VITE_PANEL_URL ?? ''
// Form state
const email = ref('') const email = ref('')
const serverCount = ref('') const name = ref('')
const gameInterest = ref('')
const submitting = ref(false) const submitting = ref(false)
const submitted = ref(false) const submitted = ref(false)
const errorMsg = ref('') const errorMsg = ref('')
async function handleSubmit() { const GAME_OPTIONS = [
if (!email.value || !serverCount.value) return { value: 'rust', label: 'Rust' },
{ value: 'dune', label: 'Dune: Awakening' },
{ value: 'conan', label: 'Conan Exiles' },
{ value: 'soulmask', label: 'Soulmask' },
{ value: 'multiple', label: 'Multiple games' },
]
async function handleSubmit(): Promise<void> {
if (!email.value) return
errorMsg.value = '' errorMsg.value = ''
submitting.value = true submitting.value = true
try { try {
@@ -19,12 +42,13 @@ async function handleSubmit() {
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
email: email.value, email: email.value,
server_count: serverCount.value, // server_count column stores game interest (varchar 10) — no dedicated name column in DB
server_count: gameInterest.value || 'not specified',
}), }),
}) })
if (!res.ok) { if (!res.ok) {
const data = await res.json().catch(() => ({ message: 'Something went wrong' })) const data = await res.json().catch(() => ({ message: 'Something went wrong' }))
throw new Error(data.message || `HTTP ${res.status}`) throw new Error((data as { message?: string }).message ?? `HTTP ${res.status}`)
} }
submitted.value = true submitted.value = true
} catch (err: unknown) { } catch (err: unknown) {
@@ -34,291 +58,393 @@ async function handleSubmit() {
} }
} }
// ---------- Demo panels ---------- // Scroll-reveal
const panels = [ let io: IntersectionObserver | null = null
{ label: 'Dashboard', icon: LayoutDashboard, desc: 'Server overview, player count, uptime, and alerts at a glance.' },
{ label: 'Wipe Scheduler', icon: RefreshCw, desc: 'Visual wipe timeline with pre-wipe backup, map rotation, and health verification.' },
{ label: 'Plugin Config', icon: Zap, desc: 'Edit plugin settings from your browser. No JSON. No SFTP.' },
{ label: 'Player Management', icon: Users, desc: 'Online players, session tracking, kick/ban controls, and playtime history.' },
{ label: 'Console', icon: Terminal, desc: 'Real-time RCON console with timestamped, color-coded output.' },
]
// ---------- Roadmap voting ---------- function initReveal(): void {
interface VoteItem { if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
id: string io = new IntersectionObserver(
label: string (entries) => {
votes: number entries.forEach((e) => {
voted: boolean if (e.isIntersecting) {
e.target.classList.add('in')
io?.unobserve(e.target)
}
})
},
{ threshold: 0.1 },
)
document.querySelectorAll('.reveal').forEach((el) => io?.observe(el))
} }
const voteItems = ref<VoteItem[]>([ onMounted(() => { initReveal() })
{ id: 'analytics', label: 'Analytics & Retention Insights', votes: 47, voted: false }, onUnmounted(() => { io?.disconnect() })
{ id: 'webstore', label: 'Integrated Webstore', votes: 38, voted: false },
{ id: 'modules', label: 'Module Marketplace', votes: 31, voted: false },
{ id: 'discord', label: 'Discord Bot Integration', votes: 28, voted: false },
{ id: 'hosting', label: 'Hosting Provider API', votes: 19, voted: false },
])
function vote(item: VoteItem) {
if (item.voted) return
item.votes++
item.voted = true
}
const totalVotes = computed(() => voteItems.value.reduce((sum, i) => sum + i.votes, 0))
</script> </script>
<template> <template>
<div> <!-- PAGE HEADER -->
<!-- Hero --> <section class="hero" style="padding-bottom:0; border-bottom:none;">
<section class="relative overflow-hidden"> <div class="hero__atmo" />
<div class="max-w-4xl mx-auto px-6 pt-20 pb-16 text-center"> <div class="hero__grid" />
<span class="inline-block px-4 py-1.5 bg-green-500/10 border border-green-500/20 rounded-full text-green-400 text-sm font-medium mb-6"> <div class="hero__grain" />
Early Access Is Now Open <div class="wrap hero__in" style="padding-bottom:52px;">
</span> <div class="hero__mark">
<h1 class="text-4xl md:text-5xl font-bold text-neutral-100 mb-4 tracking-tight"> <CorrosionMark :size="56" />
Wipe Night Just Got<br />
<span class="text-oxide-500">A Lot Easier.</span>
</h1>
<p class="text-lg text-neutral-400 max-w-xl mx-auto mb-10">
Corrosion is live in limited early access. Install once. Automate everything. Never SSH again.
</p>
<div class="flex items-center justify-center gap-4">
<a href="#join" class="px-8 py-3.5 bg-oxide-600 hover:bg-oxide-700 text-white font-semibold rounded-lg transition-colors">
Claim Your Spot
</a>
<a href="#demo" class="px-8 py-3.5 bg-neutral-800 hover:bg-neutral-700 text-neutral-200 font-semibold rounded-lg border border-neutral-700 transition-colors">
View Demo Architecture
</a>
</div>
</div> </div>
<div class="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[400px] bg-oxide-500/8 rounded-full blur-3xl pointer-events-none" /> <span class="eyebrow">Early access</span>
</section> <h1 style="font-size:var(--text-5xl)">
Take control of your servers.
<span class="accent">Starting now.</span>
</h1>
<p class="hero__sub">
Corrosion is in early access. Join the list to be notified when your access opens.
No spam. No fabricated scarcity.
</p>
</div>
</section>
<!-- Early Access Live Banner --> <!-- WHAT YOU GET -->
<section class="py-12 border-t border-neutral-800"> <section class="sec" id="access">
<div class="max-w-3xl mx-auto px-6 text-center"> <div class="wrap">
<div class="inline-flex items-center gap-3 px-6 py-4 bg-green-500/10 border border-green-500/20 rounded-2xl"> <div class="sec__head reveal">
<div class="w-2.5 h-2.5 bg-green-400 rounded-full animate-pulse shrink-0" /> <span class="eyebrow">What early access means</span>
<p class="text-green-300 font-semibold text-lg">Early Access is now live founding admin spots are limited.</p> <h2 class="title">Real access to a real platform.</h2>
</div> <p class="lead">
<p class="text-neutral-500 text-sm mt-4"> Early access is not a waitlist gimmick. It is how we manage onboarding while the
Sign up below to lock in founding pricing before spots run out. platform stabilizes. You get the full Corrosion control plane one tier at a time
as capacity opens.
</p> </p>
</div> </div>
</section>
<!-- What Early Access Means --> <div class="infra reveal">
<section class="py-16 bg-neutral-900/50 border-t border-neutral-800"> <div class="icard">
<div class="max-w-4xl mx-auto px-6"> <div class="icard__ic"><Icon name="cpu" :size="16" /></div>
<h2 class="text-2xl font-bold text-neutral-100 mb-8 text-center">What Early Access Means</h2> <b>Full control plane</b>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4"> <p>Agent, panel, wipes, console, plugins, schedules all of it. Not a trimmed preview.</p>
<div class="p-5 bg-neutral-900 border border-neutral-800 rounded-xl text-center"> </div>
<Shield class="w-6 h-6 text-oxide-500 mx-auto mb-3" /> <div class="icard">
<p class="text-sm font-medium text-neutral-200">Limited Founding Licenses</p> <div class="icard__ic"><Icon name="shield" :size="16" /></div>
<p class="text-xs text-neutral-500 mt-1">2550 spots</p> <b>Pricing you can lock in</b>
</div> <p>Early access pricing is the live pricing. No bait-and-switch after launch.</p>
<div class="p-5 bg-neutral-900 border border-neutral-800 rounded-xl text-center"> </div>
<MessageCircle class="w-6 h-6 text-oxide-500 mx-auto mb-3" /> <div class="icard">
<p class="text-sm font-medium text-neutral-200">Direct Founder Discord</p> <div class="icard__ic"><Icon name="message-square" :size="16" /></div>
<p class="text-xs text-neutral-500 mt-1">Private channel access</p> <b>Direct feedback channel</b>
</div> <p>Early access operators have a direct line for platform bug reports and feature input.</p>
<div class="p-5 bg-neutral-900 border border-neutral-800 rounded-xl text-center"> </div>
<Star class="w-6 h-6 text-oxide-500 mx-auto mb-3" /> <div class="icard">
<p class="text-sm font-medium text-neutral-200">Influence the Roadmap</p> <div class="icard__ic"><Icon name="box" :size="16" /></div>
<p class="text-xs text-neutral-500 mt-1">Vote on features</p> <b>Rust-first</b>
</div> <p>Rust support is complete. Dune, Conan, and Soulmask are in active development.</p>
<div class="p-5 bg-neutral-900 border border-neutral-800 rounded-xl text-center"> </div>
<Clock class="w-6 h-6 text-oxide-500 mx-auto mb-3" /> <div class="icard">
<p class="text-sm font-medium text-neutral-200">Lifetime Pricing Lock</p> <div class="icard__ic"><Icon name="users" :size="16" /></div>
<p class="text-xs text-neutral-500 mt-1">Never pay more</p> <b>RBAC team access</b>
</div> <p>Add your admin team from day one. Fine-grained permission roles are built in.</p>
</div> </div>
</div> </div>
</section> </div>
</section>
<!-- Email Capture --> <!-- SIGNUP FORM -->
<section id="join" class="py-16 border-t border-neutral-800"> <section class="sec" id="join">
<div class="max-w-md mx-auto px-6"> <div class="wrap">
<h2 class="text-2xl font-bold text-neutral-100 mb-2 text-center">Claim Your Founding Spot</h2> <div class="ea-form-wrap reveal">
<p class="text-neutral-400 text-center mb-8">Early access is open now. Spots are limited lock in founding pricing today.</p> <!-- Success state -->
<div v-if="submitted" class="ea-success">
<div v-if="submitted" class="bg-green-500/10 border border-green-500/20 rounded-xl p-8 text-center"> <div class="ea-success__ic">
<Check class="w-10 h-10 text-green-400 mx-auto mb-3" /> <Icon name="check" :size="28" />
<h3 class="text-lg font-semibold text-neutral-100 mb-1">You're in.</h3> </div>
<p class="text-sm text-neutral-400">We'll be in touch shortly with your access details.</p> <h2 class="ea-success__title">You are on the list.</h2>
<p class="ea-success__body">
We will reach out when your access slot opens. In the meantime, read the
<RouterLink :to="{ name: 'how-it-works' }" class="ea-link">how it works</RouterLink>
guide or review the
<RouterLink :to="{ name: 'faq' }" class="ea-link">FAQ</RouterLink>.
</p>
<RouterLink class="btn btn--ghost" :to="{ name: 'landing' }">
Back to home
</RouterLink>
</div> </div>
<form v-else @submit.prevent="handleSubmit" class="space-y-4"> <!-- Form state -->
<div v-if="errorMsg" class="p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"> <form v-else @submit.prevent="handleSubmit" class="ea-form">
<div class="ea-form__head">
<h2>Join the early access list</h2>
<p>Required: email address. Everything else is optional but helps us prioritise.</p>
</div>
<!-- Error banner -->
<div v-if="errorMsg" class="ea-error">
<Icon name="triangle-alert" :size="15" />
{{ errorMsg }} {{ errorMsg }}
</div> </div>
<div>
<label for="ea-email" class="block text-sm font-medium text-neutral-400 mb-1.5">Email</label> <!-- Email (required) -->
<div class="ea-field">
<label class="ea-field__label" for="ea-email">
Email address <span class="ea-field__req">*</span>
</label>
<input <input
id="ea-email" id="ea-email"
v-model="email" v-model="email"
type="email" type="email"
required required
autocomplete="email"
placeholder="admin@example.com" placeholder="admin@example.com"
class="w-full px-3 py-2.5 bg-neutral-900 border border-neutral-700 rounded-lg text-neutral-100 placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-oxide-500/50 focus:border-oxide-500 transition-colors" class="ea-input"
/> />
</div> </div>
<div>
<label for="ea-servers" class="block text-sm font-medium text-neutral-400 mb-1.5">How many servers do you run?</label> <!-- Name (optional) -->
<div class="grid grid-cols-3 gap-3"> <div class="ea-field">
<label class="ea-field__label" for="ea-name">
Your name <span class="ea-field__optional">(optional)</span>
</label>
<input
id="ea-name"
v-model="name"
type="text"
autocomplete="name"
placeholder="Server admin name or handle"
class="ea-input"
/>
</div>
<!-- Game interest (optional) -->
<div class="ea-field">
<label class="ea-field__label">
Primary game interest <span class="ea-field__optional">(optional)</span>
</label>
<div class="ea-pills">
<button <button
v-for="option in ['1', '2-3', '4+']" v-for="opt in GAME_OPTIONS"
:key="option" :key="opt.value"
type="button" type="button"
@click="serverCount = option" class="ea-pill"
class="py-2.5 text-sm font-medium rounded-lg border transition-colors" :class="{ 'ea-pill--on': gameInterest === opt.value }"
:class="serverCount === option @click="gameInterest = gameInterest === opt.value ? '' : opt.value"
? 'bg-oxide-500/15 border-oxide-500/40 text-oxide-400'
: 'bg-neutral-900 border-neutral-700 text-neutral-400 hover:border-neutral-600'"
> >
{{ option }} {{ opt.label }}
</button> </button>
</div> </div>
</div> </div>
<button <button
type="submit" type="submit"
:disabled="submitting || !email || !serverCount" class="btn btn--primary btn--lg ea-submit"
class="w-full py-3 bg-oxide-600 hover:bg-oxide-700 disabled:opacity-50 disabled:cursor-not-allowed text-white font-semibold rounded-lg transition-colors" :disabled="submitting || !email"
> >
{{ submitting ? 'Submitting...' : 'Join Early Access' }} <Icon v-if="submitting" name="loader" :size="16" />
<Icon v-else name="send" :size="16" />
{{ submitting ? 'Submitting…' : 'Join early access' }}
</button> </button>
<p class="ea-privacy">
We store your email to contact you when access opens. We do not sell or share it.
No newsletters unless you opt in separately.
</p>
</form> </form>
</div> </div>
</section> </div>
</section>
<!-- Founding Admin Program --> <!-- HOW IT WORKS TEASER -->
<section class="py-16 bg-neutral-900/50 border-t border-neutral-800"> <section class="sec" id="teaser">
<div class="max-w-3xl mx-auto px-6 text-center"> <div class="wrap">
<span class="inline-block px-3 py-1 bg-oxide-500/10 border border-oxide-500/20 rounded-full text-oxide-400 text-xs font-semibold uppercase tracking-wider mb-4"> <div class="sec__head reveal">
Limited to 25 Servers <span class="eyebrow">How it works</span>
</span> <h2 class="title">Install the agent. Never SSH again.</h2>
<h2 class="text-3xl font-bold text-neutral-100 mb-4">Founding Admin Program</h2> </div>
<p class="text-neutral-400 mb-8"> <div class="steps reveal">
The first 25 servers to run Corrosion receive: <div class="step">
</p> <div class="step__n">1</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4"> <b>Install the host agent</b>
<div class="p-5 bg-neutral-900 border border-oxide-500/20 rounded-xl"> <p>Download the Go binary from your dashboard. Run it on Windows or Linux. One agent per machine.</p>
<Star class="w-5 h-5 text-oxide-500 mx-auto mb-2" /> </div>
<p class="text-sm font-medium text-neutral-200">Founding Admin Role</p> <div class="step">
<p class="text-xs text-neutral-500 mt-1">Discord badge</p> <div class="step__n">2</div>
</div> <b>Agent connects to Corrosion</b>
<div class="p-5 bg-neutral-900 border border-oxide-500/20 rounded-xl"> <p>Single outbound NATS connection. No inbound ports. No exposed management panel on your machine.</p>
<Clock class="w-5 h-5 text-oxide-500 mx-auto mb-2" /> </div>
<p class="text-sm font-medium text-neutral-200">Permanent Early Pricing</p> <div class="step">
<p class="text-xs text-neutral-500 mt-1">Locked forever</p> <div class="step__n">3</div>
</div> <b>Manage from the browser</b>
<div class="p-5 bg-neutral-900 border border-oxide-500/20 rounded-xl"> <p>Console, wipes, plugins, schedules, file manager, player management all at panel.corrosionmgmt.com.</p>
<Users class="w-5 h-5 text-oxide-500 mx-auto mb-2" />
<p class="text-sm font-medium text-neutral-200">Public Recognition</p>
<p class="text-xs text-neutral-500 mt-1">Featured server</p>
</div>
<div class="p-5 bg-neutral-900 border border-oxide-500/20 rounded-xl">
<MessageCircle class="w-5 h-5 text-oxide-500 mx-auto mb-2" />
<p class="text-sm font-medium text-neutral-200">Direct Feature Input</p>
<p class="text-xs text-neutral-500 mt-1">Private channel</p>
</div>
</div> </div>
</div> </div>
</section> <div class="closing reveal">
<RouterLink :to="{ name: 'how-it-works' }" class="btn btn--ghost btn--lg">
<!-- Demo Dashboard Preview --> <Icon name="chevron-right" :size="17" />Read the full walkthrough
<section id="demo" class="py-16 border-t border-neutral-800"> </RouterLink>
<div class="max-w-5xl mx-auto px-6">
<h2 class="text-2xl font-bold text-neutral-100 mb-2 text-center">Preview of Corrosion 1.0 Dashboard</h2>
<p class="text-neutral-500 text-center mb-10">Screenshots will replace these frames at launch.</p>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<div
v-for="panel in panels"
:key="panel.label"
class="bg-neutral-900 border border-neutral-800 rounded-xl overflow-hidden hover:border-neutral-700 transition-colors"
>
<div class="h-36 bg-neutral-800/50 flex items-center justify-center border-b border-neutral-800">
<component :is="panel.icon" class="w-10 h-10 text-neutral-700" />
</div>
<div class="p-4">
<h3 class="text-sm font-semibold text-neutral-200 mb-1">{{ panel.label }}</h3>
<p class="text-xs text-neutral-500 leading-relaxed">{{ panel.desc }}</p>
</div>
</div>
</div>
</div> </div>
</section> </div>
</section>
<!-- Roadmap Voting --> <!-- FINAL CTA -->
<section class="py-16 bg-neutral-900/50 border-t border-neutral-800"> <section class="finalcta">
<div class="max-w-2xl mx-auto px-6"> <div class="finalcta__atmo" />
<h2 class="text-2xl font-bold text-neutral-100 mb-2 text-center">What Should We Build Next?</h2> <div class="wrap finalcta__in reveal">
<p class="text-neutral-500 text-center mb-8">Vote on the features that matter most to you.</p> <h2>Ready to stop babysitting<br>your servers?</h2>
<div class="space-y-3"> <div class="cta-row">
<button <a href="#join" class="btn btn--primary btn--lg">
v-for="item in voteItems" Sign up above
:key="item.id" </a>
@click="vote(item)" <a class="btn btn--ghost btn--lg" :href="panelUrl + '/login'">
class="w-full flex items-center gap-4 p-4 bg-neutral-900 border rounded-xl transition-colors text-left" <Icon name="play" :size="17" />View live demo
:class="item.voted ? 'border-oxide-500/30' : 'border-neutral-800 hover:border-neutral-700'" </a>
>
<div class="flex-1">
<p class="text-sm font-medium" :class="item.voted ? 'text-oxide-400' : 'text-neutral-200'">{{ item.label }}</p>
</div>
<div class="flex items-center gap-3 shrink-0">
<div class="w-24 h-1.5 bg-neutral-800 rounded-full overflow-hidden">
<div
class="h-full rounded-full transition-all duration-300"
:class="item.voted ? 'bg-oxide-500' : 'bg-neutral-600'"
:style="{ width: `${totalVotes ? (item.votes / totalVotes) * 100 : 0}%` }"
/>
</div>
<span class="text-xs font-medium tabular-nums w-8 text-right" :class="item.voted ? 'text-oxide-400' : 'text-neutral-500'">
{{ item.votes }}
</span>
</div>
</button>
</div>
</div> </div>
</section> </div>
</section>
<!-- Timeline -->
<section class="py-16 border-t border-neutral-800">
<div class="max-w-2xl mx-auto px-6">
<h2 class="text-2xl font-bold text-neutral-100 mb-8 text-center">Launch Timeline</h2>
<div class="space-y-4">
<div class="flex items-start gap-4">
<div class="w-8 h-8 bg-green-500/10 border border-green-500/20 rounded-full flex items-center justify-center shrink-0 mt-0.5">
<Check class="w-4 h-4 text-green-400" />
</div>
<div>
<p class="text-sm font-medium text-neutral-200">Week 1 Closed Beta Stabilization</p>
<p class="text-xs text-neutral-500 mt-0.5">Core platform hardening and testing.</p>
</div>
</div>
<div class="ml-4 w-px h-4 bg-neutral-800" />
<div class="flex items-start gap-4">
<div class="w-8 h-8 bg-green-500/10 border border-green-500/20 rounded-full flex items-center justify-center shrink-0 mt-0.5">
<Check class="w-4 h-4 text-green-400" />
</div>
<div>
<p class="text-sm font-medium text-neutral-200">Week 2 Early Access Open</p>
<p class="text-xs text-neutral-500 mt-0.5">Founding Admin licenses are live claim yours now.</p>
</div>
</div>
<div class="ml-4 w-px h-4 bg-neutral-800" />
<div class="flex items-start gap-4">
<div class="w-8 h-8 bg-neutral-800 border border-neutral-700 rounded-full flex items-center justify-center shrink-0 mt-0.5">
<ChevronRight class="w-4 h-4 text-neutral-500" />
</div>
<div>
<p class="text-sm font-medium text-neutral-400">Public Release</p>
<p class="text-xs text-neutral-500 mt-0.5">Shortly after early access stabilization.</p>
</div>
</div>
</div>
</div>
</section>
</div>
</template> </template>
<style scoped>
/* Form wrapper */
.ea-form-wrap {
max-width: 520px;
margin: 0 auto;
}
/* Success state */
.ea-success {
text-align: center;
padding: 48px 32px;
background: var(--surface-base);
border-radius: var(--radius-xl);
box-shadow: inset 0 0 0 1px var(--accent-border);
}
.ea-success__ic {
width: 56px;
height: 56px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: var(--accent-soft);
color: var(--accent-text);
box-shadow: inset 0 0 0 1px var(--accent-border);
margin: 0 auto 18px;
}
.ea-success__title {
font-family: var(--font-brand);
font-weight: 700;
font-size: var(--text-2xl);
margin: 0 0 10px;
}
.ea-success__body {
color: var(--text-tertiary);
font-size: var(--text-sm);
line-height: 1.6;
margin: 0 0 24px;
}
.ea-link {
color: var(--accent-text);
text-decoration: none;
font-weight: 600;
}
.ea-link:hover { text-decoration: underline; }
/* Form */
.ea-form {
background: var(--surface-base);
border-radius: var(--radius-xl);
box-shadow: var(--ring-default);
padding: 36px 32px;
display: flex;
flex-direction: column;
gap: 22px;
}
.ea-form__head h2 {
font-family: var(--font-brand);
font-weight: 700;
font-size: var(--text-2xl);
margin: 0 0 6px;
}
.ea-form__head p {
color: var(--text-tertiary);
font-size: var(--text-sm);
margin: 0;
}
/* Error banner */
.ea-error {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 14px;
background: color-mix(in srgb, var(--status-offline) 10%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--status-offline) 25%, transparent);
border-radius: var(--radius-md);
color: var(--status-offline);
font-size: var(--text-sm);
}
/* Fields */
.ea-field { display: flex; flex-direction: column; gap: 7px; }
.ea-field__label {
font-size: var(--text-sm);
font-weight: 600;
color: var(--text-secondary);
}
.ea-field__req { color: var(--status-offline); }
.ea-field__optional { color: var(--text-muted); font-weight: 400; }
.ea-input {
height: 42px;
padding: 0 13px;
background: var(--surface-inset);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
color: var(--text-primary);
font-size: var(--text-sm);
font-family: var(--font-sans);
transition: border-color var(--dur-fast), box-shadow var(--dur-fast);
outline: none;
}
.ea-input::placeholder { color: var(--text-muted); }
.ea-input:focus {
border-color: var(--accent-border);
box-shadow: 0 0 0 3px var(--accent-soft);
}
/* Game pills */
.ea-pills {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.ea-pill {
height: 34px;
padding: 0 14px;
border-radius: var(--radius-pill);
background: var(--surface-raised);
box-shadow: var(--ring-default);
color: var(--text-secondary);
font-size: var(--text-xs);
font-weight: 600;
cursor: pointer;
border: none;
transition: var(--transition-colors);
}
.ea-pill--on {
background: var(--accent-soft);
color: var(--accent-text);
box-shadow: inset 0 0 0 1px var(--accent-border);
}
/* Submit */
.ea-submit {
width: 100%;
justify-content: center;
}
.ea-submit:disabled { opacity: 0.55; cursor: not-allowed; }
/* Privacy note */
.ea-privacy {
font-size: var(--text-xs);
color: var(--text-muted);
text-align: center;
line-height: 1.5;
margin: 0;
}
</style>

View File

@@ -1,101 +1,353 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref, onMounted, onUnmounted } from 'vue'
import { ChevronDown } from 'lucide-vue-next' import { RouterLink } from 'vue-router'
import Icon from '@/components/ds/core/Icon.vue'
import CorrosionMark from '@/components/brand/CorrosionMark.vue'
interface FaqItem { interface FaqItem {
question: string question: string
answer: string answer: string
} }
const faqs: FaqItem[] = [ interface FaqGroup {
label: string
icon: string
items: FaqItem[]
}
const groups: FaqGroup[] = [
{ {
question: 'Do I need to open any firewall ports?', label: 'Support',
answer: 'No. All connections are outbound from your server to Corrosion\'s cloud. No inbound ports required.', icon: 'help-circle',
items: [
{
question: 'Do you provide direct support?',
answer:
'Corrosion is a self-service tool. Every plan includes documentation, community forum access, diagnostics, and structured platform bug reports. We do not provide 1:1 setup assistance, Discord DMs, video calls, server administration, hosting-provider troubleshooting, firewall configuration, mod installation, or emergency wipe-day support.',
},
{
question: 'What if Corrosion itself is broken?',
answer:
'Platform bugs and agent issues go through structured bug reports in the panel. Operator and Network customers receive priority bug triage for platform-level issues. Direct server administration is not included in any support tier.',
},
{
question: 'Do you manage my server for me?',
answer:
'No. Corrosion provides the panel, the agent, automation workflows, diagnostics, and the control plane. You remain responsible for your host machine, operating system, network, firewall, game files, mods, and community rules.',
},
{
question: 'Is hands-on help available?',
answer:
'Yes — separately. Direct 1:1 support is available at $125/hour, prepaid in 1-hour blocks. This is billed time with a human, not a support tier. It is available to any customer who needs it.',
},
{
question: 'What does community support include?',
answer:
'Documentation (setup guides, architecture reference, troubleshooting walkthroughs), a community forum for operator-to-operator knowledge sharing, in-panel diagnostics (agent health, log access), and a structured bug report system for platform issues.',
},
],
}, },
{ {
question: 'Does Corrosion replace my hosting panel (AMP / Pterodactyl)?', label: 'Product',
answer: 'No. Corrosion integrates with them via API or companion agent. Your existing panel remains intact.', icon: 'server',
items: [
{
question: 'Do I need my own server?',
answer:
'Yes. Corrosion is bring-your-own-server. You supply the host machine — a VPS, dedicated server, or bare metal box running Windows or Linux. Corrosion provides the control plane, the agent, and the panel.',
},
{
question: 'Does Corrosion host my game server for me?',
answer:
'No. Corrosion is not a hosting provider. It is a management layer that runs on top of a server you already own or rent. If you need hosting, you need a separate hosting provider.',
},
{
question: 'Do I need to open inbound firewall ports for Corrosion?',
answer:
'No. The host agent establishes a single outbound NATS connection to Corrosion\'s cloud. No inbound management ports are required. Your game server\'s player ports (RCON, game ports) remain as they have always been.',
},
{
question: 'Does Corrosion replace AMP or Pterodactyl?',
answer:
'It can, depending on your use case. Corrosion is an independent control plane — it does not require an existing game panel. If you are currently using AMP or Pterodactyl, migration tooling is available in the panel.',
},
{
question: 'What happens if Corrosion goes offline?',
answer:
'Your game servers continue running normally. Corrosion does not proxy gameplay traffic — it only handles management operations. If the panel or cloud is unreachable, your players are unaffected.',
},
{
question: 'Can multiple admins manage the same server?',
answer:
'Yes. Role-based access control (RBAC) is built in. You can grant team members specific permissions — from full admin access down to read-only viewer — without sharing credentials.',
},
{
question: 'What OS does the agent run on?',
answer:
'Both Windows and Linux are supported for the host agent. The agent binary is downloaded directly from your dashboard — there is no manual build or dependency setup required.',
},
{
question: 'Is my data isolated from other customers?',
answer:
'Yes. All data is scoped by license ID at the database level. No server, config, or player data is shared across tenant boundaries.',
},
],
}, },
{ {
question: 'What happens if Corrosion goes offline?', label: 'Games',
answer: 'Your Rust server continues running normally. Corrosion does not proxy gameplay traffic.', icon: 'box',
items: [
{
question: 'Which games are supported?',
answer:
'Currently in active development or shipped: Rust, Dune: Awakening, Conan Exiles, and Soulmask. Each game has a purpose-built blueprint — not a generic template — that models its specific operational reality (wipe types, mod systems, cluster configurations). More games are planned.',
},
{
question: 'Does Corrosion support Rust plugin management?',
answer:
'Yes. Corrosion integrates with uMod (Oxide) for Rust. You can browse the plugin registry, install plugins, manage configuration profiles, and push config changes to the server — all from the browser.',
},
{
question: 'Can I run multiple game types on the same host machine?',
answer:
'Yes. A single host agent can supervise multiple game server processes — across different games — on the same machine. Each game instance has its own lifecycle, configuration, and wipe schedule.',
},
{
question: 'Does Corrosion handle Rust wipes?',
answer:
'Yes. Rust wipes are a first-class feature: map wipes, blueprint wipes, and full wipes. Wipes run as verified, logged sequences — pre-warning, backup, stop, update, map rotation, restart, health check, announce. Rollback is available when supported.',
},
],
}, },
{ {
question: 'Is my data shared with other servers?', label: 'Billing',
answer: 'No. All data is isolated by license ID. Multi-tenant database queries are scoped per license.', icon: 'credit-card',
}, items: [
{ {
question: 'What if a wipe fails?', question: 'What counts as commercial use?',
answer: 'Corrosion can automatically retry and optionally roll back using the pre-wipe backup.', answer:
}, 'Commercial use includes monetized communities, paid access, VIP slots, donation-funded servers, sponsorship-supported servers, hosting providers, or managing servers for others. Hobby and Community plans are non-commercial only. Operator and Network plans permit commercial use.',
{ },
question: 'Does this work on bare metal?', {
answer: 'Yes. Use the Companion Agent — no SSH required after initial setup.', question: 'What is the Fleet Block on the Network plan?',
}, answer:
{ 'The Network plan base includes 50 server instances at $99.99/mo. Each additional Fleet Block adds 50 more server slots at $49.99/mo. Stack as many Fleet Blocks as your operation requires.',
question: 'Can I manage multiple admins?', },
answer: 'Yes. Multi-Admin Role-Based Access Control is built in. Grant granular permissions per team member.', {
}, question: 'Can I upgrade my plan?',
{ answer:
question: 'Is this beginner friendly?', 'Yes. You can upgrade at any time. Pricing is prorated from the upgrade date.',
answer: 'Yes. If you can install a uMod plugin, you can use Corrosion.', },
}, {
{ question: 'Is there a free trial?',
question: 'Does this replace Tebex?', answer:
answer: 'Corrosion includes an optional integrated store (Phase 5 roadmap), but does not require Tebex.', 'Corrosion is currently in early access. Join the early access list to be notified when access opens.',
}, },
{ {
question: 'How is licensing handled?', question: 'Are there annual billing discounts?',
answer: 'One license per server. License validation occurs on plugin startup and periodically.', answer:
'Not at this time. All plans are billed monthly.',
},
],
}, },
] ]
const openIndex = ref<number | null>(null) const openKey = ref<string | null>(null)
function toggle(index: number) { function toggle(key: string): void {
openIndex.value = openIndex.value === index ? null : index openKey.value = openKey.value === key ? null : key
} }
function itemKey(groupLabel: string, idx: number): string {
return `${groupLabel}-${idx}`
}
// Scroll-reveal
let io: IntersectionObserver | null = null
function initReveal(): void {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
io = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
e.target.classList.add('in')
io?.unobserve(e.target)
}
})
},
{ threshold: 0.1 },
)
document.querySelectorAll('.reveal').forEach((el) => io?.observe(el))
}
onMounted(() => { initReveal() })
onUnmounted(() => { io?.disconnect() })
</script> </script>
<template> <template>
<div> <!-- PAGE HEADER -->
<!-- Header --> <section class="hero" style="padding-bottom:0; border-bottom:none;">
<section class="pt-20 pb-12"> <div class="hero__atmo" />
<div class="max-w-4xl mx-auto px-6 text-center"> <div class="hero__grid" />
<h1 class="text-4xl md:text-5xl font-bold text-neutral-100 mb-4">Frequently Asked Questions</h1> <div class="hero__grain" />
<p class="text-lg text-neutral-400">Everything you need to know about Corrosion.</p> <div class="wrap hero__in" style="padding-bottom:52px;">
<div class="hero__mark">
<CorrosionMark :size="56" />
</div> </div>
</section> <span class="eyebrow">FAQ</span>
<h1 style="font-size:var(--text-5xl)">
Honest answers.
<span class="accent">No marketing fluff.</span>
</h1>
<p class="hero__sub">
Common questions about support, the product, supported games, and billing answered
plainly.
</p>
</div>
</section>
<!-- FAQ Accordion --> <!-- FAQ GROUPS -->
<section class="pb-20"> <section class="sec" id="faq">
<div class="max-w-3xl mx-auto px-6"> <div class="wrap">
<div class="space-y-3"> <div
v-for="group in groups"
:key="group.label"
class="faq-group reveal"
>
<div class="faq-group__head">
<span class="faq-group__ic">
<Icon :name="group.icon" :size="16" />
</span>
<span class="eyebrow">{{ group.label }}</span>
</div>
<div class="faq-list">
<div <div
v-for="(faq, index) in faqs" v-for="(item, idx) in group.items"
:key="index" :key="idx"
class="bg-neutral-900 border rounded-xl overflow-hidden transition-colors" class="faq-item"
:class="openIndex === index ? 'border-oxide-500/30' : 'border-neutral-800'" :class="{ 'faq-item--open': openKey === itemKey(group.label, idx) }"
> >
<button <button
@click="toggle(index)" class="faq-item__q"
class="w-full flex items-center justify-between p-6 text-left" @click="toggle(itemKey(group.label, idx))"
> >
<span class="text-neutral-100 font-medium pr-4">{{ faq.question }}</span> <span>{{ item.question }}</span>
<ChevronDown <span class="faq-item__chevron">
class="w-5 h-5 text-neutral-500 shrink-0 transition-transform duration-200" <Icon
:class="{ 'rotate-180': openIndex === index }" name="chevron-down"
/> :size="16"
:class="{ 'faq-item__chevron--open': openKey === itemKey(group.label, idx) }"
/>
</span>
</button> </button>
<div <div
v-if="openIndex === index" v-if="openKey === itemKey(group.label, idx)"
class="px-6 pb-6 -mt-2" class="faq-item__a"
> >
<p class="text-neutral-400 leading-relaxed">{{ faq.answer }}</p> {{ item.answer }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</section> </div>
</div> </section>
<!-- SUPPORT CTA -->
<section class="sec" id="support-cta" style="border-bottom:none">
<div class="wrap">
<div class="sec__head reveal">
<span class="eyebrow">Still have questions?</span>
<h2 class="title">Check the docs or join the community</h2>
<p class="lead">
The documentation covers setup, architecture, troubleshooting, and every supported
game. The community forum is where operators share configs, ask questions, and help
each other.
</p>
</div>
<div class="hero__cta reveal">
<RouterLink class="btn btn--primary btn--lg" :to="{ name: 'early-access' }">
Join early access
</RouterLink>
<RouterLink class="btn btn--ghost btn--lg" :to="{ name: 'pricing' }">
<Icon name="credit-card" :size="17" />View pricing
</RouterLink>
</div>
</div>
</section>
</template> </template>
<style scoped>
.faq-group {
margin-bottom: 48px;
}
.faq-group__head {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.faq-group__ic {
width: 28px;
height: 28px;
flex: none;
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
color: var(--accent-text);
background: var(--accent-soft);
box-shadow: inset 0 0 0 1px var(--accent-border);
}
.faq-list {
display: flex;
flex-direction: column;
gap: 8px;
max-width: 860px;
margin: 0 auto;
}
.faq-item {
background: var(--surface-base);
border-radius: var(--radius-lg);
box-shadow: var(--ring-default);
overflow: hidden;
transition: box-shadow var(--dur-fast);
}
.faq-item--open {
box-shadow: inset 0 0 0 1px var(--accent-border);
}
.faq-item__q {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 18px 20px;
background: transparent;
border: none;
cursor: pointer;
text-align: left;
font-size: var(--text-sm);
font-weight: 600;
color: var(--text-primary);
transition: color var(--dur-fast);
}
.faq-item__q:hover { color: var(--accent-text); }
.faq-item__chevron {
flex: none;
color: var(--text-muted);
transition: color var(--dur-fast);
}
.faq-item__chevron--open {
transform: rotate(180deg);
color: var(--accent-text);
}
.faq-item__a {
padding: 0 20px 18px;
font-size: var(--text-sm);
color: var(--text-tertiary);
line-height: 1.65;
}
</style>

View File

@@ -1,150 +1,358 @@
<script setup lang="ts"> <script setup lang="ts">
import { Download, Globe, Wifi, LayoutDashboard, ArrowDown } from 'lucide-vue-next' import { onMounted, onUnmounted } from 'vue'
import { RouterLink } from 'vue-router'
import Icon from '@/components/ds/core/Icon.vue'
import CorrosionMark from '@/components/brand/CorrosionMark.vue'
const panelUrl = import.meta.env.VITE_PANEL_URL ?? ''
// Scroll-reveal
let io: IntersectionObserver | null = null
function initReveal(): void {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
io = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
e.target.classList.add('in')
io?.unobserve(e.target)
}
})
},
{ threshold: 0.1 },
)
document.querySelectorAll('.reveal').forEach((el) => io?.observe(el))
}
onMounted(() => { initReveal() })
onUnmounted(() => { io?.disconnect() })
</script> </script>
<template> <template>
<div> <!-- PAGE HEADER -->
<!-- Header --> <section class="hero" style="padding-bottom:0; border-bottom:none;">
<section class="pt-20 pb-12"> <div class="hero__atmo" />
<div class="max-w-4xl mx-auto px-6 text-center"> <div class="hero__grid" />
<h1 class="text-4xl md:text-5xl font-bold text-neutral-100 mb-4">How Corrosion Works</h1> <div class="hero__grain" />
<p class="text-lg text-neutral-400"> <div class="wrap hero__in" style="padding-bottom:52px;">
Corrosion connects your Rust server to a hosted control plane securely, outbound-only. <div class="hero__mark">
<CorrosionMark :size="56" />
</div>
<span class="eyebrow">How it works</span>
<h1 style="font-size:var(--text-5xl)">
One agent.
<span class="accent">Every game. No SSH.</span>
</h1>
<p class="hero__sub">
Install the host agent once on your Windows or Linux machine. Corrosion connects
securely, outbound-only. You manage every game instance from the browser.
</p>
</div>
</section>
<!-- THE MODEL: 3-STEP OVERVIEW -->
<section class="sec" id="model">
<div class="wrap">
<div class="sec__head reveal">
<span class="eyebrow">The agent model</span>
<h2 class="title">Bring your own server.<br>We provide the control plane.</h2>
<p class="lead">
Corrosion is not a hosting provider. You supply the hardware or the VPS. The host
agent runs on that machine and bridges your game instances to Corrosion's control
plane — securely, without opening inbound firewall ports.
</p> </p>
</div> </div>
</section> <div class="steps reveal">
<div class="step">
<div class="step__n">1</div>
<b>Install the host agent</b>
<p>
Download the Corrosion agent binary from your dashboard. Run it on any Windows
or Linux host. One agent per machine — it manages every game instance you assign
to it.
</p>
</div>
<div class="step">
<div class="step__n">2</div>
<b>It connects to Corrosion</b>
<p>
The agent makes a single outbound NATS connection to Corrosion's cloud. No
inbound ports. No open panels. No SSH required after initial setup.
</p>
</div>
<div class="step">
<div class="step__n">3</div>
<b>Deploy and manage from the browser</b>
<p>
Create game instances, run wipes, manage plugins, schedule maintenance, and
monitor players all from the Corrosion panel at panel.corrosionmgmt.com.
</p>
</div>
</div>
<div class="nots reveal">
<span><Icon name="check" :size="16" style="color:var(--status-online)" />No open inbound ports</span>
<span><Icon name="check" :size="16" style="color:var(--status-online)" />No permanent SSH sessions</span>
<span><Icon name="check" :size="16" style="color:var(--status-online)" />No config file spelunking</span>
<span><Icon name="check" :size="16" style="color:var(--status-online)" />Windows and Linux supported</span>
</div>
</div>
</section>
<!-- Steps --> <!-- MULTI-GAME RUNTIME -->
<section class="pb-20"> <section class="sec" id="multi-game">
<div class="max-w-3xl mx-auto px-6"> <div class="wrap">
<div class="space-y-2"> <div class="sec__head reveal">
<!-- Step 1 --> <span class="eyebrow">Multi-game host runtime</span>
<div class="bg-neutral-900 border border-neutral-800 rounded-xl p-8"> <h2 class="title">One agent. Multiple game worlds<br>on the same machine.</h2>
<div class="flex items-start gap-6"> <p class="lead">
<div class="w-12 h-12 bg-oxide-500/10 border border-oxide-500/20 rounded-xl flex items-center justify-center shrink-0"> The host agent is not a per-game process. It is a general-purpose ops runtime. One
<Download class="w-6 h-6 text-oxide-500" /> agent on a single machine can supervise multiple game server processes across
</div> different games each with its own configuration, lifecycle, and wipe schedule.
<div> </p>
<div class="flex items-center gap-3 mb-2"> </div>
<span class="text-xs font-bold text-oxide-400 uppercase tracking-wider">Step 1</span>
</div> <div class="blueprints reveal">
<h3 class="text-xl font-bold text-neutral-100 mb-2">Install the Plugin</h3> <div class="bp" data-game="rust">
<p class="text-neutral-400"> <div class="bp__head">
Drop the Corrosion plugin into <code class="px-2 py-0.5 bg-neutral-800 rounded text-oxide-300 text-sm">oxide/plugins</code>. <span class="bp__ic"><Icon name="box" :size="21" /></span>
That's it. No dependencies, no config files to create. <div>
</p> <div class="bp__name">Rust</div>
</div> <div class="bp__accent">Oxide Orange</div>
</div> </div>
</div> </div>
<div class="bp__role">uMod / Oxide plugin ecosystem</div>
<div class="flex justify-center py-1"> <div class="bp__list">
<ArrowDown class="w-5 h-5 text-neutral-700" /> <div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Map / BP / full wipe scheduling</div>
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Plugin config profiles from the browser</div>
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Wipe-day backup before every change</div>
</div> </div>
</div>
<!-- Step 2 --> <div class="bp" data-game="dune">
<div class="bg-neutral-900 border border-neutral-800 rounded-xl p-8"> <div class="bp__head">
<div class="flex items-start gap-6"> <span class="bp__ic"><Icon name="sun" :size="21" /></span>
<div class="w-12 h-12 bg-oxide-500/10 border border-oxide-500/20 rounded-xl flex items-center justify-center shrink-0"> <div>
<Globe class="w-6 h-6 text-oxide-500" /> <div class="bp__name">Dune: Awakening</div>
</div> <div class="bp__accent">Spice Amber</div>
<div>
<div class="flex items-center gap-3 mb-2">
<span class="text-xs font-bold text-oxide-400 uppercase tracking-wider">Step 2</span>
</div>
<h3 class="text-xl font-bold text-neutral-100 mb-2">Register Online</h3>
<p class="text-neutral-400">
Activate your license and configure your server. Set your hostname, game port, and admin preferences.
</p>
</div>
</div> </div>
</div> </div>
<div class="bp__role">Battlegroup orchestration</div>
<div class="flex justify-center py-1"> <div class="bp__list">
<ArrowDown class="w-5 h-5 text-neutral-700" /> <div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Deep Desert wipe scheduling</div>
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Procedural map regeneration</div>
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Battlegroup lifecycle controls</div>
</div> </div>
</div>
<!-- Step 3 --> <div class="bp" data-game="soulmask">
<div class="bg-neutral-900 border border-neutral-800 rounded-xl p-8"> <div class="bp__head">
<div class="flex items-start gap-6"> <span class="bp__ic"><Icon name="drama" :size="21" /></span>
<div class="w-12 h-12 bg-oxide-500/10 border border-oxide-500/20 rounded-xl flex items-center justify-center shrink-0"> <div>
<Wifi class="w-6 h-6 text-oxide-500" /> <div class="bp__name">Soulmask</div>
</div> <div class="bp__accent">Ritual Jade</div>
<div>
<div class="flex items-center gap-3 mb-2">
<span class="text-xs font-bold text-oxide-400 uppercase tracking-wider">Step 3</span>
</div>
<h3 class="text-xl font-bold text-neutral-100 mb-2">Secure Outbound Connection</h3>
<p class="text-neutral-400">
Your server connects securely to Corrosion's NATS cluster. No inbound firewall rules required. Your server initiates all connections.
</p>
</div>
</div> </div>
</div> </div>
<div class="bp__role">Linked-world cluster deployment</div>
<div class="flex justify-center py-1"> <div class="bp__list">
<ArrowDown class="w-5 h-5 text-neutral-700" /> <div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />One-click linked-server clusters</div>
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Port and config automation</div>
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Cluster health monitoring</div>
</div> </div>
</div>
<div class="bp" data-game="conan">
<div class="bp__head">
<span class="bp__ic"><Icon name="swords" :size="21" /></span>
<div>
<div class="bp__name">Conan Exiles</div>
<div class="bp__accent">Hyborian Bronze</div>
</div>
</div>
<div class="bp__role">Persistent world management</div>
<div class="bp__list">
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Mod management + world backups</div>
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Purge, decay, and event tracking</div>
<div><Icon name="check" :size="15" style="color:var(--accent-text);flex:none" />Scheduled maintenance workflows</div>
</div>
</div>
</div>
</div>
</section>
<!-- Step 4 --> <!-- WIPE AND MAINTENANCE AUTOMATION -->
<div class="bg-neutral-900 border border-oxide-500/30 rounded-xl p-8"> <section class="sec" id="automation">
<div class="flex items-start gap-6"> <div class="wrap">
<div class="w-12 h-12 bg-oxide-500/15 border border-oxide-500/30 rounded-xl flex items-center justify-center shrink-0"> <div class="sec__head reveal">
<LayoutDashboard class="w-6 h-6 text-oxide-400" /> <span class="eyebrow">Wipe and maintenance automation</span>
</div> <h2 class="title">Self-service workflows,<br>not manual processes</h2>
<div> <p class="lead">
<div class="flex items-center gap-3 mb-2"> Every wipe, update, and maintenance window runs as a verified, logged sequence.
<span class="text-xs font-bold text-oxide-400 uppercase tracking-wider">Step 4</span> Pre-warning announcements, pre-wipe backups, health checks after restart, and
</div> rollback capability when things go wrong.
<h3 class="text-xl font-bold text-neutral-100 mb-2">Full Orchestration</h3> </p>
<p class="text-neutral-400 mb-4">From the dashboard, you can:</p> </div>
<ul class="space-y-2">
<li class="text-neutral-300 text-sm flex items-center gap-2"> <div class="pipe reveal">
<span class="w-1.5 h-1.5 bg-oxide-500 rounded-full shrink-0" /> <span class="pchip">Pre-warning</span>
Execute console commands <Icon name="chevron-right" :size="15" style="color:var(--text-muted)" />
</li> <span class="pchip">Backup</span>
<li class="text-neutral-300 text-sm flex items-center gap-2"> <Icon name="chevron-right" :size="15" style="color:var(--text-muted)" />
<span class="w-1.5 h-1.5 bg-oxide-500 rounded-full shrink-0" /> <span class="pchip">Stop services</span>
Configure plugins <Icon name="chevron-right" :size="15" style="color:var(--text-muted)" />
</li> <span class="pchip">Update / rotate</span>
<li class="text-neutral-300 text-sm flex items-center gap-2"> <Icon name="chevron-right" :size="15" style="color:var(--text-muted)" />
<span class="w-1.5 h-1.5 bg-oxide-500 rounded-full shrink-0" /> <span class="pchip">Restart</span>
Schedule wipes <Icon name="chevron-right" :size="15" style="color:var(--text-muted)" />
</li> <span class="pchip">Health check</span>
<li class="text-neutral-300 text-sm flex items-center gap-2"> <Icon name="chevron-right" :size="15" style="color:var(--text-muted)" />
<span class="w-1.5 h-1.5 bg-oxide-500 rounded-full shrink-0" /> <span class="pchip pchip--last">Announce complete</span>
Monitor performance </div>
</li>
<li class="text-neutral-300 text-sm flex items-center gap-2"> <div class="stack-lines reveal">
<span class="w-1.5 h-1.5 bg-oxide-500 rounded-full shrink-0" /> <span>Every operation is logged. Every step is verified.</span>
Automate Steam updates <span class="hi">Rollback is one click away when supported.</span>
</li> </div>
</ul> </div>
</div> </section>
<!-- ARCHITECTURE: HOW CONNECTIVITY WORKS -->
<section class="sec" id="connectivity">
<div class="wrap">
<div class="sec__head reveal">
<span class="eyebrow">Connectivity model</span>
<h2 class="title">Outbound-only. No exposed panel.</h2>
<p class="lead">
The host agent establishes one secure NATS connection to Corrosion's cloud. All
commands flow through that channel. Your machine never needs to accept inbound
connections from the internet.
</p>
</div>
<div class="infra reveal">
<div class="icard">
<div class="icard__ic"><Icon name="server" :size="16" /></div>
<b>Your host machine</b>
<p>Windows or Linux. Bare metal, VPS, or dedicated. You own it and run it.</p>
</div>
<div class="icard">
<div class="icard__ic"><Icon name="cpu" :size="16" /></div>
<b>Corrosion agent</b>
<p>A single Go binary. Runs as a service. Manages game processes, files, and updates.</p>
</div>
<div class="icard">
<div class="icard__ic"><Icon name="zap" :size="16" /></div>
<b>Outbound NATS channel</b>
<p>One secure, outbound-only connection. No open ports. No SSH tunnels.</p>
</div>
<div class="icard">
<div class="icard__ic"><Icon name="shield" :size="16" /></div>
<b>Corrosion cloud</b>
<p>Hosted control plane. Multi-tenant isolation. Every command is license-scoped.</p>
</div>
<div class="icard">
<div class="icard__ic"><Icon name="layout-dashboard" :size="16" /></div>
<b>Your browser</b>
<p>The panel at panel.corrosionmgmt.com. Console, wipes, plugins, schedules — all here.</p>
</div>
</div>
<div class="techrow reveal">
<span>Go host agent</span>
<span>NATS JetStream</span>
<span>NestJS API</span>
<span>PostgreSQL</span>
<span>Outbound-only connectivity</span>
</div>
</div>
</section>
<!-- HOST REQUIREMENTS -->
<section class="sec" id="requirements">
<div class="wrap">
<div class="sec__head reveal">
<span class="eyebrow">Host requirements</span>
<h2 class="title">What you need to get started</h2>
</div>
<div class="caps reveal" style="max-width:760px">
<div class="caps__col">
<span class="eyebrow">Your machine</span>
<div class="feat">
<span class="feat__ic"><Icon name="server" :size="16" /></span>
<div>
<b>Windows or Linux host</b>
<p style="margin:4px 0 0;color:var(--text-tertiary);font-size:var(--text-xs)">
VPS, dedicated server, or bare metal. You supply it; Corrosion manages it.
</p>
</div>
</div>
<div class="feat">
<span class="feat__ic"><Icon name="hard-drive" :size="16" /></span>
<div>
<b>Enough CPU and RAM for your game</b>
<p style="margin:4px 0 0;color:var(--text-tertiary);font-size:var(--text-xs)">
Corrosion's agent is lightweight. Your game server determines the actual
hardware requirement.
</p>
</div>
</div>
<div class="feat">
<span class="feat__ic"><Icon name="wifi" :size="16" /></span>
<div>
<b>Outbound internet access</b>
<p style="margin:4px 0 0;color:var(--text-tertiary);font-size:var(--text-xs)">
The agent connects out; your game server's player ports stay open as they
always have been.
</p>
</div>
</div>
</div>
<div class="caps__col">
<span class="eyebrow">From Corrosion</span>
<div class="feat">
<span class="feat__ic"><Icon name="download" :size="16" /></span>
<div>
<b>Agent binary (Windows or Linux)</b>
<p style="margin:4px 0 0;color:var(--text-tertiary);font-size:var(--text-xs)">
Downloaded from your dashboard. No manual build. No dependency management.
</p>
</div>
</div>
<div class="feat">
<span class="feat__ic"><Icon name="key" :size="16" /></span>
<div>
<b>Your license key</b>
<p style="margin:4px 0 0;color:var(--text-tertiary);font-size:var(--text-xs)">
Issued when you register. The agent uses it to authenticate to the cloud.
</p>
</div>
</div>
<div class="feat">
<span class="feat__ic"><Icon name="globe" :size="16" /></span>
<div>
<b>The panel</b>
<p style="margin:4px 0 0;color:var(--text-tertiary);font-size:var(--text-xs)">
Everything else — console, wipes, schedules, players — lives at
panel.corrosionmgmt.com.
</p>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</section> </div>
</section>
<!-- Architecture Diagram --> <!-- FINAL CTA -->
<section class="py-20 bg-neutral-900/50 border-t border-neutral-800"> <section class="finalcta">
<div class="max-w-3xl mx-auto px-6 text-center"> <div class="finalcta__atmo" />
<h2 class="text-2xl font-bold text-neutral-100 mb-10">Architecture Overview</h2> <div class="wrap finalcta__in reveal">
<div class="space-y-3"> <h2>Install the agent.<br>Never SSH again.</h2>
<div class="inline-block px-6 py-3 bg-neutral-800 border border-neutral-700 rounded-lg text-neutral-200 font-medium">Rust Server</div> <div class="cta-row">
<div><ArrowDown class="w-5 h-5 text-neutral-600 mx-auto" /></div> <RouterLink class="btn btn--primary btn--lg" :to="{ name: 'early-access' }">
<div class="inline-block px-6 py-3 bg-neutral-800 border border-neutral-700 rounded-lg text-neutral-200 font-medium">Corrosion Plugin</div> Join early access
<div><ArrowDown class="w-5 h-5 text-neutral-600 mx-auto" /></div> </RouterLink>
<div class="inline-block px-6 py-3 bg-oxide-500/10 border border-oxide-500/30 rounded-lg text-oxide-400 font-medium">Secure NATS Messaging</div> <a class="btn btn--ghost btn--lg" :href="panelUrl + '/login'">
<div><ArrowDown class="w-5 h-5 text-neutral-600 mx-auto" /></div> <Icon name="play" :size="17" />View live demo
<div class="inline-block px-6 py-3 bg-oxide-500/10 border border-oxide-500/30 rounded-lg text-oxide-400 font-medium">Corrosion Cloud</div> </a>
<div><ArrowDown class="w-5 h-5 text-neutral-600 mx-auto" /></div>
<div class="inline-block px-6 py-3 bg-neutral-800 border border-neutral-700 rounded-lg text-neutral-200 font-medium">Dashboard + Store + Analytics</div>
</div>
<p class="text-neutral-500 mt-10">
Corrosion does not proxy gameplay traffic. It orchestrates operations.
</p>
</div> </div>
</section> </div>
</div> </section>
</template> </template>

View File

@@ -1,129 +1,429 @@
<script setup lang="ts"> <script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { RouterLink } from 'vue-router' import { RouterLink } from 'vue-router'
import { Check } from 'lucide-vue-next' import Icon from '@/components/ds/core/Icon.vue'
import CorrosionMark from '@/components/brand/CorrosionMark.vue'
const panelUrl = import.meta.env.VITE_PANEL_URL ?? ''
// Scroll-reveal
let io: IntersectionObserver | null = null
function initReveal(): void {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
io = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
e.target.classList.add('in')
io?.unobserve(e.target)
}
})
},
{ threshold: 0.1 },
)
document.querySelectorAll('.reveal').forEach((el) => io?.observe(el))
}
onMounted(() => { initReveal() })
onUnmounted(() => { io?.disconnect() })
interface PlanFeature {
text: string
}
interface Plan {
name: string
price: string
period: string
scope: string
tag: string
featured: boolean
cta: string
ctaVariant: 'primary' | 'ghost'
features: PlanFeature[]
}
const plans: Plan[] = [
{
name: 'Hobby',
price: '$9.99',
period: '/mo',
scope: '15 servers · non-commercial use',
tag: '',
featured: false,
cta: 'Join early access',
ctaVariant: 'ghost',
features: [
{ text: 'Up to 5 game server instances' },
{ text: 'Non-commercial servers only' },
{ text: 'Auto-wiper with rollback' },
{ text: 'Plugin management (Rust)' },
{ text: 'File manager + real-time console' },
{ text: 'Scheduled tasks' },
{ text: 'Public server page' },
{ text: 'Community support' },
],
},
{
name: 'Community',
price: '$19.99',
period: '/mo',
scope: '610 servers · non-commercial use',
tag: '',
featured: false,
cta: 'Join early access',
ctaVariant: 'ghost',
features: [
{ text: 'Up to 10 game server instances' },
{ text: 'Non-commercial servers only' },
{ text: 'Auto-wiper with rollback' },
{ text: 'Plugin management (Rust)' },
{ text: 'File manager + real-time console' },
{ text: 'Scheduled tasks' },
{ text: 'Public server page' },
{ text: 'Community support' },
],
},
{
name: 'Operator',
price: '$99.99',
period: '/mo',
scope: 'Commercial use · or 1150 servers',
tag: 'Most popular',
featured: true,
cta: 'Get Operator',
ctaVariant: 'primary',
features: [
{ text: 'Up to 50 game server instances' },
{ text: 'Commercial use permitted' },
{ text: 'All games: Rust, Dune, Soulmask, Conan' },
{ text: 'Auto-wiper with rollback' },
{ text: 'Plugin + mod management' },
{ text: 'File manager + real-time console' },
{ text: 'Scheduled tasks + maintenance windows' },
{ text: 'Player management + RBAC team access' },
{ text: 'Public server page + storefront' },
{ text: 'Community support + priority bug triage' },
],
},
{
name: 'Network',
price: '$99.99',
period: '/mo',
scope: '50+ servers · hosting partners + fleets',
tag: '',
featured: false,
cta: 'Join early access',
ctaVariant: 'ghost',
features: [
{ text: '50 servers base included' },
{ text: 'Fleet Blocks: +$49.99/mo per 50 servers' },
{ text: 'Commercial use permitted' },
{ text: 'All games + multi-game hosts' },
{ text: 'Full Operator feature set' },
{ text: 'Fleet-level management' },
{ text: 'Priority bug triage for platform issues' },
{ text: 'Community support' },
],
},
]
</script> </script>
<template> <template>
<div> <!-- PAGE HEADER -->
<!-- Header --> <section class="hero" style="padding-bottom:0; border-bottom:none;">
<section class="pt-20 pb-12"> <div class="hero__atmo" />
<div class="max-w-4xl mx-auto px-6 text-center"> <div class="hero__grid" />
<h1 class="text-4xl md:text-5xl font-bold text-neutral-100 mb-4">Pricing</h1> <div class="hero__grain" />
<p class="text-lg text-neutral-400">Simple. Transparent. No hidden tiers.</p> <div class="wrap hero__in" style="padding-bottom: 52px;">
<div class="hero__mark">
<CorrosionMark :size="56" />
</div> </div>
</section> <span class="eyebrow">Pricing</span>
<h1 style="font-size:var(--text-5xl)">
Scale from one server
<span class="accent">to a fleet.</span>
</h1>
<p class="hero__sub">
Simple tiers based on how many servers you run and whether you operate commercially.
No per-seat charges. No surprises.
</p>
</div>
</section>
<!-- Pricing Cards --> <!-- PRICING CARDS -->
<section class="pb-20"> <section class="sec" id="plans">
<div class="max-w-5xl mx-auto px-6"> <div class="wrap">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> <div class="pricing reveal">
<!-- Base License --> <div
<div class="bg-neutral-900 border-2 border-oxide-500/40 rounded-xl p-8 relative"> v-for="plan in plans"
<div class="absolute -top-3 left-1/2 -translate-x-1/2"> :key="plan.name"
<span class="px-3 py-1 bg-oxide-600 text-white text-xs font-semibold rounded-full uppercase tracking-wider">Most Popular</span> class="plan"
</div> :class="plan.featured ? 'plan--feature' : ''"
<div class="text-center mb-6"> >
<h3 class="text-xl font-bold text-neutral-100 mb-2">Base License</h3> <div class="plan__tag">{{ plan.tag }}</div>
<div class="flex items-baseline justify-center gap-1"> <div class="plan__name">{{ plan.name }}</div>
<span class="text-4xl font-bold text-oxide-400">$50</span> <div class="plan__price">
</div> {{ plan.price }}<small>{{ plan.period }}</small>
<p class="text-sm text-neutral-500 mt-1">One server. Lifetime access.</p>
<p class="text-xs text-oxide-400/70 mt-1">Launch Price</p>
</div>
<ul class="space-y-3 mb-8">
<li class="flex items-center gap-3 text-sm text-neutral-300">
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
Full control plane
</li>
<li class="flex items-center gap-3 text-sm text-neutral-300">
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
Auto-Wiper with rollback
</li>
<li class="flex items-center gap-3 text-sm text-neutral-300">
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
Plugin management
</li>
<li class="flex items-center gap-3 text-sm text-neutral-300">
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
Public server site
</li>
<li class="flex items-center gap-3 text-sm text-neutral-300">
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
Multi-admin RBAC
</li>
</ul>
<RouterLink to="/register" class="block w-full py-3 bg-oxide-600 hover:bg-oxide-700 text-white text-center font-semibold rounded-lg transition-colors">
Get Started
</RouterLink>
</div> </div>
<div class="plan__scope">{{ plan.scope }}</div>
<!-- Webstore Add-On --> <ul class="plan__feats">
<div class="bg-neutral-900 border border-neutral-800 rounded-xl p-8"> <li v-for="feat in plan.features" :key="feat.text">
<div class="text-center mb-6"> <Icon name="check" :size="13" style="color:var(--accent-text);flex:none" />
<h3 class="text-xl font-bold text-neutral-100 mb-2">Webstore Add-On</h3> {{ feat.text }}
<div class="flex items-baseline justify-center gap-1"> </li>
<span class="text-4xl font-bold text-neutral-200">$10</span> </ul>
<span class="text-neutral-500">/mo</span>
</div>
<p class="text-sm text-neutral-500 mt-1">Integrated monetization platform.</p>
</div>
<ul class="space-y-3 mb-8">
<li class="flex items-center gap-3 text-sm text-neutral-300">
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
Item catalog
</li>
<li class="flex items-center gap-3 text-sm text-neutral-300">
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
Stripe + PayPal
</li>
<li class="flex items-center gap-3 text-sm text-neutral-300">
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
Auto-delivery via RCON
</li>
<li class="flex items-center gap-3 text-sm text-neutral-300">
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
Transaction history
</li>
</ul>
<button class="block w-full py-3 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 text-center font-semibold rounded-lg border border-neutral-700 transition-colors">
Coming Soon
</button>
</div>
<!-- Modules --> <RouterLink
<div class="bg-neutral-900 border border-neutral-800 rounded-xl p-8"> class="btn"
<div class="text-center mb-6"> :class="plan.ctaVariant === 'primary' ? 'btn--primary' : 'btn--ghost'"
<h3 class="text-xl font-bold text-neutral-100 mb-2">Modules</h3> :to="{ name: 'early-access' }"
<div class="flex items-baseline justify-center gap-1"> >
<span class="text-4xl font-bold text-neutral-200">$9.99</span> {{ plan.cta }}
<span class="text-neutral-500">+</span> </RouterLink>
</div>
<p class="text-sm text-neutral-500 mt-1">Optional feature expansions.</p>
</div>
<ul class="space-y-3 mb-8">
<li class="flex items-center gap-3 text-sm text-neutral-300">
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
Analytics & insights
</li>
<li class="flex items-center gap-3 text-sm text-neutral-300">
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
Discord bot integration
</li>
<li class="flex items-center gap-3 text-sm text-neutral-300">
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
Cloud backups
</li>
<li class="flex items-center gap-3 text-sm text-neutral-300">
<Check class="w-4 h-4 text-oxide-500 shrink-0" />
More on the roadmap
</li>
</ul>
<RouterLink to="/site/roadmap" class="block w-full py-3 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 text-center font-semibold rounded-lg border border-neutral-700 transition-colors">
View Roadmap
</RouterLink>
</div>
</div> </div>
</div> </div>
</section>
</div> <!-- Fleet Block -->
<div class="fleetblock reveal">
<b>Fleet Block</b>
<span class="p">+$49.99/mo</span>
<span>each additional 50 servers stack as many as your network needs.</span>
</div>
<!-- Commercial use definition -->
<p class="commercial reveal">
<b>Commercial use</b> includes monetized communities, paid access, VIP slots, donations,
sponsorship-supported servers, hosting providers, or managing servers for others.
</p>
<!-- Support model -->
<p class="support-note reveal">
Community support is included with every plan documentation, community forum,
diagnostics, and structured bug reports.
<b>Direct 1:1 support</b> is available separately at $125/hour in prepaid 1-hour blocks.
Corrosion is a tool, not a managed service.
</p>
</div>
</section>
<!-- COMPARISON TABLE -->
<section class="sec" id="compare">
<div class="wrap">
<div class="sec__head reveal">
<span class="eyebrow">Feature breakdown</span>
<h2 class="title">What is included in each tier</h2>
</div>
<div class="compare-table reveal">
<div class="compare-table__head">
<div class="compare-table__label">Feature</div>
<div>Hobby</div>
<div>Community</div>
<div class="compare-table__featured">Operator</div>
<div>Network</div>
</div>
<div class="compare-table__row">
<div class="compare-table__label">Server instances</div>
<div>15</div>
<div>610</div>
<div class="compare-table__featured">Up to 50</div>
<div>50 + Fleet Blocks</div>
</div>
<div class="compare-table__row">
<div class="compare-table__label">Commercial use</div>
<div><Icon name="x" :size="14" style="color:var(--status-offline)" /></div>
<div><Icon name="x" :size="14" style="color:var(--status-offline)" /></div>
<div class="compare-table__featured"><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
</div>
<div class="compare-table__row">
<div class="compare-table__label">Rust (Oxide/uMod)</div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div class="compare-table__featured"><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
</div>
<div class="compare-table__row">
<div class="compare-table__label">All supported games</div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div class="compare-table__featured"><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
</div>
<div class="compare-table__row">
<div class="compare-table__label">Auto-wiper + rollback</div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div class="compare-table__featured"><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
</div>
<div class="compare-table__row">
<div class="compare-table__label">Real-time console</div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div class="compare-table__featured"><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
</div>
<div class="compare-table__row">
<div class="compare-table__label">RBAC team access</div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div class="compare-table__featured"><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
</div>
<div class="compare-table__row">
<div class="compare-table__label">Public server page</div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div class="compare-table__featured"><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
</div>
<div class="compare-table__row">
<div class="compare-table__label">Priority bug triage</div>
<div><Icon name="x" :size="14" style="color:var(--status-offline)" /></div>
<div><Icon name="x" :size="14" style="color:var(--status-offline)" /></div>
<div class="compare-table__featured"><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
<div><Icon name="check" :size="14" style="color:var(--accent-text)" /></div>
</div>
</div>
</div>
</section>
<!-- SUPPORT MODEL -->
<section class="sec" id="support">
<div class="wrap">
<div class="sec__head reveal">
<span class="eyebrow">Support model</span>
<h2 class="title">Corrosion is a tool,<br>not a managed service</h2>
<p class="lead">
Every plan includes self-service support. Hands-on time is available separately at
an honest rate, when you actually need it.
</p>
</div>
<div class="infra reveal">
<div class="icard">
<div class="icard__ic"><Icon name="file-text" :size="16" /></div>
<b>Documentation</b>
<p>Setup guides, architecture reference, troubleshooting walkthroughs. Included on every plan.</p>
</div>
<div class="icard">
<div class="icard__ic"><Icon name="message-square" :size="16" /></div>
<b>Community forum</b>
<p>Operator-to-operator knowledge base. Questions, configs, and war stories. All plans.</p>
</div>
<div class="icard">
<div class="icard__ic"><Icon name="activity" :size="16" /></div>
<b>Diagnostics</b>
<p>Built-in agent health checks, log access, and structured bug reports. All plans.</p>
</div>
<div class="icard">
<div class="icard__ic"><Icon name="zap" :size="16" /></div>
<b>Priority bug triage</b>
<p>Platform bugs for Operator and Network customers go to the front of the queue.</p>
</div>
<div class="icard">
<div class="icard__ic"><Icon name="clock" :size="16" /></div>
<b>Direct 1:1 support</b>
<p>$125/hour, prepaid in 1-hour blocks. Available to any customer who needs it.</p>
</div>
</div>
<p class="closing reveal" style="font-size:var(--text-md);color:var(--text-tertiary);font-weight:500">
Direct server administration, firewall configuration, mod installation, and wipe-day
hand-holding are not included in any plan. Corrosion gives you the panel and the tools.
You run the operation.
</p>
</div>
</section>
<!-- FINAL CTA -->
<section class="finalcta">
<div class="finalcta__atmo" />
<div class="wrap finalcta__in reveal">
<h2>Ready to stop babysitting<br>your servers?</h2>
<div class="cta-row">
<RouterLink class="btn btn--primary btn--lg" :to="{ name: 'early-access' }">
Join early access
</RouterLink>
<a class="btn btn--ghost btn--lg" :href="panelUrl + '/login'">
<Icon name="play" :size="17" />View live demo
</a>
</div>
</div>
</section>
</template> </template>
<style scoped>
.plan__feats {
list-style: none;
padding: 0;
margin: 16px 0 0;
flex: 1;
display: flex;
flex-direction: column;
gap: 9px;
}
.plan__feats li {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: var(--text-xs);
color: var(--text-secondary);
line-height: 1.4;
}
.plan__feats li svg { margin-top: 1px; }
/* Comparison table */
.compare-table {
max-width: 1040px;
margin: 0 auto;
border-radius: var(--radius-xl);
overflow: hidden;
box-shadow: var(--ring-default);
}
.compare-table__head,
.compare-table__row {
display: grid;
grid-template-columns: 2fr 1fr 1fr 1.2fr 1fr;
align-items: center;
}
.compare-table__head {
background: var(--surface-raised-2);
font-family: var(--font-mono);
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: var(--tracking-caps);
color: var(--text-muted);
padding: 12px 16px;
border-bottom: 1px solid var(--border-subtle);
}
.compare-table__head > div,
.compare-table__row > div {
padding: 2px 8px;
text-align: center;
}
.compare-table__head > div:first-child,
.compare-table__row > div:first-child {
text-align: left;
padding-left: 0;
}
.compare-table__row {
background: var(--surface-base);
font-size: var(--text-sm);
color: var(--text-tertiary);
padding: 11px 16px;
border-bottom: 1px solid var(--border-subtle);
}
.compare-table__row:last-child { border-bottom: none; }
.compare-table__label { color: var(--text-secondary); font-weight: 500; }
.compare-table__featured {
background: var(--accent-soft);
color: var(--accent-text) !important;
font-weight: 600;
}
</style>

View File

@@ -1,146 +1,353 @@
<script setup lang="ts"> <script setup lang="ts">
import { Check, Circle } from 'lucide-vue-next' import { onMounted, onUnmounted } from 'vue'
import { RouterLink } from 'vue-router'
import Icon from '@/components/ds/core/Icon.vue'
import CorrosionMark from '@/components/brand/CorrosionMark.vue'
interface Phase { const panelUrl = import.meta.env.VITE_PANEL_URL ?? ''
name: string
label: string type Status = 'shipped' | 'in-progress' | 'planned'
status: 'complete' | 'current' | 'upcoming'
items: { text: string; done: boolean }[] interface RoadmapItem {
text: string
note?: string
} }
const phases: Phase[] = [ interface RoadmapGroup {
status: Status
label: string
description: string
items: RoadmapItem[]
}
const groups: RoadmapGroup[] = [
{ {
name: 'Phase 1', status: 'shipped',
label: 'Foundation', label: 'Phase 1 — Foundation',
status: 'complete', description:
'The core control plane is live. Rust server operators can install the agent, connect their server, and manage it entirely from the panel.',
items: [ items: [
{ text: 'Core control plane', done: true }, { text: 'Host agent (Windows + Linux)', note: 'Go binary, outbound NATS, zero inbound ports' },
{ text: 'Auto-Wiper with rollback', done: true }, { text: 'Core control plane', note: 'NestJS API, multi-tenant PostgreSQL, JWT RBAC' },
{ text: 'Plugin management', done: true }, { text: 'Auto-wiper with rollback', note: 'Map, BP, and full wipes as verified sequences' },
{ text: 'Public server site', done: true }, { text: 'Plugin management', note: 'Rust uMod/Oxide — browse, install, configure from the browser' },
{ text: 'Real-time console', note: 'NATS-bridged live output' },
{ text: 'File manager', note: 'Browser-based file access via the agent' },
{ text: 'Scheduled tasks and maintenance windows' },
{ text: 'Player management and RBAC team access' },
{ text: 'Public server page', note: 'Live status, wipe countdown, player count' },
{ text: 'SteamCMD game update automation' },
{ text: 'Discord and notification webhooks' },
], ],
}, },
{ {
name: 'Phase 2', status: 'in-progress',
label: 'Analytics', label: 'Multi-game expansion',
status: 'current', description:
'The agent and control plane are being extended with per-game blueprints. Rust is the reference implementation; Dune, Conan, and Soulmask follow the same agent model with game-specific operational logic.',
items: [ items: [
{ text: 'Player retention tracking', done: false }, { text: 'Dune: Awakening blueprint', note: 'Deep Desert wipe scheduling, battlegroup lifecycle' },
{ text: 'Wipe performance insights', done: false }, { text: 'Conan Exiles blueprint', note: 'Persistent world management, mod support, purge tracking' },
{ text: 'Population heatmaps', done: false }, { text: 'Soulmask blueprint', note: 'Linked-world cluster deployment, port automation' },
{ text: 'Multi-instance host runtime', note: 'One agent managing N game processes on the same machine' },
{ text: 'Per-game wipe and event scheduling' },
], ],
}, },
{ {
name: 'Phase 3', status: 'planned',
label: 'Status Platform', label: 'API access and integrations',
status: 'upcoming', description:
'Operator-grade API access so you can build your own tooling on top of the Corrosion control plane.',
items: [ items: [
{ text: 'Public uptime tracking', done: false }, { text: 'Public REST API for server management' },
{ text: 'Server health dashboard', done: false }, { text: 'Webhook events (wipe completed, server down, player banned)' },
{ text: 'API key management per license' },
], ],
}, },
{ {
name: 'Phase 4', status: 'planned',
label: 'Module Marketplace', label: 'Integrated storefront',
status: 'upcoming', description:
'A native store layer for server communities — item catalog, VIP packages, and automated in-game delivery. No Tebex dependency required.',
items: [ items: [
{ text: 'Loot manager', done: false }, { text: 'Item catalog and categories' },
{ text: 'Event systems', done: false }, { text: 'PayPal and Stripe payment processing' },
{ text: 'Advanced gameplay modules', done: false }, { text: 'Automated in-game delivery via RCON/agent' },
{ text: 'Transaction history and revenue dashboard' },
], ],
}, },
{ {
name: 'Phase 5', status: 'planned',
label: 'Integrated Webstore', label: 'Fleet management for hosting partners',
status: 'upcoming', description:
'Tools for hosting providers and multi-server operations running 50+ instances across multiple physical hosts.',
items: [ items: [
{ text: 'Native item store', done: false }, { text: 'Fleet-level dashboards and health monitoring' },
{ text: 'Automated delivery', done: false }, { text: 'Multi-host agent orchestration' },
{ text: 'Revenue dashboard', done: false }, { text: 'Bulk wipe and update scheduling across a fleet' },
{ text: 'Fleet Block capacity management' },
], ],
}, },
{ {
name: 'Phase 6', status: 'planned',
label: 'B2B Hosting Integration', label: 'More games',
status: 'upcoming', description:
'Corrosion\'s agent model is not Rust-specific. The platform is being designed to support any game that can be managed via process control, file operations, and RCON-style commands.',
items: [ items: [
{ text: 'White-label panel', done: false }, { text: 'Additional survival and sandbox games' },
{ text: 'Bulk license provisioning', done: false }, { text: 'Community-requested game blueprints' },
{ text: 'SSO integration', done: false },
], ],
}, },
] ]
function phaseStatusClass(status: string): string { function statusLabel(s: Status): string {
switch (status) { if (s === 'shipped') return 'Shipped'
case 'complete': return 'bg-green-500/10 text-green-400 border-green-500/20' if (s === 'in-progress') return 'In progress'
case 'current': return 'bg-oxide-500/10 text-oxide-400 border-oxide-500/20' return 'Planned'
default: return 'bg-neutral-800 text-neutral-500 border-neutral-700'
}
} }
function phaseStatusLabel(status: string): string { function statusIcon(s: Status): string {
switch (status) { if (s === 'shipped') return 'check'
case 'complete': return 'Shipped' if (s === 'in-progress') return 'refresh-cw'
case 'current': return 'In Progress' return 'circle'
default: return 'Planned'
}
} }
// Scroll-reveal
let io: IntersectionObserver | null = null
function initReveal(): void {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
io = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
e.target.classList.add('in')
io?.unobserve(e.target)
}
})
},
{ threshold: 0.1 },
)
document.querySelectorAll('.reveal').forEach((el) => io?.observe(el))
}
onMounted(() => { initReveal() })
onUnmounted(() => { io?.disconnect() })
</script> </script>
<template> <template>
<div> <!-- PAGE HEADER -->
<!-- Header --> <section class="hero" style="padding-bottom:0; border-bottom:none;">
<section class="pt-20 pb-12"> <div class="hero__atmo" />
<div class="max-w-4xl mx-auto px-6 text-center"> <div class="hero__grid" />
<h1 class="text-4xl md:text-5xl font-bold text-neutral-100 mb-4">Roadmap</h1> <div class="hero__grain" />
<p class="text-lg text-neutral-400"> <div class="wrap hero__in" style="padding-bottom:52px;">
Corrosion isn't a single plugin release. It's infrastructure for the Rust ecosystem. <div class="hero__mark">
</p> <CorrosionMark :size="56" />
</div> </div>
</section> <span class="eyebrow">Roadmap</span>
<h1 style="font-size:var(--text-5xl)">
Where Corrosion
<span class="accent">is going.</span>
</h1>
<p class="hero__sub">
No specific dates. No fabricated percentages. Status labels only: Shipped, In progress,
Planned. This roadmap reflects what is actually being built.
</p>
</div>
</section>
<!-- Timeline --> <!-- STATUS LEGEND -->
<section class="pb-20"> <section class="sec" style="padding:32px 0; border-bottom: 1px solid var(--border-subtle);">
<div class="max-w-3xl mx-auto px-6"> <div class="wrap">
<div class="space-y-6"> <div class="rm-legend reveal">
<div <div class="rm-badge rm-badge--shipped">
v-for="phase in phases" <Icon name="check" :size="13" />Shipped
:key="phase.name" </div>
class="bg-neutral-900 border rounded-xl p-8 transition-colors" <div class="rm-badge rm-badge--progress">
:class="phase.status === 'current' ? 'border-oxide-500/30' : 'border-neutral-800'" <Icon name="refresh-cw" :size="13" />In progress
> </div>
<div class="flex items-center justify-between mb-5"> <div class="rm-badge rm-badge--planned">
<div> <Icon name="circle" :size="13" />Planned
<span class="text-xs font-bold text-neutral-500 uppercase tracking-wider">{{ phase.name }}</span>
<h3 class="text-xl font-bold text-neutral-100">{{ phase.label }}</h3>
</div>
<span
class="text-xs font-semibold px-3 py-1 rounded-full border"
:class="phaseStatusClass(phase.status)"
>
{{ phaseStatusLabel(phase.status) }}
</span>
</div>
<ul class="space-y-3">
<li
v-for="item in phase.items"
:key="item.text"
class="flex items-center gap-3"
>
<Check v-if="item.done" class="w-4 h-4 text-green-400 shrink-0" />
<Circle v-else class="w-4 h-4 text-neutral-600 shrink-0" />
<span
class="text-sm"
:class="item.done ? 'text-neutral-300' : 'text-neutral-500'"
>
{{ item.text }}
</span>
</li>
</ul>
</div>
</div> </div>
</div> </div>
</section> </div>
</div> </section>
<!-- ROADMAP GROUPS -->
<section class="sec" id="roadmap">
<div class="wrap">
<div
v-for="group in groups"
:key="group.label"
class="rm-group reveal"
:data-status="group.status"
>
<div class="rm-group__head">
<div
class="rm-group__badge"
:class="`rm-badge--${group.status}`"
>
<Icon :name="statusIcon(group.status)" :size="13" />
{{ statusLabel(group.status) }}
</div>
<h3 class="rm-group__title">{{ group.label }}</h3>
</div>
<p class="rm-group__desc">{{ group.description }}</p>
<ul class="rm-group__list">
<li
v-for="item in group.items"
:key="item.text"
class="rm-item"
>
<span class="rm-item__dot" :class="`rm-item__dot--${group.status}`" />
<span>
{{ item.text }}
<span v-if="item.note" class="rm-item__note"> {{ item.note }}</span>
</span>
</li>
</ul>
</div>
</div>
</section>
<!-- HONEST NOTE -->
<section class="sec" style="padding:40px 0; border-bottom:none;">
<div class="wrap">
<div class="closing reveal">
This roadmap reflects real development priorities, not marketing promises.
Timelines are not published because they depend on real-world testing and operator
feedback. <span class="accent">Join early access to influence what gets built next.</span>
</div>
<div class="hero__cta reveal" style="margin-top:28px">
<RouterLink class="btn btn--primary btn--lg" :to="{ name: 'early-access' }">
Join early access
</RouterLink>
<a class="btn btn--ghost btn--lg" :href="panelUrl + '/login'">
<Icon name="play" :size="17" />View live demo
</a>
</div>
</div>
</section>
</template> </template>
<style scoped>
/* Legend */
.rm-legend {
display: flex;
align-items: center;
gap: 14px;
flex-wrap: wrap;
}
/* Badges */
.rm-badge {
display: inline-flex;
align-items: center;
gap: 6px;
height: 26px;
padding: 0 10px;
border-radius: var(--radius-pill);
font-family: var(--font-mono);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .05em;
}
.rm-badge--shipped {
background: color-mix(in srgb, var(--status-online) 14%, transparent);
color: var(--status-online);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--status-online) 28%, transparent);
}
.rm-badge--in-progress, .rm-badge--progress {
background: var(--accent-soft);
color: var(--accent-text);
box-shadow: inset 0 0 0 1px var(--accent-border);
}
.rm-badge--planned {
background: var(--surface-raised-2);
color: var(--text-muted);
box-shadow: var(--ring-default);
}
/* Groups */
.rm-group {
max-width: 860px;
margin: 0 auto 40px;
padding: 28px 28px 24px;
background: var(--surface-base);
border-radius: var(--radius-xl);
box-shadow: var(--ring-default);
}
.rm-group[data-status="in-progress"] {
box-shadow: inset 0 0 0 1px var(--accent-border);
}
.rm-group[data-status="shipped"] {
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--status-online) 22%, transparent);
}
.rm-group__head {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 10px;
}
.rm-group__badge {
display: inline-flex;
align-items: center;
gap: 6px;
height: 24px;
padding: 0 10px;
border-radius: var(--radius-pill);
font-family: var(--font-mono);
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .05em;
flex: none;
}
.rm-group__title {
font-family: var(--font-brand);
font-weight: 700;
font-size: var(--text-xl);
margin: 0;
}
.rm-group__desc {
font-size: var(--text-sm);
color: var(--text-tertiary);
line-height: 1.6;
margin: 0 0 18px;
max-width: 680px;
}
.rm-group__list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 10px;
}
.rm-item {
display: flex;
align-items: flex-start;
gap: 10px;
font-size: var(--text-sm);
color: var(--text-secondary);
}
.rm-item__dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex: none;
margin-top: 5px;
}
.rm-item__dot--shipped { background: var(--status-online); }
.rm-item__dot--in-progress { background: var(--accent); }
.rm-item__dot--planned {
background: transparent;
box-shadow: inset 0 0 0 1.5px var(--text-muted);
}
.rm-item__note {
color: var(--text-muted);
font-size: var(--text-xs);
}
</style>