diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc
index 96bce55..1e94cd8 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..52d7cf8 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -176,6 +176,10 @@ CONTACT_EMAIL_TO = [
# When both TLS and SSL flags are enabled, prefer SSL explicitly
if EMAIL_USE_SSL:
EMAIL_USE_TLS = False
+# Increase upload limits for multiple HTML files
+DATA_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 10MB
+FILE_UPLOAD_MAX_MEMORY_SIZE = 10485760 # 10MB
+
# Default primary key field type
# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field
diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc
index a5ed392..566b263 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..b67afc4
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 e061640..26ea022 100644
Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ
diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc
index 5a69659..1b53857 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 2a36fd6..99b0c21 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..0ffa668 100644
--- a/core/admin.py
+++ b/core/admin.py
@@ -1,3 +1,23 @@
from django.contrib import admin
-# Register your models here.
+from .models import HtmlBundle, HtmlDocument, HtmlExport
+
+
+@admin.register(HtmlBundle)
+class HtmlBundleAdmin(admin.ModelAdmin):
+ list_display = ("title", "created_at")
+ search_fields = ("title",)
+ ordering = ("-created_at",)
+
+
+@admin.register(HtmlDocument)
+class HtmlDocumentAdmin(admin.ModelAdmin):
+ list_display = ("original_name", "bundle", "order")
+ search_fields = ("original_name",)
+ list_filter = ("bundle",)
+
+
+@admin.register(HtmlExport)
+class HtmlExportAdmin(admin.ModelAdmin):
+ list_display = ("file_name", "bundle", "created_at")
+ list_filter = ("bundle",)
diff --git a/core/forms.py b/core/forms.py
new file mode 100644
index 0000000..2eeb0de
--- /dev/null
+++ b/core/forms.py
@@ -0,0 +1,14 @@
+from django import forms
+
+
+class BundleUploadForm(forms.Form):
+ title = forms.CharField(
+ label="Bundle title",
+ required=False,
+ widget=forms.TextInput(
+ attrs={
+ "class": "form-control",
+ "placeholder": "Q2 policy updates",
+ }
+ ),
+ )
diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py
new file mode 100644
index 0000000..fcdb5f4
--- /dev/null
+++ b/core/migrations/0001_initial.py
@@ -0,0 +1,45 @@
+# Generated by Django 5.2.7 on 2026-02-18 21:57
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='HtmlBundle',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('title', models.CharField(blank=True, max_length=200)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='HtmlDocument',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('original_name', models.CharField(max_length=255)),
+ ('order', models.PositiveIntegerField(default=1)),
+ ('content_text', models.TextField()),
+ ('bundle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='documents', to='core.htmlbundle')),
+ ],
+ options={
+ 'ordering': ['order', 'id'],
+ },
+ ),
+ migrations.CreateModel(
+ name='HtmlExport',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('file_name', models.CharField(max_length=255)),
+ ('bundle', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='exports', to='core.htmlbundle')),
+ ],
+ ),
+ ]
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..33e150b
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..fa06591 100644
--- a/core/models.py
+++ b/core/models.py
@@ -1,3 +1,31 @@
from django.db import models
-# Create your models here.
+
+class HtmlBundle(models.Model):
+ title = models.CharField(max_length=200, blank=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+
+ def __str__(self) -> str:
+ return self.title or f"Bundle {self.pk}"
+
+
+class HtmlDocument(models.Model):
+ bundle = models.ForeignKey(HtmlBundle, related_name="documents", on_delete=models.CASCADE)
+ original_name = models.CharField(max_length=255)
+ order = models.PositiveIntegerField(default=1)
+ content_text = models.TextField()
+
+ class Meta:
+ ordering = ["order", "id"]
+
+ def __str__(self) -> str:
+ return f"{self.original_name} ({self.bundle_id})"
+
+
+class HtmlExport(models.Model):
+ bundle = models.ForeignKey(HtmlBundle, related_name="exports", on_delete=models.CASCADE)
+ created_at = models.DateTimeField(auto_now_add=True)
+ file_name = models.CharField(max_length=255)
+
+ def __str__(self) -> str:
+ return self.file_name
diff --git a/core/templates/base.html b/core/templates/base.html
index 1e7e5fb..fd48d28 100644
--- a/core/templates/base.html
+++ b/core/templates/base.html
@@ -3,7 +3,9 @@
+
{% block title %}Knowledge Base{% endblock %}
+ {% block meta %}
{% if project_description %}
@@ -13,13 +15,19 @@
{% endif %}
+ {% endblock %}
{% load static %}
+
+
+
+
{% block head %}{% endblock %}
-
+
{% block content %}{% endblock %}
+
diff --git a/core/templates/core/bundle_detail.html b/core/templates/core/bundle_detail.html
new file mode 100644
index 0000000..85572ae
--- /dev/null
+++ b/core/templates/core/bundle_detail.html
@@ -0,0 +1,107 @@
+{% extends "base.html" %}
+
+{% block title %}{{ page_title }}{% endblock %}
+
+{% block meta %}
+
+
+
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+ HTML Bundle to PDF
+
+
+
+
+ {% if messages %}
+
+ {% for message in messages %}
+
{{ message }}
+ {% endfor %}
+
+ {% endif %}
+
+
+
+
{{ bundle.title|default:"Untitled bundle" }}
+
Created {{ bundle.created_at|date:"M d, Y · H:i" }} · {{ documents|length }} HTML files
+
+
Generate PDF
+
+
+
+
+
+
+
Order your files
+ Lower number = earlier
+
+
+
+
+
+
+
Export history
+ {% if exports %}
+
+ {% for export in exports %}
+
+
+
{{ export.file_name }}
+
{{ export.created_at|date:"M d, Y · H:i" }}
+
+
Download
+
+ {% endfor %}
+
+ {% else %}
+
No exports yet. Generate your first PDF to start history.
+ {% endif %}
+
+
+
Next steps
+
Need quick changes? Update the order and export again. The PDF is always a single, continuous document.
+
Download PDF
+
+
+
+
+
+
+
+{% endblock %}
diff --git a/core/templates/core/bundle_list.html b/core/templates/core/bundle_list.html
new file mode 100644
index 0000000..4885cf1
--- /dev/null
+++ b/core/templates/core/bundle_list.html
@@ -0,0 +1,59 @@
+{% extends "base.html" %}
+
+{% block title %}{{ page_title }}{% endblock %}
+
+{% block meta %}
+
+
+
+{% endblock %}
+
+{% block content %}
+
+
+
+
+
+ HTML Bundle to PDF
+
+
+
+
+
+
+
All bundles
+
Review and re-download any previously generated bundle.
+
+
Create new bundle
+
+
+ {% if bundles %}
+
+ {% for bundle in bundles %}
+
+
+
+
+
{{ bundle.title|default:"Untitled bundle" }}
+
{{ bundle.created_at|date:"M d, Y · H:i" }}
+
{{ bundle.documents.count }} HTML files
+
+
Open
+
+
+
+ {% endfor %}
+
+ {% else %}
+
+
No bundles yet. Upload HTML files to create your first bundle.
+
+ {% endif %}
+
+
+
+
+{% endblock %}
diff --git a/core/templates/core/index.html b/core/templates/core/index.html
index faec813..c7582b6 100644
--- a/core/templates/core/index.html
+++ b/core/templates/core/index.html
@@ -1,145 +1,152 @@
{% extends "base.html" %}
-{% block title %}{{ project_name }}{% endblock %}
+{% block title %}{{ page_title }}{% endblock %}
-{% block head %}
-
-
-
-
+{% block meta %}
+
+
+
{% endblock %}
{% block content %}
-
-
-
Analyzing your requirements and generating your app…
-
- Loading…
-
-
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" }}
-
+
+
+
+
+
+ HTML Bundle to PDF
+
+
+
+
+
+
+
+
Internal doc bundler
+
Combine multiple HTML files into a single PDF in minutes.
+
+ Upload multiple HTML files, set the order, and download one clean PDF. Built for staff who
+ need to bundle text-heavy documents without manual copy/paste.
+
+
+
+
+
+
+ Average prep time
+ Under 3 mins
+
+
Upload → reorder → export
+
One continuous PDF with clean spacing.
+
+
+
What gets bundled
+
Policies · Updates · Release notes · Staff memos
+
+
+
+
+
+
+ {% if messages %}
+
+ {% for message in messages %}
+
{{ message }}
+ {% endfor %}
+
+ {% endif %}
+
+
+
+
Create a new bundle
+
Drop in your HTML files. We will keep their order and let you refine it on the next screen.
+
+
Continuous PDF output
+
+
+
+
+
+
+
+
+
+
+
Recent bundles
+
See all
+
+ {% if bundles %}
+
+ {% for bundle in bundles %}
+
+
+
{{ bundle.title|default:"Untitled bundle" }}
+
{{ bundle.created_at|date:"M d, Y · H:i" }} · {{ bundle.documents.count }} files
+
+
Open
+
+ {% endfor %}
+
+ {% else %}
+
No bundles yet. Upload HTML files to create your first bundle.
+ {% endif %}
+
+
+
+
+
Latest exports
+ {% if exports %}
+
+ {% for export in exports %}
+
+
+
{{ export.bundle.title|default:"Untitled bundle" }}
+
{{ export.created_at|date:"M d, Y · H:i" }}
+
+
Download
+
+ {% endfor %}
+
+ {% else %}
+
No exports yet. Generate your first PDF to see history here.
+ {% endif %}
+
+
+
+
-
-
- Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
-
-{% endblock %}
\ No newline at end of file
+
+
+{% endblock %}
diff --git a/core/urls.py b/core/urls.py
index 6299e3d..f71b517 100644
--- a/core/urls.py
+++ b/core/urls.py
@@ -1,7 +1,11 @@
from django.urls import path
-from .views import home
+from .views import bundle_detail, bundle_download, bundle_list, export_download, home
urlpatterns = [
path("", home, name="home"),
+ path("bundles/", bundle_list, name="bundle_list"),
+ path("bundles/
/", bundle_detail, name="bundle_detail"),
+ path("bundles//download/", bundle_download, name="bundle_download"),
+ path("exports//download/", export_download, name="export_download"),
]
diff --git a/core/views.py b/core/views.py
index c9aed12..a7c389c 100644
--- a/core/views.py
+++ b/core/views.py
@@ -1,25 +1,199 @@
-import os
-import platform
+from html.parser import HTMLParser
+from io import BytesIO
+import textwrap
-from django import get_version as django_version
-from django.shortcuts import render
+from django.contrib import messages
+from django.http import HttpResponse
+from django.shortcuts import get_object_or_404, redirect, render
from django.utils import timezone
+from django.utils.text import slugify
+
+from reportlab.lib.pagesizes import letter
+from reportlab.pdfgen import canvas
+
+from .forms import BundleUploadForm
+from .models import HtmlBundle, HtmlDocument, HtmlExport
+
+
+class _HtmlTextExtractor(HTMLParser):
+ block_tags = {
+ "p",
+ "div",
+ "br",
+ "li",
+ "h1",
+ "h2",
+ "h3",
+ "h4",
+ "h5",
+ "h6",
+ "section",
+ "article",
+ "header",
+ "footer",
+ }
+
+ def __init__(self):
+ super().__init__()
+ self.parts = []
+
+ def handle_starttag(self, tag, attrs):
+ if tag in self.block_tags:
+ self.parts.append("\n")
+
+ def handle_endtag(self, tag):
+ if tag in self.block_tags:
+ self.parts.append("\n")
+
+ def handle_data(self, data):
+ if data.strip():
+ self.parts.append(data)
+
+
+def _extract_text_from_html(html_bytes: bytes) -> str:
+ html_text = html_bytes.decode("utf-8", errors="replace")
+ parser = _HtmlTextExtractor()
+ parser.feed(html_text)
+ raw_text = "".join(parser.parts)
+ lines = [line.strip() for line in raw_text.splitlines()]
+ cleaned = "\n".join([line for line in lines if line])
+ return cleaned
+
+
+def _build_pdf(bundle: HtmlBundle) -> BytesIO:
+ buffer = BytesIO()
+ pdf = canvas.Canvas(buffer, pagesize=letter)
+ width, height = letter
+ margin = 72
+ line_height = 14
+ y = height - margin
+
+ documents = bundle.documents.all()
+ for document in documents:
+ text = document.content_text or ""
+ paragraphs = text.splitlines() or [""]
+ for paragraph in paragraphs:
+ wrapped = textwrap.wrap(paragraph, width=95) or [""]
+ for line in wrapped:
+ if y < margin:
+ pdf.showPage()
+ y = height - margin
+ pdf.setFont("Helvetica", 11)
+ pdf.drawString(margin, y, line)
+ y -= line_height
+ y -= 6
+ y -= 12
+
+ pdf.save()
+ buffer.seek(0)
+ return buffer
def home(request):
- """Render the landing screen with loader and environment details."""
- host_name = request.get_host().lower()
- agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic"
- now = timezone.now()
+ file_errors = []
+ form = BundleUploadForm()
+ if request.method == "POST":
+ form = BundleUploadForm(request.POST)
+ files = request.FILES.getlist("files")
+ if form.is_valid():
+ if not files:
+ file_errors.append("No files were received. Please ensure you have selected files and try again.")
+ else:
+ invalid_files = [f.name for f in files if not f.name.lower().endswith((".html", ".htm"))]
+ if invalid_files:
+ file_errors.append(
+ f"Only .html or .htm files are supported. Invalid: {', '.join(invalid_files[:3])}"
+ )
+ else:
+ title = form.cleaned_data.get("title", "").strip()
+ bundle = HtmlBundle.objects.create(title=title)
+ for index, uploaded in enumerate(files, start=1):
+ content_text = _extract_text_from_html(uploaded.read())
+ HtmlDocument.objects.create(
+ bundle=bundle,
+ original_name=uploaded.name,
+ order=index,
+ content_text=content_text,
+ )
+ messages.success(request, f"Bundle '{bundle.title or 'Untitled'}' created with {len(files)} files.")
+ return redirect("bundle_detail", bundle_id=bundle.id)
+
+ bundles = HtmlBundle.objects.order_by("-created_at")[:5]
+ exports = HtmlExport.objects.select_related("bundle").order_by("-created_at")[:5]
context = {
- "project_name": "New Style",
- "agent_brand": agent_brand,
- "django_version": django_version(),
- "python_version": platform.python_version(),
- "current_time": now,
- "host_name": host_name,
- "project_description": os.getenv("PROJECT_DESCRIPTION", ""),
- "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
+ "page_title": "HTML Bundle to PDF",
+ "page_description": "Upload multiple HTML files, arrange the order, and export a single PDF instantly.",
+ "form": form,
+ "file_errors": file_errors,
+ "bundles": bundles,
+ "exports": exports,
}
return render(request, "core/index.html", context)
+
+
+def bundle_list(request):
+ bundles = HtmlBundle.objects.order_by("-created_at")
+ context = {
+ "page_title": "All Bundles",
+ "page_description": "Browse recent HTML bundles and download combined PDFs.",
+ "bundles": bundles,
+ }
+ return render(request, "core/bundle_list.html", context)
+
+
+def bundle_detail(request, bundle_id: int):
+ bundle = get_object_or_404(HtmlBundle, pk=bundle_id)
+ documents = bundle.documents.all()
+ exports = bundle.exports.order_by("-created_at")
+
+ if request.method == "POST" and request.POST.get("action") == "update_order":
+ updates = []
+ for document in documents:
+ field_name = f"order_{document.id}"
+ raw_value = request.POST.get(field_name)
+ if raw_value is None:
+ continue
+ try:
+ order_value = int(raw_value)
+ if order_value < 1:
+ raise ValueError
+ except ValueError:
+ messages.error(request, "Order values must be positive numbers.")
+ return redirect("bundle_detail", bundle_id=bundle.id)
+ document.order = order_value
+ updates.append(document)
+ if updates:
+ HtmlDocument.objects.bulk_update(updates, ["order"])
+ messages.success(request, "Order updated.")
+ return redirect("bundle_detail", bundle_id=bundle.id)
+
+ context = {
+ "page_title": bundle.title or "Untitled bundle",
+ "page_description": "Review your bundle and generate a combined PDF.",
+ "bundle": bundle,
+ "documents": documents,
+ "exports": exports,
+ }
+ return render(request, "core/bundle_detail.html", context)
+
+
+def bundle_download(request, bundle_id: int):
+ bundle = get_object_or_404(HtmlBundle, pk=bundle_id)
+ slug = slugify(bundle.title) or "bundle"
+ timestamp = timezone.now().strftime("%Y%m%d-%H%M")
+ file_name = f"{slug}-{timestamp}.pdf"
+ HtmlExport.objects.create(bundle=bundle, file_name=file_name)
+
+ pdf_buffer = _build_pdf(bundle)
+ response = HttpResponse(pdf_buffer.getvalue(), content_type="application/pdf")
+ response["Content-Disposition"] = f'attachment; filename="{file_name}"'
+ return response
+
+
+def export_download(request, export_id: int):
+ export = get_object_or_404(HtmlExport, pk=export_id)
+ pdf_buffer = _build_pdf(export.bundle)
+ response = HttpResponse(pdf_buffer.getvalue(), content_type="application/pdf")
+ response["Content-Disposition"] = f'attachment; filename="{export.file_name}"'
+ return response
diff --git a/requirements.txt b/requirements.txt
index e22994c..0f80449 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,3 +1,4 @@
Django==5.2.7
mysqlclient==2.2.7
python-dotenv==1.1.1
+reportlab==4.2.0
diff --git a/static/css/custom.css b/static/css/custom.css
index 925f6ed..f89242f 100644
--- a/static/css/custom.css
+++ b/static/css/custom.css
@@ -1,4 +1,210 @@
/* Custom styles for the application */
-body {
- font-family: system-ui, -apple-system, sans-serif;
+:root {
+ --ink-900: #0b1f24;
+ --ink-700: #1e3a40;
+ --ink-500: #456870;
+ --primary-600: #1f6f5b;
+ --primary-500: #2f8f76;
+ --accent-500: #f2c94c;
+ --accent-600: #e7b93a;
+ --coral-500: #e07a5f;
+ --sand-50: #f6f4ef;
+ --sand-100: #efeae2;
+ --surface-0: #ffffff;
+ --shadow-soft: 0 16px 40px rgba(11, 31, 36, 0.12);
+ --shadow-card: 0 12px 28px rgba(11, 31, 36, 0.1);
+ --radius-xl: 28px;
+ --radius-lg: 20px;
+ --radius-md: 14px;
+ --radius-sm: 10px;
+ --gradient-hero: linear-gradient(120deg, rgba(47, 143, 118, 0.15), rgba(242, 201, 76, 0.2), rgba(224, 122, 95, 0.12));
+}
+
+body {
+ font-family: "DM Sans", system-ui, -apple-system, sans-serif;
+ color: var(--ink-900);
+ background: radial-gradient(circle at top left, #f7f5f2 0%, #f4f0e8 45%, #f1e8dc 100%);
+ min-height: 100vh;
+}
+
+h1, h2, h3, h4, h5 {
+ font-family: "Space Grotesk", "DM Sans", sans-serif;
+ color: var(--ink-900);
+ letter-spacing: -0.02em;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+.app-shell {
+ background: var(--surface-0);
+ border-radius: var(--radius-xl);
+ box-shadow: var(--shadow-soft);
+ border: 1px solid rgba(30, 58, 64, 0.08);
+}
+
+.app-nav {
+ padding: 1.2rem 1.8rem;
+ border-bottom: 1px solid rgba(30, 58, 64, 0.08);
+ background: rgba(255, 255, 255, 0.9);
+ backdrop-filter: blur(10px);
+}
+
+.brand-mark {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.65rem;
+ font-weight: 700;
+ font-size: 1.1rem;
+}
+
+.brand-dot {
+ width: 14px;
+ height: 14px;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--primary-500), var(--accent-500));
+ box-shadow: 0 0 0 6px rgba(47, 143, 118, 0.15);
+}
+
+.hero {
+ padding: 4.5rem 2.5rem 3.5rem;
+ border-radius: var(--radius-xl);
+ position: relative;
+ overflow: hidden;
+ background: var(--gradient-hero);
+}
+
+.hero::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E%3Ccircle cx='24' cy='24' r='2.2' fill='rgba(30,58,64,0.15)'/%3E%3Ccircle cx='96' cy='58' r='1.8' fill='rgba(30,58,64,0.12)'/%3E%3Ccircle cx='54' cy='94' r='2' fill='rgba(30,58,64,0.1)'/%3E%3C/svg%3E");
+ opacity: 0.35;
+ pointer-events: none;
+}
+
+.hero-content {
+ position: relative;
+ z-index: 1;
+}
+
+.hero h1 {
+ font-size: clamp(2.6rem, 3vw + 1.2rem, 3.6rem);
+ line-height: 1.1;
+}
+
+.hero p {
+ font-size: 1.1rem;
+ color: var(--ink-700);
+}
+
+.primary-btn {
+ background: var(--primary-600);
+ border: none;
+ color: #ffffff;
+ font-weight: 600;
+ padding: 0.75rem 1.4rem;
+ border-radius: 999px;
+ box-shadow: 0 12px 18px rgba(31, 111, 91, 0.24);
+}
+
+.primary-btn:hover {
+ background: var(--primary-500);
+ color: #ffffff;
+}
+
+.ghost-btn {
+ border-radius: 999px;
+ border: 1px solid rgba(31, 111, 91, 0.25);
+ color: var(--primary-600);
+ padding: 0.7rem 1.2rem;
+ font-weight: 600;
+}
+
+.pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ padding: 0.35rem 0.8rem;
+ border-radius: 999px;
+ background: rgba(31, 111, 91, 0.12);
+ color: var(--ink-700);
+ font-size: 0.85rem;
+ font-weight: 600;
+}
+
+.workflow-card {
+ border-radius: var(--radius-lg);
+ border: 1px solid rgba(30, 58, 64, 0.1);
+ background: var(--surface-0);
+ box-shadow: var(--shadow-card);
+ padding: 1.8rem;
+}
+
+.section-title {
+ font-size: 1.5rem;
+}
+
+.file-preview {
+ background: var(--sand-50);
+ border: 1px dashed rgba(30, 58, 64, 0.15);
+ border-radius: var(--radius-md);
+ padding: 1rem;
+ font-size: 0.95rem;
+ color: var(--ink-500);
+}
+
+.stat-card {
+ padding: 1rem 1.2rem;
+ border-radius: var(--radius-md);
+ border: 1px solid rgba(30, 58, 64, 0.1);
+ background: rgba(255, 255, 255, 0.8);
+}
+
+.list-card {
+ border-radius: var(--radius-md);
+ border: 1px solid rgba(30, 58, 64, 0.08);
+ padding: 1.2rem;
+ background: var(--surface-0);
+}
+
+.muted-text {
+ color: var(--ink-500);
+}
+
+.order-input {
+ width: 72px;
+ border-radius: var(--radius-sm);
+ border: 1px solid rgba(30, 58, 64, 0.2);
+ padding: 0.4rem 0.5rem;
+}
+
+.app-footer {
+ color: var(--ink-500);
+ font-size: 0.9rem;
+}
+
+.badge-soft {
+ background: rgba(242, 201, 76, 0.2);
+ color: var(--ink-700);
+ border-radius: 999px;
+ padding: 0.35rem 0.75rem;
+ font-weight: 600;
+ font-size: 0.8rem;
+}
+
+.upload-drop {
+ border-radius: var(--radius-md);
+ border: 2px dashed rgba(47, 143, 118, 0.4);
+ padding: 1.2rem;
+ background: rgba(47, 143, 118, 0.06);
+}
+
+.alert-custom {
+ border-radius: var(--radius-md);
+ border: 1px solid rgba(31, 111, 91, 0.2);
+ background: rgba(31, 111, 91, 0.08);
+ color: var(--ink-700);
}