Merge: t-paliad-198 — Determinator row-cascade Slice 3 (mobile polish + inline search + tooltip polish — cascade redesign complete)

This commit is contained in:
mAi
2026-05-16 00:58:50 +02:00
5 changed files with 453 additions and 49 deletions

View File

@@ -2296,14 +2296,20 @@ function initPathwayFork() {
// t-paliad-180: search escape-hatch (Pathway B Direkt-suchen
// button) routes to ?mode=filter. Reset link drops the cascade
// slug back to root, keeping mode / perspective / inbox alone.
// t-paliad-198 Slice 3: Direkt-suchen icon expands an inline search
// overlay over the row stack — the legacy ?mode=filter route is kept
// for deep-link compatibility but is no longer the user-facing path.
document.getElementById("fristen-row-search-link")?.addEventListener("click", () => {
setPathwayURL("b", "filter");
showBMode("filter");
setInlineSearchActive(true);
});
document.getElementById("fristen-row-search-panel-back")?.addEventListener("click", () => {
setInlineSearchActive(false);
});
document.getElementById("fristen-row-reset")?.addEventListener("click", () => {
currentActiveRow = null;
navigateB1("");
});
initInlineSearch();
// Reselect: drop the locked context, return to Step 1.
document.getElementById("fristen-step1-summary-reselect")?.addEventListener("click", () => {
@@ -2524,7 +2530,10 @@ function nodeStepQuestion(n: EventCategoryNode): string {
// without losing the rest. Slice 2 will add project-driven prefills +
// auto-walk; Slice 3 polishes mobile + the search escape-hatch.
type RowKind = "mode" | "perspective" | "inbox" | "cascade";
// t-paliad-198 Slice 3: mode kind retired — the icon-button at the top
// of the row stack (`#fristen-row-search-link`) replaces the mode row,
// toggling an inline search overlay instead of routing to ?mode=filter.
type RowKind = "perspective" | "inbox" | "cascade";
type RowState = "active" | "answered" | "prefilled";
interface RowOption {
@@ -2594,11 +2603,6 @@ function inboxOptionLabel(value: string): string {
return t("deadlines.inbox.all");
}
function modeOptionLabel(value: string): string {
return value === "filter"
? t("deadlines.pathway.b.mode.filter")
: t("deadlines.pathway.b.mode.tree");
}
// Slice 2: cascade-segment ↔ fristenrechner-code bridge. The event_categories
// taxonomy uses kebab-case segments under the `cms-eingang.*` buckets to
@@ -2688,24 +2692,6 @@ function buildRowStack(currentSlug: string): RowSpec[] {
return projectRef;
};
// R0 — Mode. Always present, defaults to "tree" so a fresh visit
// renders as answered. ändern flips to active and lets the user
// re-pick; selecting "filter" routes to ?mode=filter (replaces the
// cascade with B2 search, handled by the row click wiring).
const mode = readBModeFromURL();
rows.push({
kind: "mode",
rowId: "mode",
state: currentActiveRow === "mode" ? "active" : "answered",
question: t("deadlines.row.mode.question"),
options: [
{ value: "tree", label: t("deadlines.pathway.b.mode.tree") },
{ value: "filter", label: t("deadlines.pathway.b.mode.filter") },
],
pickedValue: mode,
pickedLabel: modeOptionLabel(mode),
});
// R1 — Perspective. Default null ("Beide") = no filter; the row
// renders as answered with that label. t-paliad-164 carry-over: a
// project-bound perspective shows as `is-prefilled` with the "aus Akte"
@@ -2958,6 +2944,12 @@ function renderRowStack(currentSlug: string) {
const rows = buildRowStack(currentSlug);
stack.innerHTML = rows.map((r, i) => rowHtml(r, i + 1)).join("");
maybeShowAutoWalkTooltip(stack, rows);
// t-paliad-198 Slice 3: bring the active row into view on every
// render. Particularly important on mobile, where a chip pick may
// push the next active row below the fold; the helper is a no-op
// when the row is already in view, so desktop doesn't see jumpy
// scrolling.
autoscrollToActiveRow(stack);
// Wire chip picks (active rows).
stack.querySelectorAll<HTMLButtonElement>(".fristen-row-chip").forEach((btn) => {
@@ -2998,14 +2990,9 @@ function renderRowStack(currentSlug: string) {
// every render); the other kinds update their per-kind state + clear
// the "currently being edited" override.
function handleRowPick(rowId: string, value: string) {
if (rowId === "mode") {
const next: BMode = value === "filter" ? "filter" : "tree";
setPathwayURL("b", next);
currentActiveRow = null;
cascadeAutoWalkStopAfter = null;
showBMode(next);
return;
}
// t-paliad-198 Slice 3: any chip pick is also a "user interacted"
// signal — the auto-walk tooltip's job is done.
dismissAutoWalkTooltip();
if (rowId === "perspective") {
const next: Perspective = value === "claimant" || value === "defendant" ? value : null;
writePerspectiveToURL(next);
@@ -3044,9 +3031,12 @@ function handleRowPick(rowId: string, value: string) {
// auto-walked, caps the auto-walk depth so the row turns active
// in-place without changing the URL.
function handleRowEdit(rowId: string) {
if (rowId === "mode" || rowId === "perspective" || rowId === "inbox") {
if (rowId === "perspective" || rowId === "inbox") {
currentActiveRow = rowId;
renderRowStack(readB1PathFromURL());
// t-paliad-198 Slice 3: any explicit ändern click counts as user
// interaction, so the auto-walk tooltip is no longer needed.
dismissAutoWalkTooltip();
return;
}
if (rowId.startsWith("cascade:")) {
@@ -3062,6 +3052,7 @@ function handleRowEdit(rowId: string) {
currentActiveRow = null;
cascadeAutoWalkStopAfter = null;
navigateB1(revertSlug);
dismissAutoWalkTooltip();
return;
}
// Row K is auto-walked (beyond the URL trail). Suppress auto-walk
@@ -3070,9 +3061,170 @@ function handleRowEdit(rowId: string) {
cascadeAutoWalkStopAfter = idx;
currentActiveRow = null;
renderRowStack(urlSlug);
dismissAutoWalkTooltip();
}
}
// ============================================================================
// t-paliad-198 Slice 3 — inline search overlay + autoscroll-to-active +
// tooltip polish.
// ============================================================================
//
// Inline search replaces the Slice 1 mode-toggle row: clicking the
// `🔍 Direkt suchen` icon in the row-stack header expands a search input
// over the row stack and renders results into the same #fristen-b1-results
// container the cascade narrows into. ESC inside the input or the back
// button collapses it. State is ephemeral — no URL change — so a user can
// step into search to check a name, then return to the cascade with all
// their picks intact. Deep-link path ?mode=filter still routes to the
// legacy B2 panel for backwards compatibility, but is no longer exposed
// in the cascade UI.
let inlineSearchActive = false;
let inlineSearchSeq = 0;
let inlineSearchDebounce: number | undefined;
function setInlineSearchActive(active: boolean) {
if (inlineSearchActive === active) return;
inlineSearchActive = active;
const panel = document.getElementById("fristen-row-search-panel");
const header = document.getElementById("fristen-row-stack-header");
const stack = document.getElementById("fristen-row-stack");
const results = document.getElementById("fristen-b1-results");
const trigger = document.getElementById("fristen-row-search-link") as HTMLButtonElement | null;
const input = document.getElementById("fristen-row-search-panel-input") as HTMLInputElement | null;
if (!panel || !stack) return;
panel.hidden = !active;
stack.hidden = active;
// Reset link belongs to the cascade affordance set; hide while the
// user is in search mode so the header reads as "← back to tree" only.
if (header) header.classList.toggle("is-inline-search-active", active);
if (trigger) trigger.setAttribute("aria-expanded", active ? "true" : "false");
if (active) {
if (input) {
input.focus();
if (input.value.trim() !== "") {
scheduleInlineSearch(0);
} else if (results) {
// No query yet — clear results so a stale cascade leaf panel
// doesn't show alongside the search input.
results.innerHTML = "";
results.classList.remove("is-loading", "is-no-hits");
}
}
} else {
// On collapse, restore the cascade results for the current slug
// so the user picks up exactly where they left off.
runB1Search(cascadeEffectiveSlug || readB1PathFromURL());
}
}
function scheduleInlineSearch(delayMs = 180) {
if (inlineSearchDebounce !== undefined) window.clearTimeout(inlineSearchDebounce);
inlineSearchDebounce = window.setTimeout(runInlineSearch, delayMs);
}
async function runInlineSearch() {
const input = document.getElementById("fristen-row-search-panel-input") as HTMLInputElement | null;
const results = document.getElementById("fristen-b1-results");
const clearBtn = document.getElementById("fristen-row-search-panel-clear") as HTMLButtonElement | null;
if (!input || !results) return;
const q = input.value.trim();
if (clearBtn) clearBtn.hidden = q.length === 0;
if (q === "") {
results.innerHTML = "";
results.classList.remove("is-loading", "is-no-hits");
return;
}
results.classList.remove("is-no-hits");
results.classList.add("is-loading");
if (results.childElementCount === 0) {
results.innerHTML = `<div class="fristen-search-status">${escHtml(t("deadlines.search.loading"))}</div>`;
}
const seq = ++inlineSearchSeq;
try {
const url = new URL("/api/tools/fristenrechner/search", window.location.origin);
url.searchParams.set("q", q);
url.searchParams.set("limit", "12");
const forums = getActiveForumsParam();
if (forums) url.searchParams.set("forum", forums);
const r = await fetch(url.toString(), { credentials: "same-origin" });
if (seq !== inlineSearchSeq) return;
results.classList.remove("is-loading");
if (!r.ok) {
results.innerHTML = `<div class="fristen-search-status fristen-search-error">${escHtml(t("deadlines.search.no_hits"))}</div>`;
return;
}
const data = (await r.json()) as SearchResponse;
if (seq !== inlineSearchSeq) return;
if (!data.cards || data.cards.length === 0) {
results.innerHTML = `<div class="fristen-search-status">${escHtml(t("deadlines.search.no_hits"))}</div>`;
results.classList.add("is-no-hits");
return;
}
renderSearchResultsInto("fristen-b1-results", data);
} catch {
if (seq !== inlineSearchSeq) return;
results.classList.remove("is-loading");
results.innerHTML = `<div class="fristen-search-status fristen-search-error">${escHtml(t("deadlines.search.no_hits"))}</div>`;
}
}
function initInlineSearch() {
const input = document.getElementById("fristen-row-search-panel-input") as HTMLInputElement | null;
const clearBtn = document.getElementById("fristen-row-search-panel-clear") as HTMLButtonElement | null;
if (input) {
input.addEventListener("input", () => scheduleInlineSearch());
input.addEventListener("keydown", (ev) => {
if (ev.key === "Escape") {
if (input.value.trim() !== "") {
// First ESC: clear the input; second ESC: close the panel.
input.value = "";
scheduleInlineSearch(0);
return;
}
setInlineSearchActive(false);
}
});
}
if (clearBtn) {
clearBtn.addEventListener("click", () => {
if (!input) return;
input.value = "";
input.focus();
scheduleInlineSearch(0);
});
}
}
// autoscrollToActiveRow brings the active cascade row into view when a
// user pick on a higher row causes descendants to drop and the next
// active row is now below the fold. The 60px headroom matches the
// pattern used by the Akte picker (Step 1) and other paliad forms so
// the row's chip body is visible without scrolling further. Desktop +
// mobile share this behaviour; the visual change is only noticeable on
// narrow viewports where the row stack scrolls.
function autoscrollToActiveRow(stack: HTMLElement) {
const active = stack.querySelector<HTMLElement>(".fristen-row.is-active");
if (!active) return;
// Skip if the active row is already fully visible (avoid jumpy
// scroll-on-every-render on desktop).
const rect = active.getBoundingClientRect();
if (rect.top >= 60 && rect.bottom <= window.innerHeight - 16) return;
const targetY = window.scrollY + rect.top - 60;
window.scrollTo({ top: Math.max(0, targetY), behavior: "smooth" });
}
// dismissAutoWalkTooltip removes the live tooltip element (if any) and
// flips the localStorage suppression key so future renders skip it.
// Called from any meaningful user interaction (chip pick, ändern,
// dismiss-button) — once the user has touched the cascade they no
// longer need the inference hint.
function dismissAutoWalkTooltip() {
document.querySelector(".fristen-row-autowalk-tip")?.remove();
try { localStorage.setItem(cascadeTooltipDismissedKey, "1"); } catch { /* private mode */ }
}
// maybeShowAutoWalkTooltip surfaces a one-time hint when ≥ 2 cascade
// rows render in the prefilled (auto-walked) state. Per design §11.3
// and Q11 (deferred to v2 nice-to-have), the tooltip only appears the
@@ -3086,11 +3238,14 @@ function maybeShowAutoWalkTooltip(stack: HTMLElement, rows: RowSpec[]) {
try { dismissed = localStorage.getItem(cascadeTooltipDismissedKey) === "1"; } catch { /* private mode */ }
if (dismissed) return;
// Inject the tooltip element. It's a sibling of the row stack, slotted
// just above the first prefilled row via DOM insertion.
// just above the first prefilled row via DOM insertion. The
// `is-entering` class drives the fade-in + slide-down animation in
// CSS (Slice 3 polish); we remove it on the next animation frame so
// the CSS transition kicks in.
const firstPrefilled = stack.querySelector(".fristen-row.is-prefilled");
if (!firstPrefilled) return;
const tip = document.createElement("div");
tip.className = "fristen-row-autowalk-tip";
tip.className = "fristen-row-autowalk-tip is-entering";
tip.setAttribute("role", "status");
tip.innerHTML = `
<span class="fristen-row-autowalk-tip-icon" aria-hidden="true">&#9432;</span>
@@ -3101,10 +3256,20 @@ function maybeShowAutoWalkTooltip(stack: HTMLElement, rows: RowSpec[]) {
aria-label="${escAttr(t("deadlines.row.autowalk.dismiss"))}">
&times;
</button>`;
firstPrefilled.parentElement?.insertBefore(tip, firstPrefilled);
// Desktop: tip sits above the prefilled row so it reads as a header.
// Mobile (<640px): tip sits below the row so the user's eye lands on
// the row's "aus Akte" tag first and reads the explanation beneath
// — matches design §7 + the natural reading pattern on small screens.
const isMobile = window.matchMedia && window.matchMedia("(max-width: 640px)").matches;
if (isMobile) {
firstPrefilled.parentElement?.insertBefore(tip, firstPrefilled.nextSibling);
} else {
firstPrefilled.parentElement?.insertBefore(tip, firstPrefilled);
}
// Trigger the transition: remove the is-entering offset on next frame.
window.requestAnimationFrame(() => tip.classList.remove("is-entering"));
tip.querySelector<HTMLButtonElement>(".fristen-row-autowalk-tip-dismiss")?.addEventListener("click", () => {
tip.remove();
try { localStorage.setItem(cascadeTooltipDismissedKey, "1"); } catch { /* private mode */ }
dismissAutoWalkTooltip();
});
}

View File

@@ -379,6 +379,10 @@ const translations: Record<Lang, Record<string, string>> = {
"deadlines.row.search.link.title": "Direkt nach einer Frist suchen — überspringt den Entscheidungsbaum",
"deadlines.row.autowalk.tooltip": "Diese Schritte ergeben sich aus Ihrer Akte. Klicken Sie „ändern\", um eine Antwort manuell anzupassen.",
"deadlines.row.autowalk.dismiss": "Hinweis schließen",
"deadlines.row.search.panel.back": "Zurück zum Entscheidungsbaum",
"deadlines.row.search.panel.back.title": "Inline-Suche schließen und zum Entscheidungsbaum zurückkehren",
"deadlines.row.search.panel.placeholder": "Frist suchen — z. B. „Klageschrift\", „Posteingang Hinweisbeschluss\"…",
"deadlines.row.search.panel.clear": "Eingabe leeren",
"deadlines.inbox.label": "Wo kam es an?",
"deadlines.inbox.cms.title": "UPC — über CMS",
"deadlines.inbox.bea.title": "Nationale Verfahren — über beA",
@@ -2943,6 +2947,10 @@ const translations: Record<Lang, Record<string, string>> = {
"deadlines.row.search.link.title": "Search directly for a deadline — skips the decision tree",
"deadlines.row.autowalk.tooltip": "These steps were derived from your matter. Click \"edit\" to override any answer manually.",
"deadlines.row.autowalk.dismiss": "Dismiss hint",
"deadlines.row.search.panel.back": "Back to decision tree",
"deadlines.row.search.panel.back.title": "Close inline search and return to the decision tree",
"deadlines.row.search.panel.placeholder": "Search for a deadline — e.g. \"statement of claim\", \"hint order\"…",
"deadlines.row.search.panel.clear": "Clear input",
"deadlines.inbox.label": "Where did it arrive?",
"deadlines.inbox.cms.title": "UPC — via CMS",
"deadlines.inbox.bea.title": "National-DE — via beA",

View File

@@ -234,18 +234,24 @@ export function renderFristenrechner(): string {
<span data-i18n="deadlines.pathway.b.title">Frist eintragen aufgrund Ereignis</span>
</h2>
{/* B1 panel — row-stack cascade (t-paliad-180 Slice 1).
Mode / perspective / inbox / cascade-steps all render as
`.fristen-row` instances inside `#fristen-row-stack`. The
stack-header above hosts the "Direkt suchen" escape hatch
(Pathway B → ?mode=filter) and the "Pfad zurücksetzen"
link, both of which used to be embedded in the cascade's
breadcrumb. `#fristen-b1-results` is unchanged — concept
cards still render below as the cascade narrows. */}
{/* B1 panel — row-stack cascade.
`#fristen-row-stack` hosts the perspective / inbox /
cascade rows (t-paliad-180 Slice 1; t-paliad-197 Slice 2
added project-driven prefills + auto-walk). The
stack-header above carries the inline-search trigger
(t-paliad-198 Slice 3 — clicking expands
`#fristen-row-search-panel` over the row stack instead
of routing to the legacy B2 surface) and the reset link.
`#fristen-b1-results` is unchanged — it renders concept
cards for both cascade-narrowing AND inline-search
results, so users see the same card layout regardless
of how they reached a deadline rule. */}
<div className="fristen-b1-panel" id="fristen-b1-panel" data-mode="tree" hidden>
<div className="fristen-row-stack-header">
<div className="fristen-row-stack-header" id="fristen-row-stack-header">
<button type="button" className="fristen-row-search-link" id="fristen-row-search-link"
data-i18n-title="deadlines.row.search.link.title"
aria-expanded="false"
aria-controls="fristen-row-search-panel"
title="Direkt nach einer Frist suchen">
<span aria-hidden="true">&#128269;</span>{" "}
<span data-i18n="deadlines.row.search.link">Direkt suchen</span>
@@ -257,6 +263,44 @@ export function renderFristenrechner(): string {
<span data-i18n="deadlines.row.reset">Pfad zur&uuml;cksetzen</span>
</button>
</div>
{/* Inline search overlay (t-paliad-198 Slice 3). Hidden by
default; the search icon-button in the stack header
toggles it open / closed. While open, the row stack is
hidden and the search input drives `#fristen-b1-results`
directly — same surface the cascade leaf populates so
the user sees one consistent concept-card list. */}
<div className="fristen-row-search-panel" id="fristen-row-search-panel" hidden role="search">
<button type="button" className="fristen-row-search-panel-back" id="fristen-row-search-panel-back"
data-i18n-title="deadlines.row.search.panel.back.title"
title="Zur&uuml;ck zum Entscheidungsbaum">
<span aria-hidden="true">&larr;</span>{" "}
<span data-i18n="deadlines.row.search.panel.back">Zur&uuml;ck zum Entscheidungsbaum</span>
</button>
<div className="fristen-row-search-panel-input-wrap">
<svg className="fristen-row-search-panel-icon" width="18" height="18" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" aria-hidden="true">
<circle cx="11" cy="11" r="7"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<input
type="search"
id="fristen-row-search-panel-input"
className="fristen-row-search-panel-input"
autocomplete="off"
spellcheck="false"
data-i18n-placeholder="deadlines.row.search.panel.placeholder"
placeholder="Frist suchen&hellip;"
aria-label="Frist suchen"
/>
<button type="button" className="fristen-row-search-panel-clear" id="fristen-row-search-panel-clear"
data-i18n-title="deadlines.row.search.panel.clear" title="Eingabe leeren" hidden>
<span aria-hidden="true">&times;</span>
</button>
</div>
</div>
<div className="fristen-row-stack" id="fristen-row-stack" aria-live="polite"></div>
<div className="fristen-b1-results" id="fristen-b1-results" aria-live="polite"></div>
</div>

View File

@@ -1099,6 +1099,10 @@ export type I18nKey =
| "deadlines.row.reset.title"
| "deadlines.row.search.link"
| "deadlines.row.search.link.title"
| "deadlines.row.search.panel.back"
| "deadlines.row.search.panel.back.title"
| "deadlines.row.search.panel.clear"
| "deadlines.row.search.panel.placeholder"
| "deadlines.save.cta"
| "deadlines.save.cta.adhoc.hint"
| "deadlines.save.error"

View File

@@ -1733,6 +1733,109 @@ input[type="range"]::-moz-range-thumb {
margin-bottom: 1.5rem;
}
/* When inline search is active, the stack-header's reset link is the
* only chrome that doesn't make sense — the search panel has its own
* "← back to decision tree" button at the top. */
.fristen-row-stack-header.is-inline-search-active .fristen-row-search-link,
.fristen-row-stack-header.is-inline-search-active .fristen-row-reset-link {
display: none;
}
/* t-paliad-198 Slice 3 — inline search overlay. Expands above the row
* stack when the `🔍 Direkt suchen` button is clicked. The row stack
* itself is hidden via the JS toggle on `#fristen-row-stack`; the
* search input drives the same `#fristen-b1-results` container the
* cascade narrows into. */
.fristen-row-search-panel {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.25rem;
padding: 0.85rem 1rem 1rem;
border: 1px solid var(--color-accent);
border-radius: 8px;
background: color-mix(in srgb, var(--color-accent) 4%, var(--color-bg));
}
.fristen-row-search-panel-back {
display: inline-flex;
align-items: center;
gap: 0.35rem;
align-self: flex-start;
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
padding: 0.25rem 0.5rem;
font-size: 0.85rem;
border-radius: 4px;
}
.fristen-row-search-panel-back:hover {
color: var(--color-text);
background: var(--color-bg-muted);
}
.fristen-row-search-panel-back:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 1px;
}
.fristen-row-search-panel-input-wrap {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.75rem;
border: 1px solid var(--color-border);
border-radius: 6px;
background: var(--color-bg);
}
.fristen-row-search-panel-input-wrap:focus-within {
border-color: var(--color-accent);
}
.fristen-row-search-panel-icon {
flex-shrink: 0;
color: var(--color-text-muted);
}
.fristen-row-search-panel-input {
flex: 1 1 auto;
border: none;
background: transparent;
color: var(--color-text);
font-size: 1rem;
padding: 0.4rem 0;
outline: none;
}
.fristen-row-search-panel-input::placeholder {
color: var(--color-text-muted);
}
.fristen-row-search-panel-clear {
flex-shrink: 0;
background: none;
border: none;
color: var(--color-text-muted);
font-size: 1.2rem;
line-height: 1;
cursor: pointer;
padding: 0.1rem 0.4rem;
border-radius: 4px;
}
.fristen-row-search-panel-clear:hover {
color: var(--color-text);
background: var(--color-bg-muted);
}
.fristen-row-search-panel-clear:focus-visible {
outline: 2px solid var(--color-accent);
outline-offset: 1px;
}
.fristen-row {
display: flex;
flex-direction: column;
@@ -1880,6 +1983,34 @@ input[type="range"]::-moz-range-thumb {
background: color-mix(in srgb, var(--color-accent) 8%, transparent);
font-size: 0.85rem;
color: var(--color-text);
/* t-paliad-198 Slice 3: fade-in + slight slide-down on first paint.
* The is-entering class is applied at injection and removed on the
* next animation frame so the transition fires from the initial
* offset state to the resting state. ~200ms keeps it subtle — the
* user notices the hint without it feeling theatrical. */
opacity: 1;
transform: translateY(0);
transition: opacity 200ms ease, transform 200ms ease;
}
.fristen-row-autowalk-tip.is-entering {
opacity: 0;
transform: translateY(-4px);
}
/* Mobile (<640px): the tooltip sits below the prefilled row instead of
* above it, so the user's eye lands on the row's "aus Akte" tag first
* and reads the explanation beneath. The transform offset flips
* accordingly. */
@media (max-width: 640px) {
.fristen-row.is-prefilled + .fristen-row-autowalk-tip,
.fristen-row-autowalk-tip {
margin: 0 0 0.4rem;
font-size: 0.8rem;
}
.fristen-row-autowalk-tip.is-entering {
transform: translateY(4px);
}
}
.fristen-row-autowalk-tip-icon {
@@ -1998,6 +2129,50 @@ input[type="range"]::-moz-range-thumb {
flex: 1;
}
/* t-paliad-198 Slice 3 — responsive row-stack breakpoints (design §7).
*
* Default (above): desktop layout — head + chip body share the row, chips
* wrap into a 3-column grid via auto-fill 220px.
*
* 7681023px (tablet density): same head layout but chip grid drops to
* 2 columns (auto-fill 200px) so each chip stays comfortably tappable.
*
* 640767px (small tablet / large phone): head wraps so the ändern
* affordance moves to its own line; chip grid is single-column to keep
* the chip body legible.
*
* <640px (phone): chips full-width single-column; ändern stays
* permanently visible (no hover state to reveal it on touch); the row
* num badge tucks into the same line as the label to save vertical
* space.
*/
@media (max-width: 1023px) {
.fristen-row-body {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
}
}
@media (max-width: 767px) {
.fristen-row {
padding: 0.6rem 0.75rem;
}
.fristen-row.is-active {
padding: 0.85rem 0.85rem;
}
.fristen-row-head {
gap: 0.4rem;
}
.fristen-row-body {
grid-template-columns: 1fr;
gap: 0.4rem;
margin-left: 1.9rem;
}
.fristen-row-edit {
opacity: 1;
margin-left: auto;
}
}
@media (max-width: 640px) {
.fristen-row-body {
grid-template-columns: 1fr;
@@ -2005,10 +2180,18 @@ input[type="range"]::-moz-range-thumb {
}
.fristen-row-head {
gap: 0.4rem;
flex-wrap: wrap;
}
.fristen-row-answer {
flex-basis: 100%;
margin-left: 2rem;
}
.fristen-row-edit {
opacity: 1;
}
.fristen-row-chip {
padding: 0.75rem 0.85rem;
}
}
.fristen-b1-loosen-link {