From c28fc8154479cfc4d6cf3f1e806f24e4afe7793f Mon Sep 17 00:00:00 2001
From: Shashank Bangalore Lakshman
Date: Tue, 19 May 2026 16:58:26 -0700
Subject: [PATCH 1/4] Build PI-hub: research dashboard for the eSPUD PI
A Next.js + TypeScript app for managing a Principal Investigator's
research practice. Single PI runs many projects, each with its own
team, plan, setup, log, artifacts, and target venues.
Features:
- PI profile (identity, contact, online profiles, focus areas,
expertise, education, publications)
- Projects (status, plan, setup, Discord + Overleaf links,
contributors, artifacts, append-only exploration log)
- First-class team Members with avatars, role, expertise, and
per-project assignment toggles
- Call-for-paper deadline tracking with timeline visualization, live
D:HH:MM:SS countdown, and per-assignment assessment status
(not started / in progress / submitted / accepted / rejected /
late / abandoned)
- Dashboard with greeting, breadcrumb path, status overview donut,
top-3 upcoming deadlines, activity feed, contributor leaderboard
- Global URL-backed search across projects, members, expertise,
and CFP topics
- Archive instead of delete; /archives page restores or permanently
deletes items
- One-click PDF report export per project (@react-pdf/renderer)
Storage: markdown files under content/ (YAML frontmatter is the
source of truth; body is auto-generated). No external database.
Co-Authored-By: Claude Opus 4.7
---
.gitignore | 8 +
CONTRIBUTING.md | 118 ++
README.md | 66 +-
app/api/cfps/[id]/route.ts | 45 +
app/api/cfps/route.ts | 36 +
app/api/members/[id]/route.ts | 45 +
app/api/members/route.ts | 34 +
app/api/pi/route.ts | 46 +
app/api/projects/[id]/report/route.ts | 41 +
app/api/projects/[id]/route.ts | 34 +
app/api/projects/route.ts | 38 +
app/api/store/route.ts | 9 +
app/archives/page.tsx | 155 +++
app/cfps/[id]/page.tsx | 219 ++++
app/cfps/new/page.tsx | 82 ++
app/cfps/page.tsx | 269 +++++
app/globals.css | 962 +++++++++++++++
app/icon.png | Bin 0 -> 83360 bytes
app/layout.tsx | 30 +
app/members/[id]/page.tsx | 201 ++++
app/members/new/page.tsx | 66 +
app/members/page.tsx | 110 ++
app/page.tsx | 311 +++++
app/pi/page.tsx | 344 ++++++
app/projects/[id]/page.tsx | 670 +++++++++++
app/projects/new/page.tsx | 53 +
app/projects/page.tsx | 108 ++
components/Bits.tsx | 190 +++
components/Nav.tsx | 126 ++
components/Topbar.tsx | 100 ++
content/.gitkeep | 0
content/pi.md | 100 ++
lib/fs-store.ts | 403 +++++++
lib/report.tsx | 396 ++++++
lib/store.tsx | 354 ++++++
lib/types.ts | 146 +++
next.config.mjs | 3 +
package-lock.json | 1603 +++++++++++++++++++++++++
package.json | 25 +
public/logo.png | Bin 0 -> 83360 bytes
tsconfig.json | 21 +
41 files changed, 7566 insertions(+), 1 deletion(-)
create mode 100644 .gitignore
create mode 100644 CONTRIBUTING.md
create mode 100644 app/api/cfps/[id]/route.ts
create mode 100644 app/api/cfps/route.ts
create mode 100644 app/api/members/[id]/route.ts
create mode 100644 app/api/members/route.ts
create mode 100644 app/api/pi/route.ts
create mode 100644 app/api/projects/[id]/report/route.ts
create mode 100644 app/api/projects/[id]/route.ts
create mode 100644 app/api/projects/route.ts
create mode 100644 app/api/store/route.ts
create mode 100644 app/archives/page.tsx
create mode 100644 app/cfps/[id]/page.tsx
create mode 100644 app/cfps/new/page.tsx
create mode 100644 app/cfps/page.tsx
create mode 100644 app/globals.css
create mode 100644 app/icon.png
create mode 100644 app/layout.tsx
create mode 100644 app/members/[id]/page.tsx
create mode 100644 app/members/new/page.tsx
create mode 100644 app/members/page.tsx
create mode 100644 app/page.tsx
create mode 100644 app/pi/page.tsx
create mode 100644 app/projects/[id]/page.tsx
create mode 100644 app/projects/new/page.tsx
create mode 100644 app/projects/page.tsx
create mode 100644 components/Bits.tsx
create mode 100644 components/Nav.tsx
create mode 100644 components/Topbar.tsx
create mode 100644 content/.gitkeep
create mode 100644 content/pi.md
create mode 100644 lib/fs-store.ts
create mode 100644 lib/report.tsx
create mode 100644 lib/store.tsx
create mode 100644 lib/types.ts
create mode 100644 next.config.mjs
create mode 100644 package-lock.json
create mode 100644 package.json
create mode 100644 public/logo.png
create mode 100644 tsconfig.json
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6eef131
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules
+.next
+out
+dist
+.env*.local
+.DS_Store
+*.tsbuildinfo
+next-env.d.ts
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..9525286
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,118 @@
+# Contributing to PI-hub
+
+A short guide to setting up, validating, and deploying PI-hub.
+
+## Prerequisites
+
+- **Node.js 20+** (tested with 22). On macOS, you may have multiple node installs — verify with `node --version`.
+- **npm** (bundled with Node).
+
+If a Homebrew node install is broken (missing dylibs), prefer the `/usr/local/bin` install or use a version manager like `nvm`/`fnm`.
+
+## Local development
+
+```bash
+npm install
+npm run dev
+```
+
+Then open http://localhost:3000. The dev server hot-reloads on file changes.
+
+Data is read/written under `content/` (gitignored content lives next to checked-in seed data). If `content/` is empty, the app starts with a default PI shell and no projects.
+
+## Project layout
+
+```
+app/
+├── layout.tsx # shell + StoreProvider
+├── page.tsx # dashboard
+├── pi/page.tsx # PI profile editor
+├── projects/ # list, new, detail
+├── members/ # list, new, detail
+├── cfps/ # list, new, detail (deadlines)
+├── archives/page.tsx # restore archived items
+└── api/ # REST routes that read/write content/*.md
+ ├── store/route.ts # combined store snapshot
+ ├── pi/route.ts
+ ├── projects/[id]/route.ts
+ ├── projects/[id]/report/route.ts # PDF export
+ ├── members/[id]/route.ts
+ └── cfps/[id]/route.ts
+components/
+├── Nav.tsx # sidebar with SVG icons
+├── Topbar.tsx # greeting, breadcrumb, search, add buttons
+└── Bits.tsx # shared UI bits: avatars, badges, countdown
+lib/
+├── types.ts # PI, Project, Member, CallForPaper, etc.
+├── store.tsx # client store + mutations via fetch
+├── fs-store.ts # server-only filesystem read/write
+└── report.tsx # @react-pdf/renderer Document
+content/ # data lives here (markdown files)
+public/ # static assets (logo.png)
+```
+
+## Conventions
+
+- **Server-only modules** under `lib/fs-store.ts` and `lib/report.tsx` may use Node APIs (`fs`, `path`). Never import them from a client component.
+- **Client mutations** go through `useStore()` (`lib/store.tsx`). Mutations are optimistic on the client and persisted by calling the matching API route.
+- **Markdown round-trip:** YAML frontmatter is the source of truth. The body is regenerated on every write — never expect to round-trip arbitrary body edits.
+- **Archive, don't delete.** UI exposes "Archive" (sets `archivedAt`). The DELETE API endpoints exist but are not exercised by the UI. The `/archives` page restores items by clearing `archivedAt`.
+
+## Validation
+
+Before pushing changes, run:
+
+```bash
+npm run lint # ESLint via next lint
+npm run build # full type-check + production build
+```
+
+`npm run build` is the most thorough check — it catches TS errors that `next dev` swallows, and confirms the route tree compiles.
+
+Also worth verifying manually:
+
+- Open `/`, `/projects`, `/members`, `/cfps`, `/archives`, `/pi`, plus a project / member / CFP detail page.
+- Create one of each entity, assign relationships, archive, restore.
+- Click **Export PDF** on a project and open the file — verify there's no overlapping text and pagination is clean.
+- Check the topbar breadcrumb resolves UUIDs to friendly names.
+
+## Deployment to Vercel
+
+1. Push the repo to GitHub.
+2. Import the repo on https://vercel.com — accept the default Next.js settings; no env vars required.
+3. Set the production branch (typically `main`).
+4. Deploy.
+
+The Next.js framework preset on Vercel handles the build. The `@react-pdf/renderer` PDF endpoint runs as a Node serverless function (`export const runtime = "nodejs"` is already set in `app/api/projects/[id]/report/route.ts`).
+
+### About the read-only filesystem
+
+Vercel's serverless functions cannot persist writes to the project filesystem. That means:
+
+- **Reads work** for whatever you committed under `content/` at deploy time.
+- **Writes via the UI** appear to succeed but do not persist across requests / cold starts.
+
+To update production data: edit locally, commit the `content/*.md` files, push, and let Vercel redeploy. For a writable production deployment, self-host (Render, Fly, a VM) with a persistent filesystem mounted at `content/`.
+
+## Adding a new entity
+
+When adding a new top-level entity (similar to Project / Member / CFP):
+
+1. Define the type in `lib/types.ts` and add it to the `Store` shape.
+2. Add `coerce`, `read`, `readAll`, `write`, `delete`, and a `render…Body` helper to `lib/fs-store.ts`. Include the entity directory in `ensureDirs()`. Make sure `readStore()` returns it.
+3. Add `app/api//route.ts` (GET, POST) and `app/api//[id]/route.ts` (GET, PATCH, DELETE).
+4. Extend the client `useStore()` in `lib/store.tsx` with add / update / archive (= update with `archivedAt`) functions.
+5. Build pages under `app//`: list, new, detail.
+6. Add a sidebar entry in `components/Nav.tsx` (include an SVG icon in the `Icon` component switch).
+7. Filter archived items out of the list pages; surface them on `/archives` with a Restore button.
+
+## Reporting bugs / requesting features
+
+Open an issue with:
+
+- What you expected
+- What happened instead
+- Steps to reproduce (which page, which entity, which click)
+- Browser + Node version
+
+For visual issues, a screenshot saves a thousand words.
diff --git a/README.md b/README.md
index e0b1b38..f3b6e64 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,66 @@
# PI-hub
-Principal Investigator Hub
+
+A research hub for a Principal Investigator: a single place to plan projects, track teams, watch submission deadlines, and keep a durable log of what was tried and learned.
+
+## Who this is for
+
+PI-hub serves the PI of **eSPUD** — an independent research collective exploring on-device AI: models small enough to run on phones, sensors, wearables, and constrained hardware. We believe meaningful intelligence shouldn't require a datacenter round-trip, and that AI ethics — data use, consent, bias, downstream impact — should be reviewed before anything leaves the lab. We publish what we learn, including what didn't work.
+
+The app assumes one PI managing many projects, each with its own contributors, plan, setup, log, artifacts, and target venues.
+
+## Features
+
+- **PI profile** — name, title, pronouns, affiliation, location, contact, online profiles (ORCID, Scholar, GitHub, LinkedIn, X), bio, focus areas, expertise tags, education and selected publications.
+- **Projects** — name, description, status (exploration / planning / active / paused / archived), Discord link, research plan, research setup, contributor lists, artifacts, append-only exploration log.
+- **Members** — first-class team roster with avatars, role, affiliation, GitHub, expertise tags, and per-project assignment toggles. Names are unique within a project.
+- **Call-for-paper deadlines** — track venues with abstract / submission / notification / conference dates, topics, location, notes. A live timeline visualizes the next 6 months with color-coded urgency (green → amber → red as the deadline approaches; gray once passed).
+- **Live countdown** — the next upcoming deadline is shown as a big D : HH : MM : SS ticker, updating every second. Compact countdowns appear on every CFP card and project assignment.
+- **Assessment scoring** — each project assigned to a CFP carries a status: `not started`, `in progress`, `submitted`, `accepted`, `rejected`, `late`, `abandoned`. Statuses surface as color-coded badges everywhere the assignment appears.
+- **Dashboard** — greeting, breadcrumb path, status overview donut, recent activity feed, top contributors, project table with avatar stacks and log-activity progress.
+- **Global search** — searches projects, descriptions, members, expertise, and CFP topics. The query lives in the URL so it survives refresh.
+- **Archives** — projects, members, and CFPs are archived rather than deleted. A dedicated `/archives` page lists everything that's been put away and lets you restore any item back to the active lists. Project assignments and CFP relationships are preserved across archive/restore.
+- **PDF export** — every project has an "Export PDF" button that generates a clean, well-spaced report including the plan, setup, team, target venues with status, artifacts, and the full exploration log.
+
+## Stack
+
+- Next.js 15 (App Router) + React 19 + TypeScript
+- Plain CSS (no UI framework)
+- `@react-pdf/renderer` for PDF generation
+- `js-yaml` for markdown frontmatter parsing
+- **Storage: markdown files on disk** — no external database
+
+## Storage model
+
+Everything lives under `content/`:
+
+```
+content/
+├── pi.md # the single PI profile
+├── projects/.md # one file per project
+├── members/.md # one file per team member
+└── cfps/.md # one file per call for papers
+```
+
+Each file has YAML frontmatter (the source of truth) plus a generated, human-readable markdown body. Editing in the UI rewrites the corresponding file via the API routes under `app/api/`.
+
+Relationships are stored on the parent: a project's `memberIds: []` references members, and its `cfpAssignments: [{cfpId, status, notes, assignedAt}]` references CFPs. Archived items carry an `archivedAt: ` field; empty means active.
+
+## Quick start
+
+```bash
+npm install
+npm run dev
+```
+
+Open http://localhost:3000.
+
+## Deployment
+
+The app deploys cleanly as a Next.js project on Vercel. The serverless filesystem is read-only at runtime, so:
+
+- **Reads work fine.** Whatever is committed under `content/` shows up in production.
+- **Writes don't persist on Vercel.** To update production data, edit locally, commit the markdown files in `content/`, and redeploy.
+
+For a self-hosted node deployment, writes persist normally on the host's filesystem.
+
+See [CONTRIBUTING.md](./CONTRIBUTING.md) for development, validation, and deployment details.
diff --git a/app/api/cfps/[id]/route.ts b/app/api/cfps/[id]/route.ts
new file mode 100644
index 0000000..f830bf2
--- /dev/null
+++ b/app/api/cfps/[id]/route.ts
@@ -0,0 +1,45 @@
+import { NextResponse } from "next/server";
+import { deleteCFP, readCFP, readProjects, writeCFP, writeProject } from "@/lib/fs-store";
+import type { CallForPaper } from "@/lib/types";
+
+export const dynamic = "force-dynamic";
+
+export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const cfp = await readCFP(id);
+ if (!cfp) return NextResponse.json({ error: "not found" }, { status: 404 });
+ return NextResponse.json(cfp);
+}
+
+export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const existing = await readCFP(id);
+ if (!existing) return NextResponse.json({ error: "not found" }, { status: 404 });
+ const patch = (await req.json()) as Partial;
+ const updated: CallForPaper = {
+ ...existing,
+ ...patch,
+ id: existing.id,
+ createdAt: existing.createdAt,
+ updatedAt: new Date().toISOString(),
+ topics: Array.isArray(patch.topics) ? patch.topics.filter((x): x is string => typeof x === "string") : existing.topics,
+ };
+ await writeCFP(updated);
+ return NextResponse.json(updated);
+}
+
+export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ await deleteCFP(id);
+ const projects = await readProjects();
+ for (const p of projects) {
+ if (p.cfpAssignments.some((a) => a.cfpId === id)) {
+ await writeProject({
+ ...p,
+ cfpAssignments: p.cfpAssignments.filter((a) => a.cfpId !== id),
+ updatedAt: new Date().toISOString(),
+ });
+ }
+ }
+ return NextResponse.json({ ok: true });
+}
diff --git a/app/api/cfps/route.ts b/app/api/cfps/route.ts
new file mode 100644
index 0000000..2874ffc
--- /dev/null
+++ b/app/api/cfps/route.ts
@@ -0,0 +1,36 @@
+import { NextResponse } from "next/server";
+import { randomUUID } from "crypto";
+import { readCFPs, writeCFP } from "@/lib/fs-store";
+import type { CallForPaper } from "@/lib/types";
+
+export const dynamic = "force-dynamic";
+
+export async function GET() {
+ return NextResponse.json(await readCFPs());
+}
+
+export async function POST(req: Request) {
+ const data = (await req.json()) as Partial;
+ if (!data.name || !data.name.trim()) {
+ return NextResponse.json({ error: "name is required" }, { status: 400 });
+ }
+ const now = new Date().toISOString();
+ const cfp: CallForPaper = {
+ id: randomUUID(),
+ name: data.name.trim(),
+ venue: (data.venue ?? "").trim(),
+ url: (data.url ?? "").trim(),
+ abstractDeadline: (data.abstractDeadline ?? "").trim(),
+ submissionDeadline: (data.submissionDeadline ?? "").trim(),
+ notificationDate: (data.notificationDate ?? "").trim(),
+ conferenceDate: (data.conferenceDate ?? "").trim(),
+ location: (data.location ?? "").trim(),
+ topics: Array.isArray(data.topics) ? data.topics.filter((x): x is string => typeof x === "string") : [],
+ notes: (data.notes ?? "").trim(),
+ archivedAt: "",
+ createdAt: now,
+ updatedAt: now,
+ };
+ await writeCFP(cfp);
+ return NextResponse.json(cfp);
+}
diff --git a/app/api/members/[id]/route.ts b/app/api/members/[id]/route.ts
new file mode 100644
index 0000000..f1c6279
--- /dev/null
+++ b/app/api/members/[id]/route.ts
@@ -0,0 +1,45 @@
+import { NextResponse } from "next/server";
+import { deleteMember, readMember, readProjects, writeMember, writeProject } from "@/lib/fs-store";
+import type { Member } from "@/lib/types";
+
+export const dynamic = "force-dynamic";
+
+export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const member = await readMember(id);
+ if (!member) return NextResponse.json({ error: "not found" }, { status: 404 });
+ return NextResponse.json(member);
+}
+
+export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const existing = await readMember(id);
+ if (!existing) return NextResponse.json({ error: "not found" }, { status: 404 });
+ const patch = (await req.json()) as Partial;
+ const updated: Member = {
+ ...existing,
+ ...patch,
+ id: existing.id,
+ createdAt: existing.createdAt,
+ updatedAt: new Date().toISOString(),
+ expertise: Array.isArray(patch.expertise) ? patch.expertise.filter((x): x is string => typeof x === "string") : existing.expertise,
+ };
+ await writeMember(updated);
+ return NextResponse.json(updated);
+}
+
+export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ await deleteMember(id);
+ const projects = await readProjects();
+ for (const p of projects) {
+ if (p.memberIds.includes(id)) {
+ await writeProject({
+ ...p,
+ memberIds: p.memberIds.filter((mid) => mid !== id),
+ updatedAt: new Date().toISOString(),
+ });
+ }
+ }
+ return NextResponse.json({ ok: true });
+}
diff --git a/app/api/members/route.ts b/app/api/members/route.ts
new file mode 100644
index 0000000..b148e45
--- /dev/null
+++ b/app/api/members/route.ts
@@ -0,0 +1,34 @@
+import { NextResponse } from "next/server";
+import { randomUUID } from "crypto";
+import { readMembers, writeMember } from "@/lib/fs-store";
+import type { Member } from "@/lib/types";
+
+export const dynamic = "force-dynamic";
+
+export async function GET() {
+ return NextResponse.json(await readMembers());
+}
+
+export async function POST(req: Request) {
+ const data = (await req.json()) as Partial;
+ if (!data.name || !data.name.trim()) {
+ return NextResponse.json({ error: "name is required" }, { status: 400 });
+ }
+ const now = new Date().toISOString();
+ const member: Member = {
+ id: randomUUID(),
+ name: data.name.trim(),
+ role: (data.role ?? "").trim(),
+ email: (data.email ?? "").trim(),
+ affiliation: (data.affiliation ?? "").trim(),
+ bio: (data.bio ?? "").trim(),
+ avatarUrl: (data.avatarUrl ?? "").trim(),
+ github: (data.github ?? "").trim(),
+ expertise: Array.isArray(data.expertise) ? data.expertise.filter((x): x is string => typeof x === "string") : [],
+ archivedAt: "",
+ createdAt: now,
+ updatedAt: now,
+ };
+ await writeMember(member);
+ return NextResponse.json(member);
+}
diff --git a/app/api/pi/route.ts b/app/api/pi/route.ts
new file mode 100644
index 0000000..15a596e
--- /dev/null
+++ b/app/api/pi/route.ts
@@ -0,0 +1,46 @@
+import { NextResponse } from "next/server";
+import { readPI, writePI } from "@/lib/fs-store";
+import type { PI } from "@/lib/types";
+
+export const dynamic = "force-dynamic";
+
+function clean(s: unknown): string {
+ return typeof s === "string" ? s : "";
+}
+
+function cleanList(v: unknown, predicate: (x: unknown) => x is T): T[] {
+ return Array.isArray(v) ? v.filter(predicate) : [];
+}
+
+const isString = (x: unknown): x is string => typeof x === "string";
+
+export async function GET() {
+ return NextResponse.json(await readPI());
+}
+
+export async function PUT(req: Request) {
+ const body = (await req.json()) as Partial;
+ const pi: PI = {
+ name: clean(body.name),
+ title: clean(body.title),
+ pronouns: clean(body.pronouns),
+ affiliation: clean(body.affiliation),
+ location: clean(body.location),
+ timezone: clean(body.timezone),
+ avatarUrl: clean(body.avatarUrl),
+ email: clean(body.email),
+ website: clean(body.website),
+ orcid: clean(body.orcid),
+ googleScholar: clean(body.googleScholar),
+ github: clean(body.github),
+ linkedin: clean(body.linkedin),
+ twitter: clean(body.twitter),
+ bio: clean(body.bio),
+ focusAreas: cleanList(body.focusAreas, isString),
+ expertise: cleanList(body.expertise, isString),
+ education: Array.isArray(body.education) ? body.education : [],
+ publications: Array.isArray(body.publications) ? body.publications : [],
+ };
+ await writePI(pi);
+ return NextResponse.json(pi);
+}
diff --git a/app/api/projects/[id]/report/route.ts b/app/api/projects/[id]/report/route.ts
new file mode 100644
index 0000000..a331deb
--- /dev/null
+++ b/app/api/projects/[id]/report/route.ts
@@ -0,0 +1,41 @@
+import { renderToBuffer } from "@react-pdf/renderer";
+import { readCFPs, readMembers, readPI, readProject } from "@/lib/fs-store";
+import { ProjectReport } from "@/lib/report";
+
+export const dynamic = "force-dynamic";
+export const runtime = "nodejs";
+
+function slugify(s: string): string {
+ return s
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/^-+|-+$/g, "")
+ .slice(0, 80) || "project";
+}
+
+export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const project = await readProject(id);
+ if (!project) {
+ return new Response(JSON.stringify({ error: "not found" }), {
+ status: 404,
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+ const [pi, members, cfps] = await Promise.all([readPI(), readMembers(), readCFPs()]);
+ const generatedAt = new Date().toISOString();
+
+ const buffer = await renderToBuffer(
+ ProjectReport({ project, pi, members, cfps, generatedAt }) as never,
+ );
+
+ const filename = `${slugify(project.name)}-report.pdf`;
+ return new Response(buffer as unknown as BodyInit, {
+ status: 200,
+ headers: {
+ "Content-Type": "application/pdf",
+ "Content-Disposition": `attachment; filename="${filename}"`,
+ "Cache-Control": "no-store",
+ },
+ });
+}
diff --git a/app/api/projects/[id]/route.ts b/app/api/projects/[id]/route.ts
new file mode 100644
index 0000000..a8794c8
--- /dev/null
+++ b/app/api/projects/[id]/route.ts
@@ -0,0 +1,34 @@
+import { NextResponse } from "next/server";
+import { deleteProject, readProject, writeProject } from "@/lib/fs-store";
+import type { Project } from "@/lib/types";
+
+export const dynamic = "force-dynamic";
+
+export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const project = await readProject(id);
+ if (!project) return NextResponse.json({ error: "not found" }, { status: 404 });
+ return NextResponse.json(project);
+}
+
+export async function PATCH(req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ const existing = await readProject(id);
+ if (!existing) return NextResponse.json({ error: "not found" }, { status: 404 });
+ const patch = (await req.json()) as Partial;
+ const updated: Project = {
+ ...existing,
+ ...patch,
+ id: existing.id,
+ createdAt: existing.createdAt,
+ updatedAt: new Date().toISOString(),
+ };
+ await writeProject(updated);
+ return NextResponse.json(updated);
+}
+
+export async function DELETE(_req: Request, { params }: { params: Promise<{ id: string }> }) {
+ const { id } = await params;
+ await deleteProject(id);
+ return NextResponse.json({ ok: true });
+}
diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts
new file mode 100644
index 0000000..efc398d
--- /dev/null
+++ b/app/api/projects/route.ts
@@ -0,0 +1,38 @@
+import { NextResponse } from "next/server";
+import { randomUUID } from "crypto";
+import { readProjects, writeProject } from "@/lib/fs-store";
+import type { Project } from "@/lib/types";
+
+export const dynamic = "force-dynamic";
+
+export async function GET() {
+ return NextResponse.json(await readProjects());
+}
+
+export async function POST(req: Request) {
+ const data = (await req.json()) as { name?: string; description?: string };
+ if (!data.name || !data.name.trim()) {
+ return NextResponse.json({ error: "name is required" }, { status: 400 });
+ }
+ const now = new Date().toISOString();
+ const project: Project = {
+ id: randomUUID(),
+ name: data.name.trim(),
+ description: (data.description ?? "").trim(),
+ status: "exploration",
+ plan: "",
+ setup: "",
+ discord: "",
+ overleaf: "",
+ contributors: [],
+ memberIds: [],
+ artifacts: [],
+ log: [],
+ cfpAssignments: [],
+ archivedAt: "",
+ createdAt: now,
+ updatedAt: now,
+ };
+ await writeProject(project);
+ return NextResponse.json(project);
+}
diff --git a/app/api/store/route.ts b/app/api/store/route.ts
new file mode 100644
index 0000000..10fb1ab
--- /dev/null
+++ b/app/api/store/route.ts
@@ -0,0 +1,9 @@
+import { NextResponse } from "next/server";
+import { readStore } from "@/lib/fs-store";
+
+export const dynamic = "force-dynamic";
+
+export async function GET() {
+ const store = await readStore();
+ return NextResponse.json(store);
+}
diff --git a/app/archives/page.tsx b/app/archives/page.tsx
new file mode 100644
index 0000000..f434dd0
--- /dev/null
+++ b/app/archives/page.tsx
@@ -0,0 +1,155 @@
+"use client";
+import Link from "next/link";
+import { useMemo } from "react";
+import { useStore } from "@/lib/store";
+import { StatusBadge } from "@/components/Bits";
+
+export default function ArchivesPage() {
+ const { store, loaded, updateProject, updateMember, updateCFP, deleteProject, deleteMember, deleteCFP } = useStore();
+
+ const projects = useMemo(
+ () => store.projects.filter((p) => p.archivedAt).sort((a, b) => b.archivedAt.localeCompare(a.archivedAt)),
+ [store.projects],
+ );
+ const members = useMemo(
+ () => store.members.filter((m) => m.archivedAt).sort((a, b) => b.archivedAt.localeCompare(a.archivedAt)),
+ [store.members],
+ );
+ const cfps = useMemo(
+ () => store.cfps.filter((c) => c.archivedAt).sort((a, b) => b.archivedAt.localeCompare(a.archivedAt)),
+ [store.cfps],
+ );
+
+ if (!loaded) return Loading…
;
+
+ const totalArchived = projects.length + members.length + cfps.length;
+
+ return (
+
+
+ Archives
+
+ {totalArchived === 0
+ ? "Nothing archived yet. Items you archive will appear here and can be restored."
+ : `${totalArchived} archived item${totalArchived === 1 ? "" : "s"}. Restore any to bring it back into the main lists.`}
+
+
+
+
+
+
Projects ({projects.length})
+
+ {projects.length === 0 ? (
+ No archived projects.
+ ) : (
+
+ {projects.map((p) => (
+
+
+
+
{p.name}
+ {p.description &&
{p.description}
}
+
+ Archived {new Date(p.archivedAt).toLocaleDateString()} · last updated {new Date(p.updatedAt).toLocaleDateString()}
+
+
+
+
+ updateProject(p.id, { archivedAt: "" })}>Restore
+ {
+ if (confirm(`Permanently delete "${p.name}"? This removes its markdown file and cannot be undone.`)) {
+ deleteProject(p.id);
+ }
+ }}
+ >
+ Delete
+
+
+
+
+ ))}
+
+ )}
+
+
+
+
+
Members ({members.length})
+
+ {members.length === 0 ? (
+ No archived members.
+ ) : (
+
+ {members.map((m) => (
+
+
+
+
{m.name}
+ {m.role &&
{m.role}
}
+
+ Archived {new Date(m.archivedAt).toLocaleDateString()}
+
+
+
+ updateMember(m.id, { archivedAt: "" })}>Restore
+ {
+ if (confirm(`Permanently delete ${m.name}? Their markdown file is removed and they are unassigned from all projects. This cannot be undone.`)) {
+ deleteMember(m.id);
+ }
+ }}
+ >
+ Delete
+
+
+
+
+ ))}
+
+ )}
+
+
+
+
+
Calls for papers ({cfps.length})
+
+ {cfps.length === 0 ? (
+ No archived CFPs.
+ ) : (
+
+ {cfps.map((c) => (
+
+
+
+
{c.name}
+ {c.venue &&
{c.venue}
}
+
+ Archived {new Date(c.archivedAt).toLocaleDateString()}
+ {c.submissionDeadline ? ` · submission was ${c.submissionDeadline}` : ""}
+
+
+
+ updateCFP(c.id, { archivedAt: "" })}>Restore
+ {
+ if (confirm(`Permanently delete CFP "${c.name}"? Its markdown file is removed and it is unassigned from all projects. This cannot be undone.`)) {
+ deleteCFP(c.id);
+ }
+ }}
+ >
+ Delete
+
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/app/cfps/[id]/page.tsx b/app/cfps/[id]/page.tsx
new file mode 100644
index 0000000..a2263c6
--- /dev/null
+++ b/app/cfps/[id]/page.tsx
@@ -0,0 +1,219 @@
+"use client";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { use, useEffect, useMemo, useState } from "react";
+import { useStore } from "@/lib/store";
+import { CFPStatusBadge, Countdown, daysUntil, deadlineUrgency } from "@/components/Bits";
+import { CFP_STATUSES, type CallForPaper, type ProjectCFPStatus } from "@/lib/types";
+
+function fmtDate(d: string): string {
+ if (!d) return "—";
+ return new Date(d + "T00:00:00").toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
+}
+
+export default function CFPDetailPage({ params }: { params: Promise<{ id: string }> }) {
+ const { id } = use(params);
+ const {
+ store,
+ loaded,
+ updateCFP,
+ assignProjectToCFP,
+ updateCFPAssignment,
+ unassignProjectFromCFP,
+ } = useStore();
+ const router = useRouter();
+ const cfp = store.cfps.find((c) => c.id === id);
+ const [form, setForm] = useState(cfp ?? null);
+ const [savedAt, setSavedAt] = useState(null);
+
+ useEffect(() => {
+ if (cfp) setForm(cfp);
+ }, [cfp]);
+
+ const dirty = useMemo(() => {
+ if (!form || !cfp) return false;
+ return JSON.stringify(form) !== JSON.stringify(cfp);
+ }, [form, cfp]);
+
+ if (!loaded) return Loading…
;
+ if (!cfp || !form) {
+ return (
+
+
CFP not found
+ Back to deadlines
+
+ );
+ }
+
+ const set = (key: K, value: CallForPaper[K]) =>
+ setForm({ ...form, [key]: value });
+
+ const save = async () => {
+ await updateCFP(cfp.id, form);
+ setSavedAt(Date.now());
+ };
+
+ const assigned = store.projects
+ .map((p) => {
+ const a = p.cfpAssignments.find((x) => x.cfpId === cfp.id);
+ return a ? { project: p, assignment: a } : null;
+ })
+ .filter((x): x is { project: typeof store.projects[number]; assignment: typeof store.projects[number]["cfpAssignments"][number] } => !!x);
+ const available = store.projects.filter((p) => !p.cfpAssignments.some((a) => a.cfpId === cfp.id));
+
+ return (
+
+ {cfp.archivedAt && (
+
+ Archived {new Date(cfp.archivedAt).toLocaleDateString()}
+ updateCFP(cfp.id, { archivedAt: "" })}>Restore
+
+ )}
+
+
+
← All CFPs
+
+
+ {dirty ? "Save changes" : "Saved"}
+
+ {!cfp.archivedAt ? (
+ {
+ if (confirm(`Archive CFP "${cfp.name}"? It can be restored from /archives.`)) {
+ updateCFP(cfp.id, { archivedAt: new Date().toISOString() });
+ router.push("/cfps");
+ }
+ }}
+ >
+ Archive
+
+ ) : (
+ updateCFP(cfp.id, { archivedAt: "" })}>
+ Restore
+
+ )}
+
+
+
+
+
+
{form.name}
+
+ submission {form.submissionDeadline ? `· ${fmtDate(form.submissionDeadline)}` : "not set"}
+
+
+ {form.venue &&
{form.venue}{form.location ? ` · ${form.location}` : ""}
}
+ {form.url &&
{form.url}
}
+ {form.submissionDeadline && (
+
+
+
+ )}
+ {savedAt && !dirty &&
Saved {new Date(savedAt).toLocaleTimeString()}
}
+
+
+
+
+ Details
+
+ set("name", e.target.value)} />
+ set("venue", e.target.value)} />
+ set("url", e.target.value)} placeholder="https://…" />
+ set("location", e.target.value)} placeholder="e.g. Vancouver" />
+
+
+ set("abstractDeadline", e.target.value)} />
+ set("submissionDeadline", e.target.value)} />
+ set("notificationDate", e.target.value)} />
+ set("conferenceDate", e.target.value)} />
+
+
+ set("topics", e.target.value.split(",").map((s) => s.trim()).filter(Boolean))}
+ placeholder="e.g. on-device AI, compression, edge inference"
+ />
+
+
+
+
+
+
+
+
Project assignments
+ {assigned.length} project{assigned.length === 1 ? "" : "s"} targeting this CFP
+
+
+ {assigned.length === 0 ? (
+ No projects assigned yet.
+ ) : (
+
+ {assigned.map(({ project, assignment }) => {
+ const days = daysUntil(cfp.submissionDeadline);
+ return (
+
+
+
+
{project.name}
+ {project.description &&
{project.description}
}
+ {assignment.notes &&
{assignment.notes}
}
+
+ Assigned {new Date(assignment.assignedAt).toLocaleDateString()} · deadline {Number.isFinite(days) ? (days < 0 ? `${Math.abs(days)}d ago` : `in ${days}d`) : "no date"}
+
+
+
+
+ updateCFPAssignment(project.id, cfp.id, { status: e.target.value as ProjectCFPStatus })}
+ >
+ {CFP_STATUSES.map((s) => (
+ {s.replace(/_/g, " ")}
+ ))}
+
+ unassignProjectFromCFP(project.id, cfp.id)}>Unassign
+
+
+
+ );
+ })}
+
+ )}
+
+ {available.length > 0 && (
+ <>
+
+ Add a project
+
+ {available.map((p) => (
+
+
+
+
{p.name}
+ {p.description &&
{p.description}
}
+
+
assignProjectToCFP(p.id, cfp.id)}>Assign
+
+
+ ))}
+
+ >
+ )}
+
+
+ );
+}
+
+function Field({ label, children }: { label: string; children: React.ReactNode }) {
+ return (
+
+ {label}
+ {children}
+
+ );
+}
diff --git a/app/cfps/new/page.tsx b/app/cfps/new/page.tsx
new file mode 100644
index 0000000..40de14a
--- /dev/null
+++ b/app/cfps/new/page.tsx
@@ -0,0 +1,82 @@
+"use client";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { useStore } from "@/lib/store";
+
+export default function NewCFPPage() {
+ const { addCFP } = useStore();
+ const router = useRouter();
+ const [name, setName] = useState("");
+ const [venue, setVenue] = useState("");
+ const [url, setUrl] = useState("");
+ const [submissionDeadline, setSubmissionDeadline] = useState("");
+
+ return (
+
+
+ New call for papers
+ Track a venue and its deadlines. You can flesh out the rest after creating.
+
+
+
+
+ );
+}
diff --git a/app/cfps/page.tsx b/app/cfps/page.tsx
new file mode 100644
index 0000000..2fd30db
--- /dev/null
+++ b/app/cfps/page.tsx
@@ -0,0 +1,269 @@
+"use client";
+import Link from "next/link";
+import { Suspense, useMemo } from "react";
+import { useSearchParams } from "next/navigation";
+import { useStore } from "@/lib/store";
+import { CFPStatusBadge, Countdown, daysUntil, deadlineUrgency } from "@/components/Bits";
+import type { CallForPaper, Project } from "@/lib/types";
+
+const TIMELINE_DAYS_AHEAD = 180;
+const TIMELINE_DAYS_BEHIND = 30;
+
+function fmtDate(d: string): string {
+ if (!d) return "—";
+ return new Date(d + "T00:00:00").toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" });
+}
+
+function deadlineLabel(days: number): string {
+ if (!Number.isFinite(days)) return "no date";
+ if (days < 0) return `${Math.abs(days)} day${Math.abs(days) === 1 ? "" : "s"} ago`;
+ if (days === 0) return "today";
+ return `in ${days} day${days === 1 ? "" : "s"}`;
+}
+
+function CFPsInner() {
+ const { store, loaded } = useStore();
+ const params = useSearchParams();
+ const q = params.get("q") ?? "";
+
+ const filtered = useMemo(() => {
+ const needle = q.toLowerCase();
+ const items = store.cfps
+ .filter((c) => !c.archivedAt)
+ .filter((c) =>
+ !needle ||
+ c.name.toLowerCase().includes(needle) ||
+ c.venue.toLowerCase().includes(needle) ||
+ c.topics.some((t) => t.toLowerCase().includes(needle)),
+ );
+ return items.sort((a, b) => {
+ const da = a.submissionDeadline || "9999-12-31";
+ const db = b.submissionDeadline || "9999-12-31";
+ return da.localeCompare(db);
+ });
+ }, [store.cfps, q]);
+
+ if (!loaded) return Loading…
;
+
+ return (
+
+
+
+
Deadlines & Calls for Papers
+
+ {store.cfps.filter((c) => !c.archivedAt).length} venue{store.cfps.filter((c) => !c.archivedAt).length === 1 ? "" : "s"} tracked · projects assigned to a CFP show on the timeline with assessment status
+
+
+ + New CFP
+
+
+ {store.cfps.length > 0 &&
}
+ {store.cfps.length > 0 &&
}
+
+ {filtered.length === 0 ? (
+
+ {q ? "No CFPs match your search." : <>No calls for papers tracked yet. Add one.>}
+
+ ) : (
+
+ {filtered.map((c) => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+function NextDeadline({ cfps }: { cfps: CallForPaper[] }) {
+ const upcoming = cfps
+ .filter((c) => c.submissionDeadline && deadlineUrgency(c.submissionDeadline) !== "passed")
+ .sort((a, b) => a.submissionDeadline.localeCompare(b.submissionDeadline));
+ const next = upcoming[0];
+ if (!next) return null;
+ return (
+
+
+
Next deadline
+
{next.name}
+
+ {next.venue ? `${next.venue} · ` : ""}submission {fmtDate(next.submissionDeadline)}
+
+
+ Open CFP →
+
+
+
+
+ );
+}
+
+function CFPCard({ cfp, projects }: { cfp: CallForPaper; projects: Project[] }) {
+ const assigned = projects
+ .map((p) => {
+ const a = p.cfpAssignments.find((x) => x.cfpId === cfp.id);
+ return a ? { project: p, status: a.status } : null;
+ })
+ .filter((x): x is { project: Project; status: import("@/lib/types").ProjectCFPStatus } => !!x);
+
+ const days = daysUntil(cfp.submissionDeadline);
+ const urgency = deadlineUrgency(cfp.submissionDeadline);
+
+ return (
+
+
+
+
+
{cfp.name}
+
+ {urgency === "passed" ? "passed" : `submission ${deadlineLabel(days)}`}
+
+ {cfp.submissionDeadline && (
+
+ )}
+ {cfp.url &&
CFP page ↗ }
+
+ {cfp.venue &&
{cfp.venue}{cfp.location ? ` · ${cfp.location}` : ""}
}
+
+
+
+
+ {cfp.abstractDeadline && }
+ {cfp.submissionDeadline && }
+ {cfp.notificationDate && }
+ {cfp.conferenceDate && }
+
+
+ {cfp.topics.length > 0 && (
+
+ {cfp.topics.map((t) => (
+ {t}
+ ))}
+
+ )}
+
+
+
+
+
Projects assigned ({assigned.length})
+
+ {assigned.length === 0 ? (
+
No projects assigned. Open a project and add this CFP as a target.
+ ) : (
+
+ {assigned.map(({ project, status }) => (
+
+
+
+
{project.name}
+ {project.description &&
{project.description}
}
+
+
+
+
+ ))}
+
+ )}
+
+ );
+}
+
+function DatePill({ label, date, primary = false }: { label: string; date: string; primary?: boolean }) {
+ const urgency = deadlineUrgency(date);
+ return (
+
+
{label}
+
{fmtDate(date)}
+
+ {deadlineLabel(daysUntil(date))}
+
+
+ );
+}
+
+function Timeline({ cfps, projects }: { cfps: CallForPaper[]; projects: Project[] }) {
+ const now = Date.now();
+ const start = now - TIMELINE_DAYS_BEHIND * 86400000;
+ const end = now + TIMELINE_DAYS_AHEAD * 86400000;
+ const range = end - start;
+
+ const todayPct = ((now - start) / range) * 100;
+
+ type Marker = { cfp: CallForPaper; pct: number; urgency: ReturnType };
+ const markers: Marker[] = [];
+ for (const c of cfps) {
+ if (!c.submissionDeadline) continue;
+ const t = new Date(c.submissionDeadline + "T23:59:59").getTime();
+ if (t < start || t > end) continue;
+ markers.push({ cfp: c, pct: ((t - start) / range) * 100, urgency: deadlineUrgency(c.submissionDeadline) });
+ }
+ markers.sort((a, b) => a.pct - b.pct);
+
+ // month ticks
+ const monthTicks: { pct: number; label: string }[] = [];
+ const d = new Date(start);
+ d.setDate(1);
+ while (d.getTime() < end) {
+ const pct = ((d.getTime() - start) / range) * 100;
+ if (pct >= 0 && pct <= 100) {
+ monthTicks.push({ pct, label: d.toLocaleDateString(undefined, { month: "short" }) });
+ }
+ d.setMonth(d.getMonth() + 1);
+ }
+
+ return (
+
+
+
Timeline
+ past {TIMELINE_DAYS_BEHIND} d · next {TIMELINE_DAYS_AHEAD} d
+
+
+
+
+ {monthTicks.map((m, i) => (
+
{m.label}
+ ))}
+ {markers.map((m, i) => {
+ const assignedCount = projects.filter((p) => p.cfpAssignments.some((a) => a.cfpId === m.cfp.id)).length;
+ return (
+
+
+ {m.cfp.name} · {fmtDate(m.cfp.submissionDeadline)}
+ {assignedCount > 0 ? ` · ${assignedCount} project${assignedCount === 1 ? "" : "s"}` : ""}
+
+
+ );
+ })}
+
+
+ {markers.length === 0 && (
+
+ No deadlines fall within the visible window.
+
+ )}
+
+
+
+
+
+
+
+ );
+}
+
+function LegendDot({ color, label }: { color: string; label: string }) {
+ return (
+
+
+ {label}
+
+ );
+}
+
+export default function CFPsPage() {
+ return (
+ Loading… }>
+
+
+ );
+}
diff --git a/app/globals.css b/app/globals.css
new file mode 100644
index 0000000..0097af4
--- /dev/null
+++ b/app/globals.css
@@ -0,0 +1,962 @@
+:root {
+ --page-bg: #d9def5;
+ --page-bg-2: #b9c3ee;
+ --shell: #f6f7fb;
+ --card: #ffffff;
+ --card-2: #f3f5fb;
+ --text: #14172a;
+ --muted: #7c8198;
+ --border: #e7eaf3;
+ --border-strong: #d6dbe7;
+
+ --side-bg: #161826;
+ --side-fg: #a9aec9;
+ --side-fg-active: #ffffff;
+ --side-hover: #1f2237;
+ --side-active: #2a2e4a;
+
+ --accent: #5468ff;
+ --accent-2: #4054e6;
+ --accent-soft: #eef0ff;
+
+ --green: #10b981;
+ --green-soft: #d8f3e6;
+ --orange: #f59e0b;
+ --orange-soft: #fff1d6;
+ --purple: #8b5cf6;
+ --purple-soft: #ede4ff;
+ --pink: #ec4899;
+ --pink-soft: #fde6f1;
+ --danger: #e02d3c;
+ --danger-soft: #ffe1e4;
+
+ --shadow-sm: 0 1px 2px rgba(20, 23, 42, 0.04);
+ --shadow-md: 0 4px 12px -2px rgba(20, 23, 42, 0.06), 0 1px 3px rgba(20, 23, 42, 0.04);
+ --shadow-lg: 0 20px 60px -20px rgba(20, 23, 42, 0.18);
+ --radius-sm: 10px;
+ --radius: 16px;
+ --radius-lg: 22px;
+
+ color-scheme: light;
+}
+
+* { box-sizing: border-box; }
+
+html, body {
+ margin: 0;
+ padding: 0;
+ background: linear-gradient(140deg, var(--page-bg) 0%, var(--page-bg-2) 100%);
+ color: var(--text);
+ font-family: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif;
+ font-size: 14.5px;
+ line-height: 1.5;
+ height: 100vh;
+ overflow: hidden;
+}
+
+a { color: var(--accent); text-decoration: none; }
+a:hover { text-decoration: underline; }
+
+/* App shell ----------------------------------------------------- */
+.page {
+ padding: 16px;
+ height: 100vh;
+ overflow: hidden;
+}
+
+.shell {
+ background: var(--shell);
+ border-radius: var(--radius-lg);
+ height: calc(100vh - 32px);
+ display: grid;
+ grid-template-columns: 72px 1fr;
+ box-shadow: var(--shadow-lg);
+ overflow: hidden;
+}
+
+/* Sidebar ------------------------------------------------------- */
+.sidebar {
+ background: var(--side-bg);
+ color: var(--side-fg);
+ padding: 18px 12px;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 18px;
+ height: 100%;
+ overflow-y: auto;
+}
+.brand-dot {
+ width: 38px; height: 38px;
+ border-radius: 12px;
+ background: linear-gradient(135deg, #6d7cff 0%, #a78bfa 100%);
+ display: grid; place-items: center;
+ color: white; font-weight: 800; font-size: 16px;
+ letter-spacing: -0.02em;
+}
+.brand-logo {
+ width: 48px;
+ height: 48px;
+ border-radius: 12px;
+ overflow: hidden;
+ display: grid;
+ place-items: center;
+ background: #0f1115;
+ flex-shrink: 0;
+}
+.brand-logo img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
+.brand-logo:hover { text-decoration: none; filter: brightness(1.1); }
+.side-nav {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ width: 100%;
+ align-items: center;
+}
+.side-nav a {
+ width: 44px; height: 44px;
+ border-radius: 12px;
+ display: grid; place-items: center;
+ color: var(--side-fg);
+ font-size: 18px;
+ position: relative;
+}
+.side-nav a:hover { background: var(--side-hover); color: var(--side-fg-active); text-decoration: none; }
+.side-nav a.active {
+ background: var(--side-active);
+ color: var(--side-fg-active);
+ box-shadow: inset 0 0 0 1px rgba(255,255,255,0.06);
+}
+.side-nav a.active::before {
+ content: "";
+ position: absolute;
+ left: -12px;
+ top: 10px; bottom: 10px;
+ width: 3px;
+ border-radius: 0 4px 4px 0;
+ background: linear-gradient(180deg, #8b5cf6, #5468ff);
+}
+.side-nav a .tip {
+ position: absolute;
+ left: 56px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: var(--text);
+ color: #fff;
+ font-size: 12px;
+ padding: 4px 8px;
+ border-radius: 6px;
+ white-space: nowrap;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 80ms;
+ z-index: 30;
+}
+.side-nav a:hover .tip { opacity: 1; }
+
+.side-archive {
+ margin-top: auto;
+ padding-top: 12px;
+ position: relative;
+ display: flex;
+ justify-content: center;
+ width: 100%;
+}
+.side-archive::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 8px;
+ right: 8px;
+ height: 1px;
+ background: rgba(255, 255, 255, 0.08);
+}
+.side-archive a {
+ width: 44px; height: 44px;
+ border-radius: 12px;
+ display: grid; place-items: center;
+ color: var(--side-fg);
+ font-size: 18px;
+ position: relative;
+}
+.side-archive a:hover { background: var(--side-hover); color: var(--side-fg-active); text-decoration: none; }
+.side-archive a.active {
+ background: var(--side-active);
+ color: var(--side-fg-active);
+ box-shadow: inset 0 0 0 1px rgba(255,255,255,0.06);
+}
+.side-archive a .tip {
+ position: absolute;
+ left: 56px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: var(--text);
+ color: #fff;
+ font-size: 12px;
+ padding: 4px 8px;
+ border-radius: 6px;
+ white-space: nowrap;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 80ms;
+ z-index: 30;
+}
+.side-archive a:hover .tip { opacity: 1; }
+
+.side-foot {
+ margin-top: 0;
+ padding-top: 12px;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 8px;
+}
+.side-foot::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 8px;
+ right: 8px;
+ height: 1px;
+ background: rgba(255, 255, 255, 0.08);
+}
+.side-plus.alt {
+ background: linear-gradient(135deg, #f472b6 0%, #f59e0b 100%);
+ box-shadow: 0 6px 18px -6px rgba(244, 114, 182, 0.55);
+}
+.side-plus {
+ width: 44px; height: 44px;
+ border-radius: 12px;
+ display: grid;
+ place-items: center;
+ background: linear-gradient(135deg, #6d7cff 0%, #a78bfa 100%);
+ color: #fff;
+ position: relative;
+ box-shadow: 0 6px 18px -6px rgba(108, 100, 255, 0.55);
+}
+.side-plus:hover { filter: brightness(1.05); text-decoration: none; }
+.side-plus .tip {
+ position: absolute;
+ left: 56px;
+ top: 50%;
+ transform: translateY(-50%);
+ background: var(--text);
+ color: #fff;
+ font-size: 12px;
+ padding: 4px 8px;
+ border-radius: 6px;
+ white-space: nowrap;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 80ms;
+ z-index: 30;
+}
+.side-plus:hover .tip { opacity: 1; }
+
+/* Content + topbar --------------------------------------------- */
+.content {
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ height: 100%;
+ overflow: hidden;
+}
+
+.topbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 20px 28px;
+ flex-wrap: wrap;
+ flex-shrink: 0;
+ border-bottom: 1px solid var(--border);
+ background: var(--shell);
+}
+.greeting { margin: 0; font-size: 22px; letter-spacing: -0.02em; }
+
+.crumbs {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ flex-wrap: wrap;
+ font-variant-numeric: tabular-nums;
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
+ font-size: 12px;
+ text-transform: lowercase;
+}
+.crumb { display: inline-flex; align-items: center; gap: 4px; }
+.crumb-sep { color: var(--border-strong); }
+.crumb-current { color: var(--text); font-weight: 600; }
+.crumbs a { color: var(--muted); }
+.crumbs a:hover { color: var(--accent); text-decoration: none; }
+.greeting .muted { font-weight: 400; }
+.topbar-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex: 1;
+ justify-content: flex-end;
+ flex-wrap: wrap;
+}
+.topbar-add {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.btn-add {
+ color: #fff;
+ border: 0;
+ font-weight: 600;
+ transition: background 120ms, box-shadow 120ms, transform 120ms;
+}
+.btn-add:hover { text-decoration: none; transform: translateY(-1px); }
+.btn-add-project {
+ background: linear-gradient(135deg, #5468ff 0%, #4054e6 100%);
+ box-shadow: 0 2px 8px -2px rgba(84, 104, 255, 0.45);
+}
+.btn-add-project:hover {
+ background: #e7ebff;
+ color: #1e2a8a;
+ box-shadow: 0 6px 14px -4px rgba(30, 42, 138, 0.25);
+}
+.btn-add-member {
+ background: linear-gradient(135deg, #ec4899 0%, #8b5cf6 100%);
+ box-shadow: 0 2px 8px -2px rgba(236, 72, 153, 0.45);
+}
+.btn-add-member:hover {
+ background: #fce4f0;
+ color: #831843;
+ box-shadow: 0 6px 14px -4px rgba(131, 24, 67, 0.25);
+}
+.btn-add-cfp {
+ background: linear-gradient(135deg, #f59e0b 0%, #ef4444 100%);
+ box-shadow: 0 2px 8px -2px rgba(245, 158, 11, 0.45);
+}
+.btn-add-cfp:hover {
+ background: #fdecc8;
+ color: #92400e;
+ box-shadow: 0 6px 14px -4px rgba(146, 64, 14, 0.25);
+}
+.search {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ padding: 9px 16px 9px 38px;
+ width: min(380px, 100%);
+ font-size: 13.5px;
+ font-family: inherit;
+ color: var(--text);
+ background-image: url("data:image/svg+xml;utf8, ");
+ background-repeat: no-repeat;
+ background-position: 13px 50%;
+}
+.search:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); }
+
+.main {
+ padding: 24px 28px 32px;
+ min-width: 0;
+ flex: 1;
+ overflow-y: auto;
+ overscroll-behavior: contain;
+}
+
+/* Layout primitives -------------------------------------------- */
+.stack > * + * { margin-top: 22px; }
+.row { display: flex; gap: 12px; align-items: center; }
+.row-between { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
+.grid { display: grid; gap: 16px; }
+.grid-2 { display: grid; gap: 16px; grid-template-columns: 1.6fr 1fr; }
+.grid-3 { display: grid; gap: 16px; grid-template-columns: repeat(3, minmax(0, 1fr)); }
+
+/* Typography --------------------------------------------------- */
+h1 { font-size: 22px; margin: 0 0 6px; letter-spacing: -0.02em; }
+h2 { font-size: 15px; margin: 0 0 12px; letter-spacing: -0.005em; font-weight: 600; }
+h3 { font-size: 13px; margin: 0 0 8px; color: var(--muted); font-weight: 600; }
+p { margin: 0 0 8px; }
+.muted { color: var(--muted); }
+.small { font-size: 12px; }
+
+/* Cards & inputs ----------------------------------------------- */
+.card {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ padding: 18px;
+ box-shadow: var(--shadow-sm);
+}
+.card-hover:hover { border-color: var(--border-strong); }
+.card .card-header {
+ display: flex; align-items: center; justify-content: space-between;
+ margin-bottom: 14px; gap: 12px;
+}
+.card .card-header h2 { margin: 0; }
+
+.btn {
+ display: inline-flex; align-items: center; gap: 6px;
+ padding: 8px 14px;
+ border-radius: 10px;
+ background: var(--card);
+ border: 1px solid var(--border);
+ color: var(--text);
+ cursor: pointer;
+ font-size: 13.5px;
+ font-family: inherit;
+ line-height: 1.2;
+ transition: background 120ms, border-color 120ms;
+}
+.btn:hover { background: var(--card-2); text-decoration: none; }
+.btn-primary {
+ background: var(--text); border-color: var(--text); color: #fff;
+}
+.btn-primary:hover { background: #25293f; border-color: #25293f; }
+.btn-accent { background: var(--accent); border-color: var(--accent); color: #fff; }
+.btn-accent:hover { background: var(--accent-2); border-color: var(--accent-2); }
+.btn-danger { color: var(--danger); border-color: #f3c5ca; background: #fff5f6; }
+.btn-danger:hover { background: var(--danger-soft); }
+
+.input, .textarea, .select {
+ width: 100%;
+ background: var(--card);
+ border: 1px solid var(--border);
+ color: var(--text);
+ border-radius: 10px;
+ padding: 9px 12px;
+ font-size: 14px;
+ font-family: inherit;
+}
+.textarea { min-height: 100px; resize: vertical; }
+.input:focus, .textarea:focus, .select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); }
+
+label { display: block; font-size: 12.5px; color: var(--muted); margin-bottom: 5px; font-weight: 500; }
+.field { margin-bottom: 14px; }
+
+/* Badges ------------------------------------------------------- */
+.badge {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 11.5px;
+ padding: 3px 9px;
+ border-radius: 999px;
+ background: var(--card-2);
+ color: var(--muted);
+ border: 1px solid transparent;
+ font-weight: 500;
+ text-transform: capitalize;
+ letter-spacing: 0;
+}
+.badge::before {
+ content: ""; width: 6px; height: 6px; border-radius: 999px; background: currentColor;
+}
+.badge.exploration { color: #6d28d9; background: var(--purple-soft); }
+.badge.planning { color: #2742c3; background: var(--accent-soft); }
+.badge.active { color: #047857; background: var(--green-soft); }
+.badge.paused { color: #92590b; background: var(--orange-soft); }
+.badge.archived { color: #4b5563; background: #eef0f4; }
+
+/* Stats (compact pills) ---------------------------------------- */
+.stats {
+ display: flex;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+.stat {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 10px 14px;
+ display: inline-flex;
+ align-items: baseline;
+ gap: 8px;
+ font-size: 13px;
+ box-shadow: var(--shadow-sm);
+}
+.stat .stat-num { font-size: 18px; font-weight: 700; color: var(--text); }
+.stat .stat-label { color: var(--muted); font-size: 11.5px; }
+
+/* Donut -------------------------------------------------------- */
+.donut-wrap { display: flex; align-items: center; gap: 24px; flex-wrap: wrap; }
+.donut {
+ width: 180px; height: 180px;
+ border-radius: 50%;
+ position: relative;
+ flex-shrink: 0;
+}
+.donut::after {
+ content: "";
+ position: absolute;
+ inset: 26px;
+ background: var(--card);
+ border-radius: 50%;
+}
+.donut-inner {
+ position: absolute; inset: 0;
+ display: flex; flex-direction: column;
+ align-items: center; justify-content: center;
+ z-index: 1;
+}
+.donut-num { font-size: 32px; font-weight: 700; letter-spacing: -0.02em; }
+.donut-label { color: var(--muted); font-size: 12px; }
+
+.legend { display: flex; flex-direction: column; gap: 8px; flex: 1; min-width: 160px; }
+.legend-row {
+ display: flex; align-items: center; justify-content: space-between; gap: 10px;
+ font-size: 13px;
+}
+.legend-row .dot {
+ width: 10px; height: 10px; border-radius: 3px; display: inline-block; margin-right: 8px;
+}
+.legend-row .name { flex: 1; text-transform: capitalize; }
+.legend-row .pct { color: var(--muted); font-variant-numeric: tabular-nums; }
+
+/* Project table ------------------------------------------------ */
+.ptable { width: 100%; border-collapse: collapse; font-size: 13.5px; }
+.ptable th {
+ text-align: left;
+ font-weight: 500;
+ color: var(--muted);
+ padding: 8px 10px;
+ border-bottom: 1px solid var(--border);
+ font-size: 12px;
+}
+.ptable td {
+ padding: 12px 10px;
+ border-bottom: 1px solid var(--border);
+ vertical-align: middle;
+}
+.ptable tr:last-child td { border-bottom: none; }
+.ptable td a { color: var(--text); font-weight: 600; }
+.ptable td a:hover { color: var(--accent); }
+
+/* Avatar stack ------------------------------------------------- */
+.avatars { display: inline-flex; align-items: center; }
+.avatar {
+ width: 28px; height: 28px; border-radius: 50%;
+ display: grid; place-items: center;
+ font-size: 11px; font-weight: 600;
+ color: #fff;
+ border: 2px solid var(--card);
+ margin-left: -8px;
+}
+.avatar:first-child { margin-left: 0; }
+.avatar.more { background: var(--card-2); color: var(--muted); }
+
+/* Progress bar ------------------------------------------------- */
+.progress {
+ height: 6px;
+ background: var(--card-2);
+ border-radius: 999px;
+ overflow: hidden;
+ min-width: 80px;
+ flex: 1;
+}
+.progress > span {
+ display: block;
+ height: 100%;
+ border-radius: 999px;
+ background: linear-gradient(90deg, var(--accent), #8b5cf6);
+}
+.progress-wrap { display: flex; align-items: center; gap: 10px; }
+.progress-wrap .count { color: var(--muted); font-variant-numeric: tabular-nums; min-width: 28px; text-align: right; }
+
+/* Activity feed ------------------------------------------------ */
+.activity { display: flex; flex-direction: column; gap: 14px; }
+.activity-item {
+ display: grid;
+ grid-template-columns: 36px 1fr;
+ gap: 12px;
+ align-items: start;
+}
+.activity-item .avatar { margin-left: 0; }
+.activity-item .body { font-size: 13.5px; }
+.activity-item .body .when { color: var(--muted); font-size: 12px; }
+.activity-item .body p { margin: 4px 0 0; white-space: pre-wrap; }
+
+/* Lists & helpers --------------------------------------------- */
+.list { list-style: none; padding: 0; margin: 0; }
+.list > li + li { margin-top: 10px; }
+
+.empty {
+ border: 1px dashed var(--border-strong);
+ border-radius: var(--radius);
+ padding: 28px;
+ text-align: center;
+ color: var(--muted);
+ background: var(--card);
+}
+
+.section-title {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+ gap: 12px;
+ flex-wrap: wrap;
+}
+.section-title h2 { margin: 0; }
+
+.divider { height: 1px; background: var(--border); margin: 16px 0; }
+
+.kv { display: grid; grid-template-columns: 130px 1fr; gap: 8px 16px; }
+.kv dt { color: var(--muted); font-size: 13px; }
+.kv dd { margin: 0; }
+
+/* PI hero & profile UI ----------------------------------------- */
+.pi-hero {
+ display: grid;
+ grid-template-columns: 96px 1fr auto;
+ gap: 20px;
+ align-items: center;
+}
+.pi-avatar {
+ width: 96px; height: 96px;
+ border-radius: 24px;
+ object-fit: cover;
+ background: linear-gradient(135deg, #6d7cff 0%, #a78bfa 100%);
+ display: grid;
+ place-items: center;
+ color: #fff;
+ font-size: 32px;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+}
+.pi-name { margin: 0; font-size: 26px; letter-spacing: -0.02em; }
+.pi-sub { color: var(--muted); margin: 2px 0 0; font-size: 14px; }
+.pi-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; }
+
+.tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ font-size: 12px;
+ padding: 3px 10px;
+ border-radius: 999px;
+ background: var(--accent-soft);
+ color: #2742c3;
+ font-weight: 500;
+}
+.tag.alt { background: var(--purple-soft); color: #5b21b6; }
+
+.tag-input {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 6px;
+}
+.tag-input:focus-within { border-color: var(--accent); box-shadow: 0 0 0 4px var(--accent-soft); }
+.chips { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
+.chip {
+ background: var(--accent-soft);
+ color: #2742c3;
+ border-radius: 999px;
+ padding: 3px 4px 3px 10px;
+ display: inline-flex;
+ gap: 2px;
+ align-items: center;
+ font-size: 12.5px;
+}
+.chip button {
+ background: transparent;
+ border: 0;
+ color: inherit;
+ cursor: pointer;
+ padding: 0 6px;
+ font-size: 14px;
+ line-height: 1;
+}
+.chip button:hover { color: var(--danger); }
+.tag-input input {
+ border: 0;
+ background: transparent;
+ padding: 4px 6px;
+ flex: 1;
+ min-width: 140px;
+ outline: none;
+ font-size: 13.5px;
+ color: var(--text);
+}
+
+.list-item {
+ background: var(--card-2);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 12px 14px;
+}
+.list-item + .list-item { margin-top: 8px; }
+.list-item .row-between { gap: 16px; }
+
+.section-grid-2 { display: grid; gap: 12px; grid-template-columns: repeat(2, minmax(0, 1fr)); }
+.section-grid-3 { display: grid; gap: 12px; grid-template-columns: repeat(3, minmax(0, 1fr)); }
+.section-grid-4 { display: grid; gap: 12px; grid-template-columns: repeat(4, minmax(0, 1fr)); }
+
+.toggle {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+ user-select: none;
+ font-size: 13px;
+ color: var(--muted);
+}
+.toggle input { appearance: none; width: 36px; height: 20px; border-radius: 999px; background: var(--card-2); border: 1px solid var(--border); position: relative; cursor: pointer; transition: background 120ms; }
+.toggle input::after {
+ content: "";
+ position: absolute;
+ top: 2px; left: 2px;
+ width: 14px; height: 14px;
+ border-radius: 50%;
+ background: #fff;
+ box-shadow: 0 1px 2px rgba(0,0,0,0.15);
+ transition: transform 120ms;
+}
+.toggle input:checked { background: var(--accent); border-color: var(--accent); }
+.toggle input:checked::after { transform: translateX(16px); }
+.toggle input:checked + span { color: var(--accent); font-weight: 600; }
+
+.discord-btn {
+ background: #5865f2;
+ border-color: #5865f2;
+ color: #fff;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+.discord-btn:hover { background: #4752c4; border-color: #4752c4; color: #fff; text-decoration: none; }
+.overleaf-btn {
+ background: #138a07;
+ border-color: #138a07;
+ color: #fff;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+.overleaf-btn:hover { background: #0e6b05; border-color: #0e6b05; color: #fff; text-decoration: none; }
+
+/* Timeline ----------------------------------------------------- */
+.timeline {
+ position: relative;
+ background: var(--card-2);
+ border: 1px solid var(--border);
+ border-radius: 14px;
+ padding: 20px 16px 36px;
+ overflow: hidden;
+}
+.timeline-axis {
+ position: relative;
+ height: 4px;
+ background: var(--border);
+ border-radius: 2px;
+ margin-top: 36px;
+}
+.timeline-today {
+ position: absolute;
+ top: -8px;
+ bottom: -20px;
+ width: 2px;
+ background: var(--accent);
+ border-radius: 1px;
+}
+.timeline-today::after {
+ content: "today";
+ position: absolute;
+ top: -18px;
+ left: 50%;
+ transform: translateX(-50%);
+ color: var(--accent);
+ font-size: 11px;
+ font-weight: 600;
+}
+.tl-month {
+ position: absolute;
+ bottom: -22px;
+ font-size: 11px;
+ color: var(--muted);
+ transform: translateX(-50%);
+}
+.tl-marker {
+ position: absolute;
+ top: -28px;
+ width: 2px;
+ background: var(--text);
+ height: 36px;
+ transform: translateX(-50%);
+}
+.tl-marker::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translate(-50%, -55%);
+ width: 14px; height: 14px;
+ border-radius: 50%;
+ background: var(--card);
+ border: 3px solid var(--text);
+}
+.tl-marker.urgent::before { border-color: #e02d3c; }
+.tl-marker.soon::before { border-color: #f59e0b; }
+.tl-marker.ok::before { border-color: #10b981; }
+.tl-marker.passed::before { border-color: #9ca3af; }
+.tl-marker.urgent { background: #e02d3c; }
+.tl-marker.soon { background: #f59e0b; }
+.tl-marker.ok { background: #10b981; }
+.tl-marker.passed { background: #9ca3af; }
+.tl-label {
+ position: absolute;
+ top: -54px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: var(--card);
+ border: 1px solid var(--border);
+ padding: 3px 8px;
+ border-radius: 6px;
+ font-size: 11.5px;
+ white-space: nowrap;
+ box-shadow: var(--shadow-sm);
+ pointer-events: none;
+}
+
+/* CFP card ----------------------------------------------------- */
+.cfp-card { position: relative; }
+.cfp-card .deadline-pill {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 4px 10px;
+ border-radius: 999px;
+ font-size: 12px;
+ font-weight: 600;
+ background: var(--card-2);
+ border: 1px solid var(--border);
+}
+.deadline-pill.urgent { background: var(--danger-soft); color: #b91c1c; border-color: #f5c6cb; }
+.deadline-pill.soon { background: var(--orange-soft); color: #92590b; border-color: #f3dca0; }
+.deadline-pill.ok { background: var(--green-soft); color: #047857; border-color: #b9e6c9; }
+.deadline-pill.passed { background: var(--card-2); color: var(--muted); }
+
+/* Countdown ---------------------------------------------------- */
+.countdown {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+}
+.cd-unit {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 8px 12px;
+ min-width: 58px;
+ box-shadow: var(--shadow-sm);
+}
+.cd-num {
+ font-size: 22px;
+ font-weight: 700;
+ font-variant-numeric: tabular-nums;
+ letter-spacing: -0.02em;
+ line-height: 1;
+}
+.cd-label {
+ font-size: 10px;
+ color: var(--muted);
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ margin-top: 4px;
+}
+.cd-sep {
+ font-size: 22px;
+ color: var(--muted);
+ padding: 0 2px;
+ font-weight: 600;
+ line-height: 1;
+}
+.countdown.passed .cd-num { color: var(--muted); }
+.countdown.passed .cd-unit { background: var(--card-2); }
+
+.countdown-compact {
+ font-variant-numeric: tabular-nums;
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--text);
+}
+.countdown-compact.passed { color: var(--muted); font-weight: 400; }
+
+.next-deadline {
+ background: linear-gradient(135deg, #1c1f2e 0%, #2e3550 100%);
+ color: #fff;
+ border-radius: var(--radius);
+ padding: 22px 24px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 24px;
+ flex-wrap: wrap;
+ box-shadow: var(--shadow-md);
+}
+.next-deadline h2 { color: #fff; margin: 0 0 4px; font-size: 14px; opacity: 0.7; text-transform: uppercase; letter-spacing: 0.06em; }
+.next-deadline .name { font-size: 22px; font-weight: 700; letter-spacing: -0.02em; }
+.next-deadline .meta { color: rgba(255,255,255,0.65); font-size: 13px; margin-top: 4px; }
+.next-deadline a { color: #c4ccff; }
+.next-deadline .cd-unit { background: rgba(255,255,255,0.08); border-color: rgba(255,255,255,0.08); color: #fff; }
+.next-deadline .cd-num { color: #fff; }
+.next-deadline .cd-label { color: rgba(255,255,255,0.55); }
+.next-deadline .cd-sep { color: rgba(255,255,255,0.4); }
+
+/* Archive banner ---------------------------------------------- */
+.archive-banner {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ background: var(--orange-soft);
+ color: #92590b;
+ border: 1px solid #f3dca0;
+ border-radius: var(--radius);
+ padding: 10px 16px;
+ font-size: 13px;
+ font-weight: 500;
+}
+@media (max-width: 720px) {
+ .pi-hero { grid-template-columns: 64px 1fr; }
+ .pi-avatar { width: 64px; height: 64px; font-size: 22px; border-radius: 18px; }
+ .pi-hero .pi-actions { grid-column: 1 / -1; }
+ .section-grid-2, .section-grid-3, .section-grid-4 { grid-template-columns: 1fr; }
+}
+
+/* Mobile ------------------------------------------------------- */
+@media (max-width: 880px) {
+ html, body { height: 100dvh; }
+ .page { padding: 0; height: 100dvh; }
+ .shell { border-radius: 0; height: 100dvh; grid-template-columns: 1fr; grid-template-rows: auto 1fr; }
+ .sidebar {
+ flex-direction: row;
+ padding: 10px 12px;
+ border-bottom: 1px solid #2a2e4a;
+ gap: 10px;
+ align-items: center;
+ height: auto;
+ overflow-x: auto;
+ overflow-y: visible;
+ }
+ .side-nav { flex-direction: row; gap: 4px; width: auto; }
+ .side-nav a { width: 38px; height: 38px; font-size: 16px; }
+ .side-nav a.active::before { display: none; }
+ .side-foot { flex-direction: row; padding-top: 0; }
+ .side-foot::before { display: none; }
+ .topbar { padding: 14px 16px; }
+ .greeting { font-size: 18px; }
+ .main { padding: 18px 16px 32px; }
+ .grid-2, .grid-3 { grid-template-columns: 1fr; }
+ .kv { grid-template-columns: 1fr; gap: 2px 0; }
+ .kv dt { margin-top: 6px; }
+ .ptable th:nth-child(2), .ptable td:nth-child(2),
+ .ptable th:nth-child(3), .ptable td:nth-child(3) { display: none; }
+}
diff --git a/app/icon.png b/app/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..5bfaf20e2d652aaa54d75194c9c3f41145eaa16c
GIT binary patch
literal 83360
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