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 %} + + {% 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 @@ +
+ {% csrf_token %} +
+
+ + {{ form.title }} + {% if form.title.help_text %}
{{ form.title.help_text }}
{% endif %} + {% for error in form.title.errors %}
{{ error }}
{% endfor %} +
+
+ + {{ form.public_url }} + {% if form.public_url.help_text %}
{{ form.public_url.help_text }}
{% endif %} + {% for error in form.public_url.errors %}
{{ error }}
{% endfor %} +
+
+ + {{ form.node_version }} +
{{ form.node_version.help_text }}
+
+
+ + {{ form.package_manager }} +
{{ form.package_manager.help_text }}
+
+
+ + {{ form.email_provider }} +
{{ form.email_provider.help_text }}
+
+
+
+
+
+ +
{{ form.postgres_ready.help_text }}
+
+
{{ form.postgres_ready }}
+
+
+
+ +
{{ form.redis_ready.help_text }}
+
+
{{ form.redis_ready }}
+
+
+
+ +
{{ form.temporal_ready.help_text }}
+
+
{{ form.temporal_ready }}
+
+
+
+
+ + {{ form.upload_strategy }} +
{{ form.upload_strategy.help_text }}
+
+
+ + {{ form.notes }} + {% for error in form.notes.errors %}
{{ error }}
{% endfor %} +
+
+ {% if form.non_field_errors %} +
{{ form.non_field_errors }}
+ {% endif %} +
+

Saving a brief generates a live detail page with prerequisite status, env variables, and boot commands.

+ +
+
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 %}
+
+
+
+
+
+ Bootstrap +

Suggested command path

+
+ Back to briefs +
+
+ {% 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" }} -

-
- -{% 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; + } }