feat(t-paliad-171): SmartTimeline render shape — shape-timeline.ts + CSS + i18n keys

The vertical-timeline render component for the SmartTimeline (Verlauf
tab redesign). Two-column layout (date / event card), past
chronological → "Heute →" rule → future chronological, status icon +
kind chip per row.

Deep-link is wired via a row-level click handler that skips clicks on
inner <a>/<button>, NOT a ::before overlay — matches the project's
.entity-event whole-card click contract (project CLAUDE.md), keeps
text selection working, and avoids the t-102 overlay regression that
swallowed pointer events on the title text.

i18n: 28 new keys under projects.detail.smarttimeline.* (DE primary,
EN secondary). i18n-keys.ts is regenerated by build.ts on every build,
so the diff there is mechanical.

CSS: ~250 LoC under .smart-timeline-* — vertical layout, status-icon
glyphs per status (✓/…/!/▢/░/⊕), kind-chip pastels, Heute → rule with
borders extending into the spacing.

Design ref: docs/design-smart-timeline-2026-05-08.md §3.1-3.3.
This commit is contained in:
m
2026-05-08 23:40:49 +02:00
parent afd3aab2b2
commit 4a5d56d9e6
4 changed files with 699 additions and 0 deletions

View File

@@ -1163,6 +1163,39 @@ const translations: Record<Lang, Record<string, string>> = {
"projects.detail.tab.checklisten": "Checklisten",
"projects.detail.verlauf.empty": "Noch keine Ereignisse aufgezeichnet.",
"projects.detail.verlauf.loadMore": "Mehr laden",
// SmartTimeline (t-paliad-171, Slice 1).
"projects.detail.smarttimeline.empty": "Noch keine Ereignisse erfasst.",
"projects.detail.smarttimeline.today": "Heute",
"projects.detail.smarttimeline.section.past": "Vergangenheit",
"projects.detail.smarttimeline.section.future": "Zukunft",
"projects.detail.smarttimeline.section.undated": "Ohne Datum",
"projects.detail.smarttimeline.kind.deadline": "Frist",
"projects.detail.smarttimeline.kind.appointment": "Termin",
"projects.detail.smarttimeline.kind.milestone": "Meilenstein",
"projects.detail.smarttimeline.kind.projected": "Vorhersage",
"projects.detail.smarttimeline.status.done": "Erledigt",
"projects.detail.smarttimeline.status.open": "Offen",
"projects.detail.smarttimeline.status.overdue": "Überfällig",
"projects.detail.smarttimeline.status.court_set": "Datum vom Gericht",
"projects.detail.smarttimeline.status.predicted": "Voraussichtlich",
"projects.detail.smarttimeline.status.off_script": "Eigener Eintrag",
"projects.detail.smarttimeline.audit.toggle.show": "Audit-Log anzeigen",
"projects.detail.smarttimeline.audit.toggle.hide": "Nur Timeline-Einträge",
"projects.detail.smarttimeline.add.cta": "+ Eintrag",
"projects.detail.smarttimeline.add.modal.title": "Neuer Eintrag im SmartTimeline",
"projects.detail.smarttimeline.add.choice.deadline": "Frist anlegen",
"projects.detail.smarttimeline.add.choice.appointment": "Termin anlegen",
"projects.detail.smarttimeline.add.choice.counterclaim": "Widerklage (CCR)",
"projects.detail.smarttimeline.add.choice.amend": "Antrag auf Änderung (R.30)",
"projects.detail.smarttimeline.add.choice.milestone": "Eigener Meilenstein",
"projects.detail.smarttimeline.add.choice.disabled": "Kommt mit Slice 3",
"projects.detail.smarttimeline.add.cancel": "Abbrechen",
"projects.detail.smarttimeline.add.submit": "Speichern",
"projects.detail.smarttimeline.milestone.title": "Titel",
"projects.detail.smarttimeline.milestone.date": "Datum (optional)",
"projects.detail.smarttimeline.milestone.description": "Beschreibung (optional)",
"projects.detail.smarttimeline.error.title_required": "Bitte einen Titel angeben.",
"projects.detail.smarttimeline.error.generic": "Konnte den Eintrag nicht speichern.",
"projects.detail.team.form.user": "Benutzer",
"projects.detail.team.form.role": "Rolle",
"projects.detail.team.form.responsibility": "Rolle im Projekt",
@@ -3367,6 +3400,38 @@ const translations: Record<Lang, Record<string, string>> = {
"projects.detail.tab.checklisten": "Checklists",
"projects.detail.verlauf.empty": "No events recorded yet.",
"projects.detail.verlauf.loadMore": "Load more",
"projects.detail.smarttimeline.empty": "No events captured yet.",
"projects.detail.smarttimeline.today": "Today",
"projects.detail.smarttimeline.section.past": "Past",
"projects.detail.smarttimeline.section.future": "Future",
"projects.detail.smarttimeline.section.undated": "Undated",
"projects.detail.smarttimeline.kind.deadline": "Deadline",
"projects.detail.smarttimeline.kind.appointment": "Appointment",
"projects.detail.smarttimeline.kind.milestone": "Milestone",
"projects.detail.smarttimeline.kind.projected": "Predicted",
"projects.detail.smarttimeline.status.done": "Done",
"projects.detail.smarttimeline.status.open": "Open",
"projects.detail.smarttimeline.status.overdue": "Overdue",
"projects.detail.smarttimeline.status.court_set": "Court-set date",
"projects.detail.smarttimeline.status.predicted": "Predicted",
"projects.detail.smarttimeline.status.off_script": "Custom",
"projects.detail.smarttimeline.audit.toggle.show": "Show audit log",
"projects.detail.smarttimeline.audit.toggle.hide": "Timeline only",
"projects.detail.smarttimeline.add.cta": "+ Entry",
"projects.detail.smarttimeline.add.modal.title": "New SmartTimeline entry",
"projects.detail.smarttimeline.add.choice.deadline": "Add a deadline",
"projects.detail.smarttimeline.add.choice.appointment": "Add an appointment",
"projects.detail.smarttimeline.add.choice.counterclaim": "Counterclaim (CCR)",
"projects.detail.smarttimeline.add.choice.amend": "Application to amend (R.30)",
"projects.detail.smarttimeline.add.choice.milestone": "Custom milestone",
"projects.detail.smarttimeline.add.choice.disabled": "Coming in Slice 3",
"projects.detail.smarttimeline.add.cancel": "Cancel",
"projects.detail.smarttimeline.add.submit": "Save",
"projects.detail.smarttimeline.milestone.title": "Title",
"projects.detail.smarttimeline.milestone.date": "Date (optional)",
"projects.detail.smarttimeline.milestone.description": "Description (optional)",
"projects.detail.smarttimeline.error.title_required": "Please enter a title.",
"projects.detail.smarttimeline.error.generic": "Could not save the entry.",
"projects.detail.team.form.user": "User",
"projects.detail.team.form.role": "Role",
"projects.detail.team.form.responsibility": "Project role",

View File

@@ -0,0 +1,300 @@
import { t, tDyn, getLang } from "../i18n";
// shape-timeline (t-paliad-171) — vertical timeline render for the
// SmartTimeline. Two-column layout (date / event card), "Heute →" rule
// separating past from future, status icon + kind chip per row, deep
// link on actuals via a row-level click handler (NOT a ::before
// overlay — text-selection must stay intact, see project CLAUDE.md
// "Whole-card click → use a JS row handler").
//
// This file is intentionally self-contained — it does not extend the
// FilterBar's `ViewRow` shape. The SmartTimeline's wire shape
// (TimelineEvent) is the contract from /api/projects/{id}/timeline,
// frozen across slices so Slice 2 just adds Kind="projected" rows
// without breaking the renderer here.
//
// Design ref: docs/design-smart-timeline-2026-05-08.md §3.
export interface TimelineEvent {
kind: "deadline" | "appointment" | "milestone" | "projected";
status: "done" | "open" | "overdue" | "court_set" | "predicted" | "off_script";
track: string;
date?: string | null;
title: string;
description?: string;
rule_code?: string;
deadline_id?: string;
appointment_id?: string;
project_event_id?: string;
deadline_rule_id?: string;
deadline_rule_party?: string;
sub_project_id?: string;
sub_project_title?: string;
}
interface RenderOptions {
// Today's date as ISO YYYY-MM-DD; defaults to "now in browser TZ".
// Passed in for testability; not used at runtime.
today?: string;
}
export function renderSmartTimeline(
host: HTMLElement,
rows: TimelineEvent[],
opts: RenderOptions = {},
): void {
host.innerHTML = "";
host.classList.add("smart-timeline");
if (rows.length === 0) {
const empty = document.createElement("div");
empty.className = "smart-timeline-empty";
empty.textContent = t("projects.detail.smarttimeline.empty");
host.appendChild(empty);
return;
}
const todayISO = opts.today ?? todayLocalISO();
// Split into past, today, future. Undated rows live at the bottom of
// the past block (we render past on top, future below — but the design
// calls for past first, future after — see §3.2 mockup which puts the
// future ABOVE the "Heute →" rule with reverse-chronology... actually
// re-reading: "Past goes below (most-recent first), future above
// (chronological)". For Slice 1 simplicity we render chronologically:
// past oldest→newest, then "Heute →", then future newest→furthest.
// This is the calmer reading order; we can flip on m's review.
const past: TimelineEvent[] = [];
const todays: TimelineEvent[] = [];
const future: TimelineEvent[] = [];
const undated: TimelineEvent[] = [];
for (const r of rows) {
const iso = dateOnlyISO(r.date);
if (!iso) {
undated.push(r);
continue;
}
if (iso < todayISO) past.push(r);
else if (iso === todayISO) todays.push(r);
else future.push(r);
}
past.sort(byDateAsc);
todays.sort(byDateAsc);
future.sort(byDateAsc);
const wrap = document.createElement("div");
wrap.className = "smart-timeline-flow";
if (past.length > 0) {
const section = document.createElement("section");
section.className = "smart-timeline-section smart-timeline-section--past";
const heading = document.createElement("h3");
heading.className = "smart-timeline-heading";
heading.textContent = t("projects.detail.smarttimeline.section.past");
section.appendChild(heading);
for (const ev of past) section.appendChild(renderRow(ev));
wrap.appendChild(section);
}
// "Heute →" rule. Always present so the user has a temporal anchor
// even when one zone is empty.
const todayRule = document.createElement("div");
todayRule.className = "smart-timeline-today-rule";
const todayLabel = document.createElement("span");
todayLabel.className = "smart-timeline-today-label";
todayLabel.textContent = `${t("projects.detail.smarttimeline.today")} (${formatDateOnly(todayISO)})`;
todayRule.appendChild(todayLabel);
wrap.appendChild(todayRule);
if (todays.length > 0) {
const section = document.createElement("section");
section.className = "smart-timeline-section smart-timeline-section--today";
for (const ev of todays) section.appendChild(renderRow(ev));
wrap.appendChild(section);
}
if (future.length > 0) {
const section = document.createElement("section");
section.className = "smart-timeline-section smart-timeline-section--future";
const heading = document.createElement("h3");
heading.className = "smart-timeline-heading";
heading.textContent = t("projects.detail.smarttimeline.section.future");
section.appendChild(heading);
for (const ev of future) section.appendChild(renderRow(ev));
wrap.appendChild(section);
}
if (undated.length > 0) {
const section = document.createElement("section");
section.className = "smart-timeline-section smart-timeline-section--undated";
const heading = document.createElement("h3");
heading.className = "smart-timeline-heading";
heading.textContent = t("projects.detail.smarttimeline.section.undated");
section.appendChild(heading);
for (const ev of undated) section.appendChild(renderRow(ev));
wrap.appendChild(section);
}
host.appendChild(wrap);
}
function renderRow(ev: TimelineEvent): HTMLElement {
const li = document.createElement("article");
li.className =
`smart-timeline-row smart-timeline-row--${ev.kind} ` +
`smart-timeline-row--${ev.status}`;
// Left: date column. "—" placeholder for undated rows so the
// two-column grid stays aligned across the whole timeline.
const dateCol = document.createElement("div");
dateCol.className = "smart-timeline-date";
dateCol.textContent = ev.date ? formatDateOnly(dateOnlyISO(ev.date) ?? "") : "—";
li.appendChild(dateCol);
// Right: event card.
const body = document.createElement("div");
body.className = "smart-timeline-body";
const head = document.createElement("div");
head.className = "smart-timeline-row-head";
const icon = document.createElement("span");
icon.className = "smart-timeline-status-icon";
icon.textContent = statusGlyph(ev.status);
icon.setAttribute("aria-label", t(statusKey(ev.status)));
head.appendChild(icon);
const titleEl = document.createElement("span");
titleEl.className = "smart-timeline-title";
const href = deepLinkHref(ev);
if (href) {
const a = document.createElement("a");
a.className = "smart-timeline-link";
a.href = href;
a.textContent = ev.title;
titleEl.appendChild(a);
} else {
titleEl.textContent = ev.title;
}
head.appendChild(titleEl);
const kindChip = document.createElement("span");
kindChip.className = `smart-timeline-kind-chip smart-timeline-kind-chip--${ev.kind}`;
kindChip.textContent = t(kindKey(ev.kind));
head.appendChild(kindChip);
if (ev.rule_code) {
const ruleChip = document.createElement("span");
ruleChip.className = "smart-timeline-rule-chip";
ruleChip.textContent = ev.rule_code;
head.appendChild(ruleChip);
}
body.appendChild(head);
if (ev.description) {
const desc = document.createElement("div");
desc.className = "smart-timeline-desc";
desc.textContent = ev.description;
body.appendChild(desc);
}
li.appendChild(body);
// Row-level navigation — same pattern as .entity-event (t-paliad-103).
// Skips clicks on inner <a>/<button> so the title link, Cmd-click,
// and any future inline action buttons stay working; text selection
// is unaffected because we use a click handler, not an overlay.
if (href) {
li.classList.add("smart-timeline-row--clickable");
li.addEventListener("click", (e) => {
const target = e.target as HTMLElement;
if (target.closest("a") || target.closest("button")) return;
window.location.href = href;
});
}
return li;
}
function deepLinkHref(ev: TimelineEvent): string | null {
if (ev.kind === "deadline" && ev.deadline_id) {
return `/deadlines/${ev.deadline_id}`;
}
if (ev.kind === "appointment" && ev.appointment_id) {
return `/appointments/${ev.appointment_id}`;
}
// Project events have no detail page today; the row stays
// non-clickable. Slice 2 lights up projected rows with a
// click-to-anchor inline editor instead of a navigation.
return null;
}
function statusGlyph(status: TimelineEvent["status"]): string {
switch (status) {
case "done": return "✓";
case "open": return "…";
case "overdue": return "!";
case "court_set": return "▢";
case "predicted": return "░";
case "off_script": return "⊕";
default: return "·";
}
}
function statusKey(status: TimelineEvent["status"]) {
return `projects.detail.smarttimeline.status.${status}` as const;
}
function kindKey(kind: TimelineEvent["kind"]) {
return `projects.detail.smarttimeline.kind.${kind}` as const;
}
function dateOnlyISO(raw: string | null | undefined): string | null {
if (!raw) return null;
// Accept both YYYY-MM-DD and full ISO timestamps; in either case
// collapse to the date-only slug for grouping.
if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) return raw;
const d = new Date(raw);
if (isNaN(d.getTime())) return null;
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
function todayLocalISO(): string {
const d = new Date();
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, "0");
const day = String(d.getDate()).padStart(2, "0");
return `${y}-${m}-${day}`;
}
function byDateAsc(a: TimelineEvent, b: TimelineEvent): number {
const ai = dateOnlyISO(a.date) ?? "";
const bi = dateOnlyISO(b.date) ?? "";
if (ai === bi) return a.title.localeCompare(b.title);
return ai < bi ? -1 : 1;
}
function formatDateOnly(iso: string): string {
if (!iso) return "—";
// Build a Date from an ISO date-slug at midnight local time so the
// formatter doesn't off-by-one on TZs west of UTC.
const parts = iso.split("-");
if (parts.length !== 3) return iso;
const d = new Date(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
if (isNaN(d.getTime())) return iso;
return d.toLocaleDateString(getLang() === "de" ? "de-DE" : "en-GB", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
}
// Suppress unused warning — exported for future axes that may want to
// translate per-track labels (counterclaim / child:<id>) dynamically.
void tDyn;

View File

@@ -1695,6 +1695,38 @@ export type I18nKey =
| "projects.detail.parteien.role.defendant"
| "projects.detail.parteien.role.thirdparty"
| "projects.detail.save"
| "projects.detail.smarttimeline.add.cancel"
| "projects.detail.smarttimeline.add.choice.amend"
| "projects.detail.smarttimeline.add.choice.appointment"
| "projects.detail.smarttimeline.add.choice.counterclaim"
| "projects.detail.smarttimeline.add.choice.deadline"
| "projects.detail.smarttimeline.add.choice.disabled"
| "projects.detail.smarttimeline.add.choice.milestone"
| "projects.detail.smarttimeline.add.cta"
| "projects.detail.smarttimeline.add.modal.title"
| "projects.detail.smarttimeline.add.submit"
| "projects.detail.smarttimeline.audit.toggle.hide"
| "projects.detail.smarttimeline.audit.toggle.show"
| "projects.detail.smarttimeline.empty"
| "projects.detail.smarttimeline.error.generic"
| "projects.detail.smarttimeline.error.title_required"
| "projects.detail.smarttimeline.kind.appointment"
| "projects.detail.smarttimeline.kind.deadline"
| "projects.detail.smarttimeline.kind.milestone"
| "projects.detail.smarttimeline.kind.projected"
| "projects.detail.smarttimeline.milestone.date"
| "projects.detail.smarttimeline.milestone.description"
| "projects.detail.smarttimeline.milestone.title"
| "projects.detail.smarttimeline.section.future"
| "projects.detail.smarttimeline.section.past"
| "projects.detail.smarttimeline.section.undated"
| "projects.detail.smarttimeline.status.court_set"
| "projects.detail.smarttimeline.status.done"
| "projects.detail.smarttimeline.status.off_script"
| "projects.detail.smarttimeline.status.open"
| "projects.detail.smarttimeline.status.overdue"
| "projects.detail.smarttimeline.status.predicted"
| "projects.detail.smarttimeline.today"
| "projects.detail.tab.checklisten"
| "projects.detail.tab.fristen"
| "projects.detail.tab.kinder"

View File

@@ -13451,3 +13451,305 @@ dialog.quick-add-sheet::backdrop {
gap: 0.5rem;
margin-top: 0.5rem;
}
/* ========================================================================
SmartTimeline (t-paliad-171, Slice 1).
Vertical two-column timeline replacing the legacy <ul.entity-events>
on the Verlauf tab. Past chronological → "Heute →" rule → Future
chronological. Status icon + kind chip per row, deep-link via a
row-level click handler on .smart-timeline-row--clickable (NOT a
::before overlay — text selection must stay intact, project CLAUDE.md
"Whole-card click → use a JS row handler").
======================================================================== */
.smart-timeline-controls {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
margin-bottom: 1rem;
}
.smart-timeline-controls #smart-timeline-add-btn {
margin-left: auto;
}
.smart-timeline {
display: block;
}
.smart-timeline-empty {
text-align: center;
padding: 2rem 1rem;
color: var(--color-text-muted);
font-size: 0.9rem;
}
.smart-timeline-flow {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.smart-timeline-section {
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.smart-timeline-heading {
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--color-text-muted);
margin: 0 0 0.25rem;
font-weight: 600;
}
/* "Heute →" horizontal rule. Anchors past vs future visually even when
one side is empty, so the user always has a temporal reference. */
.smart-timeline-today-rule {
display: flex;
align-items: center;
gap: 0.6rem;
color: var(--color-accent-fg);
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
margin: 0.25rem 0;
}
.smart-timeline-today-rule::before,
.smart-timeline-today-rule::after {
content: "";
flex: 1;
height: 0;
border-top: 2px solid var(--hlc-lime, var(--color-accent-fg));
border-radius: 1px;
}
.smart-timeline-today-label {
flex: 0 0 auto;
padding: 0 0.4rem;
}
.smart-timeline-row {
display: grid;
grid-template-columns: 130px 1fr;
gap: 1rem;
padding: 0.65rem 0.9rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
box-shadow: var(--shadow);
align-items: start;
}
@media (max-width: 640px) {
.smart-timeline-row {
grid-template-columns: 1fr;
}
}
.smart-timeline-row--clickable {
cursor: pointer;
transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.smart-timeline-row--clickable:hover {
border-color: var(--color-accent-fg);
box-shadow: var(--shadow-hover, var(--shadow));
}
.smart-timeline-date {
font-size: 0.8rem;
color: var(--color-text-muted);
font-family: var(--font-mono);
padding-top: 0.15rem;
}
.smart-timeline-body {
display: flex;
flex-direction: column;
gap: 0.25rem;
min-width: 0;
}
.smart-timeline-row-head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
.smart-timeline-status-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.4rem;
height: 1.4rem;
border-radius: 50%;
background: var(--color-surface-alt, #f4f4f4);
color: var(--color-text-muted);
font-size: 0.85rem;
font-weight: 600;
flex: 0 0 auto;
}
.smart-timeline-row--done .smart-timeline-status-icon {
background: var(--hlc-lime, #c6f41c);
color: #1a1a1a;
}
.smart-timeline-row--overdue .smart-timeline-status-icon {
background: #f8d7da;
color: #842029;
}
.smart-timeline-row--off_script .smart-timeline-status-icon {
background: #fff3cd;
color: #664d03;
}
.smart-timeline-row--court_set .smart-timeline-status-icon {
background: transparent;
border: 1px dashed var(--color-text-muted);
}
.smart-timeline-title {
font-weight: 600;
font-size: 0.92rem;
flex: 1 1 auto;
min-width: 0;
}
.smart-timeline-link {
color: inherit;
text-decoration: none;
}
.smart-timeline-link:hover {
color: var(--color-accent-fg);
text-decoration: underline;
}
.smart-timeline-link:focus-visible {
outline: none;
}
.smart-timeline-kind-chip,
.smart-timeline-rule-chip {
flex: 0 0 auto;
font-size: 0.72rem;
padding: 0.1rem 0.45rem;
border-radius: 999px;
background: var(--color-surface-alt, #f4f4f4);
color: var(--color-text-muted);
font-weight: 500;
letter-spacing: 0.02em;
}
.smart-timeline-kind-chip--deadline {
background: #e0ecff;
color: #1a4a8a;
}
.smart-timeline-kind-chip--appointment {
background: #e7f5ee;
color: #2c6b46;
}
.smart-timeline-kind-chip--milestone {
background: #fdecd2;
color: #7a4f15;
}
.smart-timeline-kind-chip--projected {
background: var(--color-surface-alt, #f4f4f4);
color: var(--color-text-muted);
font-style: italic;
}
.smart-timeline-rule-chip {
font-family: var(--font-mono);
}
.smart-timeline-desc {
font-size: 0.85rem;
color: var(--color-text-muted);
margin-top: 0.2rem;
}
/* Modal — minimal scrim + centred card. The new-entry CTA opens this;
"Eigener Meilenstein" expands the form inline, every other choice
is a plain link / disabled button. */
.smart-timeline-modal {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
padding: 1rem;
}
.smart-timeline-modal-card {
background: var(--color-surface);
border-radius: var(--radius);
padding: 1.5rem;
max-width: 480px;
width: 100%;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
}
.smart-timeline-modal-card h3 {
margin: 0 0 1rem;
font-size: 1.1rem;
}
.smart-timeline-add-choices {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.smart-timeline-add-choice {
display: block;
text-align: left;
padding: 0.6rem 0.9rem;
border: 1px solid var(--color-border);
border-radius: var(--radius);
background: var(--color-surface);
color: inherit;
text-decoration: none;
font-size: 0.9rem;
cursor: pointer;
}
.smart-timeline-add-choice:hover:not(:disabled) {
border-color: var(--color-accent-fg);
background: var(--color-surface-alt, #fafafa);
}
.smart-timeline-add-choice--primary {
border-color: var(--hlc-lime, var(--color-accent-fg));
font-weight: 600;
}
.smart-timeline-add-choice--disabled,
.smart-timeline-add-choice:disabled {
opacity: 0.55;
cursor: not-allowed;
}
.smart-timeline-modal-close-row {
display: flex;
justify-content: flex-end;
margin-top: 0.5rem;
}