`;
-
}).join("");
+ let isToday =
+ day.getFullYear() === today.getFullYear()
+ && day.getMonth() === today.getMonth()
+ && day.getDate() === today.getDate();
+
+ return `
+
+ `;
+}
+
+// ---------- create / propose event ----------
+
+function openCreateEventModal(prefill = {}){
+ if(cachedRoomsForForm.length === 0){
+ alert("You need to be in a room before you can create an event.");
+ return;
+ }
+
+ let kindValue = prefill.kind || "proposal";
+
+ let roomOptions = cachedRoomsForForm.map(room => `
+
+ `);
+
+ onEventTypeChange();
+}
+
+function onEventTypeChange(){
+ let kind = document.getElementById("event-type").value;
+ let wrap = document.getElementById("event-min-people-wrap");
+ wrap.style.display = (kind === "proposal") ? "block" : "none";
+}
+
+async function handleCreateEvent(event){
+ event.preventDefault();
+
+ let kind = document.getElementById("event-type").value;
+ let roomId = parseInt(document.getElementById("event-room").value, 10);
+ let title = document.getElementById("event-title").value;
+ let description = document.getElementById("event-description").value;
+ let startLocal = document.getElementById("event-start").value;
+ let endLocal = document.getElementById("event-end").value;
+ let messageBox = document.getElementById("create-event-message");
+
+ let payload = {
+ room_id: roomId,
+ title,
+ description,
+ start_time: toBackendDateTime(startLocal),
+ end_time: toBackendDateTime(endLocal)
+ };
+
+ // Field values follow the convention from the create_event() example
+ // in events.py: type="proposal", visibility="room", status="proposed".
+ // "busy" / "private" extend that same pattern for the two other kinds.
+ if(kind === "proposal"){
+ payload.type = "proposal";
+ payload.status = "proposed";
+ payload.visibility = "room";
+ payload.min_people = parseInt(document.getElementById("event-min-people").value, 10) || 1;
+ } else if(kind === "busy"){
+ payload.type = "busy";
+ payload.status = "busy";
+ payload.visibility = "room";
+ payload.min_people = 0;
+ } else {
+ payload.type = "private";
+ payload.status = "confirmed";
+ payload.visibility = "private";
+ payload.min_people = 0;
+ }
+
+ try{
+ let result = await apiFetch("/events", {
+ method: "POST",
+ body: JSON.stringify(payload)
+ });
+
+ // POST /events returns 200 with status:"failed" (not an exception)
+ // when the room membership check fails.
+ if(result.status === "failed"){
+ messageBox.textContent =
+ result.reason === "not_room_member"
+ ? "you're not a member of that room"
+ : (result.reason || "could not create event");
+ return;
+ }
+
+ closeModal();
+
+ let activeLink = document.querySelector("nav a.active");
+ showPage(activeLink ? activeLink.dataset.page : "dashboard");
+
+ } catch(error){
+ messageBox.textContent = error.message;
+ }
+}
+
+// ---------- modal infrastructure ----------
+
+function ensureModalRoot(){
+ let modalRoot = document.getElementById("modal-root");
+
+ if(!modalRoot){
+ modalRoot = document.createElement("div");
+ modalRoot.id = "modal-root";
+ document.body.appendChild(modalRoot);
+ }
+
+ return modalRoot;
+}
+
+function openModal(innerHtml){
+ let modalRoot = ensureModalRoot();
+
+ modalRoot.innerHTML = `
+
`;
+ document.addEventListener("keydown", handleModalEscape);
}
+function closeModal(){
+ let modalRoot = document.getElementById("modal-root");
+ if(modalRoot) modalRoot.innerHTML = "";
+ document.removeEventListener("keydown", handleModalEscape);
+}
+
+function handleModalEscape(event){
+ if(event.key === "Escape") closeModal();
+}
// ---------- rooms page ----------
-
async function loadRooms(){
-
- let content =
- document.getElementById("content");
-
+ let content = document.getElementById("content");
content.innerHTML = `
Loading...
`;
-
try{
+ let roomsData = await apiFetch("/rooms");
+ let rooms = roomsData.rooms || [];
+ cachedRoomsForForm = rooms;
- let roomsData =
- await apiFetch("/rooms");
-
- let rooms =
- roomsData.rooms || [];
+ let actionsHtml = `
+
+ `;
if(rooms.length === 0){
-
content.innerHTML = `
Your shared rooms appear here.
`;
-
return;
-
}
-
- let roomsHtml =
- rooms.map(room => `
-
-
${room.room_name}
-
invite code: ${room.invite_code || "-"}
-
+ let roomsHtml = rooms.map(room => `
+
+
${room.name}
+
invite code: ${room.invite_code || "-"}
+
+
+
+
- `).join("");
-
+
+ `).join("");
content.innerHTML = `
Rooms
+ ${actionsHtml}
${roomsHtml}
`;
} catch(error){
-
content.innerHTML = `
Rooms
Could not load rooms: ${error.message}
`;
-
}
-
}
+async function handleSearchRoom(event){
+ event.preventDefault();
+
+ let code = document.getElementById("search-room-code").value;
+ let resultBox = document.getElementById("search-room-result");
+
+ resultBox.innerHTML = `
Searching...
`;
+
+ try{
+ let result = await apiFetch("/search-room", {
+ method: "POST",
+ body: JSON.stringify({ invite_code: code })
+ });
+
+ if(result.status === "failed"){
+ resultBox.innerHTML = `
No room found for that code.
`;
+ return;
+ }
+
+ resultBox.innerHTML = `
+
+
${result.room_name}
+
+
+
+
+ `;
+
+ } catch(error){
+ resultBox.innerHTML = `
${error.message}
`;
+ }
+}
+
+async function handleJoinRoomByCode(code){
+ try{
+ let result = await apiFetch("/join-room", {
+ method: "POST",
+ body: JSON.stringify({ invite_code: code })
+ });
+
+ if(result.status === "failed"){
+ alert("Could not join: invalid code, or you're already a member.");
+ return;
+ }
+
+ showPage("rooms");
+
+ } catch(error){
+ alert(error.message);
+ }
+}
async function loadRoomEvents(roomId){
-
- let content =
- document.getElementById("content");
-
+ let content = document.getElementById("content");
content.innerHTML = `
Room events
Loading...
`;
-
try{
-
- let data =
- await apiFetch(`/rooms/${roomId}/events`);
-
- let events =
- data.events || [];
-
+ let data = await apiFetch(`/rooms/${roomId}/events`);
+ let events = data.events || [];
content.innerHTML = `
Room events
+
+
+
${renderEventCards(events)}
`;
} catch(error){
-
content.innerHTML = `
Room events
Could not load events: ${error.message}
`;
-
}
-
}
+// ---------- room detail (manage) ----------
+async function loadRoomDetail(roomId){
+ let content = document.getElementById("content");
+
+ content.innerHTML = `
Room
Loading...
`;
+
+ try{
+ let data = await apiFetch(`/rooms/${roomId}`);
+ let room = data.room;
+ let members = data.members || [];
+ let events = data.events || [];
+
+ let ownerUsername = `user #${room.owner_id}`;
+ try{
+ let owner = await apiFetch(`/users/${room.owner_id}`);
+ ownerUsername = owner.username;
+ } catch(error){
+ // keep the fallback label
+ }
+
+ content.innerHTML = `
+
${room.name}
+
+
+
Owner: ${ownerUsername}
+
Your role: ${room.role}
+
+
+
+
+
+
+
+
+
${renderMembersList(members)}
+
+
+
+
Leave or delete
+
+
+
+
+
+ `;
+
+ } catch(error){
+ content.innerHTML = `
Room
Could not load room: ${error.message}
`;
+ }
+}
+
+function renderMembersList(members){
+ if(!members.length){
+ return `
No members found.
`;
+ }
+
+ return members.map(member => `
+
+ ${member.username}${member.role ? ` (${member.role})` : ""}
+
+ `).join("");
+}
+
+async function refreshRoomMembers(roomId){
+ let listBox = document.getElementById("room-members-list");
+ if(!listBox) return;
+
+ listBox.innerHTML = `
Refreshing...
`;
+
+ try{
+ let data = await apiFetch(`/rooms/${roomId}/members`);
+ listBox.innerHTML = renderMembersList(data.members || []);
+ } catch(error){
+ listBox.innerHTML = `
Could not refresh members: ${error.message}
`;
+ }
+}
+
+async function handleLeaveRoom(roomId){
+ if(!confirm("Leave this room?")) return;
+
+ try{
+ await apiFetch(`/rooms/${roomId}/members/me`, { method: "DELETE" });
+ showPage("rooms");
+ } catch(error){
+ alert("Could not leave room: " + error.message);
+ }
+}
+
+async function handleDeleteRoom(roomId){
+ if(!confirm("Delete this room? This cannot be undone.")) return;
+
+ try{
+ let result = await apiFetch(`/room/${roomId}`, { method: "DELETE" });
+
+ if(result.status === "denied"){
+ alert("You don't have permission to delete this room.");
+ return;
+ }
+
+ if(result.status !== "deleted"){
+ // the endpoint currently always returns this until it's implemented
+ alert("Room was not deleted: " + (result.reason || "unknown reason"));
+ return;
+ }
+
+ showPage("rooms");
+
+ } catch(error){
+ alert("Could not delete room: " + error.message);
+ }
+}
// ---------- events page ----------
-
async function loadEvents(){
-
- let content =
- document.getElementById("content");
-
+ let content = document.getElementById("content");
content.innerHTML = `
Events
Loading...
`;
-
try{
-
- let data =
- await apiFetch("/events");
-
- let events =
- data.events || [];
-
+ let data = await apiFetch("/events");
+ let events = data.events || [];
content.innerHTML = `
Events
+
+
+
${renderEventCards(events)}
`;
} catch(error){
-
content.innerHTML = `
Events
Could not load events: ${error.message}
`;
-
}
-
}
-
function renderEventCards(events){
-
if(events.length === 0){
-
return `
No events scheduled.
`;
-
}
-
return events.map(event => `
-
-
${event.title}
-
${event.description || ""}
-
-
- ${formatEventTime(event.start_time, event.end_time)}
-
-
-
- status: ${event.status}
-
+
${formatEventTime(event.start_time, event.end_time)}
+
status: ${event.status}
-
-
-
-
-
-
-
+
+
+
+
-
-
+
-
`).join("");
-
}
-
function formatEventTime(start, end){
+ if(!start) return "time unknown";
- if(!start){
-
- return "time unknown";
-
- }
-
-
- let startDate =
- new Date(start);
-
- let endDate =
- end
- ? new Date(end)
- : null;
-
-
- let startText =
- startDate.toLocaleString();
-
- if(!endDate){
-
- return startText;
-
- }
+ let startDate = new Date(start);
+ let endDate = end ? new Date(end) : null;
+ let startText = startDate.toLocaleString();
+ if(!endDate) return startText;
return startText + " - " + endDate.toLocaleString();
-
}
-
async function handleRespond(eventId, status){
-
- let messageBox =
- document.getElementById(`event-response-${eventId}`);
-
+ let messageBox = document.getElementById(`event-response-${eventId}`);
try{
-
- let result =
- await apiFetch(
- `/events/${eventId}/respond?status=${status}`,
- { method: "POST" }
- );
-
-
- messageBox.textContent =
- `you responded: ${status} (${result.going} going)`;
-
+ let result = await apiFetch(`/events/${eventId}/respond?status=${status}`, { method: "POST" });
+ messageBox.textContent = `you responded: ${status} (${result.going} going)`;
} catch(error){
-
messageBox.textContent = error.message;
-
}
-
}
+// ---------- event detail / edit / delete ----------
+async function ensureCurrentUser(){
+ if(currentUserId !== null) return currentUserId;
+
+ try{
+ let me = await apiFetch("/me");
+ currentUserId = me.id;
+ } catch(error){
+ // isOwn checks just stay false
+ }
+
+ return currentUserId;
+}
+
+async function openEventDetailModal(eventId){
+ openModal(`
Loading...
`);
+
+ try{
+ await ensureCurrentUser();
+
+ let eventDetail = await apiFetch(`/events/${eventId}`);
+ let isOwn = eventDetail.creator_id === currentUserId;
+
+ openModal(`
+
${eventDetail.title}
+
${formatEventTime(eventDetail.start_time, eventDetail.end_time)}
+
${eventDetail.description || ""}
+
status: ${eventDetail.status}
+
+
Responses
+
+
+
+ ${isOwn ? `
+
+
+ ` : ""}
+
+
+ `);
+
+ try{
+ let responsesData = await apiFetch(`/events/${eventId}/responses`);
+ let responses = responsesData.responses || [];
+ let listBox = document.getElementById("event-responses-list");
+
+ if(listBox){
+ listBox.innerHTML = responses.length
+ ? responses.map(r => `
${r.username} — ${r.status}
`).join("")
+ : `
No responses yet.
`;
+ }
+ } catch(error){
+ let listBox = document.getElementById("event-responses-list");
+ if(listBox) listBox.innerHTML = `
Could not load responses: ${error.message}
`;
+ }
+
+ } catch(error){
+ openModal(`
Could not load event: ${error.message}
`);
+ }
+}
+
+async function openEditEventModal(eventId){
+ try{
+ let eventDetail = await apiFetch(`/events/${eventId}`);
+
+ openModal(`
+
Edit event
+
+ `);
+
+ } catch(error){
+ alert("Could not load event: " + error.message);
+ }
+}
+
+async function handleUpdateEvent(event, eventId){
+ event.preventDefault();
+
+ let messageBox = document.getElementById("edit-event-message");
+
+ let payload = {
+ title: document.getElementById("edit-event-title").value,
+ description: document.getElementById("edit-event-description").value,
+ start_time: toBackendDateTime(document.getElementById("edit-event-start").value),
+ end_time: toBackendDateTime(document.getElementById("edit-event-end").value),
+ min_people: parseInt(document.getElementById("edit-event-min-people").value, 10) || 1
+ };
+
+ try{
+ await apiFetch(`/events/${eventId}`, {
+ method: "PATCH",
+ body: JSON.stringify(payload)
+ });
+
+ closeModal();
+ showPage("events");
+
+ } catch(error){
+ messageBox.textContent = error.message;
+ }
+}
+
+async function handleDeleteEvent(eventId){
+ if(!confirm("Delete this event?")) return;
+
+ try{
+ await apiFetch(`/events/${eventId}`, { method: "DELETE" });
+ closeModal();
+ showPage("events");
+ } catch(error){
+ alert("Could not delete event: " + error.message);
+ }
+}
+
+// ---------- settings / accessibility ----------
+
+const A11Y_DEFAULTS = {
+ reducedMotion: false,
+ highContrast: false,
+ fontScale: "normal",
+ underlineLinks: false,
+ alwaysShowFocus: false
+};
+
+function getA11ySettings(){
+ try{
+ let stored = JSON.parse(localStorage.getItem("a11y-settings"));
+ return { ...A11Y_DEFAULTS, ...(stored || {}) };
+ } catch(error){
+ return { ...A11Y_DEFAULTS };
+ }
+}
+
+function applyA11ySettings(settings){
+ document.body.classList.toggle("reduced-motion", settings.reducedMotion);
+ document.body.classList.toggle("high-contrast", settings.highContrast);
+ document.body.classList.toggle("underline-links", settings.underlineLinks);
+ document.body.classList.toggle("always-focus", settings.alwaysShowFocus);
+
+ document.body.classList.remove("font-scale-large", "font-scale-xlarge");
+ if(settings.fontScale === "large") document.body.classList.add("font-scale-large");
+ if(settings.fontScale === "xlarge") document.body.classList.add("font-scale-xlarge");
+}
+
+function updateA11ySetting(key, value){
+ let settings = getA11ySettings();
+ settings[key] = value;
+
+ localStorage.setItem("a11y-settings", JSON.stringify(settings));
+ applyA11ySettings(settings);
+}
+
+function renderSettingsPage(content){
+ let settings = getA11ySettings();
+
+ content.innerHTML = `
+
Settings
+
+
+
Account
+
+
+
+
+
Accessibility
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Appearance
+
+
+
+
+
Diagnostics
+
+
+
+
+
+ `;
+}
+
+async function runDiagnostics(){
+ let resultBox = document.getElementById("diagnostics-result");
+ resultBox.textContent = "Checking...";
+
+ try{
+ let home = await apiFetch("/");
+ let db = await apiFetch("/database");
+ resultBox.textContent = `${home.service}: ${home.status}. Tables: ${(db.tables || []).join(", ")}`;
+ } catch(error){
+ resultBox.textContent = "Diagnostics failed: " + error.message;
+ }
+}
+
+async function loadAsciiArt(){
+ let box = document.getElementById("ascii-art-box");
+
+ try{
+ let headers = {};
+ let token = getToken();
+ if(token) headers["Authorization"] = "Bearer " + token;
+
+ let response = await fetch(API_BASE + "/ascii-art", { headers });
+ let text = await response.text();
+
+ box.textContent = text;
+ box.hidden = false;
+
+ } catch(error){
+ box.textContent = "Could not load ascii art.";
+ box.hidden = false;
+ }
+}
// ---------- theme ----------
-
function toggleTheme(){
-
document.body.classList.toggle("dark");
-
- let theme =
- document.body.classList.contains("dark")
- ? "dark"
- : "light";
-
-
- localStorage.setItem(
- "theme",
- theme
- );
-
+ let theme = document.body.classList.contains("dark") ? "dark" : "light";
+ localStorage.setItem("theme", theme);
}
-
-
// ---------- init ----------
-
-let savedTheme =
- localStorage.getItem("theme");
-
-
+let savedTheme = localStorage.getItem("theme");
if(savedTheme === "dark"){
-
document.body.classList.add("dark");
-
}
+applyA11ySettings(getA11ySettings());
if(getToken()){
-
showApp();
-
showPage("dashboard");
-
} else {
-
showLogin();
-
}
\ No newline at end of file
diff --git a/frontend/www/style.css b/frontend/www/style.css
index ae9b186..d98d6d4 100644
--- a/frontend/www/style.css
+++ b/frontend/www/style.css
@@ -1,408 +1,378 @@
+/* ============================================================
+ Design tokens
+ ------------------------------------------------------------
+ Palette: warm paper background, ink-dark sidebar "spine",
+ deep pine accent (not the generic AI-blue).
+ Type: Source Serif 4 for headings (editorial, planner-like),
+ Inter for body copy, IBM Plex Mono for time/data.
+ ============================================================ */
+
+@import url('https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,500;8..60,600&family=Inter:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap');
+
:root {
+ /* Surfaces */
+ --bg: #f7f6f3;
+ --panel: #ffffff;
+ --sidebar: #15171c;
+ --sidebar-hover: #1f222a;
- --bg: #f4f5f7;
- --panel: white;
- --sidebar: #ffffff;
- --text: #111;
- --muted: #666;
- --border: #ddd;
- --accent: #2563eb;
+ /* Text */
+ --text: #16181d;
+ --text-inverse: #eceae4;
+ --muted: #74716a;
+ --muted-inverse: #8b8e97;
+ /* Structure */
+ --border: #e5e2da;
+ --border-strong: #d3cfc3;
+
+ /* Accent */
+ --accent: #1f5c52;
+ --accent-hover: #164941;
+ --accent-tint: #e8efec;
+
+ /* Status (used as tint + left-bar, not solid fills) */
+ --status-confirmed: #1f5c52;
+ --status-confirmed-tint: #e8efec;
+ --status-busy: #a15c14;
+ --status-busy-tint: #f6ede1;
+ --status-private: #4b4d57;
+ --status-private-tint: #eceae7;
+
+ --danger: #ad2b38;
+
+ /* Scale */
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 12px;
+ --space-4: 16px;
+ --space-5: 24px;
+ --space-6: 32px;
+ --space-7: 40px;
+
+ --radius-sm: 6px;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+
+ --shadow-sm: 0 1px 2px rgba(21, 23, 28, 0.06);
+ --shadow-md: 0 4px 16px rgba(21, 23, 28, 0.08);
+
+ --ease: cubic-bezier(0.4, 0, 0.2, 1);
+
+ --font-display: "Source Serif 4", Georgia, serif;
+ --font-body: Inter, system-ui, sans-serif;
+ --font-mono: "IBM Plex Mono", ui-monospace, monospace;
}
-
-
.dark {
+ --bg: #0e0f12;
+ --panel: #17191d;
+ --sidebar: #0a0b0d;
+ --sidebar-hover: #161821;
- --bg: #080808;
- --panel: #151515;
- --sidebar: #101010;
- --text: #eee;
- --muted: #aaa;
- --border: #333;
- --accent: #38bdf8;
+ --text: #eceae4;
+ --text-inverse: #16181d;
+ --muted: #91908a;
+ --muted-inverse: #6b6d75;
+ --border: #2a2c31;
+ --border-strong: #3a3d43;
+
+ --accent: #4fa393;
+ --accent-hover: #63b6a5;
+ --accent-tint: #16221f;
+
+ --status-confirmed: #4fa393;
+ --status-confirmed-tint: #16221f;
+ --status-busy: #d1934f;
+ --status-busy-tint: #26200f;
+ --status-private: #9294a0;
+ --status-private-tint: #1c1d21;
+
+ --danger: #e2707c;
+
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
+ --shadow-md: 0 8px 24px rgba(0, 0, 0, 0.4);
}
-
-
* {
box-sizing: border-box;
}
-
-
body {
-
margin: 0;
-
background: var(--bg);
-
color: var(--text);
-
- font-family:
- Inter,
- system-ui,
- sans-serif;
-
- transition: 0.2s;
-
+ font-family: var(--font-body);
+ font-size: 14px;
+ line-height: 1.5;
+ -webkit-font-smoothing: antialiased;
+ transition: background 0.2s var(--ease), color 0.2s var(--ease);
}
-
+/* ============================================================
+ Layout
+ ============================================================ */
.layout {
-
display: grid;
-
- grid-template-columns: 260px 1fr;
-
+ grid-template-columns: 248px 1fr;
height: 100vh;
-
- display: none;
-
}
-
.sidebar {
-
background: var(--sidebar);
-
- border-right: 1px solid var(--border);
-
- padding: 25px;
-
+ color: var(--text-inverse);
+ border-right: 1px solid rgba(255, 255, 255, 0.06);
+ padding: var(--space-5) var(--space-4);
display: flex;
-
flex-direction: column;
-
}
-
-
.logo {
-
display: flex;
-
align-items: center;
-
- gap: 10px;
-
- margin-bottom: 40px;
-
+ gap: var(--space-2);
+ margin-bottom: var(--space-6);
+ padding: 0 var(--space-2);
}
-
-
.logo h2 {
-
margin: 0;
-
+ font-family: var(--font-display);
+ font-weight: 600;
+ font-size: 19px;
+ letter-spacing: -0.01em;
}
-
-
nav {
-
display: flex;
-
flex-direction: column;
-
- gap: 8px;
-
+ gap: 2px;
}
-
-
nav a {
-
cursor: pointer;
-
- padding: 12px 15px;
-
- border-radius: 10px;
-
- color: var(--muted);
-
- transition: 0.2s;
-
+ padding: 10px 14px;
+ border-radius: var(--radius-sm);
+ color: var(--muted-inverse);
+ font-size: 13.5px;
+ font-weight: 500;
+ transition: background 0.15s var(--ease), color 0.15s var(--ease);
}
+nav a:hover {
+ background: var(--sidebar-hover);
+ color: var(--text-inverse);
+}
-
-nav a:hover,
nav .active {
-
background: var(--accent);
-
- color: white;
-
+ color: #ffffff;
}
-
-
.sidebar-bottom {
-
margin-top: auto;
-
+ padding-top: var(--space-4);
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
}
-
-
button {
-
width: 100%;
-
- padding: 12px;
-
- border-radius: 10px;
-
+ padding: 10px 14px;
+ border-radius: var(--radius-sm);
border: 1px solid var(--border);
-
background: var(--panel);
-
color: var(--text);
-
+ font-family: var(--font-body);
+ font-size: 13.5px;
+ font-weight: 500;
cursor: pointer;
-
- margin-top: 8px;
-
+ margin-top: var(--space-2);
+ transition: border-color 0.15s var(--ease), background 0.15s var(--ease);
}
+button:hover {
+ border-color: var(--border-strong);
+ background: var(--bg);
+}
+button:focus-visible,
+a:focus-visible,
+input:focus-visible {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+}
+
+.sidebar button {
+ background: transparent;
+ border-color: rgba(255, 255, 255, 0.12);
+ color: var(--text-inverse);
+}
+
+.sidebar button:hover {
+ background: var(--sidebar-hover);
+ border-color: rgba(255, 255, 255, 0.2);
+}
/* Content */
.content {
-
- padding: 40px;
-
+ padding: var(--space-6) var(--space-7);
overflow-y: auto;
-
}
-
-
h1 {
-
margin-top: 0;
-
+ font-family: var(--font-display);
+ font-weight: 600;
+ font-size: 26px;
+ letter-spacing: -0.01em;
}
-
-
.card {
-
background: var(--panel);
-
border: 1px solid var(--border);
-
- border-radius: 16px;
-
- padding: 25px;
-
- margin-top: 20px;
-
- box-shadow:
- 0 10px 30px rgba(0,0,0,0.05);
-
+ border-radius: var(--radius-lg);
+ padding: var(--space-5);
+ margin-top: var(--space-4);
+ box-shadow: var(--shadow-sm);
}
-
-
/* forms */
input {
-
width: 100%;
-
- padding: 12px;
-
- border-radius: 10px;
-
+ padding: 10px 12px;
+ border-radius: var(--radius-sm);
border: 1px solid var(--border);
-
background: var(--bg);
-
color: var(--text);
-
- margin-top: 8px;
-
+ font-family: var(--font-body);
+ font-size: 13.5px;
+ margin-top: var(--space-2);
+ transition: border-color 0.15s var(--ease);
}
-
+input:focus {
+ border-color: var(--accent);
+}
form {
-
display: flex;
-
flex-direction: column;
-
}
-
-
.muted {
-
color: var(--muted);
-
}
-
-
.error {
-
- color: #e11d48;
-
+ color: var(--danger);
}
-
-
/* list items */
.list-item {
-
- padding: 10px 15px;
-
+ padding: 10px 14px;
border: 1px solid var(--border);
-
- border-radius: 10px;
-
- margin-top: 10px;
-
+ border-radius: var(--radius-md);
+ margin-top: var(--space-2);
+ transition: border-color 0.15s var(--ease);
}
-
+.list-item:hover {
+ border-color: var(--border-strong);
+}
/* event actions */
.event-actions {
-
display: flex;
-
- gap: 10px;
-
- margin-top: 15px;
-
+ gap: var(--space-2);
+ margin-top: var(--space-4);
}
-
.event-actions button {
-
margin-top: 0;
-
}
-
-
/* calendar */
.calendar-header {
-
display: flex;
-
align-items: center;
-
justify-content: space-between;
-
}
-
.calendar-header h3 {
-
margin: 0;
-
+ font-family: var(--font-display);
+ font-weight: 600;
+ font-size: 17px;
text-transform: capitalize;
-
}
-
.calendar-nav-btn {
-
width: auto;
-
- padding: 8px 14px;
-
+ padding: 6px 12px;
margin-top: 0;
-
}
-
.calendar-legend {
-
display: flex;
-
- gap: 18px;
-
- margin-top: 14px;
-
+ gap: var(--space-5);
+ margin-top: var(--space-4);
font-size: 12px;
-
color: var(--muted);
-
}
-
.legend-item {
-
display: flex;
-
align-items: center;
-
gap: 6px;
-
}
-
.legend-dot {
-
- width: 10px;
-
- height: 10px;
-
- border-radius: 50%;
-
+ width: 8px;
+ height: 8px;
+ border-radius: 2px;
display: inline-block;
-
}
-
/* week grid */
.calendar-week {
display: flex;
- margin-top: 16px;
+ margin-top: var(--space-4);
overflow-x: auto;
}
-
.calendar-gutter {
- width: 55px;
+ width: 52px;
flex-shrink: 0;
padding-top: 0px;
}
-
.calendar-hour-label {
- height: 48px; /* 2 uur = 48px */
- font-size: 11px;
+ height: 48px;
+ font-family: var(--font-mono);
+ font-size: 10.5px;
color: var(--muted);
text-align: right;
- padding-right: 8px;
+ padding-right: var(--space-2);
display: flex;
justify-content: flex-end;
align-items: flex-start;
transform: translateY(-6px);
}
-
.calendar-days {
display: flex;
flex: 1;
min-width: 700px;
}
-
.calendar-day-column {
flex: 1;
display: flex;
@@ -410,138 +380,265 @@ form {
border-left: 1px solid var(--border);
}
-
.calendar-day-header {
height: 32px;
text-align: center;
padding: 6px 0;
- font-size: 12px;
+ font-family: var(--font-mono);
+ font-size: 11px;
+ letter-spacing: 0.02em;
+ text-transform: uppercase;
color: var(--muted);
}
-
.calendar-day-track {
position: relative;
-
- height: 576px; /* 12 uur zichtbaar × 48px */
-
- background-image:
- repeating-linear-gradient(
- to bottom,
- var(--border) 0,
- var(--border) 1px,
- transparent 1px,
- transparent 48px
- );
+ height: 576px; /* 12 uur zichtbaar x 48px */
+ background-image: repeating-linear-gradient(
+ to bottom,
+ var(--border) 0,
+ var(--border) 1px,
+ transparent 1px,
+ transparent 48px
+ );
}
-
.calendar-event {
position: absolute;
-
left: 4px;
right: 4px;
-
- border-radius: 6px;
-
- padding: 3px 6px;
-
+ border-radius: var(--radius-sm);
+ padding: 4px 8px 4px 9px;
font-size: 11px;
-
- color: white;
-
+ color: var(--text);
cursor: pointer;
-
overflow: hidden;
-
- box-shadow:
- 0 2px 6px rgba(0,0,0,0.25);
-
- opacity: 0.92;
-
- border: 1px solid rgba(255,255,255,0.4);
+ border-left: 3px solid transparent;
+ background: var(--panel);
+ box-shadow: var(--shadow-sm);
+ transition: box-shadow 0.15s var(--ease);
}
+.calendar-event:hover {
+ box-shadow: var(--shadow-md);
+}
.calendar-event-time {
display: block;
- font-size: 10px;
- opacity: 0.85;
+ font-family: var(--font-mono);
+ font-size: 9.5px;
+ color: var(--muted);
}
-
.calendar-event-title {
display: block;
font-weight: 600;
-
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.event-confirmed {
-
- background: #16a34a;
-
+ background: var(--status-confirmed-tint);
+ border-left-color: var(--status-confirmed);
}
-
.event-busy {
-
- background: #f97316;
-
+ background: var(--status-busy-tint);
+ border-left-color: var(--status-busy);
}
-
.event-private {
-
- background: #6b7280;
-
+ background: var(--status-private-tint);
+ border-left-color: var(--status-private);
}
-
+.legend-dot.event-confirmed,
+.legend-dot.event-busy,
+.legend-dot.event-private {
+ border-radius: 2px;
+ border-left: none;
+}
/* login screen */
.login-screen {
-
height: 100vh;
-
width: 100%;
-
display: flex;
-
align-items: center;
-
justify-content: center;
-
+ background: var(--bg);
}
-
-
.login-box {
-
background: var(--panel);
-
border: 1px solid var(--border);
-
- border-radius: 16px;
-
- padding: 40px;
-
+ border-radius: var(--radius-lg);
+ padding: var(--space-6);
width: 320px;
-
- box-shadow:
- 0 10px 30px rgba(0,0,0,0.05);
-
+ box-shadow: var(--shadow-md);
}
-
-
.login-box h2 {
-
margin-top: 0;
-
+ margin-bottom: var(--space-5);
+ font-family: var(--font-display);
+ font-weight: 600;
text-align: center;
+}
+/* ============================================================
+ Modals
+ ============================================================ */
+
+.modal-backdrop {
+ position: fixed;
+ inset: 0;
+ background: rgba(10, 11, 13, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--space-4);
+ z-index: 100;
+}
+
+.modal-box {
+ background: var(--panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-lg);
+ padding: var(--space-5);
+ width: 100%;
+ max-width: 440px;
+ max-height: 85vh;
+ overflow-y: auto;
+ box-shadow: var(--shadow-md);
+}
+
+.modal-box h2 {
+ margin-top: 0;
+ font-family: var(--font-display);
+ font-weight: 600;
+ font-size: 19px;
+}
+
+.modal-box label {
+ display: block;
+ font-size: 12.5px;
+ color: var(--muted);
+ margin-top: var(--space-3);
+}
+
+.modal-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--space-2);
+ margin-top: var(--space-5);
+}
+
+.modal-actions button {
+ width: auto;
+ margin-top: 0;
+ padding: 8px 16px;
+}
+
+.danger-button {
+ border-color: var(--danger) !important;
+ color: var(--danger) !important;
+}
+
+/* action rows above list pages */
+
+.page-actions {
+ display: flex;
+ gap: var(--space-2);
+ margin-bottom: var(--space-2);
+}
+
+.page-actions button {
+ width: auto;
+ margin-top: 0;
+}
+
+/* settings */
+
+.settings-row {
+ display: flex;
+ align-items: center;
+ gap: var(--space-2);
+ font-size: 13.5px;
+ color: var(--text);
+ margin-top: var(--space-3);
+}
+
+.settings-row input[type="checkbox"] {
+ width: auto;
+ margin: 0;
+}
+
+.settings-row select {
+ margin-left: auto;
+ width: auto;
+ padding: 6px 10px;
+ border-radius: var(--radius-sm);
+ border: 1px solid var(--border);
+ background: var(--bg);
+ color: var(--text);
+}
+
+.ascii-art-box {
+ font-family: var(--font-mono);
+ font-size: 11px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ padding: var(--space-3);
+ overflow-x: auto;
+ margin-top: var(--space-2);
+}
+
+/* ============================================================
+ Accessibility preferences
+ ============================================================ */
+
+body.reduced-motion,
+body.reduced-motion * {
+ transition: none !important;
+ animation: none !important;
+}
+
+body.high-contrast {
+ --border: #000000;
+ --border-strong: #000000;
+ --muted: #2a2a2a;
+}
+
+body.high-contrast.dark {
+ --border: #ffffff;
+ --border-strong: #ffffff;
+ --muted: #e6e6e6;
+}
+
+body.high-contrast .calendar-event {
+ border-left-width: 4px;
+}
+
+body.underline-links a {
+ text-decoration: underline;
+}
+
+body.always-focus a:focus,
+body.always-focus button:focus,
+body.always-focus input:focus,
+body.always-focus select:focus {
+ outline: 2px solid var(--accent);
+ outline-offset: 2px;
+}
+
+body.font-scale-large {
+ font-size: 16px;
+}
+
+body.font-scale-xlarge {
+ font-size: 18px;
}
\ No newline at end of file