diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 5e8987a..99f725f 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..6c9b14a 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..5862b35 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..325de17 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..d2978f6 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..af92eb0 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..c862f0a 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,25 @@ from django.contrib import admin -# Register your models here. +from .models import Category, MomentumEntry + + +@admin.register(Category) +class CategoryAdmin(admin.ModelAdmin): + list_display = ("name", "slug", "accent_color") + prepopulated_fields = {"slug": ("name",)} + search_fields = ("name", "description") + + +@admin.register(MomentumEntry) +class MomentumEntryAdmin(admin.ModelAdmin): + list_display = ( + "title", + "entry_date", + "category", + "focus_score", + "energy_score", + "deep_work_minutes", + ) + list_filter = ("category", "entry_date") + search_fields = ("title", "takeaway", "reflection") + date_hierarchy = "entry_date" diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..7345475 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,66 @@ +from django import forms +from django.utils import timezone + +from .models import MomentumEntry + + +class MomentumEntryForm(forms.ModelForm): + class Meta: + model = MomentumEntry + fields = [ + "title", + "category", + "entry_date", + "focus_score", + "energy_score", + "deep_work_minutes", + "takeaway", + "reflection", + ] + widgets = { + "title": forms.TextInput( + attrs={"placeholder": "Shipped a habit, finished a lesson, or stayed focused"} + ), + "entry_date": forms.DateInput(attrs={"type": "date"}), + "focus_score": forms.NumberInput(attrs={"min": 1, "max": 10}), + "energy_score": forms.NumberInput(attrs={"min": 1, "max": 10}), + "deep_work_minutes": forms.NumberInput(attrs={"min": 0, "max": 960, "step": 5}), + "takeaway": forms.TextInput( + attrs={"placeholder": "One sentence that captures today’s momentum"} + ), + "reflection": forms.Textarea( + attrs={"rows": 4, "placeholder": "What worked, what felt hard, what should tomorrow look like?"} + ), + } + labels = { + "title": "What did you move forward?", + "category": "Category", + "entry_date": "Date", + "focus_score": "Focus score", + "energy_score": "Energy score", + "deep_work_minutes": "Deep-work minutes", + "takeaway": "Main takeaway", + "reflection": "Reflection", + } + help_texts = { + "focus_score": "1 = distracted, 10 = locked in.", + "energy_score": "1 = drained, 10 = fully charged.", + "deep_work_minutes": "Minutes spent on meaningful work today.", + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["entry_date"].initial = timezone.localdate() + for name, field in self.fields.items(): + css_class = "form-select" if isinstance(field.widget, forms.Select) else "form-control" + if name in {"focus_score", "energy_score", "deep_work_minutes", "entry_date"}: + css_class = "form-control" + field.widget.attrs["class"] = f"{field.widget.attrs.get('class', '')} {css_class}".strip() + field.widget.attrs.setdefault("autocomplete", "off") + self.fields["reflection"].required = False + + def clean_takeaway(self): + takeaway = self.cleaned_data["takeaway"].strip() + if len(takeaway.split()) < 3: + raise forms.ValidationError("Write a short sentence with at least three words.") + return takeaway diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..8dc8c18 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# Generated by Django 5.2.7 on 2026-04-16 11:08 + +import django.core.validators +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=80, unique=True)), + ('slug', models.SlugField(max_length=80, unique=True)), + ('description', models.CharField(max_length=160)), + ('accent_color', models.CharField(default='#0F766E', max_length=7)), + ], + options={ + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='MomentumEntry', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=120)), + ('entry_date', models.DateField()), + ('focus_score', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(10)])), + ('energy_score', models.PositiveSmallIntegerField(validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(10)])), + ('deep_work_minutes', models.PositiveIntegerField(validators=[django.core.validators.MinValueValidator(0), django.core.validators.MaxValueValidator(960)])), + ('takeaway', models.CharField(max_length=160)), + ('reflection', models.TextField(blank=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='entries', to='core.category')), + ], + options={ + 'ordering': ['-entry_date', '-created_at'], + }, + ), + ] diff --git a/core/migrations/0002_seed_demo_data.py b/core/migrations/0002_seed_demo_data.py new file mode 100644 index 0000000..a1df0d3 --- /dev/null +++ b/core/migrations/0002_seed_demo_data.py @@ -0,0 +1,115 @@ +from datetime import date + +from django.db import migrations + + +CATEGORIES = [ + { + "name": "Learning", + "slug": "learning", + "description": "Study sessions, experiments, and small breakthroughs.", + "accent_color": "#0F766E", + }, + { + "name": "Deep Work", + "slug": "deep-work", + "description": "Quiet execution blocks that move the hard work forward.", + "accent_color": "#F97316", + }, + { + "name": "Wellbeing", + "slug": "wellbeing", + "description": "Energy, routines, and recovery habits that support momentum.", + "accent_color": "#F59E0B", + }, +] + + +def seed_demo_data(apps, schema_editor): + Category = apps.get_model("core", "Category") + MomentumEntry = apps.get_model("core", "MomentumEntry") + + category_map = {} + for item in CATEGORIES: + category, _ = Category.objects.get_or_create( + slug=item["slug"], + defaults={ + "name": item["name"], + "description": item["description"], + "accent_color": item["accent_color"], + }, + ) + category_map[item["slug"]] = category + + if MomentumEntry.objects.exists(): + return + + MomentumEntry.objects.bulk_create( + [ + MomentumEntry( + category=category_map["learning"], + title="Finished a Python walkthrough", + entry_date=date(2026, 4, 10), + focus_score=8, + energy_score=7, + deep_work_minutes=95, + takeaway="Turned a tutorial into notes I can actually reuse later.", + reflection="The best part was rewriting the idea in my own words instead of copying line by line.", + ), + MomentumEntry( + category=category_map["deep-work"], + title="Protected a no-notification build block", + entry_date=date(2026, 4, 12), + focus_score=9, + energy_score=8, + deep_work_minutes=140, + takeaway="A single distraction-free block created more progress than a scattered full day.", + reflection="Turning off notifications before starting made the session feel calm and fast.", + ), + MomentumEntry( + category=category_map["wellbeing"], + title="Reset the afternoon slump", + entry_date=date(2026, 4, 14), + focus_score=6, + energy_score=8, + deep_work_minutes=60, + takeaway="A walk and lighter task list rescued the day instead of writing it off.", + reflection="I should use recovery intentionally, not only when things already feel off.", + ), + MomentumEntry( + category=category_map["learning"], + title="Built the first Django tracker flow", + entry_date=date(2026, 4, 15), + focus_score=8, + energy_score=8, + deep_work_minutes=125, + takeaway="Shipping a tiny end-to-end slice feels more motivating than polishing ideas in isolation.", + reflection="Seeing create, list, and detail views together makes the project feel real.", + ), + ] + ) + + +def reverse_seed_demo_data(apps, schema_editor): + Category = apps.get_model("core", "Category") + MomentumEntry = apps.get_model("core", "MomentumEntry") + MomentumEntry.objects.filter( + title__in=[ + "Finished a Python walkthrough", + "Protected a no-notification build block", + "Reset the afternoon slump", + "Built the first Django tracker flow", + ] + ).delete() + Category.objects.filter(slug__in=["learning", "deep-work", "wellbeing"]).delete() + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0001_initial"), + ] + + operations = [ + migrations.RunPython(seed_demo_data, reverse_seed_demo_data), + ] 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..5fd75c8 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_seed_demo_data.cpython-311.pyc b/core/migrations/__pycache__/0002_seed_demo_data.cpython-311.pyc new file mode 100644 index 0000000..1141550 Binary files /dev/null and b/core/migrations/__pycache__/0002_seed_demo_data.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..2de2282 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,47 @@ +from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models +from django.urls import reverse -# Create your models here. + +class Category(models.Model): + name = models.CharField(max_length=80, unique=True) + slug = models.SlugField(max_length=80, unique=True) + description = models.CharField(max_length=160) + accent_color = models.CharField(max_length=7, default="#0F766E") + + class Meta: + ordering = ["name"] + + def __str__(self): + return self.name + + +class MomentumEntry(models.Model): + category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name="entries") + title = models.CharField(max_length=120) + entry_date = models.DateField() + focus_score = models.PositiveSmallIntegerField( + validators=[MinValueValidator(1), MaxValueValidator(10)] + ) + energy_score = models.PositiveSmallIntegerField( + validators=[MinValueValidator(1), MaxValueValidator(10)] + ) + deep_work_minutes = models.PositiveIntegerField( + validators=[MinValueValidator(0), MaxValueValidator(960)] + ) + takeaway = models.CharField(max_length=160) + reflection = models.TextField(blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ["-entry_date", "-created_at"] + + def __str__(self): + return f"{self.title} ({self.entry_date:%Y-%m-%d})" + + @property + def momentum_score(self): + return round((self.focus_score + self.energy_score) / 2, 1) + + def get_absolute_url(self): + return reverse("entry_detail", args=[self.pk]) diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..fc2bed1 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,29 @@ +{% load static %} - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} + + {% block title %}{{ page_title|default:"Momentum Atlas" }}{% endblock %} + + + {% if project_image_url %} {% endif %} - {% load static %} + + + + {% block head %}{% endblock %} {% block content %}{% endblock %} + diff --git a/core/templates/core/entry_detail.html b/core/templates/core/entry_detail.html new file mode 100644 index 0000000..ce26df7 --- /dev/null +++ b/core/templates/core/entry_detail.html @@ -0,0 +1,75 @@ +{% extends "base.html" %} + +{% block title %}{{ page_title }}{% endblock %} +{% block meta_description %}{{ meta_description }}{% endblock %} + +{% block content %} +
+
+ {% if created %} + + {% endif %} + +
+
+
+
+ {{ entry.category.name }} + +
+

