diff --git a/config/__pycache__/__init__.cpython-311.pyc b/config/__pycache__/__init__.cpython-311.pyc
index 896bb4f..f0d36cc 100644
Binary files a/config/__pycache__/__init__.cpython-311.pyc and b/config/__pycache__/__init__.cpython-311.pyc differ
diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc
index d79d6a7..91c91d6 100644
Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ
diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc
index 8cf22af..ef63af5 100644
Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ
diff --git a/config/__pycache__/wsgi.cpython-311.pyc b/config/__pycache__/wsgi.cpython-311.pyc
index a1b4aa7..59f60c7 100644
Binary files a/config/__pycache__/wsgi.cpython-311.pyc and b/config/__pycache__/wsgi.cpython-311.pyc differ
diff --git a/config/settings.py b/config/settings.py
index 291d043..7b53c10 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", ""),
]
@@ -150,9 +151,13 @@ STATIC_URL = 'static/'
STATIC_ROOT = BASE_DIR / 'staticfiles'
STATICFILES_DIRS = [
- BASE_DIR / 'static',
- BASE_DIR / 'assets',
- BASE_DIR / 'node_modules',
+ directory
+ for directory in [
+ BASE_DIR / 'static',
+ BASE_DIR / 'assets',
+ BASE_DIR / 'node_modules',
+ ]
+ if directory.exists()
]
# Email
diff --git a/core/__pycache__/__init__.cpython-311.pyc b/core/__pycache__/__init__.cpython-311.pyc
index 3f553f6..d4d602b 100644
Binary files a/core/__pycache__/__init__.cpython-311.pyc and b/core/__pycache__/__init__.cpython-311.pyc differ
diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc
index 5e8987a..727fc13 100644
Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ
diff --git a/core/__pycache__/apps.cpython-311.pyc b/core/__pycache__/apps.cpython-311.pyc
index 2fa4a49..c656fb9 100644
Binary files a/core/__pycache__/apps.cpython-311.pyc and b/core/__pycache__/apps.cpython-311.pyc differ
diff --git a/core/__pycache__/context_processors.cpython-311.pyc b/core/__pycache__/context_processors.cpython-311.pyc
index 75bf223..558bb2a 100644
Binary files a/core/__pycache__/context_processors.cpython-311.pyc and b/core/__pycache__/context_processors.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..c208736
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..0c3eda3 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..c2315bf
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..886b598 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..c124bd6 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..5b2d8a2 100644
--- a/core/admin.py
+++ b/core/admin.py
@@ -1,3 +1,34 @@
from django.contrib import admin
-# Register your models here.
+from .models import Category, ContactInquiry, Product
+
+
+@admin.register(Category)
+class CategoryAdmin(admin.ModelAdmin):
+ list_display = ("name", "sort_order")
+ prepopulated_fields = {"slug": ("name",)}
+ search_fields = ("name",)
+ ordering = ("sort_order", "name")
+
+
+@admin.register(Product)
+class ProductAdmin(admin.ModelAdmin):
+ list_display = ("name", "category", "price_from", "is_featured", "is_active")
+ list_filter = ("category", "is_featured", "is_active")
+ list_editable = ("is_featured", "is_active")
+ prepopulated_fields = {"slug": ("name",)}
+ search_fields = ("name", "short_description", "description", "material")
+
+
+@admin.register(ContactInquiry)
+class ContactInquiryAdmin(admin.ModelAdmin):
+ list_display = ("name", "subject", "preferred_contact_method", "status", "created_at")
+ list_filter = ("preferred_contact_method", "status", "created_at")
+ list_editable = ("status",)
+ search_fields = ("name", "email", "phone", "subject", "message")
+ autocomplete_fields = ("product",)
+
+
+admin.site.site_header = "Thelen Atelier Admin"
+admin.site.site_title = "Thelen Atelier"
+admin.site.index_title = "Katalog & Anfragen verwalten"
diff --git a/core/context_processors.py b/core/context_processors.py
index 0bf87c3..72b929d 100644
--- a/core/context_processors.py
+++ b/core/context_processors.py
@@ -1,13 +1,22 @@
import os
+import re
import time
+
def project_context(request):
"""
Adds project-specific environment variables to the template context globally.
"""
+ store_phone_display = os.getenv("STORE_PHONE", "0214 41243")
+ store_phone_link = re.sub(r"[^\d+]", "", store_phone_display)
+
return {
"project_description": os.getenv("PROJECT_DESCRIPTION", ""),
"project_image_url": os.getenv("PROJECT_IMAGE_URL", ""),
- # Used for cache-busting static assets
+ "store_name": os.getenv("STORE_NAME", "Juwelier Thelen"),
+ "store_phone_display": store_phone_display,
+ "store_phone_link": store_phone_link,
+ "store_email": os.getenv("STORE_EMAIL", "info@juwelierthelen.de"),
+ "store_address": os.getenv("STORE_ADDRESS", "Wiesdorfer Platz 59, 51373 Leverkusen"),
"deployment_timestamp": int(time.time()),
}
diff --git a/core/forms.py b/core/forms.py
new file mode 100644
index 0000000..eeff850
--- /dev/null
+++ b/core/forms.py
@@ -0,0 +1,65 @@
+from django import forms
+
+from .models import ContactInquiry, Product
+
+
+class ContactInquiryForm(forms.ModelForm):
+ class Meta:
+ model = ContactInquiry
+ fields = [
+ "product",
+ "name",
+ "email",
+ "phone",
+ "preferred_contact_method",
+ "subject",
+ "message",
+ ]
+ widgets = {
+ "message": forms.Textarea(attrs={"rows": 5}),
+ }
+ labels = {
+ "product": "Produkt / Kollektion",
+ "name": "Ihr Name",
+ "email": "E-Mail-Adresse",
+ "phone": "Telefonnummer",
+ "preferred_contact_method": "Bevorzugte Kontaktart",
+ "subject": "Betreff",
+ "message": "Nachricht",
+ }
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields["product"].queryset = Product.objects.filter(is_active=True).select_related("category")
+ self.fields["product"].required = False
+ self.fields["product"].empty_label = "Allgemeine Beratung"
+
+ placeholders = {
+ "name": "z. B. Anna Becker",
+ "email": "anna@example.com",
+ "phone": "+49 ...",
+ "subject": "z. B. Beratung zu Trauringen",
+ "message": "Worum geht es? Teilen Sie uns kurz Ihre Wünsche mit.",
+ }
+ for name, field in self.fields.items():
+ widget = field.widget
+ css_class = "form-select" if isinstance(widget, forms.Select) else "form-control"
+ existing = widget.attrs.get("class", "")
+ widget.attrs["class"] = f"{existing} {css_class}".strip()
+ if name in placeholders:
+ widget.attrs["placeholder"] = placeholders[name]
+
+ def clean(self):
+ cleaned_data = super().clean()
+ preferred_contact_method = cleaned_data.get("preferred_contact_method")
+ phone = (cleaned_data.get("phone") or "").strip()
+ product = cleaned_data.get("product")
+ subject = (cleaned_data.get("subject") or "").strip()
+
+ if preferred_contact_method == ContactInquiry.PreferredContactMethod.PHONE and not phone:
+ self.add_error("phone", "Bitte geben Sie eine Telefonnummer an, wenn Sie einen Rückruf wünschen.")
+
+ if product and not subject:
+ cleaned_data["subject"] = f"Anfrage zu {product.name}"
+
+ return cleaned_data
diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py
new file mode 100644
index 0000000..b938c73
--- /dev/null
+++ b/core/migrations/0001_initial.py
@@ -0,0 +1,67 @@
+# Generated by Django 5.2.7 on 2026-04-06 09:11
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Category',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=120, unique=True)),
+ ('slug', models.SlugField(unique=True)),
+ ('description', models.TextField(blank=True)),
+ ('sort_order', models.PositiveIntegerField(default=0)),
+ ],
+ options={
+ 'ordering': ['sort_order', 'name'],
+ },
+ ),
+ migrations.CreateModel(
+ name='Product',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=160)),
+ ('slug', models.SlugField(unique=True)),
+ ('short_description', models.CharField(max_length=220)),
+ ('description', models.TextField()),
+ ('material', models.CharField(blank=True, max_length=120)),
+ ('price_from', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)),
+ ('collection_note', models.CharField(blank=True, max_length=180)),
+ ('is_featured', models.BooleanField(default=False)),
+ ('is_active', models.BooleanField(default=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='products', to='core.category')),
+ ],
+ options={
+ 'ordering': ['name'],
+ },
+ ),
+ migrations.CreateModel(
+ name='ContactInquiry',
+ fields=[
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', models.CharField(max_length=120)),
+ ('email', models.EmailField(max_length=254)),
+ ('phone', models.CharField(blank=True, max_length=40)),
+ ('subject', models.CharField(blank=True, max_length=160)),
+ ('message', models.TextField()),
+ ('preferred_contact_method', models.CharField(choices=[('phone', 'Telefon'), ('email', 'E-Mail')], default='phone', max_length=10)),
+ ('status', models.CharField(choices=[('new', 'Neu'), ('contacted', 'Kontaktiert'), ('closed', 'Abgeschlossen')], default='new', max_length=20)),
+ ('created_at', models.DateTimeField(auto_now_add=True)),
+ ('product', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inquiries', to='core.product')),
+ ],
+ options={
+ 'ordering': ['-created_at'],
+ },
+ ),
+ ]
diff --git a/core/migrations/0002_seed_catalog.py b/core/migrations/0002_seed_catalog.py
new file mode 100644
index 0000000..63667d6
--- /dev/null
+++ b/core/migrations/0002_seed_catalog.py
@@ -0,0 +1,120 @@
+from django.db import migrations
+
+
+def seed_catalog(apps, schema_editor):
+ Category = apps.get_model("core", "Category")
+ Product = apps.get_model("core", "Product")
+
+ categories = {
+ "trauringe": Category.objects.update_or_create(
+ slug="trauringe",
+ defaults={
+ "name": "Trauringe",
+ "description": "Beratung für Trauringe, Materialien, Oberflächen und Gravuren.",
+ "sort_order": 1,
+ },
+ )[0],
+ "schmuck": Category.objects.update_or_create(
+ slug="schmuck",
+ defaults={
+ "name": "Schmuck",
+ "description": "Ringe, Ohrschmuck und Perlenstücke für besondere Momente.",
+ "sort_order": 2,
+ },
+ )[0],
+ "uhren": Category.objects.update_or_create(
+ slug="uhren",
+ defaults={
+ "name": "Uhren",
+ "description": "Ausgewählte Uhren mit persönlicher Beratung und Service.",
+ "sort_order": 3,
+ },
+ )[0],
+ "service": Category.objects.update_or_create(
+ slug="service",
+ defaults={
+ "name": "Services",
+ "description": "Goldschmiedearbeiten, Reparaturen, Reinigung und Extras.",
+ "sort_order": 4,
+ },
+ )[0],
+ }
+
+ products = [
+ {
+ "slug": "signature-trauringe",
+ "category": categories["trauringe"],
+ "name": "Signature Trauringe",
+ "short_description": "Warme Goldtöne, klare Linien und Platz für Ihre persönliche Gravur.",
+ "description": "Diese Beispielkollektion steht für die Trauring-Beratung von Juwelier Thelen: verschiedene Legierungen, angenehme Profile und eine ruhige, persönliche Auswahl im Geschäft.",
+ "material": "Gelb- oder Roségold",
+ "price_from": "1290.00",
+ "collection_note": "Gravur und Größenanpassung möglich",
+ "is_featured": True,
+ },
+ {
+ "slug": "perlen-ohrschmuck-edit",
+ "category": categories["schmuck"],
+ "name": "Perlen & Ohrschmuck Edit",
+ "short_description": "Leichte, elegante Schmuckstücke mit klassischer Note und moderner Wirkung.",
+ "description": "Inspiriert von den Serviceleistungen rund um Perlen und Ohrlochstechen: eine kuratierte Auswahl an Ohrschmuck und Perlendesigns für Alltag und Anlass.",
+ "material": "Perlen, Gold, Silber",
+ "price_from": "149.00",
+ "collection_note": "Ideal für Geschenkberatung",
+ "is_featured": True,
+ },
+ {
+ "slug": "atelier-automatik-uhr",
+ "category": categories["uhren"],
+ "name": "Atelier Automatik Uhr",
+ "short_description": "Zeitloses Zifferblatt, ausgewogenes Gehäuse und Beratung zu Pflege & Revision.",
+ "description": "Die neue Katalogstrecke macht Uhrenpräsentation und Service greifbarer: ausgewählte Modelle, klare Preisindikationen und direkter Kontakt für Rückfragen.",
+ "material": "Edelstahl",
+ "price_from": "895.00",
+ "collection_note": "Revision und Armbandservice auf Anfrage",
+ "is_featured": True,
+ },
+ {
+ "slug": "goldschmiede-service-paket",
+ "category": categories["service"],
+ "name": "Goldschmiede Servicepaket",
+ "short_description": "Ringweiten, Reinigung, Umarbeitung und Reparatur – digital angefragt, persönlich umgesetzt.",
+ "description": "Auch ohne Checkout bleibt der Ablauf komplett: Leistung entdecken, Kontakt aufnehmen und den Auftrag im Geschäft besprechen. Dieses Beispiel zeigt den Service-Fokus der Website.",
+ "material": "Service & Beratung",
+ "price_from": None,
+ "collection_note": "Preis nach Sichtprüfung",
+ "is_featured": True,
+ },
+ ]
+
+ for item in products:
+ Product.objects.update_or_create(
+ slug=item["slug"],
+ defaults=item,
+ )
+
+
+def unseed_catalog(apps, schema_editor):
+ Product = apps.get_model("core", "Product")
+ Category = apps.get_model("core", "Category")
+
+ Product.objects.filter(
+ slug__in=[
+ "signature-trauringe",
+ "perlen-ohrschmuck-edit",
+ "atelier-automatik-uhr",
+ "goldschmiede-service-paket",
+ ]
+ ).delete()
+ Category.objects.filter(slug__in=["trauringe", "schmuck", "uhren", "service"]).delete()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("core", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.RunPython(seed_catalog, unseed_catalog),
+ ]
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..90add7c
Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ
diff --git a/core/migrations/__pycache__/0002_seed_catalog.cpython-311.pyc b/core/migrations/__pycache__/0002_seed_catalog.cpython-311.pyc
new file mode 100644
index 0000000..79eb48e
Binary files /dev/null and b/core/migrations/__pycache__/0002_seed_catalog.cpython-311.pyc differ
diff --git a/core/migrations/__pycache__/__init__.cpython-311.pyc b/core/migrations/__pycache__/__init__.cpython-311.pyc
index 7995815..39dfe21 100644
Binary files a/core/migrations/__pycache__/__init__.cpython-311.pyc and b/core/migrations/__pycache__/__init__.cpython-311.pyc differ
diff --git a/core/models.py b/core/models.py
index 71a8362..a13c606 100644
--- a/core/models.py
+++ b/core/models.py
@@ -1,3 +1,81 @@
+from decimal import Decimal
+
from django.db import models
-# Create your models here.
+
+class Category(models.Model):
+ name = models.CharField(max_length=120, unique=True)
+ slug = models.SlugField(unique=True)
+ description = models.TextField(blank=True)
+ sort_order = models.PositiveIntegerField(default=0)
+
+ class Meta:
+ ordering = ["sort_order", "name"]
+
+ def __str__(self):
+ return self.name
+
+
+class Product(models.Model):
+ category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name="products")
+ name = models.CharField(max_length=160)
+ slug = models.SlugField(unique=True)
+ short_description = models.CharField(max_length=220)
+ description = models.TextField()
+ material = models.CharField(max_length=120, blank=True)
+ price_from = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True)
+ collection_note = models.CharField(max_length=180, blank=True)
+ is_featured = models.BooleanField(default=False)
+ is_active = models.BooleanField(default=True)
+ created_at = models.DateTimeField(auto_now_add=True)
+ updated_at = models.DateTimeField(auto_now=True)
+
+ class Meta:
+ ordering = ["name"]
+
+ def __str__(self):
+ return self.name
+
+ @property
+ def price_label(self):
+ if self.price_from is None:
+ return "Preis auf Anfrage"
+ quantized = self.price_from.quantize(Decimal("1.00"))
+ return f"ab {quantized:,.2f} €".replace(",", "X").replace(".", ",").replace("X", ".")
+
+
+class ContactInquiry(models.Model):
+ class PreferredContactMethod(models.TextChoices):
+ PHONE = "phone", "Telefon"
+ EMAIL = "email", "E-Mail"
+
+ class Status(models.TextChoices):
+ NEW = "new", "Neu"
+ CONTACTED = "contacted", "Kontaktiert"
+ CLOSED = "closed", "Abgeschlossen"
+
+ product = models.ForeignKey(
+ Product,
+ on_delete=models.SET_NULL,
+ related_name="inquiries",
+ blank=True,
+ null=True,
+ )
+ name = models.CharField(max_length=120)
+ email = models.EmailField()
+ phone = models.CharField(max_length=40, blank=True)
+ subject = models.CharField(max_length=160, blank=True)
+ message = models.TextField()
+ preferred_contact_method = models.CharField(
+ max_length=10,
+ choices=PreferredContactMethod.choices,
+ default=PreferredContactMethod.PHONE,
+ )
+ status = models.CharField(max_length=20, choices=Status.choices, default=Status.NEW)
+ created_at = models.DateTimeField(auto_now_add=True)
+
+ class Meta:
+ ordering = ["-created_at"]
+
+ def __str__(self):
+ return f"{self.name} – {self.subject or 'Allgemeine Anfrage'}"
diff --git a/core/templates/base.html b/core/templates/base.html
index 1e7e5fb..7ee44b3 100644
--- a/core/templates/base.html
+++ b/core/templates/base.html
@@ -1,25 +1,81 @@
+{% load static %}
-
-
+
- {% block title %}Knowledge Base{% endblock %}
- {% if project_description %}
-
-
-
- {% endif %}
+
+ {{ meta_title|default:store_name }}
+
{% if project_image_url %}
{% endif %}
- {% load static %}
+
+
+
+
{% block head %}{% endblock %}
-
- {% block content %}{% endblock %}
-
+
+
+
+ {% block content %}{% endblock %}
+
+
+
+
+
+
+
diff --git a/core/templates/core/catalog.html b/core/templates/core/catalog.html
new file mode 100644
index 0000000..41bc786
--- /dev/null
+++ b/core/templates/core/catalog.html
@@ -0,0 +1,73 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
+
Katalog
+
Entdecken Sie Schmuck, Uhren und Trauringe.
+
Filterbar, fokussiert und bereit für die direkte Kontaktaufnahme – ohne komplizierten Checkout.
+
+
+
+
+
+
+
+
+ {% for product in products %}
+
+
+
+ {{ product.category.name }}
+
+
+
{{ product.category.name }}
+
{{ product.name }}
+
{{ product.short_description }}
+
+ {{ product.price_label }}
+ {% if product.material %}{{ product.material }} {% endif %}
+
+
+
+
+
+ {% empty %}
+
+
+
Keine Treffer gefunden
+
Passen Sie die Filter an oder nehmen Sie direkt Kontakt auf, damit wir Ihnen passende Stücke empfehlen können.
+
+
+
+ {% endfor %}
+
+
+
+{% endblock %}
diff --git a/core/templates/core/contact.html b/core/templates/core/contact.html
new file mode 100644
index 0000000..2174c82
--- /dev/null
+++ b/core/templates/core/contact.html
@@ -0,0 +1,72 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
+
Kontakt
+
Schnell anfragen, persönlich beraten lassen.
+
Die Anfrage wird gespeichert, im Admin sichtbar gemacht und auf Wunsch zusätzlich per E-Mail versendet.
+
+
+
+
+{% endblock %}
diff --git a/core/templates/core/contact_success.html b/core/templates/core/contact_success.html
new file mode 100644
index 0000000..6338cd9
--- /dev/null
+++ b/core/templates/core/contact_success.html
@@ -0,0 +1,24 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
+
+
Vielen Dank
+
Ihre Anfrage ist eingegangen.
+
Das Team von {{ store_name }} meldet sich so schnell wie möglich bei Ihnen.
+ {% if inquiry %}
+
+
Kontakt: {{ inquiry.name }}
+
Rückmeldung via: {{ inquiry.get_preferred_contact_method_display }}
+
Produkt: {% if inquiry.product %}{{ inquiry.product.name }}{% else %}Allgemeine Beratung{% endif %}
+
+ {% endif %}
+
+
+
+
+{% endblock %}
diff --git a/core/templates/core/index.html b/core/templates/core/index.html
index faec813..942ee3d 100644
--- a/core/templates/core/index.html
+++ b/core/templates/core/index.html
@@ -1,145 +1,171 @@
{% extends "base.html" %}
-
-{% block title %}{{ project_name }}{% endblock %}
-
-{% block head %}
-
-
-
-
-{% endblock %}
+{% load static %}
{% block content %}
-
-
-
Analyzing your requirements and generating your app…
-
-
Loading…
+
+
+
+
+
Seit über 80 Jahren Vertrauen, Qualität und persönliche Beratung
+
Ein moderner Schmuckkatalog für Trauringe, Uhren und Goldschmiedeservice.
+
Entdecken Sie ausgewählte Kollektionen, lernen Sie unsere Services kennen und wechseln Sie mit einem Klick vom Stöbern zur persönlichen Beratung – telefonisch oder per E-Mail.
+
+
+
+
+
+
+
+
Store Snapshot
+
+
+ 80+
+ Jahre Erfahrung
+
+
+ 4
+ klare Katalogbereiche
+
+
+ 1
+ zentrale Anfrage pro Produkt
+
+
+
+
Wiesdorfer Platz 59
+
51373 Leverkusen · Beratung per Telefon und E-Mail
+
+
+
-
AppWizzy AI is collecting your requirements and applying the first changes.
-
This page will refresh automatically as the plan is implemented.
-
- Runtime: Django {{ django_version }} · Python {{ python_version }}
- — UTC {{ current_time|date:"Y-m-d H:i:s" }}
-
-
-
- Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
-
-{% endblock %}
\ No newline at end of file
+
+
+
+
+
+ {% for category in categories %}
+
+ {% endfor %}
+
+
+
+
+
+
+
+
Leistungen
+
Die wichtigsten Inhalte der bisherigen Website – jetzt klarer, schneller, moderner.
+
Die neue Startseite bündelt Trauringe, Schmuck, Uhren und Serviceleistungen in einem ruhigen, leicht navigierbaren Ablauf.
+
+
+ {% for item in service_highlights %}
+
+
+ {{ forloop.counter|stringformat:"02d" }}
+ {{ item.title }}
+ {{ item.text }}
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+ Auswahl
+
Featured Katalog
+
+
Alle Produkte ansehen
+
+
+ {% for product in featured_products %}
+
+
+
+ {{ product.category.name }}
+
+
+
+
+
{{ product.category.name }}
+
{{ product.name }}
+
+
{{ product.price_label }}
+
+
{{ product.short_description }}
+
Details & Anfrage
+
+
+
+ {% empty %}
+
+
+
Der Katalog ist bereit.
+
Pflegen Sie jetzt Produkte im Admin ein, damit die Startseite automatisch die ersten Highlights zeigt.
+
Produkte im Admin anlegen
+
+
+ {% endfor %}
+
+
+
+
+
+
+
+
+ Ablauf
+
Vom Produkt zur persönlichen Beratung in drei einfachen Schritten.
+
+
+ {% for step in process_steps %}
+
+
+ {{ step.step }}
+ {{ step.title }}
+ {{ step.text }}
+
+
+ {% endfor %}
+
+
+
+
+
+
+{% endblock %}
diff --git a/core/templates/core/product_detail.html b/core/templates/core/product_detail.html
new file mode 100644
index 0000000..16d60a8
--- /dev/null
+++ b/core/templates/core/product_detail.html
@@ -0,0 +1,76 @@
+{% extends "base.html" %}
+
+{% block content %}
+
+
+
← Zurück zum Katalog
+
+
+
{{ product.category.name }}
+
{{ product.name }}
+
{{ product.short_description }}
+
+
+
+
{{ product.price_label }}
+ {% if product.material %}
Material · {{ product.material }}
{% endif %}
+ {% if product.collection_note %}
{{ product.collection_note }}
{% endif %}
+
+
+
+
+
+
+
+
+
+
+
+
+ Über dieses Stück
+ {{ product.description }}
+
+
+
+
+ Warum diese neue Produktseite besser funktioniert
+
+ Klare Preis- und Materialhinweise.
+ Sofort sichtbare Kontaktoptionen per Telefon und E-Mail.
+ Direkte Verbindung in den Admin-gestützten Anfrage-Workflow.
+
+
+
+
+
+
+
+{% if related_products %}
+
+
+
+ Passend dazu
+
Weitere Stücke aus {{ product.category.name }}
+
+
+ {% for item in related_products %}
+
+
+ {{ item.category.name }}
+
+
{{ item.category.name }}
+
{{ item.name }}
+
{{ item.short_description }}
+
Details ansehen
+
+
+
+ {% endfor %}
+
+
+
+{% endif %}
+{% endblock %}
diff --git a/core/tests.py b/core/tests.py
index 7ce503c..931b5ce 100644
--- a/core/tests.py
+++ b/core/tests.py
@@ -1,3 +1,47 @@
from django.test import TestCase
+from django.urls import reverse
-# Create your tests here.
+from .models import Category, ContactInquiry, Product
+
+
+class CatalogFlowTests(TestCase):
+ def setUp(self):
+ self.category = Category.objects.create(
+ name="Trauringe",
+ slug="trauringe",
+ description="Individuelle Ringe und Beratung.",
+ sort_order=1,
+ )
+ self.product = Product.objects.create(
+ category=self.category,
+ name="Signature Trauring",
+ slug="signature-trauring",
+ short_description="Ein klassischer Ring mit warmem Goldton.",
+ description="Ausgewählte Trauringe mit Gravuroption und persönlicher Beratung.",
+ material="Gold",
+ price_from="1290.00",
+ is_featured=True,
+ )
+
+ def test_homepage_uses_catalog_content(self):
+ response = self.client.get(reverse("home"))
+ self.assertContains(response, self.product.name)
+ self.assertContains(response, self.category.name)
+
+ def test_contact_form_creates_inquiry(self):
+ response = self.client.post(
+ reverse("contact"),
+ {
+ "product": self.product.pk,
+ "name": "Anna Becker",
+ "email": "anna@example.com",
+ "phone": "+49 123",
+ "preferred_contact_method": "phone",
+ "subject": "Beratung",
+ "message": "Ich möchte einen Termin für Trauringe.",
+ },
+ )
+ self.assertRedirects(response, reverse("contact_success"))
+ self.assertEqual(ContactInquiry.objects.count(), 1)
+ inquiry = ContactInquiry.objects.first()
+ self.assertEqual(inquiry.product, self.product)
diff --git a/core/urls.py b/core/urls.py
index 6299e3d..442aad9 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 catalog, contact, contact_success, home, product_detail
urlpatterns = [
path("", home, name="home"),
+ path("katalog/", catalog, name="catalog"),
+ path("katalog/
/", product_detail, name="product_detail"),
+ path("kontakt/", contact, name="contact"),
+ path("kontakt/danke/", contact_success, name="contact_success"),
]
diff --git a/core/views.py b/core/views.py
index c9aed12..c2fc1ff 100644
--- a/core/views.py
+++ b/core/views.py
@@ -1,25 +1,186 @@
-import os
-import platform
+from django.conf import settings
+from django.core.mail import send_mail
+from django.db.models import Count, Q
+from django.shortcuts import get_object_or_404, redirect, render
-from django import get_version as django_version
-from django.shortcuts import render
-from django.utils import timezone
+from .forms import ContactInquiryForm
+from .models import Category, ContactInquiry, Product
+
+
+SERVICE_HIGHLIGHTS = [
+ {
+ "title": "Trauringe & Beratung",
+ "text": "Individuelle Beratung für Trauringe, Materialien und Gravuren – persönlich im Geschäft oder per Anfrage.",
+ },
+ {
+ "title": "Goldschmiedeservice",
+ "text": "Ringweitenänderung, Reinigung, Umarbeitung und Reparaturen mit viel Erfahrung und Fingerspitzengefühl.",
+ },
+ {
+ "title": "Uhren & Revision",
+ "text": "Ausgewählte Uhren und fachkundige Unterstützung bei Pflege, Wartung und Reparatur wertvoller Zeitmesser.",
+ },
+ {
+ "title": "Perlen, Ohrschmuck & Extras",
+ "text": "Perlkettenservice, Ohrlochstechen mit STUDEX-System sowie Geschenkgutscheine für besondere Anlässe.",
+ },
+]
+
+
+PROCESS_STEPS = [
+ {
+ "step": "01",
+ "title": "Kollektion entdecken",
+ "text": "Filtern Sie die Kollektion nach Kategorie und lassen Sie sich inspirieren.",
+ },
+ {
+ "step": "02",
+ "title": "Per Telefon oder E-Mail anfragen",
+ "text": "Zu jedem Produkt gelangen Sie in wenigen Klicks zur passenden Kontaktmöglichkeit.",
+ },
+ {
+ "step": "03",
+ "title": "Persönliche Beratung im Store",
+ "text": "Im Geschäft in Leverkusen klärt das Team Details, Anpassungen und Verfügbarkeit.",
+ },
+]
+
+
+def _catalog_categories():
+ return Category.objects.annotate(
+ product_total=Count("products", filter=Q(products__is_active=True))
+ )
+
+
+def _send_inquiry_notification(inquiry):
+ if not settings.CONTACT_EMAIL_TO:
+ return
+
+ subject = inquiry.subject or "Neue Anfrage über die Website"
+ lines = [
+ f"Name: {inquiry.name}",
+ f"E-Mail: {inquiry.email}",
+ f"Telefon: {inquiry.phone or '-'}",
+ f"Bevorzugte Kontaktart: {inquiry.get_preferred_contact_method_display()}",
+ f"Produkt: {inquiry.product.name if inquiry.product else 'Allgemeine Beratung'}",
+ "",
+ inquiry.message,
+ ]
+
+ try:
+ send_mail(
+ subject=f"Website-Anfrage: {subject}",
+ message="\n".join(lines),
+ from_email=settings.DEFAULT_FROM_EMAIL,
+ recipient_list=settings.CONTACT_EMAIL_TO,
+ fail_silently=True,
+ reply_to=[inquiry.email],
+ )
+ except Exception:
+ return
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()
+ featured_products = Product.objects.filter(is_active=True, is_featured=True).select_related("category")[:4]
+ categories = _catalog_categories().filter(product_total__gt=0)
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", ""),
+ "meta_title": "Juwelier Thelen | Moderner Schmuck- & Trauringkatalog",
+ "meta_description": "Moderner Katalog für Trauringe, Schmuck, Uhren und Goldschmiedeservice – mit schneller Kontaktaufnahme per Telefon oder E-Mail.",
+ "featured_products": featured_products,
+ "categories": categories,
+ "service_highlights": SERVICE_HIGHLIGHTS,
+ "process_steps": PROCESS_STEPS,
}
return render(request, "core/index.html", context)
+
+
+def catalog(request):
+ selected_category = request.GET.get("category", "").strip()
+ query = request.GET.get("q", "").strip()
+
+ products = Product.objects.filter(is_active=True).select_related("category")
+ if selected_category:
+ products = products.filter(category__slug=selected_category)
+ if query:
+ products = products.filter(
+ Q(name__icontains=query)
+ | Q(short_description__icontains=query)
+ | Q(description__icontains=query)
+ | Q(material__icontains=query)
+ | Q(category__name__icontains=query)
+ )
+
+ context = {
+ "meta_title": "Katalog | Juwelier Thelen",
+ "meta_description": "Entdecken Sie Trauringe, Schmuck und Uhren – klar gefiltert und bereit für eine persönliche Beratung.",
+ "products": products,
+ "categories": _catalog_categories(),
+ "selected_category": selected_category,
+ "query": query,
+ }
+ return render(request, "core/catalog.html", context)
+
+
+def product_detail(request, slug):
+ product = get_object_or_404(
+ Product.objects.filter(is_active=True).select_related("category"),
+ slug=slug,
+ )
+ related_products = (
+ Product.objects.filter(is_active=True, category=product.category)
+ .exclude(pk=product.pk)
+ .select_related("category")[:3]
+ )
+
+ context = {
+ "meta_title": f"{product.name} | Juwelier Thelen",
+ "meta_description": product.short_description,
+ "product": product,
+ "related_products": related_products,
+ }
+ return render(request, "core/product_detail.html", context)
+
+
+def contact(request):
+ selected_product = None
+ product_slug = request.GET.get("product", "").strip()
+
+ if product_slug:
+ selected_product = Product.objects.filter(is_active=True, slug=product_slug).first()
+
+ if request.method == "POST":
+ form = ContactInquiryForm(request.POST)
+ if form.is_valid():
+ inquiry = form.save()
+ _send_inquiry_notification(inquiry)
+ request.session["contact_inquiry_id"] = inquiry.pk
+ return redirect("contact_success")
+ else:
+ initial = {}
+ if selected_product:
+ initial["product"] = selected_product
+ initial["subject"] = f"Anfrage zu {selected_product.name}"
+ form = ContactInquiryForm(initial=initial)
+
+ context = {
+ "meta_title": "Kontakt | Juwelier Thelen",
+ "meta_description": "Fragen zu Trauringen, Schmuck, Uhren oder Serviceleistungen? Kontaktieren Sie Juwelier Thelen per Telefon oder E-Mail.",
+ "form": form,
+ "selected_product": selected_product,
+ }
+ return render(request, "core/contact.html", context)
+
+
+def contact_success(request):
+ inquiry_id = request.session.get("contact_inquiry_id")
+ inquiry = None
+ if inquiry_id:
+ inquiry = ContactInquiry.objects.filter(pk=inquiry_id).select_related("product").first()
+
+ context = {
+ "meta_title": "Anfrage erhalten | Juwelier Thelen",
+ "meta_description": "Vielen Dank für Ihre Anfrage. Das Team meldet sich zeitnah telefonisch oder per E-Mail zurück.",
+ "inquiry": inquiry,
+ }
+ return render(request, "core/contact_success.html", context)
diff --git a/static/css/custom.css b/static/css/custom.css
index 925f6ed..ce06a33 100644
--- a/static/css/custom.css
+++ b/static/css/custom.css
@@ -1,4 +1,632 @@
-/* Custom styles for the application */
-body {
- font-family: system-ui, -apple-system, sans-serif;
+/* Brand system */
+:root {
+ --color-bg: #f5efe7;
+ --color-bg-soft: #ece4d7;
+ --color-surface: rgba(255, 250, 244, 0.82);
+ --color-surface-strong: #fffaf4;
+ --color-ink: #171616;
+ --color-muted: #5f605f;
+ --color-brand: #b08a52;
+ --color-brand-dark: #8d6a37;
+ --color-accent: #1d4339;
+ --color-accent-soft: #dce9e3;
+ --color-line: rgba(23, 22, 22, 0.08);
+ --shadow-soft: 0 24px 70px rgba(28, 24, 19, 0.12);
+ --shadow-card: 0 18px 40px rgba(43, 31, 18, 0.1);
+ --radius-xl: 32px;
+ --radius-lg: 24px;
+ --radius-md: 18px;
+ --radius-sm: 14px;
+ --space-section: clamp(4.5rem, 7vw, 7rem);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ margin: 0;
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
+ color: var(--color-ink);
+ background:
+ radial-gradient(circle at top left, rgba(176, 138, 82, 0.18), transparent 30%),
+ radial-gradient(circle at 85% 15%, rgba(29, 67, 57, 0.14), transparent 26%),
+ linear-gradient(180deg, #fbf7f1 0%, var(--color-bg) 46%, #efe8dc 100%);
+ line-height: 1.6;
+}
+
+body::before,
+body::after {
+ content: "";
+ position: fixed;
+ inset: auto;
+ border-radius: 50%;
+ filter: blur(10px);
+ pointer-events: none;
+ z-index: -1;
+}
+
+body::before {
+ width: 18rem;
+ height: 18rem;
+ top: 6rem;
+ right: 4vw;
+ background: rgba(176, 138, 82, 0.12);
+}
+
+body::after {
+ width: 12rem;
+ height: 12rem;
+ bottom: 10rem;
+ left: 3vw;
+ background: rgba(29, 67, 57, 0.1);
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+.navbar-brand strong,
+.footer-title {
+ font-family: 'Cormorant Garamond', Georgia, serif;
+ letter-spacing: -0.02em;
+ line-height: 1.05;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+a:hover {
+ color: var(--color-brand-dark);
+}
+
+.site-header {
+ background: rgba(251, 247, 241, 0.78);
+ backdrop-filter: blur(24px);
+ border-bottom: 1px solid rgba(23, 22, 22, 0.05);
+}
+
+.navbar-brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.85rem;
+ font-size: 1.05rem;
+}
+
+.navbar-brand small {
+ display: block;
+ color: var(--color-muted);
+ font-size: 0.78rem;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+.brand-mark {
+ width: 2.6rem;
+ height: 2.6rem;
+ display: inline-grid;
+ place-items: center;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--color-brand), #ddc08b);
+ color: white;
+ font-family: 'Cormorant Garamond', Georgia, serif;
+ font-size: 1.45rem;
+ box-shadow: 0 12px 30px rgba(176, 138, 82, 0.3);
+}
+
+.nav-link {
+ color: var(--color-ink);
+ font-weight: 500;
+ padding: 0.75rem 1rem !important;
+}
+
+.nav-link:hover,
+.nav-link:focus {
+ color: var(--color-brand-dark);
+}
+
+.section-shell {
+ padding: var(--space-section) 0;
+}
+
+.page-hero-compact {
+ padding-bottom: 2.5rem;
+}
+
+.hero-section {
+ position: relative;
+ overflow: hidden;
+}
+
+.hero-title,
+.page-title {
+ font-size: clamp(3rem, 5vw, 5.4rem);
+ margin-bottom: 1.4rem;
+}
+
+.hero-copy,
+.page-copy,
+.section-heading p,
+.product-body p,
+.service-card p,
+.process-card p,
+.contact-info p,
+.detail-copy p,
+.detail-side p,
+.form-note,
+.footer-title + p {
+ color: var(--color-muted);
+ font-size: 1.05rem;
+}
+
+.hero-copy,
+.page-copy {
+ max-width: 44rem;
+}
+
+.eyebrow {
+ display: inline-block;
+ margin-bottom: 1rem;
+ color: var(--color-accent);
+ text-transform: uppercase;
+ letter-spacing: 0.18em;
+ font-size: 0.77rem;
+ font-weight: 700;
+}
+
+.glass-panel,
+.product-card,
+.service-card,
+.category-card,
+.empty-state,
+.process-shell,
+.success-card {
+ background: linear-gradient(180deg, rgba(255, 250, 244, 0.9), rgba(255, 255, 255, 0.72));
+ border: 1px solid rgba(255, 255, 255, 0.7);
+ box-shadow: var(--shadow-soft);
+ backdrop-filter: blur(18px);
+ border-radius: var(--radius-xl);
+}
+
+.hero-search,
+.filter-panel,
+.form-panel {
+ padding: 1.35rem;
+}
+
+.hero-actions {
+ margin-top: 1.8rem;
+}
+
+.hero-showcase {
+ padding: 2rem;
+ min-height: 27rem;
+ position: relative;
+ overflow: hidden;
+}
+
+.hero-orb {
+ position: absolute;
+ border-radius: 50%;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(176, 138, 82, 0.12));
+ border: 1px solid rgba(176, 138, 82, 0.16);
+}
+
+.hero-orb-lg {
+ width: 13rem;
+ height: 13rem;
+ top: 2rem;
+ right: -1rem;
+}
+
+.hero-orb-sm {
+ width: 5rem;
+ height: 5rem;
+ left: 1rem;
+ bottom: 2rem;
+}
+
+.stat-grid {
+ position: relative;
+ z-index: 1;
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 1rem;
+}
+
+.stat-card {
+ padding: 1.4rem;
+ border-radius: var(--radius-md);
+ background: rgba(255, 255, 255, 0.72);
+ border: 1px solid rgba(23, 22, 22, 0.05);
+ box-shadow: var(--shadow-card);
+}
+
+.stat-card strong {
+ display: block;
+ font-family: 'Cormorant Garamond', Georgia, serif;
+ font-size: 2.4rem;
+}
+
+.stat-card span,
+.showcase-note,
+.product-category,
+.detail-label {
+ color: var(--color-muted);
+}
+
+.showcase-note {
+ position: absolute;
+ left: 2rem;
+ right: 2rem;
+ bottom: 2rem;
+ padding: 1.1rem 1.2rem;
+ border-radius: var(--radius-md);
+ background: rgba(29, 67, 57, 0.08);
+}
+
+.category-card,
+.service-card,
+.product-card {
+ padding: 1.6rem;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ transition: transform 0.25s ease, box-shadow 0.25s ease;
+}
+
+.category-card:hover,
+.service-card:hover,
+.product-card:hover,
+.contact-banner:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 28px 70px rgba(28, 24, 19, 0.16);
+}
+
+.category-chip,
+.product-visual span,
+.process-step {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 999px;
+ padding: 0.35rem 0.75rem;
+ font-size: 0.78rem;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+}
+
+.category-chip,
+.process-step {
+ background: rgba(29, 67, 57, 0.09);
+ color: var(--color-accent);
+}
+
+.product-visual {
+ min-height: 12rem;
+ border-radius: var(--radius-lg);
+ margin-bottom: 1.25rem;
+ background:
+ radial-gradient(circle at 28% 28%, rgba(255, 255, 255, 0.95), transparent 36%),
+ linear-gradient(140deg, rgba(176, 138, 82, 0.22), rgba(29, 67, 57, 0.18));
+ position: relative;
+ overflow: hidden;
+}
+
+.product-visual::before,
+.product-visual::after {
+ content: "";
+ position: absolute;
+ border-radius: 999px;
+ border: 1px solid rgba(255, 255, 255, 0.75);
+}
+
+.product-visual::before {
+ width: 7rem;
+ height: 7rem;
+ right: 1.25rem;
+ top: 1.25rem;
+}
+
+.product-visual::after {
+ width: 3.6rem;
+ height: 3.6rem;
+ left: 1.4rem;
+ bottom: 1.1rem;
+}
+
+.product-visual span {
+ position: absolute;
+ left: 1rem;
+ top: 1rem;
+ background: rgba(255, 250, 244, 0.9);
+ color: var(--color-accent);
+}
+
+.product-body {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+}
+
+.product-body h2,
+.product-body h3,
+.service-card h3,
+.category-card h2,
+.process-card h3,
+.contact-banner h2,
+.contact-info h2,
+.detail-copy h2,
+.detail-side h2,
+.form-panel h2 {
+ font-size: clamp(1.8rem, 2vw, 2.4rem);
+ margin-bottom: 0.9rem;
+}
+
+.product-category {
+ margin-bottom: 0.25rem;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ font-size: 0.75rem;
+ font-weight: 700;
+}
+
+.product-price,
+.detail-price {
+ color: var(--color-brand-dark);
+ font-size: 1.1rem;
+}
+
+.product-meta {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ margin-top: auto;
+ padding-top: 1rem;
+ color: var(--color-muted);
+}
+
+.text-link,
+.back-link {
+ color: var(--color-accent);
+ font-weight: 700;
+}
+
+.text-link:hover,
+.back-link:hover {
+ color: var(--color-brand-dark);
+}
+
+.section-heading {
+ max-width: 42rem;
+ margin-bottom: 2rem;
+}
+
+.section-heading h2,
+.footer-title {
+ font-size: clamp(2.4rem, 3vw, 3.6rem);
+ margin-bottom: 0.8rem;
+}
+
+.service-icon {
+ width: 3rem;
+ height: 3rem;
+ display: inline-grid;
+ place-items: center;
+ border-radius: 50%;
+ margin-bottom: 1rem;
+ background: rgba(176, 138, 82, 0.12);
+ color: var(--color-brand-dark);
+ font-weight: 700;
+}
+
+.process-shell,
+.contact-banner,
+.success-card,
+.detail-card,
+.detail-copy,
+.detail-side,
+.contact-info {
+ padding: 2rem;
+}
+
+.process-card {
+ padding: 1.2rem;
+ border-radius: var(--radius-md);
+ background: rgba(255, 255, 255, 0.55);
+ border: 1px solid rgba(23, 22, 22, 0.05);
+ height: 100%;
+}
+
+.contact-banner {
+ padding: 2rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 2rem;
+}
+
+.contact-method {
+ margin-bottom: 1.5rem;
+}
+
+.contact-method span,
+.footer-heading {
+ display: block;
+ margin-bottom: 0.35rem;
+ color: var(--color-muted);
+ font-size: 0.84rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+}
+
+.contact-method a,
+.site-footer a {
+ font-size: 1.05rem;
+ font-weight: 600;
+}
+
+.selected-product-note {
+ margin-top: 2rem;
+ padding: 1.25rem;
+ border-radius: var(--radius-md);
+ background: rgba(29, 67, 57, 0.08);
+}
+
+.site-footer {
+ border-top: 1px solid rgba(23, 22, 22, 0.06);
+ background: rgba(255, 250, 244, 0.65);
+ backdrop-filter: blur(18px);
+}
+
+.btn {
+ border-radius: 999px;
+ padding: 0.9rem 1.45rem;
+ font-weight: 700;
+ letter-spacing: 0.01em;
+ border-width: 1px;
+ transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
+}
+
+.btn:hover,
+.btn:focus {
+ transform: translateY(-1px);
+}
+
+.btn-brand {
+ color: #fff;
+ background: linear-gradient(135deg, var(--color-brand-dark), var(--color-brand));
+ border-color: transparent;
+ box-shadow: 0 16px 32px rgba(176, 138, 82, 0.28);
+}
+
+.btn-brand:hover,
+.btn-brand:focus {
+ color: #fff;
+ background: linear-gradient(135deg, #7a5b2f, var(--color-brand-dark));
+}
+
+.btn-ghost {
+ color: var(--color-ink);
+ background: rgba(255, 255, 255, 0.58);
+ border-color: rgba(23, 22, 22, 0.08);
+}
+
+.btn-ghost:hover,
+.btn-ghost:focus {
+ background: rgba(255, 255, 255, 0.88);
+ border-color: rgba(23, 22, 22, 0.14);
+}
+
+.form-control,
+.form-select {
+ min-height: 3.4rem;
+ border-radius: 1rem;
+ border: 1px solid rgba(23, 22, 22, 0.1);
+ background: rgba(255, 255, 255, 0.92);
+ color: var(--color-ink);
+ padding: 0.85rem 1rem;
+}
+
+textarea.form-control {
+ min-height: 9rem;
+}
+
+.form-control:focus,
+.form-select:focus,
+.btn:focus,
+.nav-link:focus,
+a:focus {
+ outline: none;
+ border-color: rgba(176, 138, 82, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(176, 138, 82, 0.16);
+}
+
+.field-error {
+ color: #9b2d2d;
+ font-size: 0.88rem;
+ margin-top: 0.45rem;
+}
+
+.empty-state,
+.success-card {
+ text-align: center;
+ padding: 3rem 2rem;
+}
+
+.success-card {
+ max-width: 48rem;
+}
+
+.success-summary {
+ margin-top: 1.5rem;
+ padding: 1.2rem;
+ border-radius: var(--radius-md);
+ background: rgba(29, 67, 57, 0.08);
+}
+
+.filter-panel {
+ position: relative;
+ z-index: 2;
+}
+
+.detail-list {
+ padding-left: 1.1rem;
+ margin: 0;
+}
+
+.detail-list li {
+ margin-bottom: 0.8rem;
+ color: var(--color-muted);
+}
+
+.back-link {
+ display: inline-block;
+ margin-bottom: 1.25rem;
+}
+
+@media (max-width: 991.98px) {
+ .hero-showcase {
+ min-height: auto;
+ }
+
+ .contact-banner {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .section-shell {
+ padding: 4rem 0;
+ }
+
+ .hero-title,
+ .page-title {
+ font-size: 2.6rem;
+ }
+
+ .glass-panel,
+ .product-card,
+ .service-card,
+ .category-card,
+ .contact-banner,
+ .success-card,
+ .process-shell,
+ .detail-card,
+ .detail-copy,
+ .detail-side,
+ .contact-info {
+ border-radius: 1.5rem;
+ }
}
diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css
index 108056f..ce06a33 100644
--- a/staticfiles/css/custom.css
+++ b/staticfiles/css/custom.css
@@ -1,21 +1,632 @@
-
+/* Brand system */
: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);
+ --color-bg: #f5efe7;
+ --color-bg-soft: #ece4d7;
+ --color-surface: rgba(255, 250, 244, 0.82);
+ --color-surface-strong: #fffaf4;
+ --color-ink: #171616;
+ --color-muted: #5f605f;
+ --color-brand: #b08a52;
+ --color-brand-dark: #8d6a37;
+ --color-accent: #1d4339;
+ --color-accent-soft: #dce9e3;
+ --color-line: rgba(23, 22, 22, 0.08);
+ --shadow-soft: 0 24px 70px rgba(28, 24, 19, 0.12);
+ --shadow-card: 0 18px 40px rgba(43, 31, 18, 0.1);
+ --radius-xl: 32px;
+ --radius-lg: 24px;
+ --radius-md: 18px;
+ --radius-sm: 14px;
+ --space-section: clamp(4.5rem, 7vw, 7rem);
}
+
+* {
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
body {
margin: 0;
- font-family: 'Inter', sans-serif;
- background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
- color: var(--text-color);
- display: flex;
- justify-content: center;
- align-items: center;
- min-height: 100vh;
- text-align: center;
- overflow: hidden;
- position: relative;
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
+ color: var(--color-ink);
+ background:
+ radial-gradient(circle at top left, rgba(176, 138, 82, 0.18), transparent 30%),
+ radial-gradient(circle at 85% 15%, rgba(29, 67, 57, 0.14), transparent 26%),
+ linear-gradient(180deg, #fbf7f1 0%, var(--color-bg) 46%, #efe8dc 100%);
+ line-height: 1.6;
+}
+
+body::before,
+body::after {
+ content: "";
+ position: fixed;
+ inset: auto;
+ border-radius: 50%;
+ filter: blur(10px);
+ pointer-events: none;
+ z-index: -1;
+}
+
+body::before {
+ width: 18rem;
+ height: 18rem;
+ top: 6rem;
+ right: 4vw;
+ background: rgba(176, 138, 82, 0.12);
+}
+
+body::after {
+ width: 12rem;
+ height: 12rem;
+ bottom: 10rem;
+ left: 3vw;
+ background: rgba(29, 67, 57, 0.1);
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+.navbar-brand strong,
+.footer-title {
+ font-family: 'Cormorant Garamond', Georgia, serif;
+ letter-spacing: -0.02em;
+ line-height: 1.05;
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+a:hover {
+ color: var(--color-brand-dark);
+}
+
+.site-header {
+ background: rgba(251, 247, 241, 0.78);
+ backdrop-filter: blur(24px);
+ border-bottom: 1px solid rgba(23, 22, 22, 0.05);
+}
+
+.navbar-brand {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.85rem;
+ font-size: 1.05rem;
+}
+
+.navbar-brand small {
+ display: block;
+ color: var(--color-muted);
+ font-size: 0.78rem;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+}
+
+.brand-mark {
+ width: 2.6rem;
+ height: 2.6rem;
+ display: inline-grid;
+ place-items: center;
+ border-radius: 50%;
+ background: linear-gradient(135deg, var(--color-brand), #ddc08b);
+ color: white;
+ font-family: 'Cormorant Garamond', Georgia, serif;
+ font-size: 1.45rem;
+ box-shadow: 0 12px 30px rgba(176, 138, 82, 0.3);
+}
+
+.nav-link {
+ color: var(--color-ink);
+ font-weight: 500;
+ padding: 0.75rem 1rem !important;
+}
+
+.nav-link:hover,
+.nav-link:focus {
+ color: var(--color-brand-dark);
+}
+
+.section-shell {
+ padding: var(--space-section) 0;
+}
+
+.page-hero-compact {
+ padding-bottom: 2.5rem;
+}
+
+.hero-section {
+ position: relative;
+ overflow: hidden;
+}
+
+.hero-title,
+.page-title {
+ font-size: clamp(3rem, 5vw, 5.4rem);
+ margin-bottom: 1.4rem;
+}
+
+.hero-copy,
+.page-copy,
+.section-heading p,
+.product-body p,
+.service-card p,
+.process-card p,
+.contact-info p,
+.detail-copy p,
+.detail-side p,
+.form-note,
+.footer-title + p {
+ color: var(--color-muted);
+ font-size: 1.05rem;
+}
+
+.hero-copy,
+.page-copy {
+ max-width: 44rem;
+}
+
+.eyebrow {
+ display: inline-block;
+ margin-bottom: 1rem;
+ color: var(--color-accent);
+ text-transform: uppercase;
+ letter-spacing: 0.18em;
+ font-size: 0.77rem;
+ font-weight: 700;
+}
+
+.glass-panel,
+.product-card,
+.service-card,
+.category-card,
+.empty-state,
+.process-shell,
+.success-card {
+ background: linear-gradient(180deg, rgba(255, 250, 244, 0.9), rgba(255, 255, 255, 0.72));
+ border: 1px solid rgba(255, 255, 255, 0.7);
+ box-shadow: var(--shadow-soft);
+ backdrop-filter: blur(18px);
+ border-radius: var(--radius-xl);
+}
+
+.hero-search,
+.filter-panel,
+.form-panel {
+ padding: 1.35rem;
+}
+
+.hero-actions {
+ margin-top: 1.8rem;
+}
+
+.hero-showcase {
+ padding: 2rem;
+ min-height: 27rem;
+ position: relative;
+ overflow: hidden;
+}
+
+.hero-orb {
+ position: absolute;
+ border-radius: 50%;
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.9), rgba(176, 138, 82, 0.12));
+ border: 1px solid rgba(176, 138, 82, 0.16);
+}
+
+.hero-orb-lg {
+ width: 13rem;
+ height: 13rem;
+ top: 2rem;
+ right: -1rem;
+}
+
+.hero-orb-sm {
+ width: 5rem;
+ height: 5rem;
+ left: 1rem;
+ bottom: 2rem;
+}
+
+.stat-grid {
+ position: relative;
+ z-index: 1;
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 1rem;
+}
+
+.stat-card {
+ padding: 1.4rem;
+ border-radius: var(--radius-md);
+ background: rgba(255, 255, 255, 0.72);
+ border: 1px solid rgba(23, 22, 22, 0.05);
+ box-shadow: var(--shadow-card);
+}
+
+.stat-card strong {
+ display: block;
+ font-family: 'Cormorant Garamond', Georgia, serif;
+ font-size: 2.4rem;
+}
+
+.stat-card span,
+.showcase-note,
+.product-category,
+.detail-label {
+ color: var(--color-muted);
+}
+
+.showcase-note {
+ position: absolute;
+ left: 2rem;
+ right: 2rem;
+ bottom: 2rem;
+ padding: 1.1rem 1.2rem;
+ border-radius: var(--radius-md);
+ background: rgba(29, 67, 57, 0.08);
+}
+
+.category-card,
+.service-card,
+.product-card {
+ padding: 1.6rem;
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ transition: transform 0.25s ease, box-shadow 0.25s ease;
+}
+
+.category-card:hover,
+.service-card:hover,
+.product-card:hover,
+.contact-banner:hover {
+ transform: translateY(-4px);
+ box-shadow: 0 28px 70px rgba(28, 24, 19, 0.16);
+}
+
+.category-chip,
+.product-visual span,
+.process-step {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: 999px;
+ padding: 0.35rem 0.75rem;
+ font-size: 0.78rem;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+}
+
+.category-chip,
+.process-step {
+ background: rgba(29, 67, 57, 0.09);
+ color: var(--color-accent);
+}
+
+.product-visual {
+ min-height: 12rem;
+ border-radius: var(--radius-lg);
+ margin-bottom: 1.25rem;
+ background:
+ radial-gradient(circle at 28% 28%, rgba(255, 255, 255, 0.95), transparent 36%),
+ linear-gradient(140deg, rgba(176, 138, 82, 0.22), rgba(29, 67, 57, 0.18));
+ position: relative;
+ overflow: hidden;
+}
+
+.product-visual::before,
+.product-visual::after {
+ content: "";
+ position: absolute;
+ border-radius: 999px;
+ border: 1px solid rgba(255, 255, 255, 0.75);
+}
+
+.product-visual::before {
+ width: 7rem;
+ height: 7rem;
+ right: 1.25rem;
+ top: 1.25rem;
+}
+
+.product-visual::after {
+ width: 3.6rem;
+ height: 3.6rem;
+ left: 1.4rem;
+ bottom: 1.1rem;
+}
+
+.product-visual span {
+ position: absolute;
+ left: 1rem;
+ top: 1rem;
+ background: rgba(255, 250, 244, 0.9);
+ color: var(--color-accent);
+}
+
+.product-body {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+}
+
+.product-body h2,
+.product-body h3,
+.service-card h3,
+.category-card h2,
+.process-card h3,
+.contact-banner h2,
+.contact-info h2,
+.detail-copy h2,
+.detail-side h2,
+.form-panel h2 {
+ font-size: clamp(1.8rem, 2vw, 2.4rem);
+ margin-bottom: 0.9rem;
+}
+
+.product-category {
+ margin-bottom: 0.25rem;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+ font-size: 0.75rem;
+ font-weight: 700;
+}
+
+.product-price,
+.detail-price {
+ color: var(--color-brand-dark);
+ font-size: 1.1rem;
+}
+
+.product-meta {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ margin-top: auto;
+ padding-top: 1rem;
+ color: var(--color-muted);
+}
+
+.text-link,
+.back-link {
+ color: var(--color-accent);
+ font-weight: 700;
+}
+
+.text-link:hover,
+.back-link:hover {
+ color: var(--color-brand-dark);
+}
+
+.section-heading {
+ max-width: 42rem;
+ margin-bottom: 2rem;
+}
+
+.section-heading h2,
+.footer-title {
+ font-size: clamp(2.4rem, 3vw, 3.6rem);
+ margin-bottom: 0.8rem;
+}
+
+.service-icon {
+ width: 3rem;
+ height: 3rem;
+ display: inline-grid;
+ place-items: center;
+ border-radius: 50%;
+ margin-bottom: 1rem;
+ background: rgba(176, 138, 82, 0.12);
+ color: var(--color-brand-dark);
+ font-weight: 700;
+}
+
+.process-shell,
+.contact-banner,
+.success-card,
+.detail-card,
+.detail-copy,
+.detail-side,
+.contact-info {
+ padding: 2rem;
+}
+
+.process-card {
+ padding: 1.2rem;
+ border-radius: var(--radius-md);
+ background: rgba(255, 255, 255, 0.55);
+ border: 1px solid rgba(23, 22, 22, 0.05);
+ height: 100%;
+}
+
+.contact-banner {
+ padding: 2rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 2rem;
+}
+
+.contact-method {
+ margin-bottom: 1.5rem;
+}
+
+.contact-method span,
+.footer-heading {
+ display: block;
+ margin-bottom: 0.35rem;
+ color: var(--color-muted);
+ font-size: 0.84rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.12em;
+}
+
+.contact-method a,
+.site-footer a {
+ font-size: 1.05rem;
+ font-weight: 600;
+}
+
+.selected-product-note {
+ margin-top: 2rem;
+ padding: 1.25rem;
+ border-radius: var(--radius-md);
+ background: rgba(29, 67, 57, 0.08);
+}
+
+.site-footer {
+ border-top: 1px solid rgba(23, 22, 22, 0.06);
+ background: rgba(255, 250, 244, 0.65);
+ backdrop-filter: blur(18px);
+}
+
+.btn {
+ border-radius: 999px;
+ padding: 0.9rem 1.45rem;
+ font-weight: 700;
+ letter-spacing: 0.01em;
+ border-width: 1px;
+ transition: transform 0.2s ease, box-shadow 0.2s ease, background-color 0.2s ease;
+}
+
+.btn:hover,
+.btn:focus {
+ transform: translateY(-1px);
+}
+
+.btn-brand {
+ color: #fff;
+ background: linear-gradient(135deg, var(--color-brand-dark), var(--color-brand));
+ border-color: transparent;
+ box-shadow: 0 16px 32px rgba(176, 138, 82, 0.28);
+}
+
+.btn-brand:hover,
+.btn-brand:focus {
+ color: #fff;
+ background: linear-gradient(135deg, #7a5b2f, var(--color-brand-dark));
+}
+
+.btn-ghost {
+ color: var(--color-ink);
+ background: rgba(255, 255, 255, 0.58);
+ border-color: rgba(23, 22, 22, 0.08);
+}
+
+.btn-ghost:hover,
+.btn-ghost:focus {
+ background: rgba(255, 255, 255, 0.88);
+ border-color: rgba(23, 22, 22, 0.14);
+}
+
+.form-control,
+.form-select {
+ min-height: 3.4rem;
+ border-radius: 1rem;
+ border: 1px solid rgba(23, 22, 22, 0.1);
+ background: rgba(255, 255, 255, 0.92);
+ color: var(--color-ink);
+ padding: 0.85rem 1rem;
+}
+
+textarea.form-control {
+ min-height: 9rem;
+}
+
+.form-control:focus,
+.form-select:focus,
+.btn:focus,
+.nav-link:focus,
+a:focus {
+ outline: none;
+ border-color: rgba(176, 138, 82, 0.5);
+ box-shadow: 0 0 0 0.2rem rgba(176, 138, 82, 0.16);
+}
+
+.field-error {
+ color: #9b2d2d;
+ font-size: 0.88rem;
+ margin-top: 0.45rem;
+}
+
+.empty-state,
+.success-card {
+ text-align: center;
+ padding: 3rem 2rem;
+}
+
+.success-card {
+ max-width: 48rem;
+}
+
+.success-summary {
+ margin-top: 1.5rem;
+ padding: 1.2rem;
+ border-radius: var(--radius-md);
+ background: rgba(29, 67, 57, 0.08);
+}
+
+.filter-panel {
+ position: relative;
+ z-index: 2;
+}
+
+.detail-list {
+ padding-left: 1.1rem;
+ margin: 0;
+}
+
+.detail-list li {
+ margin-bottom: 0.8rem;
+ color: var(--color-muted);
+}
+
+.back-link {
+ display: inline-block;
+ margin-bottom: 1.25rem;
+}
+
+@media (max-width: 991.98px) {
+ .hero-showcase {
+ min-height: auto;
+ }
+
+ .contact-banner {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+}
+
+@media (max-width: 767.98px) {
+ .section-shell {
+ padding: 4rem 0;
+ }
+
+ .hero-title,
+ .page-title {
+ font-size: 2.6rem;
+ }
+
+ .glass-panel,
+ .product-card,
+ .service-card,
+ .category-card,
+ .contact-banner,
+ .success-card,
+ .process-shell,
+ .detail-card,
+ .detail-copy,
+ .detail-side,
+ .contact-info {
+ border-radius: 1.5rem;
+ }
}