Auto commit: 2026-04-16T11:11:36.691Z

This commit is contained in:
Flatlogic Bot 2026-04-16 11:11:36 +00:00
parent fe1d69a1b2
commit 28d370c224
22 changed files with 1914 additions and 172 deletions

Binary file not shown.

Binary file not shown.

View File

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

66
core/forms.py Normal file
View File

@ -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 todays 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

View File

@ -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'],
},
),
]

View File

@ -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),
]

View File

@ -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])

View File

@ -1,25 +1,29 @@
<!DOCTYPE html>
{% load static %}
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{% block title %}Knowledge Base{% endblock %}</title>
{% if project_description %}
<meta name="description" content="{{ project_description }}">
<meta property="og:description" content="{{ project_description }}">
<meta property="twitter:description" content="{{ project_description }}">
{% endif %}
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{{ page_title|default:"Momentum Atlas" }}{% endblock %}</title>
<meta name="description" content="{% block meta_description %}{{ meta_description|default:project_description }}{% endblock %}">
<meta name="keywords" content="productivity dashboard, personal tracker, python django app">
<meta name="author" content="Flatlogic">
{% if project_image_url %}
<meta property="og:image" content="{{ project_image_url }}">
<meta property="twitter:image" content="{{ project_image_url }}">
{% endif %}
{% load static %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Manrope:wght@600;700;800&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
<link rel="stylesheet" href="{% static 'css/custom.css' %}?v={{ deployment_timestamp }}">
{% block head %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous" defer></script>
</body>
</html>

View File

@ -0,0 +1,75 @@
{% extends "base.html" %}
{% block title %}{{ page_title }}{% endblock %}
{% block meta_description %}{{ meta_description }}{% endblock %}
{% block content %}
<div class="subpage-shell">
<div class="container py-5">
{% if created %}
<div class="alert custom-alert alert-success mb-4" role="alert">
Nice—your check-in was saved successfully. This detail page is the confirmation step of the MVP workflow.
</div>
{% endif %}
<div class="row g-4">
<div class="col-lg-8">
<article class="glass-panel detail-panel h-100">
<div class="entry-card-top mb-3">
<span class="category-badge">{{ entry.category.name }}</span>
<span class="entry-date">{{ entry.entry_date|date:"F j, Y" }}</span>
</div>
<h1>{{ entry.title }}</h1>
<p class="detail-lead">{{ entry.takeaway }}</p>
<div class="detail-metrics">
<div class="metric-card detail-metric">
<span>Focus</span>
<strong>{{ entry.focus_score }}/10</strong>
</div>
<div class="metric-card detail-metric">
<span>Energy</span>
<strong>{{ entry.energy_score }}/10</strong>
</div>
<div class="metric-card detail-metric">
<span>Deep work</span>
<strong>{{ entry.deep_work_minutes }}m</strong>
</div>
<div class="metric-card detail-metric">
<span>Momentum</span>
<strong>{{ entry.momentum_score }}/10</strong>
</div>
</div>
<section class="reflection-block">
<h2>Reflection</h2>
<p>{{ entry.reflection|default:"No extra reflection was added for this day."|linebreaksbr }}</p>
</section>
</article>
</div>
<div class="col-lg-4">
<aside class="glass-panel sidebar-panel h-100">
<div class="section-heading compact mb-3">
<span class="eyebrow">Next actions</span>
<h2>Keep the loop going</h2>
</div>
<div class="d-grid gap-3 mb-4">
<a class="btn btn-accent" href="{% url 'home' %}#check-in">Log another day</a>
<a class="btn btn-ghost" href="{% url 'entry_list' %}">View all entries</a>
<a class="btn btn-ghost" href="/admin/">Open admin</a>
</div>
<h3 class="h5">More in {{ entry.category.name }}</h3>
<div class="related-list">
{% for related in related_entries %}
<a class="related-item" href="{% url 'entry_detail' related.pk %}">
<strong>{{ related.title }}</strong>
<span>{{ related.entry_date|date:"M j" }} · {{ related.takeaway }}</span>
</a>
{% empty %}
<p class="mb-0 text-muted">This is the first entry in this category.</p>
{% endfor %}
</div>
</aside>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,57 @@
{% extends "base.html" %}
{% block title %}{{ page_title }}{% endblock %}
{% block meta_description %}{{ meta_description }}{% endblock %}
{% block content %}
<div class="subpage-shell">
<div class="container py-5">
<div class="subpage-header glass-panel mb-4">
<div>
<span class="eyebrow">History</span>
<h1>All momentum entries</h1>
<p>Filter by category and open any check-in for its confirmation/detail view.</p>
</div>
<div class="d-flex flex-wrap gap-2">
<a class="btn btn-ghost" href="{% url 'home' %}">Back to dashboard</a>
<a class="btn btn-accent" href="{% url 'home' %}#check-in">New check-in</a>
</div>
</div>
<div class="filter-row mb-4">
<a class="filter-chip {% if not selected_slug %}active{% endif %}" href="{% url 'entry_list' %}">All categories</a>
{% for category in categories %}
<a class="filter-chip {% if selected_slug == category.slug %}active{% endif %}" href="{% url 'entry_list' %}?category={{ category.slug }}">{{ category.name }} · {{ category.entry_total }}</a>
{% endfor %}
</div>
{% if entries %}
<div class="row g-4">
{% for entry in entries %}
<div class="col-lg-4 col-md-6">
<article class="glass-panel entry-card h-100">
<div class="entry-card-top">
<span class="category-badge">{{ entry.category.name }}</span>
<span class="entry-date">{{ entry.entry_date|date:"M j, Y" }}</span>
</div>
<h2 class="h4"><a href="{% url 'entry_detail' entry.pk %}">{{ entry.title }}</a></h2>
<p>{{ entry.takeaway }}</p>
<div class="entry-stats">
<span>Focus {{ entry.focus_score }}/10</span>
<span>Energy {{ entry.energy_score }}/10</span>
<span>{{ entry.deep_work_minutes }} min</span>
</div>
</article>
</div>
{% endfor %}
</div>
{% else %}
<div class="glass-panel empty-state">
<h2>No entries for this filter</h2>
<p>Try a different category or create a new check-in from the dashboard.</p>
<a class="btn btn-accent mt-3" href="{% url 'home' %}#check-in">Create an entry</a>
</div>
{% endif %}
</div>
</div>
{% endblock %}

View File

@ -1,145 +1,227 @@
{% extends "base.html" %}
{% block title %}{{ project_name }}{% endblock %}
{% block head %}
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
<style>
: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);
}
* {
box-sizing: border-box;
}
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;
}
body::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><path d='M-10 10L110 10M10 -10L10 110' stroke-width='1' stroke='rgba(255,255,255,0.05)'/></svg>");
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% {
background-position: 0% 0%;
}
100% {
background-position: 100% 100%;
}
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2.5rem 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.25);
}
h1 {
font-size: clamp(2.2rem, 3vw + 1.2rem, 3.2rem);
font-weight: 700;
margin: 0 0 1.2rem;
letter-spacing: -0.02em;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
opacity: 0.92;
}
.loader {
margin: 1.5rem auto;
width: 56px;
height: 56px;
border: 4px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.runtime code {
background: rgba(0, 0, 0, 0.25);
padding: 0.15rem 0.45rem;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
border: 0;
}
footer {
position: absolute;
bottom: 1rem;
width: 100%;
text-align: center;
font-size: 0.85rem;
opacity: 0.75;
}
</style>
{% endblock %}
{% block title %}{{ page_title }}{% endblock %}
{% block meta_description %}{{ meta_description }}{% endblock %}
{% block content %}
<main>
<div class="card">
<h1>Analyzing your requirements and generating your app…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint">AppWizzy AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will refresh automatically as the plan is implemented.</p>
<p class="runtime">
Runtime: Django <code>{{ django_version }}</code> · Python <code>{{ python_version }}</code>
— UTC <code>{{ current_time|date:"Y-m-d H:i:s" }}</code>
</p>
</div>
</main>
<footer>
Page updated: {{ current_time|date:"Y-m-d H:i:s" }} (UTC)
</footer>
{% endblock %}
<div class="site-shell">
<header class="site-header">
<nav class="navbar navbar-expand-lg navbar-dark main-nav">
<div class="container py-3">
<a class="navbar-brand brand-mark" href="{% url 'home' %}">Momentum Atlas</a>
<button class="navbar-toggler border-0 shadow-none" type="button" data-bs-toggle="collapse" data-bs-target="#mainNav" aria-controls="mainNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="mainNav">
<ul class="navbar-nav ms-auto align-items-lg-center gap-lg-2">
<li class="nav-item"><a class="nav-link" href="#check-in">New check-in</a></li>
<li class="nav-item"><a class="nav-link" href="#weekly-trend">Weekly trend</a></li>
<li class="nav-item"><a class="nav-link" href="{% url 'entry_list' %}">All entries</a></li>
<li class="nav-item"><a class="nav-link nav-pill" href="/admin/">Admin</a></li>
</ul>
</div>
</div>
</nav>
<section class="hero-section">
<div class="hero-orb hero-orb-one"></div>
<div class="hero-orb hero-orb-two"></div>
<div class="container py-5 position-relative">
<div class="row align-items-center g-5">
<div class="col-lg-6">
<span class="eyebrow">Interesting Python project · daily tracker MVP</span>
<h1 class="hero-title">Track your momentum like a product, not a spreadsheet.</h1>
<p class="hero-copy">Log a single daily check-in, watch your focus and energy trend over time, and turn a simple Django app into a polished personal dashboard you can actually share.</p>
<div class="hero-actions d-flex flex-wrap gap-3">
<a class="btn btn-accent btn-lg" href="#check-in">Log todays momentum</a>
<a class="btn btn-ghost btn-lg" href="{% url 'entry_list' %}">Browse history</a>
</div>
<div class="hero-meta d-flex flex-wrap gap-4 mt-4">
<div>
<span class="hero-meta-label">Runtime</span>
<strong>Django {{ django_version }} · Python {{ python_version }}</strong>
</div>
<div>
<span class="hero-meta-label">Updated</span>
<strong>{{ current_time|date:"M j, Y · H:i" }} UTC</strong>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="glass-panel insight-panel">
<div class="panel-label">30-day snapshot</div>
<div class="row g-3">
<div class="col-6">
<article class="metric-card">
<span>Total check-ins</span>
<strong>{{ stats.total_entries }}</strong>
</article>
</div>
<div class="col-6">
<article class="metric-card">
<span>Active days</span>
<strong>{{ stats.active_days }}</strong>
</article>
</div>
<div class="col-6">
<article class="metric-card">
<span>Avg. focus</span>
<strong>{{ stats.avg_focus }}/10</strong>
</article>
</div>
<div class="col-6">
<article class="metric-card">
<span>Deep work</span>
<strong>{{ stats.total_minutes }}m</strong>
</article>
</div>
</div>
<div class="spotlight-note mt-4">
<span class="panel-label">Momentum spotlight</span>
<p>{{ stats.spotlight }}</p>
<div class="chip-row">
<span class="trend-chip">Top lane: {{ stats.top_category }}</span>
<span class="trend-chip">Avg. energy: {{ stats.avg_energy }}/10</span>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
</header>
<main>
<section class="section-block" id="check-in">
<div class="container">
{% if messages %}
<div class="message-stack mb-4">
{% for message in messages %}
<div class="alert alert-{{ message.tags|default:'info' }} custom-alert mb-0" role="alert">{{ message }}</div>
{% endfor %}
</div>
{% endif %}
<div class="row g-4 align-items-start">
<div class="col-xl-7">
<div class="section-heading">
<span class="eyebrow">Workflow widget</span>
<h2>Capture one meaningful check-in.</h2>
<p>Each entry becomes a usable artifact: you log the day, land on a confirmation detail view, and keep building a real momentum history.</p>
</div>
<div class="glass-panel form-panel">
<form method="post" novalidate>
{% csrf_token %}
<div class="row g-3">
{% for field in form %}
<div class="col-12 {% if field.name == 'entry_date' or field.name == 'focus_score' or field.name == 'energy_score' or field.name == 'deep_work_minutes' %}col-md-6{% endif %}">
<label class="form-label" for="{{ field.id_for_label }}">{{ field.label }}</label>
{{ field }}
{% if field.help_text %}
<div class="form-hint">{{ field.help_text }}</div>
{% endif %}
{% if field.errors %}
<div class="invalid-feedback d-block">
{% for error in field.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
</div>
<div class="d-flex flex-wrap gap-3 align-items-center mt-4">
<button type="submit" class="btn btn-accent btn-lg">Save check-in</button>
<span class="form-hint mb-0">Default categories are ready, and you can manage them later from Django Admin.</span>
</div>
</form>
</div>
</div>
<div class="col-xl-5">
<div class="section-heading compact">
<span class="eyebrow">What this MVP already does</span>
<h2>Thin slice, real workflow.</h2>
</div>
<div class="stack-grid">
<article class="glass-panel feature-card">
<h3>Create</h3>
<p>Submit a structured daily entry with validation, categories, and tangible metrics.</p>
</article>
<article class="glass-panel feature-card">
<h3>Confirm</h3>
<p>Each new entry opens its own detail page with a success state and related history.</p>
</article>
<article class="glass-panel feature-card">
<h3>Review</h3>
<p>Use weekly bars and the history page to notice patterns instead of collecting dead data.</p>
</article>
</div>
</div>
</div>
</div>
</section>
<section class="section-block section-muted" id="weekly-trend">
<div class="container">
<div class="d-flex flex-wrap justify-content-between align-items-end gap-3 mb-4">
<div class="section-heading compact mb-0">
<span class="eyebrow">7-day insight</span>
<h2>Focus and energy trend</h2>
</div>
<a class="text-link" href="{% url 'entry_list' %}">Open full history</a>
</div>
<div class="glass-panel chart-panel">
<div class="trend-chart">
{% for day in weekly_trend %}
<article class="trend-day">
<div class="trend-bars">
<div class="trend-bar focus level-{{ day.focus_level }}" aria-label="Focus {{ day.focus }} out of 10"></div>
<div class="trend-bar energy level-{{ day.energy_level }}" aria-label="Energy {{ day.energy }} out of 10"></div>
</div>
<div class="trend-values">{{ day.focus }}/{{ day.energy }}</div>
<strong>{{ day.label }}</strong>
<span>{{ day.minutes }}m</span>
</article>
{% endfor %}
</div>
</div>
</div>
</section>
<section class="section-block">
<div class="container">
<div class="d-flex flex-wrap justify-content-between align-items-end gap-3 mb-4">
<div class="section-heading compact mb-0">
<span class="eyebrow">Recent check-ins</span>
<h2>Momentum history preview</h2>
</div>
<a class="text-link" href="{% url 'entry_list' %}">See every entry</a>
</div>
{% if recent_entries %}
<div class="row g-4">
{% for entry in recent_entries %}
<div class="col-lg-4 col-md-6">
<article class="glass-panel entry-card h-100">
<div class="entry-card-top">
<span class="category-badge">{{ entry.category.name }}</span>
<span class="entry-date">{{ entry.entry_date|date:"M j" }}</span>
</div>
<h3><a href="{% url 'entry_detail' entry.pk %}">{{ entry.title }}</a></h3>
<p>{{ entry.takeaway }}</p>
<div class="entry-stats">
<span>Focus {{ entry.focus_score }}/10</span>
<span>Energy {{ entry.energy_score }}/10</span>
<span>{{ entry.deep_work_minutes }} min</span>
</div>
</article>
</div>
{% endfor %}
</div>
{% else %}
<div class="glass-panel empty-state">
<h3>No entries yet</h3>
<p>Start with one check-in above and this dashboard will instantly become your first useful Python product demo.</p>
</div>
{% endif %}
</div>
</section>
</main>
</div>
{% endblock %}

View File

@ -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)

View File

@ -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/<int:pk>/", entry_detail, name="entry_detail"),
]

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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;
}
}