{{ entry.title }}

+

{{ entry.takeaway }}

+
+
+ Focus + {{ entry.focus_score }}/10 +
+
+ Energy + {{ entry.energy_score }}/10 +
+
+ Deep work + {{ entry.deep_work_minutes }}m +
+
+ Momentum + {{ entry.momentum_score }}/10 +
+
+
+

Reflection

+

{{ entry.reflection|default:"No extra reflection was added for this day."|linebreaksbr }}

+
+
+
+
+ +
+
+
+
+{% endblock %} diff --git a/core/templates/core/entry_list.html b/core/templates/core/entry_list.html new file mode 100644 index 0000000..ae3a103 --- /dev/null +++ b/core/templates/core/entry_list.html @@ -0,0 +1,57 @@ +{% extends "base.html" %} + +{% block title %}{{ page_title }}{% endblock %} +{% block meta_description %}{{ meta_description }}{% endblock %} + +{% block content %} +
+
+
+
+ History +

All momentum entries

+

Filter by category and open any check-in for its confirmation/detail view.

+
+ +
+ +
+ All categories + {% for category in categories %} + {{ category.name }} · {{ category.entry_total }} + {% endfor %} +
+ + {% if entries %} +
+ {% for entry in entries %} +
+
+
+ {{ entry.category.name }} + +
+

