diff --git a/.gitignore b/.gitignore
index 41486c3..5dd11f0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -456,5 +456,5 @@ poetry.toml
# LSP config files
pyrightconfig.json
-
+*.md
# End of https://www.toptal.com/developers/gitignore/api/python,django,macos,pycharm
\ No newline at end of file
diff --git a/apps/accounts/admin.py b/apps/accounts/admin.py
index d57fc59..8cf7e90 100644
--- a/apps/accounts/admin.py
+++ b/apps/accounts/admin.py
@@ -5,6 +5,12 @@ from .models import ClientProfile
@admin.register(ClientProfile)
class ClientProfileAdmin(admin.ModelAdmin):
- list_display = ("user", "company", "phone", "timezone", "updated_at")
- search_fields = ("user__username", "user__email", "user__first_name", "user__last_name", "company")
- list_filter = ("timezone",)
+ list_display = ("user", "company", "job_title", "phone", "updated_at")
+ search_fields = (
+ "user__username",
+ "user__email",
+ "user__first_name",
+ "user__last_name",
+ "company",
+ "job_title",
+ )
diff --git a/apps/accounts/forms.py b/apps/accounts/forms.py
index 6c4ea2a..7df4b6b 100644
--- a/apps/accounts/forms.py
+++ b/apps/accounts/forms.py
@@ -11,7 +11,6 @@ class SignUpForm(UserCreationForm):
email = forms.EmailField(required=True)
company = forms.CharField(max_length=200, required=False)
phone = forms.CharField(max_length=50, required=False)
- timezone = forms.ChoiceField(choices=ClientProfile.TIMEZONE_CHOICES, required=True)
class Meta:
model = User
@@ -19,7 +18,7 @@ class SignUpForm(UserCreationForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- for field_name in ("username", "first_name", "last_name", "email", "company", "phone", "timezone"):
+ for field_name in ("username", "first_name", "last_name", "email", "company", "phone"):
if field_name in self.fields:
self.fields[field_name].widget.attrs.setdefault("class", "form-control")
for field_name in ("password1", "password2"):
@@ -42,7 +41,6 @@ class SignUpForm(UserCreationForm):
user=user,
company=self.cleaned_data.get("company", ""),
phone=self.cleaned_data.get("phone", ""),
- timezone=self.cleaned_data.get("timezone", "UTC"),
)
return user
@@ -65,11 +63,17 @@ class ClientProfileForm(forms.ModelForm):
class Meta:
model = ClientProfile
- fields = ("company", "phone", "timezone")
+ fields = ("avatar", "company", "job_title", "phone", "bio")
widgets = {
+ "avatar": forms.ClearableFileInput(
+ attrs={"class": "profile-avatar-input", "accept": "image/jpeg,image/png,image/webp"}
+ ),
"company": forms.TextInput(attrs={"class": "form-control"}),
+ "job_title": forms.TextInput(attrs={"class": "form-control"}),
"phone": forms.TextInput(attrs={"class": "form-control"}),
- "timezone": forms.Select(attrs={"class": "form-control"}),
+ "bio": forms.Textarea(
+ attrs={"class": "form-control", "rows": 4, "placeholder": "A short introduction to your work and research interests"}
+ ),
}
def __init__(self, *args, **kwargs):
diff --git a/apps/accounts/migrations/0002_remove_clientprofile_timezone_clientprofile_avatar_and_more.py b/apps/accounts/migrations/0002_remove_clientprofile_timezone_clientprofile_avatar_and_more.py
new file mode 100644
index 0000000..dca671e
--- /dev/null
+++ b/apps/accounts/migrations/0002_remove_clientprofile_timezone_clientprofile_avatar_and_more.py
@@ -0,0 +1,34 @@
+# Generated by Django 5.2.15 on 2026-08-01 12:31
+
+import apps.accounts.models
+import django.core.validators
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('accounts', '0001_initial'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='clientprofile',
+ name='timezone',
+ ),
+ migrations.AddField(
+ model_name='clientprofile',
+ name='avatar',
+ field=models.ImageField(blank=True, upload_to=apps.accounts.models.profile_avatar_upload_to, validators=[django.core.validators.FileExtensionValidator(['jpg', 'jpeg', 'png', 'webp']), apps.accounts.models.validate_avatar_size]),
+ ),
+ migrations.AddField(
+ model_name='clientprofile',
+ name='bio',
+ field=models.TextField(blank=True, max_length=600),
+ ),
+ migrations.AddField(
+ model_name='clientprofile',
+ name='job_title',
+ field=models.CharField(blank=True, max_length=150),
+ ),
+ ]
diff --git a/apps/accounts/models.py b/apps/accounts/models.py
index f2a9ec7..b1297a7 100644
--- a/apps/accounts/models.py
+++ b/apps/accounts/models.py
@@ -1,33 +1,39 @@
+from pathlib import Path
+from uuid import uuid4
+
from django.conf import settings
+from django.core.exceptions import ValidationError
+from django.core.validators import FileExtensionValidator
from django.db import models
-class ClientProfile(models.Model):
- TIMEZONE_CHOICES = [
- ("UTC", "UTC"),
- ("America/New_York", "America/New York"),
- ("America/Chicago", "America/Chicago"),
- ("America/Denver", "America/Denver"),
- ("America/Los_Angeles", "America/Los Angeles"),
- ("Europe/London", "Europe/London"),
- ("Europe/Berlin", "Europe/Berlin"),
- ("Asia/Dubai", "Asia/Dubai"),
- ("Asia/Tehran", "Asia/Tehran"),
- ("Asia/Tokyo", "Asia/Tokyo"),
- ("Australia/Sydney", "Australia/Sydney"),
- ]
+def profile_avatar_upload_to(instance, filename):
+ suffix = Path(filename).suffix.lower()
+ return f"accounts/avatars/{instance.user_id}/{uuid4().hex}{suffix}"
+
+def validate_avatar_size(image):
+ if image.size > 5 * 1024 * 1024:
+ raise ValidationError("Profile pictures must be 5 MB or smaller.")
+
+
+class ClientProfile(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="client_profile",
)
company = models.CharField(max_length=200, blank=True)
+ job_title = models.CharField(max_length=150, blank=True)
phone = models.CharField(max_length=50, blank=True)
- timezone = models.CharField(
- max_length=64,
- choices=TIMEZONE_CHOICES,
- default="UTC",
+ bio = models.TextField(max_length=600, blank=True)
+ avatar = models.ImageField(
+ upload_to=profile_avatar_upload_to,
+ blank=True,
+ validators=[
+ FileExtensionValidator(["jpg", "jpeg", "png", "webp"]),
+ validate_avatar_size,
+ ],
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
diff --git a/apps/accounts/tests/test_accounts.py b/apps/accounts/tests/test_accounts.py
index 9f40f01..51ba63f 100644
--- a/apps/accounts/tests/test_accounts.py
+++ b/apps/accounts/tests/test_accounts.py
@@ -16,7 +16,6 @@ class SignUpFormTest(TestCase):
"email": "ada@example.com",
"company": "Analytical Engines",
"phone": "+1 555 0100",
- "timezone": "UTC",
"password1": "Str0ngPass!word",
"password2": "Str0ngPass!word",
}
@@ -26,7 +25,6 @@ class SignUpFormTest(TestCase):
self.assertEqual(user.email, "ada@example.com")
profile = ClientProfile.objects.get(user=user)
self.assertEqual(profile.company, "Analytical Engines")
- self.assertEqual(profile.timezone, "UTC")
class AccountViewsTest(TestCase):
@@ -34,6 +32,15 @@ class AccountViewsTest(TestCase):
response = self.client.get(reverse("accounts:signup"))
self.assertEqual(response.status_code, 200)
+ def test_logged_out_nav_exposes_login_and_registration(self):
+ response = self.client.get(reverse("pages:home"))
+ navbar = response.content.decode().split("", 1)[0]
+
+ self.assertIn("Login", navbar)
+ self.assertIn("Start a Project", navbar)
+ self.assertIn(reverse("accounts:login"), navbar)
+ self.assertIn(reverse("accounts:signup"), navbar)
+
def test_signup_creates_account_and_logs_in(self):
response = self.client.post(
reverse("accounts:signup"),
@@ -44,7 +51,6 @@ class AccountViewsTest(TestCase):
"email": "grace@example.com",
"company": "",
"phone": "",
- "timezone": "UTC",
"password1": "Str0ngPass!word",
"password2": "Str0ngPass!word",
},
@@ -58,6 +64,47 @@ class AccountViewsTest(TestCase):
self.assertEqual(response.status_code, 302)
self.assertIn(reverse("accounts:login"), response.url)
+ def test_authenticated_nav_uses_sidebar_for_account_actions(self):
+ user = User.objects.create_user(
+ username="workspace-user",
+ email="workspace@example.com",
+ password="Str0ngPass!word",
+ )
+ self.client.force_login(user)
+
+ response = self.client.get(reverse("projects:dashboard"))
+
+ self.assertContains(response, "Profile & account")
+ self.assertContains(response, "Project briefs")
+ navbar = response.content.decode().split("", 1)[0]
+ self.assertNotIn("My Projects", navbar)
+ self.assertNotIn("Logout", navbar)
+ self.assertIn("navbar-account", navbar)
+ self.assertIn("navbar-account-dropdown", navbar)
+ self.assertIn("Your profile", navbar)
+ self.assertIn("New project brief", navbar)
+ self.assertIn("Contact support", navbar)
+ self.assertIn("Sign out", navbar)
+ self.assertIn("default-avatar.png", navbar)
+
+ def test_profile_exposes_professional_fields_and_avatar_upload(self):
+ user = User.objects.create_user(
+ username="researcher",
+ first_name="Marie",
+ last_name="Curie",
+ email="marie@example.com",
+ password="Str0ngPass!word",
+ )
+ self.client.force_login(user)
+
+ response = self.client.get(reverse("accounts:profile"))
+
+ self.assertContains(response, 'enctype="multipart/form-data"')
+ self.assertContains(response, 'name="avatar"')
+ self.assertContains(response, 'name="job_title"')
+ self.assertContains(response, 'name="bio"')
+ self.assertNotContains(response, 'name="timezone"')
+
def test_logout_requires_post_and_clears_session(self):
self.client.login(username="newclient", password="Str0ngPass!word")
response = self.client.get(reverse("accounts:logout"))
diff --git a/apps/pages/tests/test_views.py b/apps/pages/tests/test_views.py
index 1602102..c82fa97 100644
--- a/apps/pages/tests/test_views.py
+++ b/apps/pages/tests/test_views.py
@@ -51,7 +51,7 @@ class HomeViewTest(TestCase):
from apps.projects.models import ProjectBrief
user = User.objects.create_user(username="client", email="c@example.com", password="x")
- ClientProfile.objects.create(user=user, timezone="UTC")
+ ClientProfile.objects.create(user=user)
HomepageSection.objects.create(
section_type=HomepageSection.TYPE_PROJECTS,
title="Showcase",
@@ -86,7 +86,7 @@ class HomeViewTest(TestCase):
from apps.projects.models import ProjectBrief
user = User.objects.create_user(username="c2", email="c2@example.com", password="x")
- ClientProfile.objects.create(user=user, timezone="UTC")
+ ClientProfile.objects.create(user=user)
HomepageSection.objects.create(
section_type=HomepageSection.TYPE_PROJECTS,
title="Showcase",
diff --git a/apps/projects/management/commands/seed_portal_data.py b/apps/projects/management/commands/seed_portal_data.py
index c42075b..2fad70b 100644
--- a/apps/projects/management/commands/seed_portal_data.py
+++ b/apps/projects/management/commands/seed_portal_data.py
@@ -19,7 +19,6 @@ CLIENTS = [
"password": "ClientDemo2026!",
"company": "Northside Imaging Lab",
"phone": "+1 604 555 0101",
- "timezone": "America/Vancouver",
},
{
"username": "sarah.research",
@@ -29,7 +28,6 @@ CLIENTS = [
"password": "ClientDemo2026!",
"company": "Pacific Oncology Research",
"phone": "+1 604 555 0102",
- "timezone": "America/Los_Angeles",
},
{
"username": "devteam",
@@ -39,7 +37,6 @@ CLIENTS = [
"password": "ClientDemo2026!",
"company": "BioFlow Analytics",
"phone": "+44 20 7946 0103",
- "timezone": "Europe/London",
},
]
@@ -192,7 +189,6 @@ class Command(BaseCommand):
defaults={
"company": client["company"],
"phone": client["phone"],
- "timezone": client["timezone"],
},
)
action = "Created" if created else "Updated"
diff --git a/apps/projects/tests/test_views.py b/apps/projects/tests/test_views.py
index 3e32508..044cc03 100644
--- a/apps/projects/tests/test_views.py
+++ b/apps/projects/tests/test_views.py
@@ -16,13 +16,13 @@ class ProjectPortalTest(TestCase):
first_name="Client",
last_name="User",
)
- ClientProfile.objects.create(user=self.user, timezone="UTC")
+ ClientProfile.objects.create(user=self.user)
self.other = User.objects.create_user(
username="other",
email="other@example.com",
password="Str0ngPass!word",
)
- ClientProfile.objects.create(user=self.other, timezone="UTC")
+ ClientProfile.objects.create(user=self.other)
def _brief_payload(self):
return {
diff --git a/docs/managed-projects-roadmap.md b/docs/managed-projects-roadmap.md
deleted file mode 100644
index be72e8d..0000000
--- a/docs/managed-projects-roadmap.md
+++ /dev/null
@@ -1,256 +0,0 @@
-# Managed Projects Roadmap — Simple → Advanced
-
-Expand the existing Tecvico Django site so **clients define projects; Tecvico delivers them**. No freelancer marketplace — users submit briefs, your team quotes, builds, and ships.
-
----
-
-## Is This Doable on the Current Stack?
-
-**Yes — and simpler than a marketplace.** No two-sided matching, escrow to third parties, or dispute flows between strangers. You are the sole provider.
-
-
-| Already in place | Reuse for managed projects |
-| -------------------------------- | ------------------------------------------- |
-| Django + PostgreSQL + Docker | Core backend |
-| `apps.pages` + contact form | Evolve into project brief submission |
-| File uploads (`contact_uploads`) | Brief attachments, deliverables |
-| `apps.products` CMS patterns | Service catalog / package pages |
-| Admin panel | Intake queue, quotes, assignment, delivery |
-| Email + templates | Status updates, quote sent, milestone ready |
-
-
-**New apps to add:**
-
-- `apps.accounts` — client login, org profiles (Plan 2+)
-- `apps.projects` — briefs, quotes, orders, milestones, deliverables
-- `apps.messaging` — client ↔ Tecvico threads per project
-- `apps.payments` — client invoicing via Stripe (Plan 2+)
-
-**Internal-only (admin):** team assignment, internal notes, time tracking — no public freelancer profiles.
-
-**Verdict:** Plan 1 is close to an enhanced contact flow. Plan 2–3 add quoting, payments, and a client portal — all standard Django.
-
----
-
-
-
-## Plan 1 — Simple (MVP) · 3–6 weeks
-
-**Goal:** Clients submit project briefs online; Tecvico reviews and delivers off-platform billing if needed.
-
-
-| Milestone | Weeks | Deliverable |
-| ------------------ | ----- | ----------------------------------------------- |
-| M1 Client accounts | 1 | Sign up, login, password reset |
-| M2 Project briefs | 1–2 | Structured submission form + attachments |
-| M3 Status tracking | 2–3 | Client sees request status; email notifications |
-| M4 Admin intake | 3–4 | Review queue, status updates, internal notes |
-
-
-
-
-### Feature detail
-
-**Accounts**
-
-- Email/password signup (client role only)
-- Profile: name, company, phone, timezone
-- Dashboard: list of my project requests
-
-**Project brief**
-
-- Fields: title, category (web, data, research, etc.), description, goals, budget range, desired deadline, reference links
-- Attachments: specs, mockups, datasets (reuse upload patterns)
-- Status: `submitted` → `under_review` → `quoted` → `accepted` / `declined` → `in_progress` → `delivered` → `closed`
-- Client can edit brief while status is `submitted` only
-
-**Notifications**
-
-- Email to client on status change
-- Email to admin on new submission
-
-**Admin**
-
-- Intake list with filters (status, category, date)
-- View brief + files; add internal notes (not visible to client)
-- Manually set status and paste quote text (PDF/email off-platform OK)
-
-**Out of scope:** online quotes, payments, messaging, milestones.
-
----
-
-
-
-## Plan 2 — Medium (Client Portal) · 1–2 months
-
-**Goal:** Quote, pay, and deliver on-platform. Productized packages + custom projects.
-
-
-| Milestone | Weeks | Deliverable |
-| ------------- | ----- | ----------------------------------------------------- |
-| M1 Foundation | 1–2 | Plan 1 + email verify, service catalog pages |
-| M2 Quotes | 3–4 | Admin builds quote; client accepts/declines in portal |
-| M3 Payments | 5–6 | Stripe deposit + milestone invoices |
-| M4 Delivery | 7–8 | Milestones, deliverable uploads, revision rounds |
-| M5 Comms | 9–10 | Per-project messaging, client dashboard polish |
-
-
-
-
-### Feature detail
-
-**Service catalog**
-
-- CMS pages for fixed offerings (e.g. “Landing page”, “Radiomics pipeline audit”)
-- Package tiers: scope summary, starting price, typical timeline
-- “Start from template” pre-fills brief fields
-
-**Quotes**
-
-- Admin creates quote from brief: line items, total, timeline, revision count included
-- Client receives email + in-portal quote view
-- Accept → order created; decline → brief archived with reason
-
-**Orders & milestones**
-
-- Order states: `awaiting_deposit` → `active` → `milestone_review` → `revision` → `completed`
-- Milestones: name, due date, amount, description of deliverable
-- Tecvico uploads files per milestone; client approves or requests revision (capped rounds)
-- Change requests: client submits add-on brief → admin sends revised quote
-
-**Payments (Stripe)**
-
-- Deposit on accept (e.g. 30–50%); remainder split across milestones
-- Checkout Session per invoice; receipt PDF
-- Refund policy flags (admin-only)
-
-**Messaging**
-
-- Thread per project: client ↔ assigned PM
-- File attach in thread; email digest on new message
-
-**Admin**
-
-- Assign internal owner (staff user) per project
-- Pipeline board: new → quoted → active → blocked → done
-- Basic reports: open projects, revenue this month
-
-**Out of scope:** e-sign contracts, org/team accounts, automated estimates, real-time chat.
-
----
-
-
-
-## Plan 3 — Advanced (Full Client Platform) · 2–3 months
-
-**Goal:** Scalable delivery ops — enterprise clients, SLAs, automation, integrations.
-
-
-| Milestone | Months | Deliverable |
-| --------------- | ------ | --------------------------------------------- |
-| M1 Hardening | 1 | Plan 2 + 2FA, OAuth, org accounts |
-| M2 Sales flow | 1–2 | E-sign, ballpark estimator, kickoff booking |
-| M3 Delivery ops | 2–3 | SLA timers, internal time log, task checklist |
-| M4 Enterprise | 3–4 | Multi-user orgs, API, webhooks |
-| M5 Growth | 4–5 | Referrals, analytics, PWA, knowledge base |
-
-
-
-
-### Feature detail
-
-**Auth & organizations**
-
-- Google / LinkedIn OAuth; optional 2FA
-- Organization account: owner + members, shared project list, role (viewer / submitter / billing)
-- Billing contact separate from project contact
-
-**Pre-sale**
-
-- Ballpark estimator: category + scope sliders → indicative range (admin-configured rules)
-- Discovery call booking (Calendly embed or built-in slot picker)
-- Digital SOW + NDA (DocuSign hook or in-app accept with audit trail)
-
-**Delivery workspace**
-
-- Shared project hub: brief, quote, contract, timeline, files, messages
-- Visual timeline / milestone Gantt for client
-- SLA badges: response within X hours, milestone due alerts (Celery + email)
-- Internal: task checklist, time entries per staff, capacity view (admin)
-
-**Revisions & scope**
-
-- Structured revision requests (what to change, priority)
-- Scope creep flow: auto-flag when revision count exceeded → change order quote
-
-**Payments+**
-
-- Multi-currency quotes; recurring maintenance plans (Stripe Subscription)
-- Pro-forma + final invoice; export for accounting
-
-**Enterprise & integrations**
-
-- REST API: submit brief, poll status, download deliverables
-- Webhooks: `quote.sent`, `milestone.delivered`, `project.completed`
-- SSO option (SAML) for large clients — optional phase
-
-**Knowledge & retention**
-
-- Client-facing FAQ / “how we work” tied to project phase
-- Post-delivery satisfaction survey + testimonial request
-- Referral codes (credit on next project)
-
-**Admin analytics**
-
-- Funnel: brief → quote → accept → complete
-- Utilization, avg delivery time, revenue by service line
-
-**Out of scope:** marketplace, freelancer payouts, native mobile apps.
-
----
-
-
-
-## Comparison
-
-
-| | Simple | Medium | Advanced |
-| ------------------------------- | ---------- | -------- | ----------------- |
-| Project brief submission | ✓ | ✓ | ✓ + templates |
-| Online quotes | — | ✓ | ✓ + estimator |
-| Stripe payments | — | ✓ | ✓ + subscriptions |
-| Milestones & deliverables | — | ✓ | ✓ + SLA |
-| Client ↔ Tecvico messaging | — | ✓ | ✓ + digests |
-| Service catalog / packages | — | ✓ | ✓ |
-| Org / team accounts | — | — | ✓ |
-| E-sign / contracts | — | — | ✓ |
-| API / webhooks | — | — | ✓ |
-| Internal ops (assign, time log) | notes only | pipeline | full |
-
-
----
-
-
-
-## Managed vs Marketplace
-
-
-| | Managed (this doc) | Marketplace (`marketplace-roadmap.md`) |
-| ------------ | -------------------------- | -------------------------------------- |
-| Who delivers | Tecvico team | Freelancers / clients hire each other |
-| Payments | Client → Tecvico | Escrow, payouts, commissions |
-| Complexity | Lower | Higher (trust, disputes, matching) |
-| Best when | You sell your own services | You broker others' work |
-
-
----
-
-
-
-## Recommended Path
-
-1. **Plan 1** — ship brief + status portal; keep quoting via email if needed.
-2. **Plan 2** — add quotes and Stripe once you have repeatable delivery workflow.
-3. **Plan 3** — when project volume needs SLAs, org accounts, and ops dashboards.
-
-*Timelines assume extending the current codebase; bespoke UI redesign adds ~20%.*
\ No newline at end of file
diff --git a/static/css/main.css b/static/css/main.css
index b6c5e1c..d257c5b 100644
--- a/static/css/main.css
+++ b/static/css/main.css
@@ -33,6 +33,19 @@
--text-muted: #9ca3af;
--text-link: #3051ff;
+ /* Semantic UI colors */
+ --color-success: #087a53;
+ --color-success-soft: #e9f8f2;
+ --color-warning: #9a6b0b;
+ --color-warning-soft: #fff6df;
+ --color-danger: #b42318;
+ --color-danger-soft: #fff0ee;
+ --color-info: #3051ff;
+ --color-info-soft: #eef1ff;
+ --border-default: #e3e7ee;
+ --border-strong: #cbd2dd;
+ --focus-ring: 0 0 0 3px rgba(48, 81, 255, 0.16);
+
--footer-bg: #1f2937;
--footer-bottom-bg: #000000;
@@ -42,6 +55,10 @@
--radius-xl: 20px;
--radius-pill: 999px;
+ --shadow-xs: 0 1px 2px rgba(17, 24, 39, 0.05);
+ --shadow-sm: 0 2px 8px rgba(17, 24, 39, 0.07);
+ --shadow-md: 0 10px 28px rgba(17, 24, 39, 0.10);
+
--spacing-xs: 0.5rem;
--spacing-sm: 1rem;
--spacing-md: 1.5rem;
@@ -60,8 +77,8 @@
--transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
--transition-bounce: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
- --font-sans: 'Ubuntu', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
- --font-heading: 'Open Sans', 'Ubuntu', sans-serif;
+ --font-sans: 'Open Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ --font-heading: 'Ubuntu', 'Open Sans', sans-serif;
}
/* ============================================================
@@ -104,6 +121,11 @@ a:hover { color: var(--accent-blue-light); }
ul, ol { list-style: none; }
address { font-style: normal; }
+:where(a, button, input, select, textarea):focus-visible {
+ outline: none;
+ box-shadow: var(--focus-ring);
+}
+
.sr-only {
position: absolute;
width: 1px; height: 1px;
@@ -181,18 +203,25 @@ h1, h2, h3, h4, h5, h6 {
Glass Card Component
============================================================ */
.glass-card {
- background: #ffffff;
- border: none;
+ background: var(--bg-surface);
+ border: 1px solid var(--border-default);
border-radius: var(--radius-md);
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
+ box-shadow: var(--shadow-sm);
transition: var(--transition-smooth);
overflow: hidden;
}
.glass-card:hover {
- background: #ffffff;
- box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
- transform: scale(1.02);
+ background: var(--bg-surface);
+ border-color: var(--border-default);
+ box-shadow: var(--shadow-sm);
+}
+
+a.glass-card:hover,
+.section-item-link:hover,
+.product-overview-card:has(.btn-primary):hover {
+ border-color: rgba(48, 81, 255, 0.24);
+ box-shadow: var(--shadow-md);
}
/* ============================================================
@@ -203,14 +232,16 @@ h1, h2, h3, h4, h5, h6 {
display: inline-flex;
align-items: center;
gap: 0.5rem;
- padding: 0.7rem 1.6rem;
- border-radius: var(--radius-pill);
- font-size: 0.9rem;
- font-weight: 600;
+ justify-content: center;
+ min-height: 44px;
+ padding: 0.65rem 1.25rem;
+ border-radius: var(--radius-sm);
+ font-size: 0.875rem;
+ font-weight: 700;
font-family: var(--font-sans);
cursor: pointer;
- border: none;
- transition: var(--transition-smooth);
+ border: 1px solid transparent;
+ transition: color .18s ease, background-color .18s ease, border-color .18s ease, box-shadow .18s ease, transform .18s ease;
white-space: nowrap;
text-decoration: none;
}
@@ -218,37 +249,37 @@ h1, h2, h3, h4, h5, h6 {
.btn-primary {
background: var(--accent-blue);
color: #fff;
- border: 2px solid transparent;
- border-radius: var(--radius-sm);
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
+ box-shadow: 0 5px 12px rgba(48, 81, 255, 0.18);
}
.btn-primary:hover {
- color: var(--accent-blue);
- background: transparent;
- border-color: var(--accent-blue);
- box-shadow: none;
- transform: none;
+ color: #fff;
+ background: var(--accent-blue-dark);
+ border-color: var(--accent-blue-dark);
+ box-shadow: 0 7px 16px rgba(48, 81, 255, 0.22);
+ transform: translateY(-1px);
}
.btn-ghost {
background: #fff;
- color: var(--accent-blue-dark);
- border: 2px solid transparent;
- border-radius: var(--radius-sm);
+ color: var(--accent-blue);
+ border-color: var(--border-strong);
+ box-shadow: var(--shadow-xs);
}
.btn-ghost:hover {
- background: var(--accent-blue);
+ background: var(--color-info-soft);
border-color: var(--accent-blue);
- color: #fff;
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1);
- transform: none;
+ color: var(--accent-blue-dark);
+ box-shadow: var(--shadow-xs);
+ transform: translateY(-1px);
}
-.btn-sm {
- padding: 0.5rem 1.1rem;
- font-size: 0.82rem;
+.btn-sm,
+.btn--sm {
+ min-height: 36px;
+ padding: 0.45rem 0.9rem !important;
+ font-size: 0.78rem !important;
}
/* ============================================================
@@ -394,6 +425,108 @@ h1, h2, h3, h4, h5, h6 {
padding: 0.5rem 1.25rem;
}
+.nav-link--cta:hover,
+.nav-link--cta:focus-visible,
+.nav-link--cta.active {
+ color: #fff !important;
+ background: var(--accent-blue);
+ border-color: var(--accent-blue);
+ box-shadow: none;
+}
+
+.navbar-account {
+ position: relative;
+ display: grid;
+ place-items: center;
+ width: 42px;
+ height: 42px;
+ margin-left: .35rem;
+ border: 1px solid var(--border-default);
+ border-radius: 50%;
+ background: #fff;
+ cursor: pointer;
+ list-style: none;
+}
+
+.navbar-account::-webkit-details-marker { display: none; }
+.navbar-account-menu { position: relative; }
+.navbar-account-menu > summary::marker { content: ''; }
+
+.navbar-account:hover {
+ border-color: var(--accent-blue);
+ box-shadow: var(--focus-ring);
+}
+
+.navbar-account__avatar {
+ display: grid;
+ place-items: center;
+ width: 34px;
+ height: 34px;
+ overflow: hidden;
+ border-radius: 50%;
+ background: linear-gradient(145deg, var(--accent-blue), #1736b8);
+ color: #fff;
+ font-size: .75rem;
+ font-weight: 700;
+}
+
+.navbar-account__avatar img { width: 100%; height: 100%; object-fit: cover; }
+.navbar-account__status { position: absolute; right: 1px; bottom: 1px; width: 10px; height: 10px; border: 2px solid #fff; border-radius: 50%; background: var(--color-success); }
+
+.navbar-account-dropdown {
+ position: absolute;
+ top: calc(100% + 12px);
+ right: 0;
+ width: min(320px, calc(100vw - 2rem));
+ padding: .65rem;
+ border: 1px solid var(--border-default);
+ border-radius: var(--radius-md);
+ background: #fff;
+ box-shadow: 0 18px 48px rgba(17, 24, 39, .16);
+ opacity: 0;
+ visibility: hidden;
+ transform: translateY(-6px);
+ pointer-events: none;
+ transition: opacity .16s ease, visibility .16s ease, transform .16s ease;
+ z-index: 1100;
+}
+
+.navbar-account-dropdown::before { content: ''; position: absolute; left: 0; right: 0; top: -12px; height: 12px; }
+
+.navbar-account-menu[open] .navbar-account-dropdown,
+.navbar-account-menu:focus-within .navbar-account-dropdown {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+ pointer-events: auto;
+}
+
+@media (min-width: 769px) {
+ .navbar-account-menu:hover .navbar-account-dropdown {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+ pointer-events: auto;
+ }
+}
+
+.navbar-account-dropdown__identity { display: grid; grid-template-columns: 44px minmax(0,1fr); gap: .75rem; align-items: center; padding: .65rem .7rem .85rem; }
+.navbar-account-dropdown__identity img { width: 44px; height: 44px; border-radius: 50%; object-fit: cover; }
+.navbar-account-dropdown__identity div { min-width: 0; display: grid; }
+.navbar-account-dropdown__identity strong { overflow: hidden; color: var(--text-primary); font-size: .82rem; text-overflow: ellipsis; white-space: nowrap; }
+.navbar-account-dropdown__identity span { overflow: hidden; color: var(--text-muted); font-size: .67rem; text-overflow: ellipsis; white-space: nowrap; }
+.navbar-account-dropdown__section { display: grid; gap: .15rem; padding: .5rem 0; border-top: 1px solid var(--border-default); }
+.navbar-account-dropdown__section a { display: grid; grid-template-columns: 28px minmax(0,1fr); gap: .55rem; align-items: center; min-height: 48px; padding: .5rem .7rem; border-radius: var(--radius-sm); color: #445066; }
+.navbar-account-dropdown__section a:hover { background: var(--color-info-soft); color: var(--accent-blue); }
+.navbar-account-dropdown__section a > span:first-child { display: grid; place-items: center; width: 28px; font: 700 1rem Arial,sans-serif; }
+.navbar-account-dropdown__section a > span:last-child { min-width: 0; display: grid; }
+.navbar-account-dropdown__section a strong { color: inherit; font-size: .75rem; }
+.navbar-account-dropdown__section a small { margin-top: .08rem; color: var(--text-muted); font-size: .62rem; }
+.navbar-account-dropdown__section--compact a { display: flex; min-height: 36px; padding: .45rem .7rem; font-size: .72rem; font-weight: 600; }
+.navbar-account-dropdown__section--compact form { margin: 0; }
+.navbar-account-dropdown__section--compact button { width: 100%; min-height: 36px; padding: .45rem .7rem; border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--color-danger); font: 600 .72rem var(--font-sans); text-align: left; cursor: pointer; }
+.navbar-account-dropdown__section--compact button:hover { background: var(--color-danger-soft); }
+
.nav-logout-form {
margin: 0;
}
@@ -2880,7 +3013,8 @@ a.citation-count-badge:hover {
width: 100%;
padding: 0.72rem 1rem;
border-radius: var(--radius-sm);
- border: 1px solid #d1d5db;
+ min-height: 44px;
+ border: 1px solid var(--border-strong);
background: #ffffff;
color: var(--text-primary);
font-family: inherit;
@@ -2892,9 +3026,10 @@ a.citation-count-badge:hover {
}
.form-group input:focus,
-.form-group textarea:focus {
+.form-group textarea:focus,
+.form-group select:focus {
border-color: var(--accent-blue);
- box-shadow: 0 0 0 3px rgba(48, 81, 255, 0.12);
+ box-shadow: var(--focus-ring);
background: #ffffff;
}
@@ -2905,15 +3040,37 @@ a.citation-count-badge:hover {
.form-group.has-error input,
.form-group.has-error textarea,
+.form-group.has-error select,
+.form-group:has(.errorlist) input,
+.form-group:has(.errorlist) textarea,
+.form-group:has(.errorlist) select,
.form-group.has-error input[type="file"] {
border-color: rgba(239, 68, 68, 0.5);
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
+.form-group input::placeholder,
+.form-group textarea::placeholder { color: #9099a8; opacity: 1; }
+
+.form-group input:disabled,
+.form-group textarea:disabled,
+.form-group select:disabled {
+ cursor: not-allowed;
+ background: #f2f4f7;
+ color: #7d8796;
+}
+
+.errorlist {
+ margin-top: .35rem;
+ color: var(--color-danger);
+ font-size: .76rem;
+ font-weight: 600;
+}
+
.form-field-error {
display: block;
font-size: 0.78rem;
- color: rgb(252, 165, 165);
+ color: var(--color-danger);
margin-top: 0.3rem;
min-height: 1em;
}
@@ -2947,11 +3104,6 @@ a.citation-count-badge:hover {
to { transform: rotate(360deg); }
}
-.btn--sm {
- padding: 0.45rem 1.1rem !important;
- font-size: 0.85rem !important;
-}
-
.contact-sidebar {
display: flex;
flex-direction: column;
@@ -3292,6 +3444,11 @@ a.citation-count-badge:hover {
font-size: 0.95rem;
}
+ .nav-item--account { padding-top: .5rem; border-top: 1px solid var(--border-default); }
+ .navbar-account { margin: 0; }
+ .navbar-account-dropdown { position: static; width: 100%; margin-top: .5rem; box-shadow: none; transform: none; }
+ .navbar-account-menu:not([open]) .navbar-account-dropdown { display: none; }
+
.nav-item.has-megamenu:hover .nav-arrow {
transform: none;
}
@@ -3951,14 +4108,14 @@ a.citation-count-badge:hover {
.flash-message--success,
.flash-message--info {
- background: rgba(16, 185, 129, 0.12);
- color: #047857;
+ background: var(--color-success-soft);
+ color: var(--color-success);
border: 1px solid rgba(16, 185, 129, 0.25);
}
.flash-message--error {
- background: rgba(239, 68, 68, 0.12);
- color: #b91c1c;
+ background: var(--color-danger-soft);
+ color: var(--color-danger);
border: 1px solid rgba(239, 68, 68, 0.25);
}
@@ -4077,6 +4234,179 @@ a.citation-count-badge:hover {
padding-left: 1.2rem;
}
+/* ============================================================
+ Authenticated client workspace
+ ============================================================ */
+.portal-body {
+ --dashboard-ink: #172033;
+ --dashboard-muted: #647087;
+ --dashboard-line: #e5e9f1;
+ --dashboard-canvas: #f5f7fb;
+ background: var(--dashboard-canvas);
+}
+
+.portal-body #main-content { min-height: calc(100vh - var(--navbar-height)); }
+
+.dashboard-shell {
+ display: grid;
+ grid-template-columns: 264px minmax(0, 1fr);
+ min-height: calc(100vh - var(--navbar-height));
+ max-width: 1600px;
+ margin: 0 auto;
+ background: var(--dashboard-canvas);
+}
+
+.dashboard-sidebar {
+ position: sticky;
+ top: var(--navbar-height);
+ height: calc(100vh - var(--navbar-height));
+ display: flex;
+ flex-direction: column;
+ padding: 1.75rem 1.15rem 1.25rem;
+ background: #fff;
+ border-right: 1px solid var(--dashboard-line);
+ z-index: 5;
+}
+
+.dashboard-sidebar__profile { display: flex; align-items: center; gap: .8rem; padding: 0 .55rem 1.6rem; }
+.dashboard-avatar,
+.profile-context-card__avatar {
+ display: grid; place-items: center; flex: 0 0 auto;
+ width: 42px; height: 42px; border-radius: 12px;
+ color: #fff; background: linear-gradient(145deg, var(--accent-blue), #1736b8);
+ font-weight: 700; box-shadow: 0 7px 18px rgba(48,81,255,.2);
+}
+.dashboard-avatar img,
+.profile-context-card__avatar img,
+.profile-avatar-preview img { width: 100%; height: 100%; object-fit: cover; }
+.dashboard-avatar img { border-radius: inherit; }
+.dashboard-sidebar__identity { min-width: 0; display: grid; }
+.dashboard-sidebar__identity strong { color: var(--dashboard-ink); font-size: .88rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.dashboard-sidebar__identity span { color: var(--dashboard-muted); font-size: .7rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.dashboard-nav { display: grid; gap: .28rem; }
+.dashboard-nav__label { margin: .3rem .75rem .55rem; color: #9aa3b3; font-size: .65rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; }
+.dashboard-nav__label--secondary { margin-top: 1.35rem; }
+.dashboard-nav__link { display: flex; align-items: center; gap: .78rem; min-height: 44px; padding: .65rem .75rem; border-radius: 9px; color: #505d73; font-size: .86rem; font-weight: 500; }
+.dashboard-nav__link:hover { color: var(--accent-blue); background: #f4f6ff; }
+.dashboard-nav__link.is-active { color: var(--accent-blue); background: #eef1ff; font-weight: 700; box-shadow: inset 3px 0 0 var(--accent-blue); }
+.dashboard-nav__icon { display: grid; place-items: center; width: 24px; height: 24px; color: currentColor; font-family: Arial, sans-serif; font-size: 1.05rem; font-weight: 700; }
+.dashboard-sidebar__footer { margin-top: auto; padding: 1rem .5rem 0; border-top: 1px solid var(--dashboard-line); }
+.dashboard-security-note { display: flex; gap: .6rem; margin-bottom: .8rem; color: var(--dashboard-muted); font-size: .68rem; line-height: 1.45; }
+.dashboard-security-note strong { display: block; color: #3d485b; font-size: .72rem; }
+.dashboard-security-note__mark { width: 8px; height: 8px; margin-top: 5px; flex: 0 0 auto; border-radius: 50%; background: #22a06b; box-shadow: 0 0 0 4px rgba(34,160,107,.1); }
+.dashboard-signout { width: 100%; padding: .6rem; border: 0; background: transparent; color: #7b8494; font: 600 .76rem var(--font-sans); text-align: left; cursor: pointer; }
+.dashboard-signout:hover { color: #b42318; }
+
+.dashboard-main { min-width: 0; padding: 2.4rem clamp(1.25rem, 3.5vw, 3.5rem) 4rem; }
+.dashboard-header { display: flex; justify-content: space-between; align-items: flex-end; gap: 2rem; max-width: 1180px; margin: 0 auto 2rem; }
+.dashboard-header > div:first-child { max-width: 720px; }
+.dashboard-eyebrow { margin-bottom: .45rem; color: var(--accent-gold) !important; font-size: .7rem !important; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; }
+.dashboard-header h1 { color: var(--dashboard-ink); font: 700 clamp(1.75rem, 3vw, 2.35rem)/1.15 var(--font-heading); letter-spacing: -.035em; }
+.dashboard-header p { margin-top: .55rem; color: var(--dashboard-muted); font-size: .92rem; }
+.dashboard-header__meta { margin-left: .45rem; }
+.dashboard-header__actions { display: flex; gap: .65rem; flex-wrap: wrap; }
+.dashboard-content { max-width: 1180px; margin: 0 auto; }
+.dashboard-content .btn-primary, .dashboard-header .btn-primary { box-shadow: 0 7px 16px rgba(48,81,255,.17); }
+
+.dashboard-metrics { display: grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap: 1rem; margin-bottom: 1.25rem; }
+.metric-card { display: grid; min-height: 154px; padding: 1.2rem 1.25rem; background: #fff; border: 1px solid var(--dashboard-line); border-radius: 13px; box-shadow: 0 2px 8px rgba(22,32,51,.035); }
+.metric-card__top { display: flex; align-items: center; justify-content: space-between; margin-bottom: .85rem; }
+.metric-card__icon { display: grid; place-items: center; width: 32px; height: 32px; border-radius: 9px; background: #eef1ff; color: var(--accent-blue); font: 700 .75rem var(--font-heading); }
+.metric-card__icon--gold { color: #9a6b0b; background: #fff6df; }
+.metric-card__icon--green { color: #087a53; background: #e9f8f2; }
+.metric-card__trend { color: #8b94a5; font-size: .66rem; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; }
+.metric-card > strong { color: var(--dashboard-ink); font: 700 1.55rem/1.15 var(--font-heading); letter-spacing: -.025em; }
+.metric-card > span { align-self: end; color: var(--dashboard-muted); font-size: .76rem; }
+
+.dashboard-panel { background: #fff; border: 1px solid var(--dashboard-line); border-radius: 13px; box-shadow: 0 2px 9px rgba(22,32,51,.035); overflow: hidden; }
+.dashboard-panel__header { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: 1.25rem 1.4rem; border-bottom: 1px solid var(--dashboard-line); }
+.dashboard-panel__header h2 { font-size: 1rem; letter-spacing: -.015em; }
+.dashboard-panel__header p { margin-top: .25rem; color: var(--dashboard-muted); font-size: .76rem; }
+.dashboard-panel__count { padding: .25rem .6rem; border-radius: 99px; background: #f1f3f7; color: #687386; font-size: .68rem; font-weight: 700; }
+.portal-table th, .portal-table td { border-color: var(--dashboard-line); }
+.portal-table th { padding: .78rem 1.15rem; background: #fafbfc; color: #7e8797; font-size: .65rem; letter-spacing: .07em; }
+.portal-table td { padding: 1rem 1.15rem; color: #4c586c; font-size: .78rem; }
+.portal-table tbody tr { transition: background .16s ease; }
+.portal-table tbody tr:hover { background: #fafbff; }
+.portal-table tbody tr:last-child td { border-bottom: 0; }
+.project-title-link { color: var(--dashboard-ink); font-weight: 700; }
+.project-title-link:hover, .table-action:hover { color: var(--accent-blue); }
+.table-muted { color: #9ba3b1; }
+.table-action { color: var(--accent-blue); font-size: .74rem; font-weight: 700; white-space: nowrap; }
+.status-pill { position: relative; gap: .35rem; padding: .28rem .58rem; font-size: .65rem; }
+.status-pill::before { content: ''; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
+.portal-empty { display: grid; justify-items: center; padding: 3.5rem 1.5rem; }
+.portal-empty__symbol { display: grid; place-items: center; width: 52px; height: 52px; margin-bottom: 1rem; border: 1px dashed #9daceb; border-radius: 14px; background: #f4f6ff; color: var(--accent-blue); font: 300 1.65rem Arial,sans-serif; }
+.portal-empty h3 { font-size: 1.05rem; }
+.portal-empty p { max-width: 500px; margin: .55rem auto 1.25rem; color: var(--dashboard-muted); font-size: .82rem; }
+
+.dashboard-form-layout { display: grid; grid-template-columns: minmax(0, 1fr) 280px; gap: 1.25rem; align-items: start; }
+.dashboard-form-layout--wizard { grid-template-columns: minmax(0, 1fr) 300px; }
+.dashboard-form-card { padding: 0 1.4rem 1.5rem; }
+.dashboard-form-card .dashboard-panel__header { margin: 0 -1.4rem 1.4rem; }
+.dashboard-form-card .form-group { margin-bottom: 1.15rem; }
+.dashboard-form-card .form-group label { color: #344054; font-size: .76rem; font-weight: 700; }
+.dashboard-form-card .form-group input:not([type=file]), .dashboard-form-card .form-group textarea, .dashboard-form-card .form-group select { min-height: 44px; border: 1px solid #d7dce5; border-radius: 8px; background: #fff; color: var(--dashboard-ink); font-size: .82rem; box-shadow: 0 1px 2px rgba(16,24,40,.03); }
+.dashboard-form-card .form-group textarea { min-height: 130px; }
+.dashboard-form-card .form-group input:focus, .dashboard-form-card .form-group textarea:focus, .dashboard-form-card .form-group select:focus { border-color: #7790ff; outline: 3px solid rgba(48,81,255,.09); box-shadow: none; }
+.dashboard-form-card .form-hint { color: #858fa0; font-size: .69rem; }
+.dashboard-form-actions { display: flex; justify-content: flex-end; padding-top: 1.1rem; border-top: 1px solid var(--dashboard-line); }
+.profile-context-card, .brief-guidance-card { padding: 1.5rem; border-radius: 13px; background: #172449; color: #fff; box-shadow: 0 12px 30px rgba(23,36,73,.14); }
+.profile-context-card__avatar { width: 54px; height: 54px; margin-bottom: 1rem; background: rgba(255,255,255,.14); box-shadow: none; }
+.profile-context-card__avatar { overflow: hidden; }
+.profile-context-card h2, .brief-guidance-card h2 { color: #fff; font-size: 1rem; }
+.profile-context-card > p { margin-top: .3rem; color: rgba(255,255,255,.65); font-size: .72rem; overflow-wrap: anywhere; }
+.profile-context-card__rule { height: 1px; margin: 1.35rem 0; background: rgba(255,255,255,.13); }
+.profile-context-card__label { display: block; margin-bottom: .5rem; color: rgba(255,255,255,.55); font-size: .62rem; letter-spacing: .08em; text-transform: uppercase; }
+.profile-context-card__expertise { margin-top: 1rem; padding: .65rem .75rem; border: 1px solid rgba(255,255,255,.12); border-radius: var(--radius-sm); color: rgba(255,255,255,.72); font-size: .68rem; line-height: 1.5; }
+.profile-status { display: flex; align-items: center; gap: .45rem; font-size: .72rem; }
+.profile-status span { width: 7px; height: 7px; border-radius: 50%; background: #47d7a0; box-shadow: 0 0 0 4px rgba(71,215,160,.12); }
+.brief-guidance-card__eyebrow { color: #e4ba65; font-size: .64rem; font-weight: 700; letter-spacing: .1em; text-transform: uppercase; }
+.brief-guidance-card h2 { margin-top: .55rem; line-height: 1.4; }
+.brief-guidance-card ul { display: grid; gap: 1.1rem; margin-top: 1.4rem; }
+.brief-guidance-card li { display: flex; gap: .7rem; }
+.brief-guidance-card li > span { display: grid; place-items: center; width: 24px; height: 24px; flex: 0 0 auto; border-radius: 7px; background: rgba(255,255,255,.1); color: #e4ba65; font-size: .67rem; font-weight: 700; }
+.brief-guidance-card li strong { color: #fff; font-size: .72rem; }
+.brief-guidance-card li p { margin-top: .15rem; color: rgba(255,255,255,.6); font-size: .67rem; line-height: 1.5; }
+.brief-guidance-card__note { margin-top: 1.4rem; padding-top: 1rem; border-top: 1px solid rgba(255,255,255,.12); color: rgba(255,255,255,.6); font-size: .67rem; }
+.portal-detail-grid .portal-card { padding: 1.5rem; }
+.profile-avatar-field { display: grid; grid-template-columns: 82px minmax(0, 1fr); gap: 1.1rem; align-items: center; padding: 1rem; border: 1px solid var(--dashboard-line); border-radius: var(--radius-md); background: #fafbfc; }
+.profile-avatar-preview { display: grid; place-items: center; width: 82px; height: 82px; overflow: hidden; border-radius: 18px; background: linear-gradient(145deg, var(--accent-blue), #1736b8); color: #fff; font: 700 1.35rem var(--font-heading); }
+.profile-avatar-field input[type="file"] { width: 100%; padding: .45rem; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: #fff; font-size: .75rem; }
+.profile-avatar-field input[type="checkbox"] { width: auto; min-height: auto; margin: .65rem .25rem 0 .75rem; }
+.profile-form-section { padding-top: 1.25rem; margin-top: 1.25rem; border-top: 1px solid var(--dashboard-line); }
+.profile-form-section h3 { margin-bottom: 1rem; color: #465166; font-size: .8rem; letter-spacing: .06em; text-transform: uppercase; }
+
+@media (max-width: 980px) {
+ .dashboard-shell { grid-template-columns: 78px minmax(0,1fr); }
+ .dashboard-sidebar { padding-inline: .7rem; }
+ .dashboard-sidebar__profile { justify-content: center; padding-inline: 0; }
+ .dashboard-sidebar__identity, .dashboard-nav__label, .dashboard-nav__link span:last-child, .dashboard-sidebar__footer { display: none; }
+ .dashboard-nav__link { justify-content: center; padding-inline: 0; }
+ .dashboard-form-layout, .dashboard-form-layout--wizard { grid-template-columns: 1fr; }
+ .profile-context-card, .brief-guidance-card { order: -1; }
+}
+
+@media (max-width: 700px) {
+ .dashboard-shell { display: block; }
+ .dashboard-sidebar { position: static; width: 100%; height: auto; padding: .65rem 1rem; border-right: 0; border-bottom: 1px solid var(--dashboard-line); }
+ .dashboard-sidebar__profile, .dashboard-nav__label, .dashboard-sidebar__footer { display: none; }
+ .dashboard-nav { display: flex; overflow-x: auto; }
+ .dashboard-nav__link { flex: 0 0 auto; min-height: 38px; padding: .45rem .7rem; white-space: nowrap; }
+ .dashboard-nav__link span:last-child { display: inline; }
+ .dashboard-nav__icon { width: 18px; }
+ .dashboard-main { padding: 1.5rem 1rem 3rem; }
+ .dashboard-header { align-items: flex-start; flex-direction: column; gap: 1rem; }
+ .dashboard-header__actions, .dashboard-header__actions .btn-primary { width: 100%; }
+ .dashboard-header__actions .btn-primary { justify-content: center; }
+ .dashboard-metrics { grid-template-columns: 1fr; }
+ .metric-card { min-height: 128px; }
+ .dashboard-panel__header { align-items: flex-start; }
+ .portal-table { min-width: 730px; }
+ .form-grid--2 { grid-template-columns: 1fr; }
+ .profile-avatar-field { grid-template-columns: 1fr; }
+}
+
@media (max-width: 768px) {
.form-grid--2,
.portal-detail-grid {
@@ -4270,3 +4600,13 @@ a.citation-count-badge:hover {
transform: rotate(360deg);
}
}
+
+@media (prefers-reduced-motion: reduce) {
+ html { scroll-behavior: auto; }
+ *, *::before, *::after {
+ animation-duration: .01ms !important;
+ animation-iteration-count: 1 !important;
+ scroll-behavior: auto !important;
+ transition-duration: .01ms !important;
+ }
+}
diff --git a/static/images/tecvico/default-avatar.png b/static/images/tecvico/default-avatar.png
new file mode 100644
index 0000000..a174c4f
Binary files /dev/null and b/static/images/tecvico/default-avatar.png differ
diff --git a/static/images/tecvico/hero-spectral.svg b/static/images/tecvico/hero-spectral.svg
new file mode 100644
index 0000000..061865d
--- /dev/null
+++ b/static/images/tecvico/hero-spectral.svg
@@ -0,0 +1,36 @@
+
diff --git a/static/js/main.js b/static/js/main.js
index b0c1db3..71e9918 100644
--- a/static/js/main.js
+++ b/static/js/main.js
@@ -353,6 +353,24 @@ function initProjectFilters() {
});
}
+function initAccountMenu() {
+ const accountMenu = document.querySelector('.navbar-account-menu');
+ if (!accountMenu) return;
+
+ document.addEventListener('click', (event) => {
+ if (accountMenu.open && !accountMenu.contains(event.target)) {
+ accountMenu.removeAttribute('open');
+ }
+ });
+
+ document.addEventListener('keydown', (event) => {
+ if (event.key === 'Escape' && accountMenu.open) {
+ accountMenu.removeAttribute('open');
+ accountMenu.querySelector('summary')?.focus();
+ }
+ });
+}
+
function init() {
initNavbarScroll();
initMobileMenu();
@@ -362,6 +380,7 @@ function init() {
initReadMore();
initCitationCopy();
initProjectFilters();
+ initAccountMenu();
}
if (document.readyState === 'loading') {
diff --git a/templates/accounts/profile.html b/templates/accounts/profile.html
index 7cb01b3..c15d929 100644
--- a/templates/accounts/profile.html
+++ b/templates/accounts/profile.html
@@ -1,64 +1,64 @@
-{% extends "base.html" %}
+{% extends "portal_base.html" %}
+{% load static %}
-{% block title %}Profile{% endblock %}
+{% block title %}Profile & Account{% endblock %}
+{% block portal_title %}Profile & account{% endblock %}
+{% block portal_subtitle %}Build a complete professional profile so Tecvico can understand your work and communicate clearly.{% endblock %}
-{% block content %}
- Keep your contact details up to date for project communication.Your Profile
-
Your profile information is visible only to you and the Tecvico team.
You haven't provided an email address. Without it, we won't be able to respond to your message. Would you like to add one, or send without a reply address?
Thank you for reaching out. We've received your message and will get back to you as soon as possible.
- + @@ -177,7 +177,7 @@Please check your answers and try again.
- + diff --git a/templates/pages/faq.html b/templates/pages/faq.html index 5574277..3c55d73 100644 --- a/templates/pages/faq.html +++ b/templates/pages/faq.html @@ -30,6 +30,7 @@ {% for entry in faq_entries %}