first commit
This commit is contained in:
359
static/app.js
Normal file
359
static/app.js
Normal file
@@ -0,0 +1,359 @@
|
||||
const EURJPY = 182.85;
|
||||
|
||||
function mapsLink(q) {
|
||||
return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(q)}`;
|
||||
}
|
||||
function jpyToEur(jpy) {
|
||||
return jpy / EURJPY;
|
||||
}
|
||||
function eurToJpy(eur) {
|
||||
return eur * EURJPY;
|
||||
}
|
||||
function formatJpy(n) {
|
||||
return "¥" + Number(n || 0).toLocaleString("it-IT", { maximumFractionDigits: 0 });
|
||||
}
|
||||
function formatEur(n) {
|
||||
return new Intl.NumberFormat("it-IT", { style: "currency", currency: "EUR" }).format(n || 0);
|
||||
}
|
||||
|
||||
// Minimal “calendar” dataset (lorem ipsum placeholders)
|
||||
const DAYS = [
|
||||
{
|
||||
id: "d01",
|
||||
dateLabel: "14 giugno",
|
||||
title: "Tokyo · Arrivo",
|
||||
city: "Tokyo",
|
||||
base: "Asakusa",
|
||||
summary: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
|
||||
movements: [
|
||||
{ time: "10:30", from: "Narita", to: "Asakusa", mode: "Train", mapsQuery: "Narita Airport to Asakusa" },
|
||||
{ time: "15:00", from: "Asakusa", to: "Shibuya", mode: "Metro", mapsQuery: "Asakusa to Shibuya" },
|
||||
],
|
||||
attractions: [
|
||||
{ name: "Lorem Shrine", note: "Lorem ipsum dolor sit amet.", mapsQuery: "Meiji Jingu" },
|
||||
{ name: "Ipsum Crossing", note: "Sed do eiusmod tempor.", mapsQuery: "Shibuya Crossing" },
|
||||
],
|
||||
lunch: [
|
||||
{ name: "Lorem Ramen", mapsQuery: "Ramen Asakusa" },
|
||||
{ name: "Ipsum Sushi", mapsQuery: "Sushi Asakusa" },
|
||||
],
|
||||
dinner: [
|
||||
{ name: "Dolor Izakaya", mapsQuery: "Izakaya Shinjuku" },
|
||||
{ name: "Sit Tempura", mapsQuery: "Tempura Shinjuku" },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "d02",
|
||||
dateLabel: "15 giugno",
|
||||
title: "Tokyo · Tradizione",
|
||||
city: "Tokyo",
|
||||
base: "Asakusa",
|
||||
summary: "Ut enim ad minim veniam, quis nostrud exercitation ullamco.",
|
||||
movements: [
|
||||
{ time: "09:00", from: "Asakusa", to: "Senso-ji", mode: "Walk", mapsQuery: "Senso-ji" },
|
||||
{ time: "12:00", from: "Senso-ji", to: "Skytree", mode: "Walk", mapsQuery: "Tokyo Skytree" },
|
||||
],
|
||||
attractions: [
|
||||
{ name: "Senso-ji", note: "Lorem ipsum dolor sit amet.", mapsQuery: "Senso-ji" },
|
||||
{ name: "Tokyo Skytree", note: "Consectetur adipiscing elit.", mapsQuery: "Tokyo Skytree" },
|
||||
],
|
||||
lunch: [
|
||||
{ name: "Lorem Bento", mapsQuery: "Tokyo Solamachi food" },
|
||||
{ name: "Ipsum Udon", mapsQuery: "Udon Asakusa" },
|
||||
],
|
||||
dinner: [
|
||||
{ name: "Dolor Gyoza", mapsQuery: "Gyoza Akihabara" },
|
||||
{ name: "Sit Soba", mapsQuery: "Soba Asakusa" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const elCalendar = document.getElementById("calendar");
|
||||
const elDayView = document.getElementById("dayView");
|
||||
|
||||
function renderCalendar(selectedId) {
|
||||
elCalendar.innerHTML = DAYS.map((d) => {
|
||||
const active = d.id === selectedId ? " is-active" : "";
|
||||
return `
|
||||
<button class="cal-day${active}" data-id="${d.id}" type="button">
|
||||
<div class="cal-date">${d.dateLabel}</div>
|
||||
<div class="cal-title">${d.title}</div>
|
||||
</button>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
elCalendar.querySelectorAll(".cal-day").forEach((b) => {
|
||||
b.addEventListener("click", () => {
|
||||
const id = b.getAttribute("data-id");
|
||||
renderDay(id);
|
||||
renderCalendar(id);
|
||||
location.hash = "#giorno";
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function renderDay(dayId) {
|
||||
const day = DAYS.find((d) => d.id === dayId) || DAYS[0];
|
||||
|
||||
const movements = day.movements
|
||||
.map(
|
||||
(m) => `
|
||||
<div class="trow">
|
||||
<div class="t">${m.time}</div>
|
||||
<div>
|
||||
<div class="p">${m.from} → ${m.to}</div>
|
||||
<div class="d">${m.mode} · Lorem ipsum dolor sit amet.</div>
|
||||
</div>
|
||||
<div><a class="btn ghost" target="_blank" href="${mapsLink(m.mapsQuery)}">Maps</a></div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
const attractions = day.attractions
|
||||
.map(
|
||||
(a) => `
|
||||
<li>
|
||||
<a class="placeLink" target="_blank" href="${mapsLink(a.mapsQuery)}">${a.name}</a>
|
||||
<div class="small">${a.note}</div>
|
||||
</li>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
const lunch = day.lunch
|
||||
.map((p) => `<li><a class="placeLink" target="_blank" href="${mapsLink(p.mapsQuery)}">${p.name}</a></li>`)
|
||||
.join("");
|
||||
|
||||
const dinner = day.dinner
|
||||
.map((p) => `<li><a class="placeLink" target="_blank" href="${mapsLink(p.mapsQuery)}">${p.name}</a></li>`)
|
||||
.join("");
|
||||
|
||||
elDayView.innerHTML = `
|
||||
<div class="kpis">
|
||||
<div class="kpi">
|
||||
<div class="label">Dove siete</div>
|
||||
<div class="value">${day.city} · ${day.base}</div>
|
||||
</div>
|
||||
<div class="kpi">
|
||||
<div class="label">Nota</div>
|
||||
<div class="value">${day.summary}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="meal">
|
||||
<h4>Spostamenti</h4>
|
||||
<div class="timeline">${movements}</div>
|
||||
</div>
|
||||
|
||||
<div class="meal">
|
||||
<h4>Attrazioni</h4>
|
||||
<ul>${attractions}</ul>
|
||||
</div>
|
||||
|
||||
<div class="meal">
|
||||
<h4>Pranzo (suggeriti)</h4>
|
||||
<ul>${lunch}</ul>
|
||||
</div>
|
||||
|
||||
<div class="meal">
|
||||
<h4>Cena (suggeriti)</h4>
|
||||
<ul>${dinner}</ul>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function apiFetch(path, options) {
|
||||
const res = await fetch(path, {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
...options,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text || `HTTP ${res.status}`);
|
||||
}
|
||||
if (res.status === 204) return null;
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// -----------------
|
||||
// TODO UI
|
||||
// -----------------
|
||||
|
||||
const elTodoList = document.getElementById("todoList");
|
||||
const elTodoNewText = document.getElementById("todoNewText");
|
||||
const elTodoAddBtn = document.getElementById("todoAddBtn");
|
||||
|
||||
async function renderTodos() {
|
||||
const todos = await apiFetch("/api/todos");
|
||||
elTodoList.innerHTML = todos.length
|
||||
? todos
|
||||
.map(
|
||||
(t) => `
|
||||
<div class="todo-item">
|
||||
<label class="todo-check">
|
||||
<input type="checkbox" data-id="${t.id}" ${t.done ? "checked" : ""} />
|
||||
<span>${t.text}</span>
|
||||
</label>
|
||||
<div class="todo-actions">
|
||||
<button class="btn ghost todo-edit" data-id="${t.id}" type="button">Modifica</button>
|
||||
<button class="btn ghost todo-del" data-id="${t.id}" type="button">Elimina</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("")
|
||||
: `<div class="small" style="margin-top:12px">Nessuna voce. Lorem ipsum.</div>`;
|
||||
|
||||
elTodoList.querySelectorAll("input[type=checkbox]").forEach((cb) => {
|
||||
cb.addEventListener("change", async () => {
|
||||
const id = cb.getAttribute("data-id");
|
||||
await apiFetch(`/api/todos/${id}`, { method: "PATCH", body: JSON.stringify({ done: cb.checked }) });
|
||||
});
|
||||
});
|
||||
|
||||
elTodoList.querySelectorAll(".todo-del").forEach((b) => {
|
||||
b.addEventListener("click", async () => {
|
||||
const id = b.getAttribute("data-id");
|
||||
await apiFetch(`/api/todos/${id}`, { method: "DELETE" });
|
||||
await renderTodos();
|
||||
});
|
||||
});
|
||||
|
||||
elTodoList.querySelectorAll(".todo-edit").forEach((b) => {
|
||||
b.addEventListener("click", async () => {
|
||||
const id = b.getAttribute("data-id");
|
||||
const current = b.closest(".todo-item").querySelector(".todo-check span").textContent;
|
||||
const next = prompt("Modifica testo", current);
|
||||
if (next === null) return;
|
||||
await apiFetch(`/api/todos/${id}`, { method: "PATCH", body: JSON.stringify({ text: next }) });
|
||||
await renderTodos();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
elTodoAddBtn.addEventListener("click", async () => {
|
||||
const text = (elTodoNewText.value || "").trim();
|
||||
if (!text) return;
|
||||
await apiFetch("/api/todos", { method: "POST", body: JSON.stringify({ text }) });
|
||||
elTodoNewText.value = "";
|
||||
await renderTodos();
|
||||
});
|
||||
|
||||
elTodoNewText.addEventListener("keydown", async (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
elTodoAddBtn.click();
|
||||
}
|
||||
});
|
||||
|
||||
// -----------------
|
||||
// EXPENSES UI
|
||||
// -----------------
|
||||
|
||||
const elCostList = document.getElementById("costList");
|
||||
const elCostDesc = document.getElementById("costDesc");
|
||||
const elCostJpy = document.getElementById("costJpy");
|
||||
const elCostEur = document.getElementById("costEur");
|
||||
const elAddCostBtn = document.getElementById("addCostBtn");
|
||||
|
||||
async function renderExpenses() {
|
||||
const expenses = await apiFetch("/api/expenses");
|
||||
|
||||
elCostList.innerHTML = expenses.length
|
||||
? expenses
|
||||
.map(
|
||||
(c) => `
|
||||
<div class="cost-row">
|
||||
<div style="flex:1;min-width:0">
|
||||
<div class="p">${c.description}</div>
|
||||
<div class="small">${formatJpy(c.jpy)} · ${formatEur(c.eur)}</div>
|
||||
</div>
|
||||
<div class="todo-actions">
|
||||
<button class="btn ghost cost-edit" data-id="${c.id}" data-desc="${encodeURIComponent(
|
||||
c.description
|
||||
)}" data-jpy="${c.jpy}" data-eur="${c.eur}" type="button">Modifica</button>
|
||||
<button class="btn ghost cost-del" data-id="${c.id}" type="button">Elimina</button>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)
|
||||
.join("")
|
||||
: `<div class="small" style="margin-top:12px">Nessuna spesa inserita. Lorem ipsum.</div>`;
|
||||
|
||||
const totalJpy = expenses.reduce((a, c) => a + Number(c.jpy || 0), 0);
|
||||
const totalEur = expenses.reduce((a, c) => a + Number(c.eur || 0), 0);
|
||||
document.getElementById("totalJpy").textContent = formatJpy(totalJpy);
|
||||
document.getElementById("totalEur").textContent = formatEur(totalEur);
|
||||
|
||||
elCostList.querySelectorAll(".cost-del").forEach((b) => {
|
||||
b.addEventListener("click", async () => {
|
||||
const id = b.getAttribute("data-id");
|
||||
await apiFetch(`/api/expenses/${id}`, { method: "DELETE" });
|
||||
await renderExpenses();
|
||||
});
|
||||
});
|
||||
|
||||
elCostList.querySelectorAll(".cost-edit").forEach((b) => {
|
||||
b.addEventListener("click", async () => {
|
||||
const id = b.getAttribute("data-id");
|
||||
const desc = decodeURIComponent(b.getAttribute("data-desc"));
|
||||
const jpy = Number(b.getAttribute("data-jpy"));
|
||||
const eur = Number(b.getAttribute("data-eur"));
|
||||
|
||||
const nextDesc = prompt("Descrizione", desc);
|
||||
if (nextDesc === null) return;
|
||||
const nextJpyRaw = prompt("Importo JPY", String(jpy));
|
||||
if (nextJpyRaw === null) return;
|
||||
const nextJpy = Number(nextJpyRaw);
|
||||
if (!Number.isFinite(nextJpy) || nextJpy < 0) return;
|
||||
const nextEur = jpyToEur(nextJpy);
|
||||
|
||||
await apiFetch(`/api/expenses/${id}`, {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ description: nextDesc, jpy: nextJpy, eur: nextEur }),
|
||||
});
|
||||
await renderExpenses();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
elAddCostBtn.addEventListener("click", async () => {
|
||||
const description = (elCostDesc.value || "").trim();
|
||||
const jpyRaw = elCostJpy.value;
|
||||
const eurRaw = elCostEur.value;
|
||||
|
||||
if (!description) return;
|
||||
|
||||
const jpyVal = Number(jpyRaw);
|
||||
const eurVal = Number(eurRaw);
|
||||
|
||||
let jpy = 0;
|
||||
let eur = 0;
|
||||
|
||||
if (jpyVal > 0) {
|
||||
jpy = jpyVal;
|
||||
eur = jpyToEur(jpyVal);
|
||||
} else if (eurVal > 0) {
|
||||
eur = eurVal;
|
||||
jpy = eurToJpy(eurVal);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
await apiFetch("/api/expenses", { method: "POST", body: JSON.stringify({ description, jpy, eur }) });
|
||||
|
||||
elCostDesc.value = "";
|
||||
elCostJpy.value = "";
|
||||
elCostEur.value = "";
|
||||
|
||||
await renderExpenses();
|
||||
});
|
||||
|
||||
(function init() {
|
||||
document.getElementById("rate").textContent = String(EURJPY);
|
||||
renderCalendar(DAYS[0].id);
|
||||
renderDay(DAYS[0].id);
|
||||
renderTodos();
|
||||
renderExpenses();
|
||||
})();
|
||||
241
static/style.css
Normal file
241
static/style.css
Normal file
@@ -0,0 +1,241 @@
|
||||
:root{
|
||||
--bg:#f7f2ea;
|
||||
--paper:#fffaf2;
|
||||
--ink:#1b1b1b;
|
||||
--muted:#6b6b6b;
|
||||
--line:rgba(27,27,27,.12);
|
||||
--shadow:0 10px 30px rgba(0,0,0,.08);
|
||||
|
||||
/* Japan-inspired accents: vermilion + indigo + gold */
|
||||
--vermilion:#d6453d;
|
||||
--indigo:#1f2a44;
|
||||
--gold:#b08a2e;
|
||||
|
||||
--radius:18px;
|
||||
}
|
||||
|
||||
*{box-sizing:border-box}
|
||||
html{scroll-behavior:smooth}
|
||||
body{
|
||||
margin:0;
|
||||
padding:0 0 92px;
|
||||
background:
|
||||
radial-gradient(1200px 600px at 20% -10%, rgba(214,69,61,.10), transparent 60%),
|
||||
radial-gradient(900px 500px at 90% 0%, rgba(31,42,68,.10), transparent 55%),
|
||||
var(--bg);
|
||||
color:var(--ink);
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
}
|
||||
|
||||
.page{min-height:100vh}
|
||||
|
||||
.wrap{max-width:860px;margin:0 auto;padding:0 16px}
|
||||
|
||||
.topbar{
|
||||
position:sticky;
|
||||
top:0;
|
||||
z-index:20;
|
||||
background:color-mix(in srgb, var(--bg) 88%, transparent);
|
||||
backdrop-filter: blur(14px);
|
||||
border-bottom:1px solid var(--line);
|
||||
}
|
||||
.topbar-inner{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 0}
|
||||
|
||||
.brand{display:flex;align-items:center;gap:12px}
|
||||
.brand-mark{
|
||||
width:34px;height:34px;border-radius:999px;
|
||||
background:
|
||||
radial-gradient(circle at 35% 35%, rgba(255,255,255,.9), rgba(255,255,255,0) 55%),
|
||||
linear-gradient(135deg, var(--vermilion), color-mix(in srgb, var(--vermilion) 55%, var(--indigo)));
|
||||
box-shadow: 0 10px 20px rgba(214,69,61,.18);
|
||||
border:1px solid rgba(0,0,0,.08);
|
||||
}
|
||||
.brand-title{font-weight:900;letter-spacing:.2px}
|
||||
.brand-sub{font-size:12px;color:var(--muted)}
|
||||
|
||||
main{padding:16px 0}
|
||||
|
||||
.card{
|
||||
background:color-mix(in srgb, var(--paper) 92%, white);
|
||||
border:1px solid var(--line);
|
||||
border-radius:var(--radius);
|
||||
padding:16px;
|
||||
margin-bottom:14px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.section{
|
||||
font-size:16px;
|
||||
font-weight:950;
|
||||
margin:0 0 10px;
|
||||
color:var(--indigo);
|
||||
}
|
||||
|
||||
.small{font-size:12px;color:var(--muted)}
|
||||
.muted{font-size:12px;color:var(--muted);margin-top:4px}
|
||||
|
||||
.notice{
|
||||
background: rgba(31,42,68,.06);
|
||||
border:1px solid rgba(31,42,68,.18);
|
||||
color: var(--indigo);
|
||||
border-radius:14px;
|
||||
padding:10px 12px;
|
||||
font-size:12px;
|
||||
line-height:1.5;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.notice--error{
|
||||
background: rgba(214,69,61,.08);
|
||||
border-color: rgba(214,69,61,.25);
|
||||
color: #7a1f1a;
|
||||
}
|
||||
|
||||
.actions{display:flex;gap:8px;flex-wrap:wrap}
|
||||
.btn{
|
||||
appearance:none;
|
||||
border:none;
|
||||
text-decoration:none;
|
||||
display:inline-flex;
|
||||
align-items:center;
|
||||
justify-content:center;
|
||||
background: var(--indigo);
|
||||
color: white;
|
||||
border-radius: 999px;
|
||||
padding: 12px 14px;
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
min-height: 44px;
|
||||
cursor: pointer;
|
||||
max-width: 100%;
|
||||
}
|
||||
.btn.ghost{
|
||||
background: transparent;
|
||||
color: var(--indigo);
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
.btn.big-btn{width:100%}
|
||||
|
||||
.toolbar{display:grid;grid-template-columns:1fr auto;gap:10px;margin-bottom:12px}
|
||||
@media(max-width:560px){.toolbar{grid-template-columns:1fr}}
|
||||
|
||||
.search{
|
||||
display:flex;
|
||||
align-items:center;
|
||||
gap:10px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
padding: 10px 12px;
|
||||
min-width:0;
|
||||
}
|
||||
.search input{
|
||||
border:none;
|
||||
outline:none;
|
||||
background:transparent;
|
||||
color:var(--ink);
|
||||
font-size:16px;
|
||||
flex:1;
|
||||
min-height:24px;
|
||||
min-width:0;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
.kpis{display:grid;grid-template-columns:1fr 1fr;gap:10px}
|
||||
@media(max-width:560px){.kpis{grid-template-columns:1fr}}
|
||||
.kpi{
|
||||
background: rgba(255,255,255,.55);
|
||||
border:1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
padding: 12px;
|
||||
}
|
||||
.label{font-size:11px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em}
|
||||
.value{font-size:14px;font-weight:900;color:var(--indigo);margin-top:6px;line-height:1.35}
|
||||
|
||||
.meal{
|
||||
border:1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
padding: 12px;
|
||||
background: rgba(255,255,255,.55);
|
||||
margin-top: 12px;
|
||||
}
|
||||
.meal h4{margin:0 0 8px;color:var(--indigo);font-size:13px;letter-spacing:.02em}
|
||||
.meal ul{margin:0;padding-left:18px}
|
||||
.meal li{font-size:13px;line-height:1.55;margin:6px 0}
|
||||
|
||||
.timeline{display:grid;gap:10px;margin-top:10px}
|
||||
.trow{display:grid;grid-template-columns:56px 1fr auto;gap:10px;align-items:start}
|
||||
@media(max-width:560px){.trow{grid-template-columns:48px 1fr auto}}
|
||||
.t{font-size:12px;font-weight:950;color:var(--vermilion)}
|
||||
.p{font-size:14px;font-weight:900;color:var(--indigo)}
|
||||
.d{font-size:13px;line-height:1.4;color:var(--muted)}
|
||||
|
||||
.placeLink{color:var(--indigo);text-decoration:none;font-weight:900;border-bottom:1px solid rgba(31,42,68,.25)}
|
||||
.placeLink:hover{border-bottom-color: rgba(31,42,68,.55)}
|
||||
|
||||
.todo-item,.cost-row{display:flex;gap:10px;align-items:flex-start;padding:10px 0;border-bottom:1px solid var(--line)}
|
||||
.todo-item:last-child,.cost-row:last-child{border-bottom:none}
|
||||
.todo-check{display:flex;gap:10px;align-items:flex-start;flex:1;min-width:0}
|
||||
.todo-check input{margin-top:3px}
|
||||
.todo-actions{display:flex;gap:8px;flex-wrap:wrap}
|
||||
|
||||
.cost-inputs{display:grid;grid-template-columns:1.2fr .9fr .9fr;gap:10px;margin-top:10px;align-items:stretch}
|
||||
.cost-inputs > *{min-width:0}
|
||||
@media(max-width:700px){.cost-inputs{grid-template-columns:1fr}}
|
||||
|
||||
.totals{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:12px}
|
||||
|
||||
.footer{
|
||||
position:fixed;
|
||||
left:0;right:0;bottom:0;
|
||||
background: color-mix(in srgb, var(--paper) 88%, transparent);
|
||||
backdrop-filter: blur(14px);
|
||||
border-top:1px solid var(--line);
|
||||
padding:8px 8px calc(8px + env(safe-area-inset-bottom));
|
||||
}
|
||||
.footer .inner{max-width:860px;margin:0 auto;display:grid;grid-template-columns:repeat(4,1fr);gap:8px}
|
||||
.navbtn{
|
||||
text-decoration:none;
|
||||
text-align:center;
|
||||
color: var(--indigo);
|
||||
font-size: 11px;
|
||||
font-weight: 950;
|
||||
padding: 10px 6px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255,255,255,.55);
|
||||
border:1px solid var(--line);
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.anchor-offset{scroll-margin-top:84px}
|
||||
|
||||
/* Calendar */
|
||||
.calendar{display:grid;grid-template-columns:repeat(2,1fr);gap:10px;margin-top:12px}
|
||||
@media(max-width:560px){.calendar{grid-template-columns:1fr}}
|
||||
.cal-day{
|
||||
text-align:left;
|
||||
border:1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
padding: 12px;
|
||||
background: rgba(255,255,255,.55);
|
||||
cursor:pointer;
|
||||
}
|
||||
.cal-day.is-active{
|
||||
border-color: rgba(214,69,61,.35);
|
||||
box-shadow: 0 12px 26px rgba(214,69,61,.12);
|
||||
}
|
||||
.cal-date{font-size:12px;color:var(--muted);text-transform:uppercase;letter-spacing:.08em}
|
||||
.cal-title{margin-top:6px;font-size:14px;font-weight:950;color:var(--indigo)}
|
||||
|
||||
/* Auth */
|
||||
.auth-card{max-width:520px;margin:40px auto 0}
|
||||
.form{display:grid;gap:12px;margin-top:10px}
|
||||
.field{display:grid;gap:6px}
|
||||
.field input{
|
||||
border:1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
padding: 12px 12px;
|
||||
font-size: 16px;
|
||||
outline:none;
|
||||
background: rgba(255,255,255,.7);
|
||||
}
|
||||
.field input:focus{border-color: rgba(31,42,68,.35); box-shadow: 0 0 0 4px rgba(31,42,68,.08)}
|
||||
Reference in New Issue
Block a user