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 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + GLCM + 0.847 + SUV + 4.2 + + + 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 %} -
-
-
-

Your Profile

-

Keep your contact details up to date for project communication.

-
-
-
+{% block portal_content %} +
+
+

Professional profile

Your profile information is visible only to you and the Tecvico team.

+
+ {% csrf_token %} + {% if form.non_field_errors %}
{{ form.non_field_errors }}
{% endif %} -
-
-
- - {% csrf_token %} +
+ +
+ + {{ form.avatar }} +

JPG, PNG, or WebP. Maximum 5 MB. A square image works best.

+ {{ form.avatar.errors }} +
+
+
+

Identity

-
- - {{ form.first_name }} - {{ form.first_name.errors }} -
-
- - {{ form.last_name }} - {{ form.last_name.errors }} -
-
- -
- - {{ form.email }} - {{ form.email.errors }} +
{{ form.first_name }}{{ form.first_name.errors }}
+
{{ form.last_name }}{{ form.last_name.errors }}
+
{{ form.email }}

Used for project updates and secure account communication.

{{ form.email.errors }}
+
+
+

Professional information

-
- - {{ form.company }} - {{ form.company.errors }} -
-
- - {{ form.phone }} - {{ form.phone.errors }} -
+
{{ form.company }}{{ form.company.errors }}
+
{{ form.job_title }}{{ form.job_title.errors }}
+
{{ form.bio }}

Up to 600 characters about your research, technical work, or project interests.

{{ form.bio.errors }}
+
-
- - {{ form.timezone }} - {{ form.timezone.errors }} -
+
+

Contact details

+
{{ form.phone }}{{ form.phone.errors }}
+
- - -
-
-
+
+ +
+ +
{% endblock %} diff --git a/templates/accounts/signup.html b/templates/accounts/signup.html index 37b4137..d69c069 100644 --- a/templates/accounts/signup.html +++ b/templates/accounts/signup.html @@ -67,12 +67,6 @@ -
- - {{ form.timezone }} - {{ form.timezone.errors }} -
-
diff --git a/templates/base.html b/templates/base.html index 72cdf33..c3c9414 100644 --- a/templates/base.html +++ b/templates/base.html @@ -13,7 +13,7 @@ {% block extra_css %}{% endblock %} - + {% include "partials/_navbar.html" %} @@ -23,7 +23,7 @@ {% block content %}{% endblock %} - {% include "partials/_footer.html" %} + {% block footer %}{% include "partials/_footer.html" %}{% endblock %} {% block extra_js %}{% endblock %} diff --git a/templates/pages/contact.html b/templates/pages/contact.html index b17ea17..f4f51fd 100644 --- a/templates/pages/contact.html +++ b/templates/pages/contact.html @@ -149,8 +149,8 @@
@@ -164,7 +164,7 @@ - + @@ -177,7 +177,7 @@ - + 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 %}
- + {% else %}
+{% endblock %} + +{% block footer %}{% endblock %} diff --git a/templates/projects/brief_detail.html b/templates/projects/brief_detail.html index f6eec0c..6d0ea00 100644 --- a/templates/projects/brief_detail.html +++ b/templates/projects/brief_detail.html @@ -1,33 +1,14 @@ -{% extends "base.html" %} +{% extends "portal_base.html" %} {% block title %}{{ brief.title }}{% endblock %} -{% block content %} -
-
-
-
- {% if brief.category %}
{{ brief.get_category_display }}
{% endif %} -

{{ brief.title }}

-

- {{ brief.get_status_display }} - · Submitted {{ brief.submitted_at|date:"M j, Y" }} -

-
-
- Back to list - {% if brief.is_editable_by_client %} - Edit brief - {% endif %} -
-
-
-
+{% block portal_title %}{{ brief.title }}{% endblock %} +{% block portal_subtitle %}{{ brief.get_status_display }} Submitted {{ brief.submitted_at|date:"M j, Y" }}{% endblock %} +{% block portal_actions %}Back to projects{% if brief.is_editable_by_client %}Edit brief{% endif %}{% endblock %} -
-
+{% block portal_content %}
-
+

Overview

{% if brief.budget_range %}
Budget
{{ brief.get_budget_range_display }}
{% endif %} @@ -56,19 +37,17 @@
{% if brief.shows_quote_to_client %} -
+