{{ entry.title }}

+

{{ entry.takeaway }}

+
+ Focus {{ entry.focus_score }}/10 + Energy {{ entry.energy_score }}/10 + {{ entry.deep_work_minutes }} min +
+
+
+ {% endfor %} +
+ {% else %} +
+

No entries for this filter

+

Try a different category or create a new check-in from the dashboard.

+ Create an entry +
+ {% endif %} +
+
+{% endblock %} diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..22e4b66 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,227 @@ {% extends "base.html" %} -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% block title %}{{ page_title }}{% endblock %} +{% block meta_description %}{{ meta_description }}{% 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" }} -

-
-
- -{% endblock %} \ No newline at end of file +
+ + +
+
+
+ {% if messages %} +
+ {% for message in messages %} + + {% endfor %} +
+ {% endif %} +
+
+
+ Workflow widget +

Capture one meaningful check-in.

+

Each entry becomes a usable artifact: you log the day, land on a confirmation detail view, and keep building a real momentum history.

+
+
+
+ {% csrf_token %} +
+ {% for field in form %} +
+ + {{ field }} + {% if field.help_text %} +
{{ field.help_text }}
+ {% endif %} + {% if field.errors %} +
+ {% for error in field.errors %}{{ error }}{% endfor %} +
+ {% endif %} +
+ {% endfor %} +
+
+ + Default categories are ready, and you can manage them later from Django Admin. +
+
+
+
+
+
+ What this MVP already does +

Thin slice, real workflow.

+
+
+
+

Create

+

Submit a structured daily entry with validation, categories, and tangible metrics.

+
+
+

Confirm

+

Each new entry opens its own detail page with a success state and related history.

+
+
+

Review

+

Use weekly bars and the history page to notice patterns instead of collecting dead data.

+
+
+
+
+
+
+ +
+
+
+
+ 7-day insight +

Focus and energy trend

+
+ Open full history +
+
+
+ {% for day in weekly_trend %} +
+
+
+
+
+
{{ day.focus }}/{{ day.energy }}
+ {{ day.label }} + {{ day.minutes }}m +
+ {% endfor %} +
+
+
+
+ +
+
+
+
+ Recent check-ins +

Momentum history preview

+
+ See every entry +
+ {% if recent_entries %} +
+ {% for entry in recent_entries %} +
+
+
+ {{ entry.category.name }} + +
+

{{ entry.title }}

+

{{ entry.takeaway }}

+
+ Focus {{ entry.focus_score }}/10 + Energy {{ entry.energy_score }}/10 + {{ entry.deep_work_minutes }} min +
+
+
+ {% endfor %} +
+ {% else %} +
+

No entries yet

+

Start with one check-in above and this dashboard will instantly become your first useful Python product demo.

+
+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/core/tests.py b/core/tests.py index 7ce503c..c54a625 100644 --- a/core/tests.py +++ b/core/tests.py @@ -1,3 +1,37 @@ from django.test import TestCase +from django.urls import reverse +from django.utils import timezone -# Create your tests here. +from .models import Category, MomentumEntry + + +class MomentumViewsTests(TestCase): + def setUp(self): + self.category = Category.objects.create( + name="Learning", + slug="learning", + description="Tracking study sessions", + accent_color="#0F766E", + ) + + def test_home_page_loads(self): + response = self.client.get(reverse("home")) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Momentum Atlas") + + def test_post_creates_entry_and_redirects(self): + response = self.client.post( + reverse("home"), + { + "title": "Finished a Python kata", + "category": self.category.pk, + "entry_date": timezone.localdate().isoformat(), + "focus_score": 8, + "energy_score": 7, + "deep_work_minutes": 90, + "takeaway": "Made steady progress with functions today.", + "reflection": "Felt strong after removing distractions.", + }, + ) + self.assertEqual(response.status_code, 302) + self.assertEqual(MomentumEntry.objects.count(), 1) diff --git a/core/urls.py b/core/urls.py index 6299e3d..2bbe4bc 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,9 @@ from django.urls import path -from .views import home +from .views import entry_detail, entry_list, home urlpatterns = [ path("", home, name="home"), + path("entries/", entry_list, name="entry_list"), + path("entries//", entry_detail, name="entry_detail"), ] diff --git a/core/views.py b/core/views.py index c9aed12..cb3cbf6 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,164 @@ import os import platform +from datetime import timedelta from django import get_version as django_version -from django.shortcuts import render +from django.contrib import messages +from django.db.models import Avg, Count, Sum +from django.db.models.functions import Coalesce +from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone +from .forms import MomentumEntryForm +from .models import Category, MomentumEntry + + +APP_NAME = "Momentum Atlas" +APP_TAGLINE = "A polished personal dashboard for tracking focus, energy, and small wins." + + +def _build_weekly_trend(entries): + today = timezone.localdate() + start_date = today - timedelta(days=6) + trend_source = ( + entries.filter(entry_date__gte=start_date, entry_date__lte=today) + .values("entry_date") + .annotate( + avg_focus=Coalesce(Avg("focus_score"), 0.0), + avg_energy=Coalesce(Avg("energy_score"), 0.0), + total_minutes=Coalesce(Sum("deep_work_minutes"), 0), + ) + ) + by_date = {row["entry_date"]: row for row in trend_source} + + trend = [] + for offset in range(7): + day = start_date + timedelta(days=offset) + row = by_date.get(day, {}) + focus = float(row.get("avg_focus") or 0) + energy = float(row.get("avg_energy") or 0) + minutes = int(row.get("total_minutes") or 0) + focus_level = int(round((focus / 10) * 10) * 10) if focus else 0 + energy_level = int(round((energy / 10) * 10) * 10) if energy else 0 + trend.append( + { + "date": day, + "label": day.strftime("%a"), + "focus": round(focus, 1), + "energy": round(energy, 1), + "minutes": minutes, + "focus_level": max(0, min(100, focus_level)), + "energy_level": max(0, min(100, energy_level)), + } + ) + return trend + + +def _dashboard_context(): + entries = MomentumEntry.objects.select_related("category") + recent_entries = entries[:6] + last_30_days = timezone.localdate() - timedelta(days=29) + stats_window = entries.filter(entry_date__gte=last_30_days) + totals = stats_window.aggregate( + total_entries=Count("id"), + avg_focus=Coalesce(Avg("focus_score"), 0.0), + avg_energy=Coalesce(Avg("energy_score"), 0.0), + total_minutes=Coalesce(Sum("deep_work_minutes"), 0), + ) + active_days = stats_window.values("entry_date").distinct().count() + top_category = ( + stats_window.values("category__name") + .annotate(total=Count("id")) + .order_by("-total", "category__name") + .first() + ) + weekly_trend = _build_weekly_trend(entries) + + focus_average = round(float(totals["avg_focus"] or 0), 1) + energy_average = round(float(totals["avg_energy"] or 0), 1) + if focus_average >= 8: + spotlight = "Your recent focus trend is excellent—keep protecting that deep-work time." + elif focus_average >= 6: + spotlight = "Momentum is building. A little more consistency could turn this into a real streak." + else: + spotlight = "A reset week might help—shrink the task list and aim for one clear win each day." + + return { + "recent_entries": recent_entries, + "categories": Category.objects.all(), + "weekly_trend": weekly_trend, + "stats": { + "total_entries": totals["total_entries"], + "avg_focus": focus_average, + "avg_energy": energy_average, + "total_minutes": totals["total_minutes"], + "active_days": active_days, + "top_category": top_category["category__name"] if top_category else "No category yet", + "spotlight": spotlight, + }, + } + 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() + if request.method == "POST": + form = MomentumEntryForm(request.POST) + if form.is_valid(): + entry = form.save() + messages.success(request, "Momentum captured. Your new check-in is ready.") + return redirect(f"{entry.get_absolute_url()}?created=1") + messages.error(request, "Please fix the form errors and try again.") + else: + form = MomentumEntryForm() + context = { - "project_name": "New Style", + "project_name": APP_NAME, "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_description": os.getenv("PROJECT_DESCRIPTION", APP_TAGLINE), "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), + "page_title": f"{APP_NAME} | Daily focus dashboard", + "meta_description": "Track daily focus, energy, and deep-work minutes in a polished Python dashboard.", + "form": form, + **_dashboard_context(), } return render(request, "core/index.html", context) + + +def entry_list(request): + selected_slug = request.GET.get("category", "") + entries = MomentumEntry.objects.select_related("category") + if selected_slug: + entries = entries.filter(category__slug=selected_slug) + + context = { + "page_title": f"All check-ins | {APP_NAME}", + "meta_description": "Browse recent check-ins and filter your momentum history by category.", + "entries": entries, + "categories": Category.objects.annotate(entry_total=Count("entries")), + "selected_slug": selected_slug, + } + return render(request, "core/entry_list.html", context) + + +def entry_detail(request, pk): + entry = get_object_or_404(MomentumEntry.objects.select_related("category"), pk=pk) + related_entries = ( + MomentumEntry.objects.select_related("category") + .filter(category=entry.category) + .exclude(pk=entry.pk)[:3] + ) + context = { + "page_title": f"{entry.title} | {APP_NAME}", + "meta_description": entry.takeaway, + "entry": entry, + "related_entries": related_entries, + "created": request.GET.get("created") == "1", + } + return render(request, "core/entry_detail.html", context) diff --git a/static/css/custom.css b/static/css/custom.css index 925f6ed..ecc15bc 100644 --- a/static/css/custom.css +++ b/static/css/custom.css @@ -1,4 +1,540 @@ -/* Custom styles for the application */ -body { - font-family: system-ui, -apple-system, sans-serif; +/* Momentum Atlas theme */ +:root { + --brand-ink: #0f172a; + --brand-muted: #475569; + --brand-surface: #fffaf4; + --brand-surface-strong: #ffffff; + --brand-border: rgba(15, 23, 42, 0.08); + --brand-primary: #0f766e; + --brand-primary-dark: #115e59; + --brand-secondary: #f59e0b; + --brand-accent: #f97316; + --brand-highlight: #14b8a6; + --brand-glow: rgba(249, 115, 22, 0.18); + --brand-shadow: 0 28px 80px rgba(15, 23, 42, 0.12); + --radius-xl: 28px; + --radius-lg: 20px; + --radius-md: 16px; + --section-gap: clamp(4rem, 8vw, 7rem); +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + color: var(--brand-ink); + background: + radial-gradient(circle at top left, rgba(20, 184, 166, 0.14), transparent 28%), + radial-gradient(circle at bottom right, rgba(245, 158, 11, 0.14), transparent 30%), + linear-gradient(180deg, #fffdf8 0%, #fff7ed 46%, #f8fafc 100%); + min-height: 100vh; +} + +h1, +h2, +h3, +h4, +.brand-mark { + font-family: 'Manrope', 'Inter', sans-serif; + letter-spacing: -0.03em; +} + +p { + color: var(--brand-muted); + line-height: 1.7; +} + +.site-shell, +.subpage-shell { + position: relative; + overflow: hidden; +} + +.site-header { + position: relative; +} + +.main-nav { + background: rgba(15, 23, 42, 0.32); + backdrop-filter: blur(18px); + -webkit-backdrop-filter: blur(18px); +} + +.brand-mark { + color: #ffffff; + font-size: 1.35rem; + font-weight: 800; +} + +.brand-mark:hover, +.brand-mark:focus, +.nav-link:hover, +.nav-link:focus { + color: #fff7ed; +} + +.nav-link { + color: rgba(255, 255, 255, 0.82); + font-weight: 600; +} + +.nav-pill { + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 999px; + padding: 0.55rem 1rem !important; +} + +.hero-section { + position: relative; + padding: 1rem 0 4rem; + background: linear-gradient(135deg, #0f172a 0%, #0f766e 48%, #f97316 100%); + color: #ffffff; +} + +.hero-orb { + position: absolute; + border-radius: 50%; + filter: blur(18px); + opacity: 0.75; +} + +.hero-orb-one { + width: 260px; + height: 260px; + top: 7rem; + right: 8%; + background: rgba(20, 184, 166, 0.32); +} + +.hero-orb-two { + width: 190px; + height: 190px; + bottom: 3rem; + left: 6%; + background: rgba(245, 158, 11, 0.28); +} + +.hero-title { + font-size: clamp(2.8rem, 5vw, 5rem); + line-height: 0.98; + margin: 0 0 1.5rem; +} + +.hero-copy { + color: rgba(255, 255, 255, 0.86); + font-size: 1.1rem; + max-width: 40rem; +} + +.eyebrow { + display: inline-block; + margin-bottom: 1rem; + font-size: 0.82rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.16em; + color: var(--brand-accent); +} + +.hero-section .eyebrow, +.hero-section .hero-meta-label { + color: rgba(255, 247, 237, 0.78); +} + +.hero-actions, +.hero-meta { + position: relative; + z-index: 2; +} + +.hero-meta strong { + display: block; + font-size: 1rem; +} + +.hero-meta-label, +.panel-label, +.form-hint { + font-size: 0.82rem; + color: #64748b; +} + +.glass-panel { + background: rgba(255, 255, 255, 0.78); + border: 1px solid rgba(255, 255, 255, 0.42); + border-radius: var(--radius-xl); + box-shadow: var(--brand-shadow); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); +} + +.insight-panel { + padding: 2rem; +} + +.metric-card { + height: 100%; + padding: 1.2rem; + border-radius: var(--radius-md); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(255, 247, 237, 0.76)); + border: 1px solid var(--brand-border); +} + +.metric-card span { + display: block; + font-size: 0.88rem; + color: var(--brand-muted); +} + +.metric-card strong { + font-size: clamp(1.55rem, 3vw, 2rem); + color: var(--brand-ink); +} + +.spotlight-note { + padding: 1.1rem 1.2rem; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.66); + border: 1px solid rgba(15, 23, 42, 0.06); +} + +.spotlight-note p { + margin: 0.45rem 0 0.8rem; +} + +.chip-row, +.filter-row { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.trend-chip, +.filter-chip, +.category-badge { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.55rem 0.9rem; + border-radius: 999px; + font-weight: 700; + font-size: 0.88rem; + text-decoration: none; +} + +.trend-chip, +.category-badge { + background: rgba(15, 118, 110, 0.1); + color: var(--brand-primary-dark); +} + +.filter-chip { + background: rgba(255, 255, 255, 0.78); + color: var(--brand-ink); + border: 1px solid var(--brand-border); +} + +.filter-chip.active, +.filter-chip:hover, +.filter-chip:focus { + background: var(--brand-primary); + color: #ffffff; + border-color: transparent; +} + +.section-block { + padding: var(--section-gap) 0; +} + +.section-muted { + background: rgba(255, 255, 255, 0.42); +} + +.section-heading h2, +.subpage-header h1, +.detail-panel h1 { + font-size: clamp(2rem, 3vw, 3rem); + margin-bottom: 0.8rem; +} + +.section-heading.compact h2 { + font-size: clamp(1.6rem, 2vw, 2.2rem); +} + +.form-panel, +.chart-panel, +.detail-panel, +.sidebar-panel, +.subpage-header, +.empty-state { + padding: 2rem; +} + +.form-panel .form-control, +.form-panel .form-select { + border-radius: 16px; + border: 1px solid rgba(148, 163, 184, 0.35); + padding: 0.9rem 1rem; + min-height: 3.3rem; + background: rgba(255, 255, 255, 0.92); +} + +.form-panel textarea.form-control { + min-height: 8.5rem; +} + +.form-control:focus, +.form-select:focus, +.btn:focus, +.btn:active:focus, +.related-item:focus, +.entry-card a:focus { + box-shadow: 0 0 0 0.28rem rgba(20, 184, 166, 0.2); + border-color: rgba(20, 184, 166, 0.6); +} + +.form-label { + font-weight: 700; + margin-bottom: 0.55rem; +} + +.btn { + border-radius: 999px; + padding: 0.9rem 1.35rem; + font-weight: 700; + border: none; +} + +.btn-accent { + background: linear-gradient(135deg, var(--brand-secondary), var(--brand-accent)); + color: #ffffff; + box-shadow: 0 18px 40px var(--brand-glow); +} + +.btn-accent:hover, +.btn-accent:focus { + color: #ffffff; + transform: translateY(-1px); +} + +.btn-ghost { + background: rgba(255, 255, 255, 0.18); + color: var(--brand-ink); + border: 1px solid rgba(15, 23, 42, 0.12); +} + +.hero-section .btn-ghost { + color: #ffffff; + border-color: rgba(255, 255, 255, 0.28); + background: rgba(255, 255, 255, 0.12); +} + +.stack-grid { + display: grid; + gap: 1rem; +} + +.feature-card { + padding: 1.5rem; +} + +.feature-card h3 { + margin-bottom: 0.6rem; +} + +.trend-chart { + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 1rem; + align-items: end; +} + +.trend-day { + text-align: center; +} + +.trend-bars { + display: flex; + align-items: end; + justify-content: center; + gap: 0.45rem; + height: 220px; + margin-bottom: 1rem; +} + +.trend-bar { + width: 22px; + border-radius: 999px 999px 12px 12px; + min-height: 14px; + box-shadow: inset 0 -8px 18px rgba(255, 255, 255, 0.18); +} + +.trend-bar.focus { + background: linear-gradient(180deg, var(--brand-primary), var(--brand-highlight)); +} + +.trend-bar.energy { + background: linear-gradient(180deg, var(--brand-secondary), var(--brand-accent)); +} + +.trend-bar.level-0 { height: 14px; } +.trend-bar.level-10 { height: 10%; } +.trend-bar.level-20 { height: 20%; } +.trend-bar.level-30 { height: 30%; } +.trend-bar.level-40 { height: 40%; } +.trend-bar.level-50 { height: 50%; } +.trend-bar.level-60 { height: 60%; } +.trend-bar.level-70 { height: 70%; } +.trend-bar.level-80 { height: 80%; } +.trend-bar.level-90 { height: 90%; } +.trend-bar.level-100 { height: 100%; } + +.trend-values, +.entry-date, +.related-item span { + color: var(--brand-muted); + font-size: 0.9rem; +} + +.entry-card { + padding: 1.5rem; + transition: transform 0.25s ease, box-shadow 0.25s ease; +} + +.entry-card:hover, +.related-item:hover { + transform: translateY(-4px); +} + +.entry-card-top { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; +} + +.entry-card h3, +.entry-card h2 { + margin: 1rem 0 0.75rem; + font-size: 1.35rem; +} + +.entry-card a, +.text-link, +.related-item { + color: var(--brand-ink); + text-decoration: none; +} + +.text-link { + font-weight: 700; +} + +.entry-stats, +.detail-metrics { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 1.2rem; +} + +.entry-stats span { + padding: 0.45rem 0.7rem; + border-radius: 999px; + background: rgba(248, 250, 252, 0.95); + border: 1px solid rgba(148, 163, 184, 0.18); + font-size: 0.88rem; +} + +.detail-panel .detail-lead { + font-size: 1.1rem; + margin-bottom: 1.5rem; +} + +.detail-metric { + min-width: 140px; + flex: 1 1 0; +} + +.reflection-block { + margin-top: 2rem; + padding-top: 1.5rem; + border-top: 1px solid rgba(148, 163, 184, 0.2); +} + +.related-list { + display: grid; + gap: 0.85rem; +} + +.related-item { + display: block; + padding: 1rem; + border-radius: 16px; + background: rgba(255, 255, 255, 0.74); + border: 1px solid var(--brand-border); +} + +.related-item strong { + display: block; + margin-bottom: 0.35rem; +} + +.custom-alert { + border: none; + border-radius: 18px; + box-shadow: 0 14px 32px rgba(15, 23, 42, 0.08); +} + +.empty-state { + text-align: center; +} + +.subpage-shell { + padding: 1.5rem 0 3rem; +} + +.subpage-header { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 1.5rem; + align-items: center; +} + +@media (max-width: 991.98px) { + .hero-section { + padding-bottom: 3rem; + } + + .trend-chart { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} + +@media (max-width: 767.98px) { + .hero-title { + font-size: 2.8rem; + } + + .trend-chart { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .trend-bars { + height: 180px; + } + + .form-panel, + .chart-panel, + .detail-panel, + .sidebar-panel, + .subpage-header, + .empty-state, + .insight-panel { + padding: 1.4rem; + } } diff --git a/staticfiles/css/custom.css b/staticfiles/css/custom.css index 108056f..ecc15bc 100644 --- a/staticfiles/css/custom.css +++ b/staticfiles/css/custom.css @@ -1,21 +1,540 @@ - +/* Momentum Atlas theme */ :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); + --brand-ink: #0f172a; + --brand-muted: #475569; + --brand-surface: #fffaf4; + --brand-surface-strong: #ffffff; + --brand-border: rgba(15, 23, 42, 0.08); + --brand-primary: #0f766e; + --brand-primary-dark: #115e59; + --brand-secondary: #f59e0b; + --brand-accent: #f97316; + --brand-highlight: #14b8a6; + --brand-glow: rgba(249, 115, 22, 0.18); + --brand-shadow: 0 28px 80px rgba(15, 23, 42, 0.12); + --radius-xl: 28px; + --radius-lg: 20px; + --radius-md: 16px; + --section-gap: clamp(4rem, 8vw, 7rem); } + +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; + font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, sans-serif; + color: var(--brand-ink); + background: + radial-gradient(circle at top left, rgba(20, 184, 166, 0.14), transparent 28%), + radial-gradient(circle at bottom right, rgba(245, 158, 11, 0.14), transparent 30%), + linear-gradient(180deg, #fffdf8 0%, #fff7ed 46%, #f8fafc 100%); min-height: 100vh; - text-align: center; +} + +h1, +h2, +h3, +h4, +.brand-mark { + font-family: 'Manrope', 'Inter', sans-serif; + letter-spacing: -0.03em; +} + +p { + color: var(--brand-muted); + line-height: 1.7; +} + +.site-shell, +.subpage-shell { + position: relative; overflow: hidden; +} + +.site-header { position: relative; } + +.main-nav { + background: rgba(15, 23, 42, 0.32); + backdrop-filter: blur(18px); + -webkit-backdrop-filter: blur(18px); +} + +.brand-mark { + color: #ffffff; + font-size: 1.35rem; + font-weight: 800; +} + +.brand-mark:hover, +.brand-mark:focus, +.nav-link:hover, +.nav-link:focus { + color: #fff7ed; +} + +.nav-link { + color: rgba(255, 255, 255, 0.82); + font-weight: 600; +} + +.nav-pill { + border: 1px solid rgba(255, 255, 255, 0.18); + border-radius: 999px; + padding: 0.55rem 1rem !important; +} + +.hero-section { + position: relative; + padding: 1rem 0 4rem; + background: linear-gradient(135deg, #0f172a 0%, #0f766e 48%, #f97316 100%); + color: #ffffff; +} + +.hero-orb { + position: absolute; + border-radius: 50%; + filter: blur(18px); + opacity: 0.75; +} + +.hero-orb-one { + width: 260px; + height: 260px; + top: 7rem; + right: 8%; + background: rgba(20, 184, 166, 0.32); +} + +.hero-orb-two { + width: 190px; + height: 190px; + bottom: 3rem; + left: 6%; + background: rgba(245, 158, 11, 0.28); +} + +.hero-title { + font-size: clamp(2.8rem, 5vw, 5rem); + line-height: 0.98; + margin: 0 0 1.5rem; +} + +.hero-copy { + color: rgba(255, 255, 255, 0.86); + font-size: 1.1rem; + max-width: 40rem; +} + +.eyebrow { + display: inline-block; + margin-bottom: 1rem; + font-size: 0.82rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.16em; + color: var(--brand-accent); +} + +.hero-section .eyebrow, +.hero-section .hero-meta-label { + color: rgba(255, 247, 237, 0.78); +} + +.hero-actions, +.hero-meta { + position: relative; + z-index: 2; +} + +.hero-meta strong { + display: block; + font-size: 1rem; +} + +.hero-meta-label, +.panel-label, +.form-hint { + font-size: 0.82rem; + color: #64748b; +} + +.glass-panel { + background: rgba(255, 255, 255, 0.78); + border: 1px solid rgba(255, 255, 255, 0.42); + border-radius: var(--radius-xl); + box-shadow: var(--brand-shadow); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); +} + +.insight-panel { + padding: 2rem; +} + +.metric-card { + height: 100%; + padding: 1.2rem; + border-radius: var(--radius-md); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(255, 247, 237, 0.76)); + border: 1px solid var(--brand-border); +} + +.metric-card span { + display: block; + font-size: 0.88rem; + color: var(--brand-muted); +} + +.metric-card strong { + font-size: clamp(1.55rem, 3vw, 2rem); + color: var(--brand-ink); +} + +.spotlight-note { + padding: 1.1rem 1.2rem; + border-radius: var(--radius-md); + background: rgba(255, 255, 255, 0.66); + border: 1px solid rgba(15, 23, 42, 0.06); +} + +.spotlight-note p { + margin: 0.45rem 0 0.8rem; +} + +.chip-row, +.filter-row { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.trend-chip, +.filter-chip, +.category-badge { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.55rem 0.9rem; + border-radius: 999px; + font-weight: 700; + font-size: 0.88rem; + text-decoration: none; +} + +.trend-chip, +.category-badge { + background: rgba(15, 118, 110, 0.1); + color: var(--brand-primary-dark); +} + +.filter-chip { + background: rgba(255, 255, 255, 0.78); + color: var(--brand-ink); + border: 1px solid var(--brand-border); +} + +.filter-chip.active, +.filter-chip:hover, +.filter-chip:focus { + background: var(--brand-primary); + color: #ffffff; + border-color: transparent; +} + +.section-block { + padding: var(--section-gap) 0; +} + +.section-muted { + background: rgba(255, 255, 255, 0.42); +} + +.section-heading h2, +.subpage-header h1, +.detail-panel h1 { + font-size: clamp(2rem, 3vw, 3rem); + margin-bottom: 0.8rem; +} + +.section-heading.compact h2 { + font-size: clamp(1.6rem, 2vw, 2.2rem); +} + +.form-panel, +.chart-panel, +.detail-panel, +.sidebar-panel, +.subpage-header, +.empty-state { + padding: 2rem; +} + +.form-panel .form-control, +.form-panel .form-select { + border-radius: 16px; + border: 1px solid rgba(148, 163, 184, 0.35); + padding: 0.9rem 1rem; + min-height: 3.3rem; + background: rgba(255, 255, 255, 0.92); +} + +.form-panel textarea.form-control { + min-height: 8.5rem; +} + +.form-control:focus, +.form-select:focus, +.btn:focus, +.btn:active:focus, +.related-item:focus, +.entry-card a:focus { + box-shadow: 0 0 0 0.28rem rgba(20, 184, 166, 0.2); + border-color: rgba(20, 184, 166, 0.6); +} + +.form-label { + font-weight: 700; + margin-bottom: 0.55rem; +} + +.btn { + border-radius: 999px; + padding: 0.9rem 1.35rem; + font-weight: 700; + border: none; +} + +.btn-accent { + background: linear-gradient(135deg, var(--brand-secondary), var(--brand-accent)); + color: #ffffff; + box-shadow: 0 18px 40px var(--brand-glow); +} + +.btn-accent:hover, +.btn-accent:focus { + color: #ffffff; + transform: translateY(-1px); +} + +.btn-ghost { + background: rgba(255, 255, 255, 0.18); + color: var(--brand-ink); + border: 1px solid rgba(15, 23, 42, 0.12); +} + +.hero-section .btn-ghost { + color: #ffffff; + border-color: rgba(255, 255, 255, 0.28); + background: rgba(255, 255, 255, 0.12); +} + +.stack-grid { + display: grid; + gap: 1rem; +} + +.feature-card { + padding: 1.5rem; +} + +.feature-card h3 { + margin-bottom: 0.6rem; +} + +.trend-chart { + display: grid; + grid-template-columns: repeat(7, minmax(0, 1fr)); + gap: 1rem; + align-items: end; +} + +.trend-day { + text-align: center; +} + +.trend-bars { + display: flex; + align-items: end; + justify-content: center; + gap: 0.45rem; + height: 220px; + margin-bottom: 1rem; +} + +.trend-bar { + width: 22px; + border-radius: 999px 999px 12px 12px; + min-height: 14px; + box-shadow: inset 0 -8px 18px rgba(255, 255, 255, 0.18); +} + +.trend-bar.focus { + background: linear-gradient(180deg, var(--brand-primary), var(--brand-highlight)); +} + +.trend-bar.energy { + background: linear-gradient(180deg, var(--brand-secondary), var(--brand-accent)); +} + +.trend-bar.level-0 { height: 14px; } +.trend-bar.level-10 { height: 10%; } +.trend-bar.level-20 { height: 20%; } +.trend-bar.level-30 { height: 30%; } +.trend-bar.level-40 { height: 40%; } +.trend-bar.level-50 { height: 50%; } +.trend-bar.level-60 { height: 60%; } +.trend-bar.level-70 { height: 70%; } +.trend-bar.level-80 { height: 80%; } +.trend-bar.level-90 { height: 90%; } +.trend-bar.level-100 { height: 100%; } + +.trend-values, +.entry-date, +.related-item span { + color: var(--brand-muted); + font-size: 0.9rem; +} + +.entry-card { + padding: 1.5rem; + transition: transform 0.25s ease, box-shadow 0.25s ease; +} + +.entry-card:hover, +.related-item:hover { + transform: translateY(-4px); +} + +.entry-card-top { + display: flex; + justify-content: space-between; + gap: 1rem; + align-items: center; +} + +.entry-card h3, +.entry-card h2 { + margin: 1rem 0 0.75rem; + font-size: 1.35rem; +} + +.entry-card a, +.text-link, +.related-item { + color: var(--brand-ink); + text-decoration: none; +} + +.text-link { + font-weight: 700; +} + +.entry-stats, +.detail-metrics { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 1.2rem; +} + +.entry-stats span { + padding: 0.45rem 0.7rem; + border-radius: 999px; + background: rgba(248, 250, 252, 0.95); + border: 1px solid rgba(148, 163, 184, 0.18); + font-size: 0.88rem; +} + +.detail-panel .detail-lead { + font-size: 1.1rem; + margin-bottom: 1.5rem; +} + +.detail-metric { + min-width: 140px; + flex: 1 1 0; +} + +.reflection-block { + margin-top: 2rem; + padding-top: 1.5rem; + border-top: 1px solid rgba(148, 163, 184, 0.2); +} + +.related-list { + display: grid; + gap: 0.85rem; +} + +.related-item { + display: block; + padding: 1rem; + border-radius: 16px; + background: rgba(255, 255, 255, 0.74); + border: 1px solid var(--brand-border); +} + +.related-item strong { + display: block; + margin-bottom: 0.35rem; +} + +.custom-alert { + border: none; + border-radius: 18px; + box-shadow: 0 14px 32px rgba(15, 23, 42, 0.08); +} + +.empty-state { + text-align: center; +} + +.subpage-shell { + padding: 1.5rem 0 3rem; +} + +.subpage-header { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 1.5rem; + align-items: center; +} + +@media (max-width: 991.98px) { + .hero-section { + padding-bottom: 3rem; + } + + .trend-chart { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} + +@media (max-width: 767.98px) { + .hero-title { + font-size: 2.8rem; + } + + .trend-chart { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .trend-bars { + height: 180px; + } + + .form-panel, + .chart-panel, + .detail-panel, + .sidebar-panel, + .subpage-header, + .empty-state, + .insight-panel { + padding: 1.4rem; + } +}