diff --git a/ai/__pycache__/__init__.cpython-311.pyc b/ai/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..b9450fa
Binary files /dev/null and b/ai/__pycache__/__init__.cpython-311.pyc differ
diff --git a/ai/__pycache__/local_ai_api.cpython-311.pyc b/ai/__pycache__/local_ai_api.cpython-311.pyc
new file mode 100644
index 0000000..e906db1
Binary files /dev/null and b/ai/__pycache__/local_ai_api.cpython-311.pyc differ
diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc
index d79d6a7..4af15a0 100644
Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ
diff --git a/config/settings.py b/config/settings.py
index 291d043..f4bdea9 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -23,6 +23,7 @@ DEBUG = os.getenv("DJANGO_DEBUG", "true").lower() == "true"
ALLOWED_HOSTS = [
"127.0.0.1",
"localhost",
+ "testserver",
os.getenv("HOST_FQDN", ""),
]
diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc
index 5e8987a..60b731b 100644
Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ
diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc
new file mode 100644
index 0000000..41c9d6a
Binary files /dev/null and b/core/__pycache__/forms.cpython-311.pyc differ
diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc
index a251b5f..2b70046 100644
Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ
diff --git a/core/__pycache__/tests.cpython-311.pyc b/core/__pycache__/tests.cpython-311.pyc
new file mode 100644
index 0000000..bc6813f
Binary files /dev/null and b/core/__pycache__/tests.cpython-311.pyc differ
diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc
index f705988..e4a3fcc 100644
Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ
diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc
index 2f0989c..e41ea71 100644
Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ
diff --git a/core/admin.py b/core/admin.py
index 8c38f3f..03b388e 100644
--- a/core/admin.py
+++ b/core/admin.py
@@ -1,3 +1,18 @@
from django.contrib import admin
-# Register your models here.
+from .models import PostizInstallBrief
+
+
+@admin.register(PostizInstallBrief)
+class PostizInstallBriefAdmin(admin.ModelAdmin):
+ list_display = (
+ "title",
+ "public_url",
+ "node_version",
+ "package_manager",
+ "ready_services_count",
+ "updated_at",
+ )
+ list_filter = ("node_version", "package_manager", "email_provider", "upload_strategy")
+ search_fields = ("title", "public_url", "notes")
+ readonly_fields = ("created_at", "updated_at", "readiness_percent", "status_label")
diff --git a/core/forms.py b/core/forms.py
new file mode 100644
index 0000000..b62bd46
--- /dev/null
+++ b/core/forms.py
@@ -0,0 +1,71 @@
+from django import forms
+
+from .models import PostizInstallBrief
+
+
+class PostizInstallBriefForm(forms.ModelForm):
+ class Meta:
+ model = PostizInstallBrief
+ fields = [
+ "title",
+ "public_url",
+ "node_version",
+ "package_manager",
+ "postgres_ready",
+ "redis_ready",
+ "temporal_ready",
+ "email_provider",
+ "upload_strategy",
+ "notes",
+ ]
+ widgets = {
+ "title": forms.TextInput(
+ attrs={
+ "class": "form-control form-control-lg",
+ "placeholder": "Hosted Postiz workspace",
+ }
+ ),
+ "public_url": forms.URLInput(
+ attrs={
+ "class": "form-control form-control-lg",
+ "placeholder": "https://your-machine.example.com",
+ }
+ ),
+ "node_version": forms.Select(attrs={"class": "form-select"}),
+ "package_manager": forms.Select(attrs={"class": "form-select"}),
+ "email_provider": forms.Select(attrs={"class": "form-select"}),
+ "upload_strategy": forms.Select(attrs={"class": "form-select"}),
+ "notes": forms.Textarea(
+ attrs={
+ "class": "form-control",
+ "rows": 5,
+ "placeholder": "Optional notes about ports, reverse proxy decisions, or missing credentials.",
+ }
+ ),
+ "postgres_ready": forms.CheckboxInput(attrs={"class": "form-check-input"}),
+ "redis_ready": forms.CheckboxInput(attrs={"class": "form-check-input"}),
+ "temporal_ready": forms.CheckboxInput(attrs={"class": "form-check-input"}),
+ }
+ help_texts = {
+ "title": "A friendly name for this VM install brief.",
+ "public_url": "Use the public URL already attached to this machine.",
+ "node_version": "Postiz development requires Node.js 18 or newer.",
+ "package_manager": "pnpm is the default workflow in the development guide.",
+ "postgres_ready": "Check this if PostgreSQL is already available on the VM.",
+ "redis_ready": "Check this if Redis is already available on the VM.",
+ "temporal_ready": "Check this if the Temporal stack is already reachable.",
+ "email_provider": "Pick the first email path you expect to configure.",
+ "upload_strategy": "Choose how uploaded files should be stored initially.",
+ }
+
+ def clean_title(self):
+ title = self.cleaned_data["title"].strip()
+ if len(title) < 3:
+ raise forms.ValidationError("Use at least 3 characters so the brief is recognizable.")
+ return title
+
+ def clean_public_url(self):
+ public_url = self.cleaned_data["public_url"].strip()
+ if not public_url.startswith(("http://", "https://")):
+ raise forms.ValidationError("Include http:// or https:// so the reverse-proxy target is unambiguous.")
+ return public_url
diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py
new file mode 100644
index 0000000..8c6dea2
--- /dev/null
+++ b/core/migrations/0001_initial.py
@@ -0,0 +1,35 @@
+# Generated by Django 5.2.7 on 2026-04-15 14:17
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='PostizInstallBrief',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(max_length=120)),
+ ('public_url', models.URLField(help_text='Public URL where the reverse proxy will expose Postiz.')),
+ ('node_version', models.CharField(choices=[('18 LTS', '18 LTS'), ('20 LTS', '20 LTS'), ('22 Current', '22 Current')], default='20 LTS', max_length=20)),
+ ('package_manager', models.CharField(choices=[('pnpm', 'pnpm'), ('npm', 'npm')], default='pnpm', max_length=10)),
+ ('postgres_ready', models.BooleanField(default=False)),
+ ('redis_ready', models.BooleanField(default=False)),
+ ('temporal_ready', models.BooleanField(default=False)),
+ ('email_provider', models.CharField(choices=[('none', 'Skip for now'), ('resend', 'Resend'), ('smtp', 'SMTP / Nodemailer')], default='none', max_length=20)),
+ ('upload_strategy', models.CharField(choices=[('local', 'Local uploads'), ('r2', 'Cloudflare R2'), ('later', 'Decide later')], default='local', max_length=20)),
+ ('notes', models.TextField(blank=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ],
+ options={
+ 'ordering': ['-updated_at'],
+ },
+ ),
+ ]
diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc
new file mode 100644
index 0000000..f78480e
Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ
diff --git a/core/models.py b/core/models.py
index 71a8362..d4a6461 100644
--- a/core/models.py
+++ b/core/models.py
@@ -1,3 +1,80 @@
from django.db import models
+from django.urls import reverse
-# Create your models here.
+
+class PostizInstallBrief(models.Model):
+ PACKAGE_MANAGER_CHOICES = [
+ ("pnpm", "pnpm"),
+ ("npm", "npm"),
+ ]
+ NODE_VERSION_CHOICES = [
+ ("18 LTS", "18 LTS"),
+ ("20 LTS", "20 LTS"),
+ ("22 Current", "22 Current"),
+ ]
+ EMAIL_PROVIDER_CHOICES = [
+ ("none", "Skip for now"),
+ ("resend", "Resend"),
+ ("smtp", "SMTP / Nodemailer"),
+ ]
+ UPLOAD_STRATEGY_CHOICES = [
+ ("local", "Local uploads"),
+ ("r2", "Cloudflare R2"),
+ ("later", "Decide later"),
+ ]
+
+ title = models.CharField(max_length=120)
+ public_url = models.URLField(help_text="Public URL where the reverse proxy will expose Postiz.")
+ node_version = models.CharField(max_length=20, choices=NODE_VERSION_CHOICES, default="20 LTS")
+ package_manager = models.CharField(max_length=10, choices=PACKAGE_MANAGER_CHOICES, default="pnpm")
+ postgres_ready = models.BooleanField(default=False)
+ redis_ready = models.BooleanField(default=False)
+ temporal_ready = models.BooleanField(default=False)
+ email_provider = models.CharField(max_length=20, choices=EMAIL_PROVIDER_CHOICES, default="none")
+ upload_strategy = models.CharField(max_length=20, choices=UPLOAD_STRATEGY_CHOICES, default="local")
+ notes = models.TextField(blank=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ ordering = ["-updated_at"]
+
+ def __str__(self):
+ return self.title
+
+ def get_absolute_url(self):
+ return reverse("brief_detail", args=[self.pk])
+
+ @property
+ def ready_services_count(self):
+ return sum([self.postgres_ready, self.redis_ready, self.temporal_ready])
+
+ @property
+ def readiness_percent(self):
+ return int((self.ready_services_count / 3) * 100)
+
+ @property
+ def status_label(self):
+ if self.ready_services_count == 3:
+ return "Ready for repo bootstrap"
+ if self.ready_services_count == 0:
+ return "Blocked by missing services"
+ return "Infrastructure in progress"
+
+ @property
+ def status_tone(self):
+ if self.ready_services_count == 3:
+ return "success"
+ if self.ready_services_count == 0:
+ return "danger"
+ return "warning"
+
+ @property
+ def next_milestone(self):
+ if self.ready_services_count == 3:
+ return "Clone the Postiz repository and run the first development boot commands."
+ if not self.postgres_ready:
+ return "Provision PostgreSQL and capture DATABASE_URL for the .env file."
+ if not self.redis_ready:
+ return "Bring Redis online so background queues can start locally."
+ return "Prepare Temporal and point TEMPORAL_ADDRESS at the stack."
diff --git a/core/templates/base.html b/core/templates/base.html
index 1e7e5fb..4202bde 100644
--- a/core/templates/base.html
+++ b/core/templates/base.html
@@ -1,25 +1,77 @@
+{% load static %}
- {% block title %}Knowledge Base{% endblock %}
- {% if project_description %}
-
-
-
- {% endif %}
+
+ {% block title %}Postiz Native Setup Studio{% endblock %}
+
{% if project_image_url %}
{% endif %}
- {% load static %}
+
+
+
+
+
+
+
+
{% block head %}{% endblock %}
-
- {% block content %}{% endblock %}
+
+
+
+
+
+
+
+ {% if messages %}
+
+ {% for message in messages %}
+
{{ message }}
+ {% endfor %}
+
+ {% endif %}
+ {% block content %}{% endblock %}
+
+
+
+
+
+
diff --git a/core/templates/core/_brief_form.html b/core/templates/core/_brief_form.html
new file mode 100644
index 0000000..4ba5ad6
--- /dev/null
+++ b/core/templates/core/_brief_form.html
@@ -0,0 +1,74 @@
+
diff --git a/core/templates/core/brief_detail.html b/core/templates/core/brief_detail.html
new file mode 100644
index 0000000..d881f58
--- /dev/null
+++ b/core/templates/core/brief_detail.html
@@ -0,0 +1,93 @@
+{% extends "base.html" %}
+
+{% block title %}{{ brief.title }} · Install blueprint{% endblock %}
+{% block meta_description %}Review the generated Postiz install blueprint for {{ brief.title }}, including prerequisite readiness, env placeholders, and bootstrap commands.{% endblock %}
+
+{% block content %}
+
+
+
+
Install blueprint
+
{{ brief.title }}
+
{{ brief.next_milestone }}
+
+ {{ brief.status_label }}
+ {{ brief.public_url }}
+ {{ brief.node_version }} · {{ brief.package_manager }}
+
+
+
+
+
Readiness
+
+
+ {{ brief.readiness_percent }}%
+ service readiness
+
+
+
{{ brief.ready_services_count }} of 3 prerequisite services are marked ready for the initial development boot.
+
+
+
+
+
+
+
+
+
+
+ Prerequisites
+
Service checklist
+
+
+
+ {% for item in blueprint.prerequisites %}
+
+
+
+
{{ item.title }}
+
{{ item.detail }}
+
+
+ {% endfor %}
+
+ {% if brief.notes %}
+
+
Notes
+
{{ brief.notes }}
+
+ {% endif %}
+
+
+
+
+
+
+ Environment
+
Suggested .env starting point
+
+
+
+ {% for line in blueprint.env_lines %}{{ line }}{% if not forloop.last %} {% endif %}{% endfor %}
+
+
+
+
+
+ {% for command in blueprint.command_plan %}
+
+ 0{{ forloop.counter }}
+ {{ command }}
+
+ {% endfor %}
+
+
+
+
+{% endblock %}
diff --git a/core/templates/core/brief_form.html b/core/templates/core/brief_form.html
new file mode 100644
index 0000000..e3ee841
--- /dev/null
+++ b/core/templates/core/brief_form.html
@@ -0,0 +1,34 @@
+{% extends "base.html" %}
+
+{% block title %}Create install brief · Postiz Native Setup Studio{% endblock %}
+{% block meta_description %}Create a hosted Postiz install brief, capture the public URL, and save prerequisite readiness for this VM.{% endblock %}
+
+{% block content %}
+
+
+
+
Create install brief
+
Capture the first hosted Postiz setup plan.
+
Tell the app which URL, Node target, and prerequisite services you expect. After saving, you will land on a generated detail page with next actions, env placeholders, and command prompts.
+
+
+
+
+
Tracked now
+
Public URL + service readiness
+
Keep one source of truth while the VM is being prepared.
+
+
+
After save
+
Confirmation + detail blueprint
+
Jump directly into the generated install checklist and command sequence.
+
+
+
+
+
+
+
+ {% include "core/_brief_form.html" with submit_label="Generate install blueprint" %}
+
+{% endblock %}
diff --git a/core/templates/core/brief_list.html b/core/templates/core/brief_list.html
new file mode 100644
index 0000000..87074c8
--- /dev/null
+++ b/core/templates/core/brief_list.html
@@ -0,0 +1,52 @@
+{% extends "base.html" %}
+
+{% block title %}Install briefs · Postiz Native Setup Studio{% endblock %}
+{% block meta_description %}Browse saved Postiz install briefs, compare service readiness, and reopen any setup blueprint for this VM.{% endblock %}
+
+{% block content %}
+
+
+
+
Install briefs
+
Saved setup plans for this hosted environment.
+
Every brief keeps the public URL, infrastructure readiness, and next milestone visible so you can iterate without losing context.
+
+
Create another brief
+
+
+
+
+ {% if briefs %}
+
+ {% for brief in briefs %}
+
+
+
+
+
{{ brief.title }}
+
{{ brief.public_url }}
+
+
{{ brief.status_label }}
+
+
+ {{ brief.ready_services_count }}/3 infrastructure services ready
+
+ Node {{ brief.node_version }}
+ Package manager {{ brief.package_manager }}
+ Email {{ brief.get_email_provider_display }}
+
+ {{ brief.next_milestone }}
+ Open blueprint
+
+
+ {% endfor %}
+
+ {% else %}
+
+
No install briefs yet
+
Start with one brief so the app can generate the first Postiz setup blueprint for this VM.
+
Create the first brief
+
+ {% endif %}
+
+{% endblock %}
diff --git a/core/templates/core/index.html b/core/templates/core/index.html
index faec813..03ddcfb 100644
--- a/core/templates/core/index.html
+++ b/core/templates/core/index.html
@@ -1,145 +1,112 @@
{% extends "base.html" %}
-{% block title %}{{ project_name }}{% endblock %}
-
-{% block head %}
-
-
-
-
-{% endblock %}
+{% block title %}Postiz Native Setup Studio · Overview{% endblock %}
+{% block meta_description %}Design the first native Postiz install plan for this VM, track service readiness, and jump into the next development steps from one polished landing page.{% endblock %}
{% block content %}
-
-
-
Analyzing your requirements and generating your app…
-
-
Loading…
+
+
+
+
Hosted Postiz development workflow
+
Plan a native Postiz development install without losing sight of the public URL, services, or next commands.
+
This first slice turns the blank starter into a Postiz setup cockpit: a polished landing page, install brief workflow, generated prerequisite summary, and a detail view you can revisit while you wire the VM.
+
+
+
+ {{ brief_stats.total }}
+ Saved briefs
+
+
+ {{ brief_stats.ready }}
+ Ready to bootstrap
+
+
+ 3
+ Core services tracked
+
+
+
+
+
+
First delivery included
+
+
+
Runtime
+
Node 18+
+
Choose the Node target and package manager before the repo bootstrap.
+
+
+
Local services
+
Postgres · Redis · Temporal
+
Mark what is already available on the VM and see the gaps instantly.
+
+
+
Public entrypoint
+
Reverse-proxied URL
+
Keep the exposed hostname visible in every install brief and detail page.
+
+
+
-
AppWizzy AI is collecting your requirements and applying the first changes.
-
This page will refresh automatically as the plan is implemented.
-
- Runtime: Django {{ django_version }} · Python {{ python_version }}
- — UTC {{ current_time|date:"Y-m-d H:i:s" }}
-
-
-
- Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
-
-{% endblock %}
\ No newline at end of file
+
+
+
+
+
+
+
+ Workflow
+
How the thin slice works
+
+
+
+ {% for step in setup_steps %}
+
+ 0{{ forloop.counter }}
+ {{ step.title }}
+ {{ step.description }}
+
+ {% endfor %}
+
+
+
+
+
+
+
+ Recent activity
+
Install brief snapshot
+
+
See all
+
+ {% if recent_briefs %}
+
+ {% else %}
+
+
No install briefs yet
+
Create the first brief to generate a hosted Postiz setup blueprint for this machine.
+
Start the first brief
+
+ {% endif %}
+
+
+
+{% endblock %}
diff --git a/core/tests.py b/core/tests.py
index 7ce503c..9df2d93 100644
--- a/core/tests.py
+++ b/core/tests.py
@@ -1,3 +1,38 @@
from django.test import TestCase
+from django.urls import reverse
-# Create your tests here.
+from .models import PostizInstallBrief
+
+
+class PostizPagesTests(TestCase):
+ def test_home_page_loads(self):
+ response = self.client.get(reverse("home"))
+ self.assertEqual(response.status_code, 200)
+ self.assertContains(response, "Plan a native Postiz development install")
+
+ def test_create_brief_flow(self):
+ response = self.client.post(
+ reverse("brief_create"),
+ {
+ "title": "Primary VM",
+ "public_url": "https://postiz.example.com",
+ "node_version": "20 LTS",
+ "package_manager": "pnpm",
+ "postgres_ready": "on",
+ "redis_ready": "on",
+ "temporal_ready": "on",
+ "email_provider": "smtp",
+ "upload_strategy": "local",
+ "notes": "Ready to bootstrap.",
+ },
+ follow=True,
+ )
+ self.assertEqual(response.status_code, 200)
+ self.assertEqual(PostizInstallBrief.objects.count(), 1)
+ brief = PostizInstallBrief.objects.get()
+ self.assertContains(response, brief.title)
+ self.assertContains(response, "Ready for repo bootstrap")
+
+ def test_brief_list_empty_state(self):
+ response = self.client.get(reverse("brief_list"))
+ self.assertContains(response, "No install briefs yet")
diff --git a/core/urls.py b/core/urls.py
index 6299e3d..494f4bf 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -1,7 +1,10 @@
from django.urls import path
-from .views import home
+from .views import brief_create, brief_detail, brief_list, home
urlpatterns = [
path("", home, name="home"),
+ path("briefs/", brief_list, name="brief_list"),
+ path("briefs/new/", brief_create, name="brief_create"),
+ path("briefs/
/", brief_detail, name="brief_detail"),
]
diff --git a/core/views.py b/core/views.py
index c9aed12..f50933e 100644
--- a/core/views.py
+++ b/core/views.py
@@ -2,18 +2,44 @@ import os
import platform
from django import get_version as django_version
-from django.shortcuts import render
+from django.contrib import messages
+from django.db.models import Count, Q
+from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
+from .forms import PostizInstallBriefForm
+from .models import PostizInstallBrief
-def home(request):
- """Render the landing screen with loader and environment details."""
+
+SETUP_STEPS = [
+ {
+ "title": "Install the runtime",
+ "description": "Get Node.js and the selected package manager onto the VM before cloning Postiz.",
+ },
+ {
+ "title": "Wire prerequisite services",
+ "description": "Confirm PostgreSQL, Redis, and Temporal are reachable before the first app boot.",
+ },
+ {
+ "title": "Bootstrap the repository",
+ "description": "Fill the .env file, install packages, push Prisma schema changes, and start the dev processes.",
+ },
+]
+
+
+def base_context(request):
host_name = request.get_host().lower()
agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
now = timezone.now()
-
- context = {
- "project_name": "New Style",
+ stats = PostizInstallBrief.objects.aggregate(
+ total=Count("id"),
+ ready=Count(
+ "id",
+ filter=Q(postgres_ready=True, redis_ready=True, temporal_ready=True),
+ ),
+ )
+ return {
+ "project_name": "Postiz Native Setup Studio",
"agent_brand": agent_brand,
"django_version": django_version(),
"python_version": platform.python_version(),
@@ -21,5 +47,105 @@ def home(request):
"host_name": host_name,
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
+ "brief_stats": stats,
+ "setup_steps": SETUP_STEPS,
}
+
+
+def build_blueprint(brief):
+ prerequisites = [
+ {
+ "title": "Node runtime",
+ "ready": True,
+ "detail": f"Use {brief.node_version} with {brief.package_manager} for dependency management.",
+ },
+ {
+ "title": "PostgreSQL",
+ "ready": brief.postgres_ready,
+ "detail": "Prepare DATABASE_URL before copying .env.example to .env.",
+ },
+ {
+ "title": "Redis",
+ "ready": brief.redis_ready,
+ "detail": "Queue workers expect REDIS_URL on the VM.",
+ },
+ {
+ "title": "Temporal stack",
+ "ready": brief.temporal_ready,
+ "detail": "Point TEMPORAL_ADDRESS at the Temporal service before starting dev workflows.",
+ },
+ ]
+
+ env_lines = [
+ 'DATABASE_URL="postgresql://postiz-user:password@localhost:5432/postiz-db-local"',
+ 'REDIS_URL="redis://localhost:6379"',
+ 'TEMPORAL_ADDRESS="localhost:7233"',
+ f'FRONTEND_URL="{brief.public_url}"',
+ 'NEXT_PUBLIC_BACKEND_URL="http://127.0.0.1:3000"',
+ 'BACKEND_INTERNAL_URL="http://127.0.0.1:3000"',
+ ]
+
+ command_plan = [
+ "git clone https://github.com/gitroomhq/postiz-app.git",
+ "pnpm install",
+ "pnpm run prisma-db-push",
+ "pnpm run dev",
+ ]
+
+ return {
+ "prerequisites": prerequisites,
+ "env_lines": env_lines,
+ "command_plan": command_plan,
+ }
+
+
+def home(request):
+ context = base_context(request)
+ context.update(
+ {
+ "recent_briefs": PostizInstallBrief.objects.all()[:3],
+ }
+ )
return render(request, "core/index.html", context)
+
+
+def brief_create(request):
+ if request.method == "POST":
+ form = PostizInstallBriefForm(request.POST)
+ if form.is_valid():
+ brief = form.save()
+ messages.success(request, "Install brief saved. Review the generated blueprint below.")
+ return redirect("brief_detail", pk=brief.pk)
+ else:
+ form = PostizInstallBriefForm()
+
+ context = base_context(request)
+ context.update(
+ {
+ "form": form,
+ }
+ )
+ return render(request, "core/brief_form.html", context)
+
+
+def brief_list(request):
+ context = base_context(request)
+ context.update(
+ {
+ "briefs": PostizInstallBrief.objects.all(),
+ }
+ )
+ return render(request, "core/brief_list.html", context)
+
+
+def brief_detail(request, pk):
+ brief = get_object_or_404(PostizInstallBrief, pk=pk)
+ blueprint = build_blueprint(brief)
+ context = base_context(request)
+ context.update(
+ {
+ "brief": brief,
+ "blueprint": blueprint,
+ }
+ )
+ return render(request, "core/brief_detail.html", context)
diff --git a/postiz-app b/postiz-app
new file mode 160000
index 0000000..386fc7b
--- /dev/null
+++ b/postiz-app
@@ -0,0 +1 @@
+Subproject commit 386fc7b049737d5047bc83c6c19dd291e22eb28c
diff --git a/static/css/custom.css b/static/css/custom.css
index 925f6ed..6176d04 100644
--- a/static/css/custom.css
+++ b/static/css/custom.css
@@ -1,4 +1,722 @@
-/* Custom styles for the application */
-body {
- font-family: system-ui, -apple-system, sans-serif;
+/* Postiz Native Setup Studio */
+:root {
+ --bg-main: #081220;
+ --bg-panel: rgba(9, 19, 36, 0.72);
+ --bg-panel-strong: rgba(11, 24, 44, 0.92);
+ --line-soft: rgba(167, 243, 208, 0.14);
+ --line-strong: rgba(167, 243, 208, 0.22);
+ --text-main: #f8fafc;
+ --text-soft: #bfd1e3;
+ --text-muted: #8ca0b8;
+ --primary: #0f766e;
+ --primary-strong: #14b8a6;
+ --secondary: #132238;
+ --accent: #f97316;
+ --accent-soft: #fed7aa;
+ --success: #34d399;
+ --danger: #fb7185;
+ --warning: #fbbf24;
+ --shadow-soft: 0 30px 80px rgba(3, 10, 20, 0.45);
+ --radius-xl: 28px;
+ --radius-lg: 20px;
+ --radius-md: 16px;
+ --spacing-section: clamp(3rem, 7vw, 5.5rem);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body.app-shell {
+ margin: 0;
+ min-height: 100vh;
+ font-family: 'Inter', sans-serif;
+ color: var(--text-main);
+ background:
+ radial-gradient(circle at top left, rgba(20, 184, 166, 0.24), transparent 32%),
+ radial-gradient(circle at 85% 18%, rgba(249, 115, 22, 0.18), transparent 24%),
+ linear-gradient(180deg, #0b1322 0%, #08111f 48%, #060d19 100%);
+ position: relative;
+ overflow-x: hidden;
+}
+
+body.app-shell::before {
+ content: "";
+ position: fixed;
+ inset: 0;
+ background-image: linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
+ background-size: 42px 42px;
+ mask-image: linear-gradient(180deg, rgba(255, 255, 255, 0.6), transparent 78%);
+ pointer-events: none;
+}
+
+.bg-orb {
+ position: fixed;
+ border-radius: 999px;
+ filter: blur(24px);
+ opacity: 0.55;
+ pointer-events: none;
+}
+
+.bg-orb-one {
+ width: 300px;
+ height: 300px;
+ top: 6rem;
+ right: -6rem;
+ background: linear-gradient(135deg, rgba(20, 184, 166, 0.28), rgba(52, 211, 153, 0.14));
+}
+
+.bg-orb-two {
+ width: 260px;
+ height: 260px;
+ bottom: 8rem;
+ left: -5rem;
+ background: linear-gradient(135deg, rgba(249, 115, 22, 0.2), rgba(254, 215, 170, 0.12));
+}
+
+h1,
+.h1,
+.display-title,
+.page-title,
+.section-title,
+.brand-title {
+ font-family: 'Space Grotesk', sans-serif;
+ letter-spacing: -0.04em;
+}
+
+p,
+li,
+label,
+input,
+select,
+textarea,
+button {
+ font-family: 'Inter', sans-serif;
+}
+
+.site-header {
+ position: sticky;
+ top: 0;
+ z-index: 20;
+ backdrop-filter: blur(16px);
+ -webkit-backdrop-filter: blur(16px);
+ background: rgba(6, 13, 25, 0.52);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
+}
+
+.navbar-brand {
+ color: var(--text-main);
+ text-decoration: none;
+}
+
+.brand-mark {
+ width: 48px;
+ height: 48px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 16px;
+ background: linear-gradient(135deg, var(--primary), var(--primary-strong));
+ box-shadow: 0 14px 30px rgba(15, 118, 110, 0.32);
+ font-weight: 800;
+ font-size: 1.2rem;
+}
+
+.brand-title {
+ display: block;
+ font-size: 1rem;
+ font-weight: 700;
+}
+
+.brand-subtitle {
+ color: var(--text-muted);
+ font-size: 0.78rem;
+ letter-spacing: 0.02em;
+}
+
+.nav-link {
+ color: var(--text-soft) !important;
+ font-weight: 600;
+ padding: 0.75rem 1rem !important;
+ border-radius: 999px;
+}
+
+.nav-link:hover,
+.nav-link:focus-visible {
+ color: #ffffff !important;
+ background: rgba(255, 255, 255, 0.08);
+}
+
+.page-shell {
+ position: relative;
+ z-index: 1;
+}
+
+.hero-section,
+.inner-hero {
+ padding-top: var(--spacing-section);
+}
+
+.py-lg-6 {
+ padding-top: var(--spacing-section) !important;
+ padding-bottom: var(--spacing-section) !important;
+}
+
+.eyebrow-chip,
+.panel-label,
+.section-kicker,
+.info-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ border-radius: 999px;
+ padding: 0.55rem 0.9rem;
+ border: 1px solid var(--line-strong);
+ background: rgba(167, 243, 208, 0.08);
+ color: #dffdf3;
+ font-size: 0.84rem;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+.display-title,
+.page-title {
+ font-size: clamp(2.75rem, 5vw, 4.8rem);
+ line-height: 0.96;
+ margin: 0;
+ max-width: 12ch;
+}
+
+.page-title {
+ font-size: clamp(2.3rem, 4vw, 3.6rem);
+}
+
+.hero-copy {
+ max-width: 60ch;
+ color: var(--text-soft);
+ font-size: 1.08rem;
+ line-height: 1.8;
+}
+
+.hero-copy.small {
+ font-size: 0.98rem;
+ line-height: 1.7;
+}
+
+.btn {
+ border-radius: 16px;
+ padding: 0.88rem 1.3rem;
+ font-weight: 700;
+ letter-spacing: -0.01em;
+}
+
+.btn-highlight {
+ color: #081220;
+ background: linear-gradient(135deg, #5eead4, #facc15);
+ border: none;
+ box-shadow: 0 18px 35px rgba(94, 234, 212, 0.22);
+}
+
+.btn-highlight:hover,
+.btn-highlight:focus-visible {
+ color: #081220;
+ transform: translateY(-1px);
+ box-shadow: 0 24px 45px rgba(94, 234, 212, 0.28);
+}
+
+.btn-outline-light {
+ border-color: rgba(255, 255, 255, 0.18);
+ color: var(--text-main);
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.btn-outline-light:hover,
+.btn-outline-light:focus-visible {
+ background: rgba(255, 255, 255, 0.1);
+ border-color: rgba(255, 255, 255, 0.3);
+ color: #ffffff;
+}
+
+.glass-panel {
+ background: linear-gradient(180deg, rgba(10, 20, 38, 0.86), rgba(6, 15, 29, 0.92));
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ box-shadow: var(--shadow-soft);
+ border-radius: var(--radius-xl);
+ padding: 2rem;
+}
+
+.hero-panel,
+.section-panel,
+.brief-card,
+.summary-card,
+.setup-form {
+ position: relative;
+ overflow: hidden;
+}
+
+.hero-panel::after,
+.section-panel::after,
+.setup-form::after,
+.summary-card::after {
+ content: "";
+ position: absolute;
+ inset: auto -20% -30% auto;
+ width: 180px;
+ height: 180px;
+ background: radial-gradient(circle, rgba(20, 184, 166, 0.18), transparent 68%);
+ pointer-events: none;
+}
+
+.metric-row {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 1rem;
+ max-width: 44rem;
+}
+
+.metric-card,
+.stack-preview-card,
+.roadmap-card,
+.brief-preview-card,
+.info-rail-card,
+.empty-card,
+.note-panel,
+.checklist-item,
+.command-row {
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ background: rgba(255, 255, 255, 0.04);
+ border-radius: var(--radius-lg);
+}
+
+.metric-card {
+ padding: 1.15rem 1.2rem;
+}
+
+.metric-value {
+ display: block;
+ font-family: 'Space Grotesk', sans-serif;
+ font-size: 2rem;
+ font-weight: 700;
+}
+
+.metric-label,
+.progress-copy,
+.card-subtitle,
+.form-hint,
+.helper-copy,
+.next-copy,
+.text-link,
+.card-subtitle,
+.stack-preview-card p,
+.roadmap-card p,
+.brief-preview-card p,
+.info-rail-card p,
+.checklist-item p,
+.note-panel p {
+ color: var(--text-muted);
+}
+
+.stack-preview,
+.info-rail,
+.roadmap-grid,
+.brief-list-preview,
+.checklist-grid,
+.command-list {
+ display: grid;
+ gap: 1rem;
+}
+
+.stack-preview-card,
+.roadmap-card,
+.brief-preview-card,
+.info-rail-card,
+.empty-card,
+.note-panel,
+.checklist-item,
+.command-row {
+ padding: 1.25rem;
+}
+
+.stack-title,
+.roadmap-index,
+.command-index {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 3rem;
+ min-height: 3rem;
+ border-radius: 14px;
+ background: rgba(20, 184, 166, 0.12);
+ color: #d7fff7;
+ font-weight: 700;
+}
+
+.stack-preview-card strong,
+.info-rail-card strong,
+.card-title,
+.brief-preview-card h3,
+.roadmap-card h3,
+.checklist-item h3,
+.section-title {
+ display: block;
+ margin: 0.8rem 0 0.5rem;
+ color: #ffffff;
+}
+
+.section-heading-wrap {
+ display: flex;
+ justify-content: space-between;
+ align-items: end;
+ gap: 1rem;
+}
+
+.section-title {
+ font-size: clamp(1.5rem, 2.5vw, 2.25rem);
+ margin: 0.5rem 0 0;
+}
+
+.brief-preview-card {
+ display: block;
+ text-decoration: none;
+ color: inherit;
+ transition: transform 0.2s ease, border-color 0.2s ease;
+}
+
+.brief-preview-card:hover,
+.brief-preview-card:focus-visible,
+.brief-card:hover,
+.brief-card:focus-within {
+ transform: translateY(-3px);
+ border-color: rgba(94, 234, 212, 0.3);
+}
+
+.status-pill {
+ display: inline-flex;
+ align-items: center;
+ border-radius: 999px;
+ padding: 0.45rem 0.8rem;
+ font-size: 0.78rem;
+ font-weight: 700;
+ letter-spacing: 0.03em;
+ white-space: nowrap;
+}
+
+.status-success {
+ background: rgba(52, 211, 153, 0.14);
+ color: #86efac;
+}
+
+.status-warning {
+ background: rgba(251, 191, 36, 0.14);
+ color: #fde68a;
+}
+
+.status-danger {
+ background: rgba(251, 113, 133, 0.14);
+ color: #fda4af;
+}
+
+.progress-track {
+ width: 100%;
+ height: 10px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.08);
+ overflow: hidden;
+}
+
+.progress-track span {
+ display: block;
+ height: 100%;
+ border-radius: inherit;
+ background: linear-gradient(90deg, #5eead4, #facc15);
+}
+
+.progress-0 span {
+ width: 8%;
+}
+
+.progress-1 span {
+ width: 33%;
+}
+
+.progress-2 span {
+ width: 67%;
+}
+
+.progress-3 span {
+ width: 100%;
+}
+
+.info-rail,
+.stack-preview,
+.brief-list-preview,
+.command-list,
+.checklist-grid,
+.roadmap-grid {
+ grid-template-columns: 1fr;
+}
+
+.setup-form .form-control,
+.setup-form .form-select {
+ min-height: 3.5rem;
+ border-radius: 16px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ background: rgba(255, 255, 255, 0.05);
+ color: #ffffff;
+ padding: 0.85rem 1rem;
+}
+
+.setup-form textarea.form-control {
+ min-height: 11rem;
+}
+
+.setup-form .form-control::placeholder {
+ color: rgba(191, 209, 227, 0.55);
+}
+
+.setup-form .form-control:focus,
+.setup-form .form-select:focus,
+.form-check-input:focus,
+.btn:focus-visible,
+.nav-link:focus-visible,
+.text-link:focus-visible {
+ box-shadow: 0 0 0 0.25rem rgba(94, 234, 212, 0.18);
+ border-color: rgba(94, 234, 212, 0.45);
+ outline: none;
+}
+
+.form-label,
+.form-check-label {
+ font-weight: 700;
+ margin-bottom: 0.55rem;
+}
+
+.form-hint,
+.helper-copy {
+ font-size: 0.92rem;
+ line-height: 1.6;
+}
+
+.field-error {
+ color: #fecaca;
+ font-size: 0.93rem;
+ margin-top: 0.5rem;
+}
+
+.service-readiness-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 1rem;
+}
+
+.service-toggle {
+ display: flex;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: 1rem 1.1rem;
+ border-radius: 18px;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.form-check-input {
+ width: 3rem;
+ height: 1.6rem;
+ margin-top: 0;
+ background-color: rgba(255, 255, 255, 0.16);
+ border-color: rgba(255, 255, 255, 0.22);
+}
+
+.form-check-input:checked {
+ background-color: var(--primary-strong);
+ border-color: var(--primary-strong);
+}
+
+.card-title {
+ font-size: 1.35rem;
+ margin: 0;
+}
+
+.detail-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: grid;
+ gap: 0.8rem;
+}
+
+.detail-list li {
+ display: flex;
+ justify-content: space-between;
+ gap: 1rem;
+ color: var(--text-soft);
+}
+
+.next-copy {
+ line-height: 1.7;
+ font-size: 0.98rem;
+}
+
+.readiness-ring {
+ margin-inline: auto;
+ width: 220px;
+ height: 220px;
+ border-radius: 50%;
+ display: grid;
+ place-items: center;
+}
+
+.readiness-ring.progress-0 {
+ background: conic-gradient(#5eead4 0deg, #5eead4 29deg, rgba(255, 255, 255, 0.08) 29deg);
+}
+
+.readiness-ring.progress-1 {
+ background: conic-gradient(#5eead4 0deg, #5eead4 120deg, rgba(255, 255, 255, 0.08) 120deg);
+}
+
+.readiness-ring.progress-2 {
+ background: conic-gradient(#5eead4 0deg, #5eead4 240deg, rgba(255, 255, 255, 0.08) 240deg);
+}
+
+.readiness-ring.progress-3 {
+ background: conic-gradient(#5eead4 0deg, #5eead4 360deg, rgba(255, 255, 255, 0.08) 360deg);
+}
+
+.readiness-ring-inner {
+ width: 150px;
+ height: 150px;
+ border-radius: 50%;
+ background: rgba(7, 16, 30, 0.94);
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+}
+
+.readiness-ring-inner strong {
+ font-family: 'Space Grotesk', sans-serif;
+ font-size: 2.4rem;
+ line-height: 1;
+}
+
+.readiness-ring-inner span {
+ color: var(--text-muted);
+ font-size: 0.9rem;
+ margin-top: 0.4rem;
+}
+
+.checklist-item {
+ display: flex;
+ gap: 1rem;
+ align-items: flex-start;
+}
+
+.checklist-dot {
+ width: 16px;
+ height: 16px;
+ border-radius: 999px;
+ background: rgba(251, 113, 133, 0.55);
+ box-shadow: 0 0 0 8px rgba(251, 113, 133, 0.08);
+ margin-top: 0.45rem;
+ flex-shrink: 0;
+}
+
+.checklist-item.is-ready .checklist-dot {
+ background: rgba(52, 211, 153, 0.85);
+ box-shadow: 0 0 0 8px rgba(52, 211, 153, 0.08);
+}
+
+.code-panel {
+ padding: 1.25rem;
+ border-radius: var(--radius-lg);
+ background: rgba(4, 10, 20, 0.9);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.code-panel code,
+.command-row code {
+ color: #ccfbf1;
+ font-size: 0.95rem;
+ line-height: 1.9;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.command-row {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.text-link {
+ color: #bffef1;
+ font-weight: 700;
+ text-decoration: none;
+}
+
+.text-link:hover,
+.text-link:focus-visible {
+ color: #ffffff;
+}
+
+.toast-stack {
+ padding-top: 1.5rem;
+}
+
+.glass-alert {
+ border-radius: 18px;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ background: rgba(11, 24, 44, 0.86);
+ color: #ffffff;
+}
+
+.site-footer {
+ position: relative;
+ z-index: 1;
+}
+
+@media (max-width: 991.98px) {
+ .metric-row,
+ .service-readiness-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .display-title,
+ .page-title {
+ max-width: none;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .glass-panel {
+ padding: 1.4rem;
+ border-radius: 22px;
+ }
+
+ .hero-section,
+ .inner-hero,
+ .py-lg-6 {
+ padding-top: 2.5rem !important;
+ padding-bottom: 2.5rem !important;
+ }
+
+ .brand-subtitle {
+ display: none !important;
+ }
+
+ .section-heading-wrap {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .command-row {
+ align-items: flex-start;
+ flex-direction: column;
+ }
}
diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css
index 108056f..6176d04 100644
--- a/staticfiles/css/custom.css
+++ b/staticfiles/css/custom.css
@@ -1,21 +1,722 @@
-
+/* Postiz Native Setup Studio */
:root {
- --bg-color-start: #6a11cb;
- --bg-color-end: #2575fc;
- --text-color: #ffffff;
- --card-bg-color: rgba(255, 255, 255, 0.01);
- --card-border-color: rgba(255, 255, 255, 0.1);
+ --bg-main: #081220;
+ --bg-panel: rgba(9, 19, 36, 0.72);
+ --bg-panel-strong: rgba(11, 24, 44, 0.92);
+ --line-soft: rgba(167, 243, 208, 0.14);
+ --line-strong: rgba(167, 243, 208, 0.22);
+ --text-main: #f8fafc;
+ --text-soft: #bfd1e3;
+ --text-muted: #8ca0b8;
+ --primary: #0f766e;
+ --primary-strong: #14b8a6;
+ --secondary: #132238;
+ --accent: #f97316;
+ --accent-soft: #fed7aa;
+ --success: #34d399;
+ --danger: #fb7185;
+ --warning: #fbbf24;
+ --shadow-soft: 0 30px 80px rgba(3, 10, 20, 0.45);
+ --radius-xl: 28px;
+ --radius-lg: 20px;
+ --radius-md: 16px;
+ --spacing-section: clamp(3rem, 7vw, 5.5rem);
}
-body {
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body.app-shell {
margin: 0;
+ min-height: 100vh;
font-family: 'Inter', sans-serif;
- background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
- color: var(--text-color);
+ color: var(--text-main);
+ background:
+ radial-gradient(circle at top left, rgba(20, 184, 166, 0.24), transparent 32%),
+ radial-gradient(circle at 85% 18%, rgba(249, 115, 22, 0.18), transparent 24%),
+ linear-gradient(180deg, #0b1322 0%, #08111f 48%, #060d19 100%);
+ position: relative;
+ overflow-x: hidden;
+}
+
+body.app-shell::before {
+ content: "";
+ position: fixed;
+ inset: 0;
+ background-image: linear-gradient(rgba(255, 255, 255, 0.02) 1px, transparent 1px), linear-gradient(90deg, rgba(255, 255, 255, 0.02) 1px, transparent 1px);
+ background-size: 42px 42px;
+ mask-image: linear-gradient(180deg, rgba(255, 255, 255, 0.6), transparent 78%);
+ pointer-events: none;
+}
+
+.bg-orb {
+ position: fixed;
+ border-radius: 999px;
+ filter: blur(24px);
+ opacity: 0.55;
+ pointer-events: none;
+}
+
+.bg-orb-one {
+ width: 300px;
+ height: 300px;
+ top: 6rem;
+ right: -6rem;
+ background: linear-gradient(135deg, rgba(20, 184, 166, 0.28), rgba(52, 211, 153, 0.14));
+}
+
+.bg-orb-two {
+ width: 260px;
+ height: 260px;
+ bottom: 8rem;
+ left: -5rem;
+ background: linear-gradient(135deg, rgba(249, 115, 22, 0.2), rgba(254, 215, 170, 0.12));
+}
+
+h1,
+.h1,
+.display-title,
+.page-title,
+.section-title,
+.brand-title {
+ font-family: 'Space Grotesk', sans-serif;
+ letter-spacing: -0.04em;
+}
+
+p,
+li,
+label,
+input,
+select,
+textarea,
+button {
+ font-family: 'Inter', sans-serif;
+}
+
+.site-header {
+ position: sticky;
+ top: 0;
+ z-index: 20;
+ backdrop-filter: blur(16px);
+ -webkit-backdrop-filter: blur(16px);
+ background: rgba(6, 13, 25, 0.52);
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
+}
+
+.navbar-brand {
+ color: var(--text-main);
+ text-decoration: none;
+}
+
+.brand-mark {
+ width: 48px;
+ height: 48px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 16px;
+ background: linear-gradient(135deg, var(--primary), var(--primary-strong));
+ box-shadow: 0 14px 30px rgba(15, 118, 110, 0.32);
+ font-weight: 800;
+ font-size: 1.2rem;
+}
+
+.brand-title {
+ display: block;
+ font-size: 1rem;
+ font-weight: 700;
+}
+
+.brand-subtitle {
+ color: var(--text-muted);
+ font-size: 0.78rem;
+ letter-spacing: 0.02em;
+}
+
+.nav-link {
+ color: var(--text-soft) !important;
+ font-weight: 600;
+ padding: 0.75rem 1rem !important;
+ border-radius: 999px;
+}
+
+.nav-link:hover,
+.nav-link:focus-visible {
+ color: #ffffff !important;
+ background: rgba(255, 255, 255, 0.08);
+}
+
+.page-shell {
+ position: relative;
+ z-index: 1;
+}
+
+.hero-section,
+.inner-hero {
+ padding-top: var(--spacing-section);
+}
+
+.py-lg-6 {
+ padding-top: var(--spacing-section) !important;
+ padding-bottom: var(--spacing-section) !important;
+}
+
+.eyebrow-chip,
+.panel-label,
+.section-kicker,
+.info-chip {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ border-radius: 999px;
+ padding: 0.55rem 0.9rem;
+ border: 1px solid var(--line-strong);
+ background: rgba(167, 243, 208, 0.08);
+ color: #dffdf3;
+ font-size: 0.84rem;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+}
+
+.display-title,
+.page-title {
+ font-size: clamp(2.75rem, 5vw, 4.8rem);
+ line-height: 0.96;
+ margin: 0;
+ max-width: 12ch;
+}
+
+.page-title {
+ font-size: clamp(2.3rem, 4vw, 3.6rem);
+}
+
+.hero-copy {
+ max-width: 60ch;
+ color: var(--text-soft);
+ font-size: 1.08rem;
+ line-height: 1.8;
+}
+
+.hero-copy.small {
+ font-size: 0.98rem;
+ line-height: 1.7;
+}
+
+.btn {
+ border-radius: 16px;
+ padding: 0.88rem 1.3rem;
+ font-weight: 700;
+ letter-spacing: -0.01em;
+}
+
+.btn-highlight {
+ color: #081220;
+ background: linear-gradient(135deg, #5eead4, #facc15);
+ border: none;
+ box-shadow: 0 18px 35px rgba(94, 234, 212, 0.22);
+}
+
+.btn-highlight:hover,
+.btn-highlight:focus-visible {
+ color: #081220;
+ transform: translateY(-1px);
+ box-shadow: 0 24px 45px rgba(94, 234, 212, 0.28);
+}
+
+.btn-outline-light {
+ border-color: rgba(255, 255, 255, 0.18);
+ color: var(--text-main);
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.btn-outline-light:hover,
+.btn-outline-light:focus-visible {
+ background: rgba(255, 255, 255, 0.1);
+ border-color: rgba(255, 255, 255, 0.3);
+ color: #ffffff;
+}
+
+.glass-panel {
+ background: linear-gradient(180deg, rgba(10, 20, 38, 0.86), rgba(6, 15, 29, 0.92));
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ box-shadow: var(--shadow-soft);
+ border-radius: var(--radius-xl);
+ padding: 2rem;
+}
+
+.hero-panel,
+.section-panel,
+.brief-card,
+.summary-card,
+.setup-form {
+ position: relative;
+ overflow: hidden;
+}
+
+.hero-panel::after,
+.section-panel::after,
+.setup-form::after,
+.summary-card::after {
+ content: "";
+ position: absolute;
+ inset: auto -20% -30% auto;
+ width: 180px;
+ height: 180px;
+ background: radial-gradient(circle, rgba(20, 184, 166, 0.18), transparent 68%);
+ pointer-events: none;
+}
+
+.metric-row {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 1rem;
+ max-width: 44rem;
+}
+
+.metric-card,
+.stack-preview-card,
+.roadmap-card,
+.brief-preview-card,
+.info-rail-card,
+.empty-card,
+.note-panel,
+.checklist-item,
+.command-row {
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ background: rgba(255, 255, 255, 0.04);
+ border-radius: var(--radius-lg);
+}
+
+.metric-card {
+ padding: 1.15rem 1.2rem;
+}
+
+.metric-value {
+ display: block;
+ font-family: 'Space Grotesk', sans-serif;
+ font-size: 2rem;
+ font-weight: 700;
+}
+
+.metric-label,
+.progress-copy,
+.card-subtitle,
+.form-hint,
+.helper-copy,
+.next-copy,
+.text-link,
+.card-subtitle,
+.stack-preview-card p,
+.roadmap-card p,
+.brief-preview-card p,
+.info-rail-card p,
+.checklist-item p,
+.note-panel p {
+ color: var(--text-muted);
+}
+
+.stack-preview,
+.info-rail,
+.roadmap-grid,
+.brief-list-preview,
+.checklist-grid,
+.command-list {
+ display: grid;
+ gap: 1rem;
+}
+
+.stack-preview-card,
+.roadmap-card,
+.brief-preview-card,
+.info-rail-card,
+.empty-card,
+.note-panel,
+.checklist-item,
+.command-row {
+ padding: 1.25rem;
+}
+
+.stack-title,
+.roadmap-index,
+.command-index {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 3rem;
+ min-height: 3rem;
+ border-radius: 14px;
+ background: rgba(20, 184, 166, 0.12);
+ color: #d7fff7;
+ font-weight: 700;
+}
+
+.stack-preview-card strong,
+.info-rail-card strong,
+.card-title,
+.brief-preview-card h3,
+.roadmap-card h3,
+.checklist-item h3,
+.section-title {
+ display: block;
+ margin: 0.8rem 0 0.5rem;
+ color: #ffffff;
+}
+
+.section-heading-wrap {
display: flex;
+ justify-content: space-between;
+ align-items: end;
+ gap: 1rem;
+}
+
+.section-title {
+ font-size: clamp(1.5rem, 2.5vw, 2.25rem);
+ margin: 0.5rem 0 0;
+}
+
+.brief-preview-card {
+ display: block;
+ text-decoration: none;
+ color: inherit;
+ transition: transform 0.2s ease, border-color 0.2s ease;
+}
+
+.brief-preview-card:hover,
+.brief-preview-card:focus-visible,
+.brief-card:hover,
+.brief-card:focus-within {
+ transform: translateY(-3px);
+ border-color: rgba(94, 234, 212, 0.3);
+}
+
+.status-pill {
+ display: inline-flex;
+ align-items: center;
+ border-radius: 999px;
+ padding: 0.45rem 0.8rem;
+ font-size: 0.78rem;
+ font-weight: 700;
+ letter-spacing: 0.03em;
+ white-space: nowrap;
+}
+
+.status-success {
+ background: rgba(52, 211, 153, 0.14);
+ color: #86efac;
+}
+
+.status-warning {
+ background: rgba(251, 191, 36, 0.14);
+ color: #fde68a;
+}
+
+.status-danger {
+ background: rgba(251, 113, 133, 0.14);
+ color: #fda4af;
+}
+
+.progress-track {
+ width: 100%;
+ height: 10px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.08);
+ overflow: hidden;
+}
+
+.progress-track span {
+ display: block;
+ height: 100%;
+ border-radius: inherit;
+ background: linear-gradient(90deg, #5eead4, #facc15);
+}
+
+.progress-0 span {
+ width: 8%;
+}
+
+.progress-1 span {
+ width: 33%;
+}
+
+.progress-2 span {
+ width: 67%;
+}
+
+.progress-3 span {
+ width: 100%;
+}
+
+.info-rail,
+.stack-preview,
+.brief-list-preview,
+.command-list,
+.checklist-grid,
+.roadmap-grid {
+ grid-template-columns: 1fr;
+}
+
+.setup-form .form-control,
+.setup-form .form-select {
+ min-height: 3.5rem;
+ border-radius: 16px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ background: rgba(255, 255, 255, 0.05);
+ color: #ffffff;
+ padding: 0.85rem 1rem;
+}
+
+.setup-form textarea.form-control {
+ min-height: 11rem;
+}
+
+.setup-form .form-control::placeholder {
+ color: rgba(191, 209, 227, 0.55);
+}
+
+.setup-form .form-control:focus,
+.setup-form .form-select:focus,
+.form-check-input:focus,
+.btn:focus-visible,
+.nav-link:focus-visible,
+.text-link:focus-visible {
+ box-shadow: 0 0 0 0.25rem rgba(94, 234, 212, 0.18);
+ border-color: rgba(94, 234, 212, 0.45);
+ outline: none;
+}
+
+.form-label,
+.form-check-label {
+ font-weight: 700;
+ margin-bottom: 0.55rem;
+}
+
+.form-hint,
+.helper-copy {
+ font-size: 0.92rem;
+ line-height: 1.6;
+}
+
+.field-error {
+ color: #fecaca;
+ font-size: 0.93rem;
+ margin-top: 0.5rem;
+}
+
+.service-readiness-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 1rem;
+}
+
+.service-toggle {
+ display: flex;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: 1rem 1.1rem;
+ border-radius: 18px;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.form-check-input {
+ width: 3rem;
+ height: 1.6rem;
+ margin-top: 0;
+ background-color: rgba(255, 255, 255, 0.16);
+ border-color: rgba(255, 255, 255, 0.22);
+}
+
+.form-check-input:checked {
+ background-color: var(--primary-strong);
+ border-color: var(--primary-strong);
+}
+
+.card-title {
+ font-size: 1.35rem;
+ margin: 0;
+}
+
+.detail-list {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ display: grid;
+ gap: 0.8rem;
+}
+
+.detail-list li {
+ display: flex;
+ justify-content: space-between;
+ gap: 1rem;
+ color: var(--text-soft);
+}
+
+.next-copy {
+ line-height: 1.7;
+ font-size: 0.98rem;
+}
+
+.readiness-ring {
+ margin-inline: auto;
+ width: 220px;
+ height: 220px;
+ border-radius: 50%;
+ display: grid;
+ place-items: center;
+}
+
+.readiness-ring.progress-0 {
+ background: conic-gradient(#5eead4 0deg, #5eead4 29deg, rgba(255, 255, 255, 0.08) 29deg);
+}
+
+.readiness-ring.progress-1 {
+ background: conic-gradient(#5eead4 0deg, #5eead4 120deg, rgba(255, 255, 255, 0.08) 120deg);
+}
+
+.readiness-ring.progress-2 {
+ background: conic-gradient(#5eead4 0deg, #5eead4 240deg, rgba(255, 255, 255, 0.08) 240deg);
+}
+
+.readiness-ring.progress-3 {
+ background: conic-gradient(#5eead4 0deg, #5eead4 360deg, rgba(255, 255, 255, 0.08) 360deg);
+}
+
+.readiness-ring-inner {
+ width: 150px;
+ height: 150px;
+ border-radius: 50%;
+ background: rgba(7, 16, 30, 0.94);
+ display: flex;
+ flex-direction: column;
justify-content: center;
align-items: center;
- min-height: 100vh;
text-align: center;
- overflow: hidden;
- position: relative;
+}
+
+.readiness-ring-inner strong {
+ font-family: 'Space Grotesk', sans-serif;
+ font-size: 2.4rem;
+ line-height: 1;
+}
+
+.readiness-ring-inner span {
+ color: var(--text-muted);
+ font-size: 0.9rem;
+ margin-top: 0.4rem;
+}
+
+.checklist-item {
+ display: flex;
+ gap: 1rem;
+ align-items: flex-start;
+}
+
+.checklist-dot {
+ width: 16px;
+ height: 16px;
+ border-radius: 999px;
+ background: rgba(251, 113, 133, 0.55);
+ box-shadow: 0 0 0 8px rgba(251, 113, 133, 0.08);
+ margin-top: 0.45rem;
+ flex-shrink: 0;
+}
+
+.checklist-item.is-ready .checklist-dot {
+ background: rgba(52, 211, 153, 0.85);
+ box-shadow: 0 0 0 8px rgba(52, 211, 153, 0.08);
+}
+
+.code-panel {
+ padding: 1.25rem;
+ border-radius: var(--radius-lg);
+ background: rgba(4, 10, 20, 0.9);
+ border: 1px solid rgba(255, 255, 255, 0.08);
+}
+
+.code-panel code,
+.command-row code {
+ color: #ccfbf1;
+ font-size: 0.95rem;
+ line-height: 1.9;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+
+.command-row {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.text-link {
+ color: #bffef1;
+ font-weight: 700;
+ text-decoration: none;
+}
+
+.text-link:hover,
+.text-link:focus-visible {
+ color: #ffffff;
+}
+
+.toast-stack {
+ padding-top: 1.5rem;
+}
+
+.glass-alert {
+ border-radius: 18px;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ background: rgba(11, 24, 44, 0.86);
+ color: #ffffff;
+}
+
+.site-footer {
+ position: relative;
+ z-index: 1;
+}
+
+@media (max-width: 991.98px) {
+ .metric-row,
+ .service-readiness-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .display-title,
+ .page-title {
+ max-width: none;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .glass-panel {
+ padding: 1.4rem;
+ border-radius: 22px;
+ }
+
+ .hero-section,
+ .inner-hero,
+ .py-lg-6 {
+ padding-top: 2.5rem !important;
+ padding-bottom: 2.5rem !important;
+ }
+
+ .brand-subtitle {
+ display: none !important;
+ }
+
+ .section-heading-wrap {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .command-row {
+ align-items: flex-start;
+ flex-direction: column;
+ }
}