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 %} +
+
+ +
+ {% if messages %} +
+ {% for message in messages %} + + {% 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 +
+
+ {% csrf_token %} + +
+ {% for document in documents %} +
+
+
+
{{ document.original_name }}
+
{{ document.content_text|truncatechars:120 }}
+
+
+ + +
+
+
+ {% empty %} +
No documents in this bundle yet.
+ {% endfor %} +
+
+ + Changes affect the next PDF export. +
+
+
+
+
+
+

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

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" }} -

+
+
+ + +
+
+
+ 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 %} + + {% 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 +
+
+ {% csrf_token %} + {{ form.non_field_errors }} +
+
+ + {{ form.title }} + {% if form.title.errors %} +
{{ form.title.errors|join:", " }}
+ {% endif %} +
+
+ +
+ +
HTML only · We extract the text and combine it in order.
+
+ {% if file_errors %} +
{{ file_errors|join:", " }}
+ {% endif %} +
+
+
+ + You can reorder before exporting. +
+
+
+
+ +
+
+
+
+
+

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