Quote from Tecvico

{{ brief.quote_text|linebreaksbr }}

{% endif %} {% if brief.has_website_notes %} -
+

Notes from Tecvico

{{ brief.website_notes|linebreaksbr }}

{% endif %}
-
-
{% endblock %} diff --git a/templates/projects/brief_wizard.html b/templates/projects/brief_wizard.html index 52a573e..6d54309 100644 --- a/templates/projects/brief_wizard.html +++ b/templates/projects/brief_wizard.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends "portal_base.html" %} {% load static %} {% block title %}{{ form_title }}{% endblock %} @@ -7,19 +7,12 @@ {% endblock %} -{% block content %} -
-
-
-

{{ form_title }}

-

Complete each step to submit your project brief.

-
-
-
+{% block portal_title %}{{ form_title }}{% endblock %} +{% block portal_subtitle %}Turn your scientific or technical challenge into a clear, review-ready scope.{% endblock %} -
-
-
+{% block portal_content %} +
+
{% csrf_token %} @@ -140,9 +133,18 @@
-
-
-
+ + + {% endblock %} {% block extra_js %} diff --git a/templates/projects/dashboard.html b/templates/projects/dashboard.html index d77bf7a..e1a2082 100644 --- a/templates/projects/dashboard.html +++ b/templates/projects/dashboard.html @@ -1,70 +1,60 @@ -{% extends "base.html" %} +{% extends "portal_base.html" %} -{% block title %}My Projects{% endblock %} +{% block title %}Project Dashboard{% endblock %} +{% block portal_title %}Welcome back{% if user.first_name %}, {{ user.first_name }}{% endif %}.{% endblock %} +{% block portal_subtitle %}Manage your research and technology projects from one focused workspace.{% endblock %} +{% block portal_actions %}Start a new brief {% endblock %} -{% block content %} -
-
-
-
-
Client Portal
-

My Projects

-

Track briefs you've submitted to the Tecvico team.

-
- New project brief -
-
+{% block portal_content %} +
+
+
PPortfolio
+ {{ page_obj.paginator.count|default:0 }} + Total project brief{{ page_obj.paginator.count|pluralize }} +
+
+
RStructured
+ 4 steps + From scientific scope to delivery +
+
+
Private
+ Secure + Visible only to you and Tecvico +
-
-
- {% if briefs %} -
- - - - - - - - - - - - - {% for brief in briefs %} - - - - - - - - - {% endfor %} - -
TitleCategoryStatusBudgetSubmitted
{{ brief.title }}{% if brief.category %}{{ brief.get_category_display }}{% else %}—{% endif %}{{ brief.get_status_display }}{% if brief.budget_range %}{{ brief.get_budget_range_display }}{% else %}—{% endif %}{{ brief.submitted_at|date:"M j, Y" }}View
-
- - {% if page_obj.has_other_pages %} -
- {% if page_obj.has_previous %} - Previous - {% endif %} - Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }} - {% if page_obj.has_next %} - Next - {% endif %} -
- {% endif %} - - {% else %} -
-

No project briefs yet

-

Describe what you need and our team will review your request.

- Submit your first brief -
- {% endif %} +
+
+

Your project briefs

Track submissions, quotes, and delivery progress.

+ {% if briefs %}{{ page_obj.paginator.count }} total{% endif %}
+ {% if briefs %} +
+ + + + {% for brief in briefs %} + + + + + + + + + {% endfor %} + +
ProjectFieldStatusBudgetSubmittedActions
{{ brief.title }}{% if brief.category %}{{ brief.get_category_display }}{% else %}Not set{% endif %}{{ brief.get_status_display }}{% if brief.budget_range %}{{ brief.get_budget_range_display }}{% else %}Not set{% endif %}{{ brief.submitted_at|date:"M j, Y" }}View
+
+ {% if page_obj.has_other_pages %}
{% if page_obj.has_previous %}Previous{% endif %}Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}{% if page_obj.has_next %}Next{% endif %}
{% endif %} + {% else %} +
+ +

Create your first project brief

+

Define the scientific challenge, goals, timeline, and supporting materials. You can review everything before submitting.

+ Create project brief +
+ {% endif %}
{% endblock %}