feat: implement Fristenrechner (patent deadline calculator)
Go deadline engine (internal/calc/):
- 9 proceeding types: UPC (INF/REV/PI/APP), DE (INF/NULL), EPA (OPP/APP/GRANT)
- ~50 deadline rules with durations, parties, rule references
- German federal holiday computation (Easter via Anonymous Gregorian)
- Weekend/holiday adjustment with transparency (original vs adjusted dates)
- 8 unit tests covering holidays, adjustment, and full deadline chains
Frontend (Bun/TSX):
- 3-step wizard: select proceeding → enter date → view timeline
- Visual timeline with party badges, rule references, adjustment warnings
- Print-friendly layout
API: POST /api/tools/fristenrechner (protected, JSON)
GET /api/tools/proceeding-types (protected, JSON)
Route: GET /tools/fristenrechner (protected page)
Home page: Added "Werkzeuge" section with cards linking to both tools
This commit is contained in:
@@ -3,6 +3,7 @@ import { join } from "path";
|
||||
import { renderIndex } from "./src/index";
|
||||
import { renderLogin } from "./src/login";
|
||||
import { renderKostenrechner } from "./src/kostenrechner";
|
||||
import { renderFristenrechner } from "./src/fristenrechner";
|
||||
|
||||
const DIST = join(import.meta.dir, "dist");
|
||||
|
||||
@@ -16,6 +17,7 @@ async function build() {
|
||||
entrypoints: [
|
||||
join(import.meta.dir, "src/client/login.ts"),
|
||||
join(import.meta.dir, "src/client/kostenrechner.ts"),
|
||||
join(import.meta.dir, "src/client/fristenrechner.ts"),
|
||||
],
|
||||
outdir: join(DIST, "assets"),
|
||||
naming: "[name].js",
|
||||
@@ -40,6 +42,7 @@ async function build() {
|
||||
await Bun.write(join(DIST, "index.html"), renderIndex());
|
||||
await Bun.write(join(DIST, "login.html"), renderLogin("login.js"));
|
||||
await Bun.write(join(DIST, "kostenrechner.html"), renderKostenrechner());
|
||||
await Bun.write(join(DIST, "fristenrechner.html"), renderFristenrechner());
|
||||
|
||||
console.log("Build complete \u2192 dist/");
|
||||
}
|
||||
|
||||
189
frontend/src/client/fristenrechner.ts
Normal file
189
frontend/src/client/fristenrechner.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
// Fristenrechner client-side logic
|
||||
// 3-step wizard: select proceeding → enter date → view timeline
|
||||
|
||||
interface CalculatedDeadline {
|
||||
code: string;
|
||||
name: string;
|
||||
nameEN: string;
|
||||
party: string;
|
||||
isMandatory: boolean;
|
||||
ruleRef: string;
|
||||
notes?: string;
|
||||
dueDate: string;
|
||||
originalDate: string;
|
||||
wasAdjusted: boolean;
|
||||
isRootEvent: boolean;
|
||||
isCourtSet: boolean;
|
||||
}
|
||||
|
||||
interface DeadlineResponse {
|
||||
proceedingType: string;
|
||||
proceedingName: string;
|
||||
triggerDate: string;
|
||||
deadlines: CalculatedDeadline[];
|
||||
}
|
||||
|
||||
const PARTY_LABELS: Record<string, [string, string]> = {
|
||||
claimant: ["Kl\u00e4ger", "party-claimant"],
|
||||
defendant: ["Beklagter", "party-defendant"],
|
||||
court: ["Gericht", "party-court"],
|
||||
both: ["Beide", "party-both"],
|
||||
};
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
if (!dateStr) return "\u2014";
|
||||
const d = new Date(dateStr + "T00:00:00");
|
||||
return d.toLocaleDateString("de-DE", {
|
||||
weekday: "short",
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function partyBadge(party: string): string {
|
||||
const [label, cls] = PARTY_LABELS[party] || [party, "party-both"];
|
||||
return `<span class="party-badge ${cls}">${label}</span>`;
|
||||
}
|
||||
|
||||
let selectedType = "";
|
||||
|
||||
function showStep(n: number) {
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const el = document.getElementById(`step-${i}`);
|
||||
if (el) el.style.display = i <= n ? "block" : "none";
|
||||
}
|
||||
const resetBtn = document.getElementById("reset-btn")!;
|
||||
resetBtn.style.display = n > 1 ? "block" : "none";
|
||||
}
|
||||
|
||||
async function calculate() {
|
||||
const dateInput = document.getElementById("trigger-date") as HTMLInputElement;
|
||||
const triggerDate = dateInput.value;
|
||||
if (!triggerDate || !selectedType) return;
|
||||
|
||||
try {
|
||||
const resp = await fetch("/api/tools/fristenrechner", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
proceedingType: selectedType,
|
||||
triggerDate,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json();
|
||||
console.error("API error:", err);
|
||||
return;
|
||||
}
|
||||
|
||||
const data: DeadlineResponse = await resp.json();
|
||||
renderTimeline(data);
|
||||
showStep(3);
|
||||
} catch (e) {
|
||||
console.error("Fetch error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderTimeline(data: DeadlineResponse) {
|
||||
const container = document.getElementById("timeline-container")!;
|
||||
const printBtn = document.getElementById("fristen-print-btn")!;
|
||||
|
||||
let html = `<div class="timeline-header">
|
||||
<strong>${data.proceedingName}</strong>
|
||||
<span class="card-en">Trigger: ${formatDate(data.triggerDate)}</span>
|
||||
</div>`;
|
||||
|
||||
html += '<div class="timeline">';
|
||||
|
||||
for (const dl of data.deadlines) {
|
||||
const dateStr = dl.isCourtSet
|
||||
? '<span class="timeline-court-set">vom Gericht bestimmt<br><span class="card-en">set by court</span></span>'
|
||||
: `<span class="timeline-date">${formatDate(dl.dueDate)}</span>`;
|
||||
|
||||
const mandatoryBadge = dl.isMandatory
|
||||
? ""
|
||||
: '<span class="optional-badge">optional</span>';
|
||||
|
||||
const adjustedNote = dl.wasAdjusted
|
||||
? `<div class="timeline-adjusted">\u26a0 Verschoben: ${formatDate(dl.originalDate)} \u2192 ${formatDate(dl.dueDate)} (Wochenende/Feiertag)</div>`
|
||||
: "";
|
||||
|
||||
const ruleRef = dl.ruleRef
|
||||
? `<span class="timeline-rule">${dl.ruleRef}</span>`
|
||||
: "";
|
||||
|
||||
const notes = dl.notes
|
||||
? `<div class="timeline-notes">${dl.notes}</div>`
|
||||
: "";
|
||||
|
||||
html += `
|
||||
<div class="timeline-item ${dl.isRootEvent ? "timeline-root" : ""}">
|
||||
<div class="timeline-dot-col">
|
||||
<div class="timeline-dot ${dl.isRootEvent ? "dot-root" : ""}"></div>
|
||||
<div class="timeline-line"></div>
|
||||
</div>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-item-header">
|
||||
<span class="timeline-name">
|
||||
${dl.name}
|
||||
<span class="card-en">${dl.nameEN}</span>
|
||||
${mandatoryBadge}
|
||||
</span>
|
||||
${dateStr}
|
||||
</div>
|
||||
<div class="timeline-meta">
|
||||
${partyBadge(dl.party)}
|
||||
${ruleRef}
|
||||
</div>
|
||||
${adjustedNote}
|
||||
${notes}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
html += "</div>";
|
||||
container.innerHTML = html;
|
||||
printBtn.style.display = "block";
|
||||
}
|
||||
|
||||
function reset() {
|
||||
selectedType = "";
|
||||
document.querySelectorAll(".proceeding-btn").forEach((b) => b.classList.remove("active"));
|
||||
document.getElementById("timeline-container")!.innerHTML = "";
|
||||
document.getElementById("fristen-print-btn")!.style.display = "none";
|
||||
showStep(1);
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
// Proceeding type selection
|
||||
document.querySelectorAll<HTMLButtonElement>(".proceeding-btn").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
document.querySelectorAll(".proceeding-btn").forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
selectedType = btn.dataset.code!;
|
||||
|
||||
// Update trigger event name
|
||||
const name = btn.querySelector("strong")?.textContent || "";
|
||||
document.getElementById("trigger-event")!.textContent = name;
|
||||
|
||||
showStep(2);
|
||||
});
|
||||
});
|
||||
|
||||
// Calculate button
|
||||
document.getElementById("calculate-btn")!.addEventListener("click", calculate);
|
||||
|
||||
// Also calculate on Enter in date field
|
||||
document.getElementById("trigger-date")!.addEventListener("keydown", (e) => {
|
||||
if ((e as KeyboardEvent).key === "Enter") calculate();
|
||||
});
|
||||
|
||||
// Reset button
|
||||
document.getElementById("reset-btn")!.addEventListener("click", reset);
|
||||
|
||||
// Print button
|
||||
document.getElementById("fristen-print-btn")!.addEventListener("click", () => window.print());
|
||||
});
|
||||
141
frontend/src/fristenrechner.tsx
Normal file
141
frontend/src/fristenrechner.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import { h } from "./jsx";
|
||||
import { Header } from "./components/Header";
|
||||
import { Footer } from "./components/Footer";
|
||||
|
||||
interface ProceedingDef {
|
||||
code: string;
|
||||
name: string;
|
||||
nameEN: string;
|
||||
}
|
||||
|
||||
function proceedingBtn(p: ProceedingDef): string {
|
||||
return (
|
||||
<button type="button" className="proceeding-btn" data-code={p.code}>
|
||||
<strong>{p.name}</strong>
|
||||
<span className="card-en">{p.nameEN}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const UPC_TYPES: ProceedingDef[] = [
|
||||
{ code: "UPC_INF", name: "Verletzungsverfahren", nameEN: "Infringement" },
|
||||
{ code: "UPC_REV", name: "Nichtigkeitsklage", nameEN: "Revocation" },
|
||||
{ code: "UPC_PI", name: "Einstw. Ma\u00dfnahmen", nameEN: "Provisional Measures" },
|
||||
{ code: "UPC_APP", name: "Berufung", nameEN: "Appeal" },
|
||||
];
|
||||
|
||||
const DE_TYPES: ProceedingDef[] = [
|
||||
{ code: "DE_INF", name: "Verletzungsklage (LG)", nameEN: "Infringement" },
|
||||
{ code: "DE_NULL", name: "Nichtigkeitsverfahren", nameEN: "Nullity" },
|
||||
];
|
||||
|
||||
const EPA_TYPES: ProceedingDef[] = [
|
||||
{ code: "EPA_OPP", name: "Einspruchsverfahren", nameEN: "Opposition" },
|
||||
{ code: "EPA_APP", name: "Beschwerdeverfahren", nameEN: "Appeal" },
|
||||
{ code: "EP_GRANT", name: "EP-Erteilungsverfahren", nameEN: "Grant Procedure" },
|
||||
];
|
||||
|
||||
export function renderFristenrechner(): string {
|
||||
const today = new Date().toISOString().split("T")[0];
|
||||
|
||||
return "<!DOCTYPE html>" + (
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Fristenrechner — patholo</title>
|
||||
<link rel="stylesheet" href="/assets/global.css" />
|
||||
</head>
|
||||
<body>
|
||||
<Header showLogout={true} />
|
||||
|
||||
<main>
|
||||
<section className="tool-page">
|
||||
<div className="container">
|
||||
<div className="tool-header">
|
||||
<h1>Fristenrechner <span className="card-en">Patent Deadline Calculator</span></h1>
|
||||
<p className="tool-subtitle">
|
||||
Berechnung von Verfahrensfristen für UPC-, deutsche und EPA-Verfahren.
|
||||
<br />
|
||||
<span className="card-en">Calculate procedural deadlines for UPC, German, and EPA proceedings.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="fristen-wizard">
|
||||
<div className="wizard-step" id="step-1">
|
||||
<h3 className="wizard-step-label">
|
||||
<span className="step-number">1</span>
|
||||
Verfahrensart wählen <span className="card-en">Select Proceeding Type</span>
|
||||
</h3>
|
||||
|
||||
<div className="proceeding-group">
|
||||
<h4>UPC <span className="card-en">Unified Patent Court</span></h4>
|
||||
<div className="proceeding-btns">
|
||||
{UPC_TYPES.map((p) => proceedingBtn(p))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="proceeding-group">
|
||||
<h4>Deutsche Gerichte <span className="card-en">German Courts</span></h4>
|
||||
<div className="proceeding-btns">
|
||||
{DE_TYPES.map((p) => proceedingBtn(p))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="proceeding-group">
|
||||
<h4>EPA <span className="card-en">European Patent Office</span></h4>
|
||||
<div className="proceeding-btns">
|
||||
{EPA_TYPES.map((p) => proceedingBtn(p))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="wizard-step" id="step-2" style="display:none">
|
||||
<h3 className="wizard-step-label">
|
||||
<span className="step-number">2</span>
|
||||
Ausgangsdatum eingeben <span className="card-en">Enter Trigger Date</span>
|
||||
</h3>
|
||||
|
||||
<div className="date-input-group">
|
||||
<div className="date-field-row">
|
||||
<label htmlFor="trigger-event" className="date-label">Auslösendes Ereignis:</label>
|
||||
<span id="trigger-event" className="trigger-event-name">—</span>
|
||||
</div>
|
||||
<div className="date-field-row">
|
||||
<label htmlFor="trigger-date" className="date-label">Datum:</label>
|
||||
<input type="date" id="trigger-date" className="date-input" value={today} />
|
||||
</div>
|
||||
<button type="button" id="calculate-btn" className="calculate-btn">
|
||||
Fristen berechnen <span className="card-en">Calculate Deadlines</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="wizard-step" id="step-3" style="display:none">
|
||||
<h3 className="wizard-step-label">
|
||||
<span className="step-number">3</span>
|
||||
Ergebnis <span className="card-en">Result</span>
|
||||
</h3>
|
||||
|
||||
<div id="timeline-container">
|
||||
</div>
|
||||
|
||||
<button type="button" id="fristen-print-btn" className="print-btn" style="display:none">
|
||||
Drucken <span className="card-en">Print</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button type="button" id="reset-btn" className="reset-btn" style="display:none">
|
||||
← Neu berechnen <span className="card-en">Start Over</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
<script src="/assets/fristenrechner.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,8 @@ import { Footer } from "./components/Footer";
|
||||
const ICON_BOOK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"/><path d="M8 7h6"/><path d="M8 11h4"/></svg>';
|
||||
const ICON_FILE = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg>';
|
||||
const ICON_FOLDER = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>';
|
||||
const ICON_CALC = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" y1="6" x2="16" y2="6"/><line x1="8" y1="14" x2="8" y2="14.01"/><line x1="12" y1="14" x2="12" y2="14.01"/><line x1="16" y1="14" x2="16" y2="14.01"/><line x1="8" y1="18" x2="16" y2="18"/></svg>';
|
||||
const ICON_CLOCK = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>';
|
||||
|
||||
export function renderIndex(): string {
|
||||
return "<!DOCTYPE html>" + (
|
||||
@@ -54,6 +56,25 @@ export function renderIndex(): string {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="sections">
|
||||
<div className="container">
|
||||
<h3 className="section-heading">Werkzeuge <span className="card-en">Tools</span></h3>
|
||||
<div className="grid grid-2">
|
||||
<a href="/tools/kostenrechner" className="card card-link">
|
||||
<div className="card-icon" dangerouslySetInnerHTML={{ __html: ICON_CALC }} />
|
||||
<h2>Kostenrechner <span className="card-en">Cost Calculator</span></h2>
|
||||
<p>Schätzung der Verfahrenskosten für DE-Gerichte, UPC und EPA-Verfahren. Gerichts- und Anwaltskosten auf einen Blick.</p>
|
||||
</a>
|
||||
|
||||
<a href="/tools/fristenrechner" className="card card-link">
|
||||
<div className="card-icon" dangerouslySetInnerHTML={{ __html: ICON_CLOCK }} />
|
||||
<h2>Fristenrechner <span className="card-en">Deadline Calculator</span></h2>
|
||||
<p>Berechnung von Verfahrensfristen für UPC-, deutsche und EPA-Verfahren mit Feiertags-Anpassung.</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="offices">
|
||||
<div className="container">
|
||||
<h3>Standorte <span className="card-en">Offices</span></h3>
|
||||
|
||||
@@ -207,6 +207,33 @@ main {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-heading .card-en {
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.grid-2 {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.card-link {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.card-link:hover {
|
||||
border-color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* --- Offices --- */
|
||||
|
||||
.offices {
|
||||
@@ -810,6 +837,325 @@ input[type="range"]::-moz-range-thumb {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* --- Fristenrechner --- */
|
||||
|
||||
.fristen-wizard {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.wizard-step {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.wizard-step-label {
|
||||
font-size: 0.92rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.step-number {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent);
|
||||
color: #fff;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.proceeding-group {
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.proceeding-group h4 {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.proceeding-group h4 .card-en {
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
|
||||
.proceeding-btns {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.proceeding-btn {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.85rem;
|
||||
padding: 0.6rem 1rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
text-align: left;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.proceeding-btn:hover {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.proceeding-btn.active {
|
||||
border-color: var(--color-accent);
|
||||
background: rgba(101, 163, 13, 0.06);
|
||||
box-shadow: 0 0 0 3px rgba(101, 163, 13, 0.15);
|
||||
}
|
||||
|
||||
.proceeding-btn strong {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Date input */
|
||||
|
||||
.date-input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.date-field-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.date-label {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text);
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.trigger-event-name {
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
.date-input {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.92rem;
|
||||
padding: 0.5rem 0.7rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.date-input:focus {
|
||||
border-color: var(--color-accent);
|
||||
box-shadow: 0 0 0 3px rgba(101, 163, 13, 0.15);
|
||||
}
|
||||
|
||||
.calculate-btn {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
padding: 0.6rem 1.25rem;
|
||||
border: none;
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-accent);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.calculate-btn:hover {
|
||||
background: var(--color-accent-light);
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 500;
|
||||
padding: 0.5rem 1rem;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.reset-btn:hover {
|
||||
border-color: var(--color-accent);
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Timeline */
|
||||
|
||||
.timeline-header {
|
||||
margin-bottom: 1.25rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 2px solid var(--color-accent);
|
||||
}
|
||||
|
||||
.timeline-header strong {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.timeline {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.timeline-item {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
min-height: 4rem;
|
||||
}
|
||||
|
||||
.timeline-item:last-child .timeline-line {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.timeline-dot-col {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 14px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.timeline-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--color-accent);
|
||||
border: 2px solid var(--color-surface);
|
||||
box-shadow: 0 0 0 2px var(--color-accent);
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
.dot-root {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.timeline-line {
|
||||
width: 2px;
|
||||
flex: 1;
|
||||
background: var(--color-border);
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
flex: 1;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.timeline-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.timeline-name {
|
||||
font-size: 0.88rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.timeline-date {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.timeline-court-set {
|
||||
font-size: 0.82rem;
|
||||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.timeline-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.party-badge {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 99px;
|
||||
}
|
||||
|
||||
.party-claimant {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
.party-defendant {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.party-court {
|
||||
background: #f3e8ff;
|
||||
color: #6b21a8;
|
||||
}
|
||||
|
||||
.party-both {
|
||||
background: #e5e7eb;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
.optional-badge {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 500;
|
||||
padding: 0.05rem 0.4rem;
|
||||
border-radius: 99px;
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.timeline-rule {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.72rem;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.timeline-adjusted {
|
||||
font-size: 0.78rem;
|
||||
color: #d97706;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.timeline-notes {
|
||||
font-size: 0.78rem;
|
||||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
margin-top: 0.15rem;
|
||||
}
|
||||
|
||||
/* --- Responsive --- */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -821,7 +1167,7 @@ input[type="range"]::-moz-range-thumb {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.grid {
|
||||
.grid, .grid-2 {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
@@ -841,12 +1187,27 @@ input[type="range"]::-moz-range-thumb {
|
||||
.nav-right {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.proceeding-btns {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.date-field-row {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.3rem;
|
||||
}
|
||||
|
||||
.timeline-item-header {
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Print --- */
|
||||
|
||||
@media print {
|
||||
.header, .footer, .tool-input, .print-btn {
|
||||
.header, .footer, .tool-input, .print-btn, .reset-btn, #step-1, #step-2, .calculate-btn {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user