mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
ebc98f0a3f
* fix(plugins): unknown column * fix(mcp): reuse MCP sessions instead of creating one per tool call The /mcp CORS layer never set exposedHeaders, so Access-Control-Expose-Headers was absent and browser-context MCP clients (Claude Desktop connectors, Claude.ai, MCP Inspector) could not read Mcp-Session-Id off the initialize response. Unable to echo it back, every request looked like a fresh initialize: one McpServer and one session per tool call, until the per-user cap returned a 429 and the integration died until the container was restarted. The idle sweep was not at fault — it expires on lastActivity with a 1h default, so sessions born seconds apart are nowhere near expiry, hence 'cleaned 0'. - expose Mcp-Session-Id, MCP-Protocol-Version and WWW-Authenticate - evict a user's least-recently-active session at the cap rather than refusing the request, so a client that cannot persist its session id (or a proxy that strips the header) can never wedge the server - close the McpServer/transport orphaned by every session-less non-initialize POST, which was leaked: never mapped, never swept, never closed - return the cap error as JSON-RPC so clients surface the real reason - warn on session-less POSTs to make a header-stripping proxy diagnosable * chore(deps): declare @modelcontextprotocol/sdk ^1.29.0 Matches the version already resolved in the lockfile; no dependency-tree change. * docs(mcp): add the reverse proxy specs for MCP * fix(plugins): stop the row ⋯ menu being clipped by the sidebar The menu was an in-flow `absolute` div, and its ancestor (PageSidebar) is `overflow-hidden` — which clips absolutely-positioned descendants regardless of z-index. With enough plugins installed a row sits low enough that its menu runs past the sidebar's bottom edge and gets chopped, taking Delete with it, so the plugin could no longer be uninstalled from the UI. Portal the menu to <body> and position it `fixed` against the ⋯ button, flipping upward when the bottom is tight and re-anchoring on scroll/resize. A fixed child of <body> has no overflow ancestor, so nothing can clip it. Closes #1523 * feat(plugins): surface author-signature status and add a scoped re-trust override TREK has always verified an author's Ed25519 signature and TOFU-pinned the key on first install, but none of it was ever shown: a successfully-installed UNSIGNED plugin looked identical to a signed one, and a signature-refused update left the plugin quietly pinned at its old version with the reason dying in a toast. Give the four refusal conditions machine-readable codes (SIGNATURE_MISSING / _INCOMPLETE / _KEY_CHANGED / _INVALID), persist a refusal on the plugin row so the admin list keeps showing it, and badge Signed/Unsigned in the list and in Discover. Only SIGNATURE_KEY_CHANGED is overridable — an author can legitimately rotate a key; a signature that does not verify means the bytes are not what the author signed, and there is no story where waving that through is right. The override re-pins and updates in ONE call (POST :id/retrust): a re-pin that waited for a follow-up /update would leave the plugin pinned to a key no install had ever been verified against if that second call never came. The artifact must still verify under the new key, so a re-trust moves the pin from one verified key to another. assertRetrustable re-derives the condition server-side, so the UI hiding the button is a convenience, not the control, and it echoes back the full key the admin was shown so a re-key since the dialog rendered is refused. The rotation is written to the admin audit log with both fingerprints — after an incident, "which key did we move from, and to what?" is the question a single key cannot answer. * fix(plugins): let a plugin's frame reach the hosts an admin added for it The frame's connect-src was built from the manifest's http:outbound grants alone, but the child's egress guard is the UNION of those and the hosts an admin added after install for an operatorEgress plugin (a self-hosted Gotify, an ntfy — hosts the author cannot know in advance). So such a plugin WITH A UI could call the operator's host from its server and was CSP-blocked in its own iframe. Match the frame to the child. The admin consented to these hosts at install and the child already reaches them, so this widens no trust boundary that isn't already crossed. Both sources stay validated on the way in, and the interpolation filter is unchanged. * fix(plugins): report a rejected events.emit on the plugin's own log stream The host can reject an emit (an undeclared event name, a rate limit). The rejection must not escape — a detached rejection crashes the child and terminally disables the plugin over one bad emit — but swallowing it silently left an author with no way to discover that `emits` was missing from their manifest. Surface it as a warning instead. * feat(plugin-sdk): expose the raw request body for webhook signature checks A webhook author must run their HMAC over the exact bytes the sender signed. `body` is the PARSED value, and re-serializing it will not reproduce those bytes — key order, whitespace and unicode escaping all differ — so the signature never matches. Document `rawBodyBase64`, which the host already sets on auth:false routes. * fix(plugin-sdk): refuse to overwrite a released artifact, and keep the packed zip A released artifact is IMMUTABLE: the registry pins its sha256, so overwriting the bytes of a release already in the registry breaks the checksum for everyone who installed that version — they can no longer install or update it. The old code blanket-caught every `gh release create` failure (auth, network, a bad repo) and turned it into a --clobber upload. Probe for the release explicitly and refuse unless --force. Also stop deleting plugin.zip on the way out. It is the exact bytes the release and the entry's sha256 pin were computed from; a re-pack on another machine or SDK version can differ (CRLF, walk order), so anyone re-running `entry`/`sign` afterwards must hash THAT file, not a rebuild. * fix(plugin-sdk): don't fail submit when the fork already has an upstream remote `gh repo clone` of a fork may already have wired `upstream`, in which case a bare `remote add` exits non-zero and took the whole submit down. Set it either way. * fix(plugin-sdk): make the dev server behave like the host Three ways `dev` lied to an author about how their plugin would run in production: - ctx.db: one try/catch wrapped both the node:sqlite probe AND opening the database, so an mkdir/permission failure silently degraded to the in-memory stub — which swallows every write while reporting success. A db:own plugin "worked" in dev and persisted nothing. Probe separately: fail loudly on a real error, degrade only on old Node, and say plainly that the stub discards writes. - notificationChannel: the host fires it with no acting user and hands the recipient's decrypted settings in as a separate `config` argument — send(msg, config, ctx) / test(config, ctx). Firing it like an ordinary hook passed `ctx` where `config` belongs, so a channel plugin read its settings off ctx and was broken in production. - /preview pinned tripId 42 while the scaffold seeds trip 1, so the widget's first trek:invoke hit assertMember(42) and 500'd. Preview against a trip that exists. * fix(plugin-sdk): reject unknown flags, wire up create's new flags, and add --help `parse()` accepts any --x, so a flag a command does not read was silently dropped: `create --template notification-channel` cheerfully scaffolded a blank plugin. Silently ignoring an author's explicit instruction is worse than refusing it, so unknown flags are now an error, and create actually forwards --template/--egress/--required-addons. A bare --permissions used to split the string "true" into a permission literally named `true`; listFlag() now treats a valueless flag as absent. Since an unknown flag is now fatal, `--help` has to exist: it is intercepted before the flag check and prints usage on stdout with exit 0. * chore(plugins): drop three unused eslint-disable directives The no-console rule is not enabled for these files, so the directives were dead and eslint reports them as unused. * fix(plugin-sdk): bring preflight back in step with the registry's gates preflight exists to tell an author what TREK-Plugins' CI will say before they open the PR. The registry now verifies author signatures, and preflight didn't — so it drifted into the one failure mode it must never have: a false green. An author trusts a green. - Verify the signature against the artifact bytes. preflight already downloads them for the sha256 check, so this costs one call. Without it, signing with the wrong key (or re-packing after signing) sails through and is caught at review. - Check the signature SHAPE (checkSignatureShape): a key with no signed version, a signature with no key, a malformed key or signature. TREK refuses to install a half-signed entry, so such an entry is dead on arrival. - Default apiVersion to 1 before comparing. It is OPTIONAL in the manifest — install/ manifest.ts and `entry` both default it — so a manifest that legally omits it was failing preflight with "manifest apiVersion undefined != entry 1" while the registry passed it. A false RED, which teaches authors to ignore the tool. - Check requiredAddons/pluginDependencies parity, and operatorEgress without an http:outbound grant. The verifier is a port of the host's install/verify-signature.ts (the registry has its own port); sign.ts's verifyArtifact only understands the bare key/signature pair the SDK itself emits and cannot judge a minisign key. A test pins all three to the same verdicts. * feat(plugins): enforce compatibility range * chore: bump sdk version * test(e2e): repair the trip-creation specs create-trip and trip-planner have been failing for a while — long enough for three separate UI changes to drift past them, which nothing caught because CI runs vitest only and never invokes Playwright. Each failure was masking the next: - The release-notice modal greets a freshly seeded user and its backdrop swallowed the click on .add-trip-card. Added a shared dismissSystemNotices() helper (the X only shows on the notice's last page, so it has to page through first). - .modal-backdrop no longer exists — the class was namespaced to .trek-modal-backdrop so content blockers stop hiding it. - input[type=text].first() is no longer the Title field: the cover-image search inputs now sit above it, so the specs were typing the trip name into the photo search box and creating an untitled trip that never matched getByText(title). * fix(planner): gate drag & drop on pointer type, not viewport width (#1432) The 3.2.1 fix disabled drag on "mobile", but nothing in the client has ever detected touch — "mobile" was inferred from viewport width, at four independent breakpoints. A tablet is a coarse-pointer device at a *desktop* width, so an iPad (820-1366px) fell on the wrong side of all four, which is why iPhone was fixed and iPad was not: - useTripPlanner's isMobile (<768px) is what disarmed `draggable`, so on iPad rows stayed draggable and a scroll swipe became an HTML5 drag. - TripPlannerPage hardcoded isMobile={false} on the desktop PlacesSidebar, so its drop handlers and the drop-to-import overlay could never be disabled — that overlay is the reported symptom. - The arrow-button reorder fallback was revealed only below 767px, leaving iPad with no drag *and* no fallback. - touchDragPolyfill loaded drag-drop-touch at >=1024px, synthesising drags from touchmove — on a landscape iPad that re-armed the very gesture hijack 3.2.1 removed. Adds useIsTouch() ((pointer: coarse)) as a signal separate from isMobile: layout stays width-driven, so the iPad keeps the desktop two-pane planner, while every drag affordance is gated on isMobile || isTouch. The reorder arrows now show on coarse pointers, and the polyfill loads only on hybrid laptops ((pointer: fine) and (any-pointer: coarse)), which also removes the #1440 phantom-dblclick map zoom on tablets. Guarded by unit tests plus an e2e spec on real WebKit in an iPad Pro 11 context — the engine matters, since every browser on iPadOS is WebKit underneath, which is why the reporter hit this in all three they tried. * test(planner): guard the day-plan reorder arrows against phantom clicks The hover rule that reveals the reorder arrows had been dead since the TypeScript migration: it targeted `.place-row:hover .reorder-btns`, and neither class exists — the component renders `.reorder-buttons` inside an unclassed row. The buttons sat at opacity:0 on desktop with nothing to reveal them. That was not merely invisible. opacity:0 still hit-tests, so every itinerary row and day note carried an invisible, fully clickable target that silently reordered the trip: opacity: "0", visibility: "visible", pointerEvents: "auto" elementFromPoint(centre) -> BUTTON, inside .reorder-buttons The repair — pointer-events: none while hidden, plus a working .dp-row:hover/:focus-within reveal (focus-within so the buttons are also reachable by keyboard, which the file's JS-hover pattern cannot do) — lives in index.css and DayPlanSidebar.tsx. Both files also carry the #1432 drag gating, so they went with that commit rather than being split mid-file; this commit is the regression guard for them. E2E rather than unit, because jsdom does not evaluate :hover. Against the old CSS it fails on exactly the right assertion: "hidden arrows must not swallow clicks" — expected false, received true. * fix(pdf): keep the header gap on day-header overflow pages (#1531) The repeated <thead> day header (#1471) carried no gap before the first card on overflow pages: the 12px sat in .day-body's block-start padding, which a fragmented box only paints on its first fragment. Move it to the thead cell so it repeats with the header; the day's first page renders pixel-identically. * fix(budget): keep "no one paid yet" when editing an expense (#1533) The ExpenseModal payer initializer fell back to the current user whenever the edited item had no payer, so reopening an expense saved with "no one paid yet" silently reselected "You" — and re-saving then recorded the current user as the payer, corrupting the balances. An absent payer on an existing expense is a deliberate value, so only a brand-new expense defaults to me.
1137 lines
43 KiB
CSS
1137 lines
43 KiB
CSS
@tailwind base;
|
|
@tailwind components;
|
|
@tailwind utilities;
|
|
|
|
html { height: 100%; overflow: hidden; background-color: var(--bg-primary); }
|
|
body { height: 100%; overflow: auto; overscroll-behavior: none; -webkit-overflow-scrolling: touch; }
|
|
|
|
|
|
/* Journey desktop feed: hide scrollbar (right column is a sticky map, a
|
|
visible scrollbar on the left breaks the polarsteps-style reading feel). */
|
|
.journey-feed-scroll { scrollbar-width: none; -ms-overflow-style: none; }
|
|
.journey-feed-scroll::-webkit-scrollbar { display: none; }
|
|
|
|
/* Leaflet Popups — Enter-Animation vom Anchor-Tip */
|
|
.leaflet-popup {
|
|
animation: trek-popover-enter 220ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
transform-origin: bottom center;
|
|
will-change: transform, opacity;
|
|
}
|
|
.leaflet-popup-content-wrapper {
|
|
border-radius: 14px !important;
|
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18) !important;
|
|
background: var(--bg-card) !important;
|
|
color: var(--text-primary) !important;
|
|
border: 1px solid var(--border-faint);
|
|
}
|
|
.leaflet-popup-tip {
|
|
background: var(--bg-card) !important;
|
|
}
|
|
.leaflet-popup-close-button {
|
|
transition: color 150ms cubic-bezier(0.23, 1, 0.32, 1), transform 150ms cubic-bezier(0.23, 1, 0.32, 1) !important;
|
|
}
|
|
.leaflet-popup-close-button:hover {
|
|
transform: scale(1.15);
|
|
color: var(--text-primary) !important;
|
|
}
|
|
|
|
/* GL hover popup — the name/category/address card on marker hover.
|
|
Matches the Leaflet map's white hover tooltip. pointer-events:none so moving
|
|
onto the popup never steals the marker's mouseleave and causes flicker. */
|
|
.trek-map-popup { pointer-events: none; }
|
|
.trek-map-popup .mapboxgl-popup-content,
|
|
.trek-map-popup .maplibregl-popup-content {
|
|
padding: 7px 10px;
|
|
border-radius: 10px;
|
|
background: #fff;
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.16);
|
|
}
|
|
.trek-map-popup .mapboxgl-popup-tip,
|
|
.trek-map-popup .maplibregl-popup-tip {
|
|
border-top-color: #fff;
|
|
border-bottom-color: #fff;
|
|
border-left-color: #fff;
|
|
border-right-color: #fff;
|
|
}
|
|
|
|
.atlas-tooltip {
|
|
background: rgba(10, 10, 20, 0.6) !important;
|
|
backdrop-filter: blur(20px) saturate(180%) !important;
|
|
-webkit-backdrop-filter: blur(20px) saturate(180%) !important;
|
|
color: #f1f5f9 !important;
|
|
border: 1px solid rgba(255,255,255,0.1) !important;
|
|
border-radius: 14px !important;
|
|
padding: 10px 14px !important;
|
|
font-size: 12px !important;
|
|
font-family: inherit !important;
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.25) !important;
|
|
transition: none !important;
|
|
}
|
|
.atlas-tooltip::before { border-top-color: rgba(10, 10, 20, 0.6) !important; }
|
|
html:not(.dark) .atlas-tooltip {
|
|
background: rgba(255, 255, 255, 0.75) !important;
|
|
color: #0f172a !important;
|
|
border: 1px solid rgba(0,0,0,0.08) !important;
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.1) !important;
|
|
}
|
|
html:not(.dark) .atlas-tooltip::before { border-top-color: rgba(255, 255, 255, 0.75) !important; }
|
|
.leaflet-tooltip.atlas-tooltip { opacity: 1 !important; }
|
|
.leaflet-tooltip-pane { transition: none !important; }
|
|
.leaflet-fade-anim .leaflet-tooltip { transition: none !important; opacity: 1 !important; }
|
|
|
|
.dark .leaflet-control-zoom a {
|
|
background: rgba(10, 10, 20, 0.7) !important;
|
|
color: #e2e8f0 !important;
|
|
border-color: rgba(255, 255, 255, 0.1) !important;
|
|
backdrop-filter: blur(12px);
|
|
}
|
|
.dark .leaflet-control-zoom a:hover {
|
|
background: rgba(30, 30, 40, 0.8) !important;
|
|
}
|
|
@media (max-width: 767px) {
|
|
.leaflet-control-zoom { display: none !important; }
|
|
}
|
|
|
|
/* Dark mode overrides for pages using hardcoded slate-* Tailwind classes */
|
|
html.dark .bg-slate-50 { background-color: var(--bg-secondary) !important; }
|
|
html.dark .bg-white { background-color: var(--bg-card) !important; }
|
|
html.dark .bg-slate-100 { background-color: var(--bg-secondary) !important; }
|
|
html.dark .bg-slate-900.text-white { background-color: #e2e8f0 !important; color: #0f172a !important; }
|
|
html.dark .border-slate-200, html.dark .border-slate-300 { border-color: var(--border-primary) !important; }
|
|
html.dark .border-slate-100, html.dark .border-b-slate-100 { border-color: var(--border-secondary) !important; }
|
|
html.dark .text-slate-900 { color: var(--text-primary) !important; }
|
|
html.dark .text-slate-700 { color: var(--text-secondary) !important; }
|
|
html.dark .text-slate-600 { color: var(--text-muted) !important; }
|
|
html.dark .text-slate-500 { color: var(--text-muted) !important; }
|
|
html.dark .text-slate-400 { color: var(--text-faint) !important; }
|
|
html.dark .hover\:bg-slate-50:hover, html.dark .hover\:bg-slate-100:hover { background-color: var(--bg-hover) !important; }
|
|
html.dark .hover\:text-slate-900:hover { color: var(--text-primary) !important; }
|
|
html.dark .hover\:bg-slate-700:hover { background-color: var(--bg-hover) !important; }
|
|
html.dark .divide-slate-100 > :not([hidden]) ~ :not([hidden]) { border-color: var(--border-secondary) !important; }
|
|
html.dark .focus\:ring-slate-400:focus { --tw-ring-color: var(--text-faint) !important; }
|
|
html.dark input[class*="border-slate"], html.dark input[class*="text-slate"] { background: var(--bg-secondary) !important; color: var(--text-primary) !important; border-color: var(--border-primary) !important; }
|
|
html.dark .text-amber-900 { color: #fbbf24 !important; }
|
|
html.dark .text-amber-700 { color: #f59e0b !important; }
|
|
html.dark .bg-amber-50 { background-color: rgba(245,158,11,0.1) !important; }
|
|
html.dark .border-amber-200 { border-color: rgba(245,158,11,0.2) !important; }
|
|
html.dark .disabled\:bg-slate-400:disabled { background-color: var(--text-faint) !important; }
|
|
html.dark button.bg-slate-900 { background-color: #e2e8f0 !important; color: #0f172a !important; opacity: 1 !important; }
|
|
html.dark button.bg-slate-900:hover { background-color: #cbd5e1 !important; }
|
|
html.dark button.bg-slate-900:disabled { background-color: #ffffff !important; color: #000000 !important; opacity: 0.4 !important; }
|
|
html.dark span.bg-slate-900 { background-color: #e2e8f0 !important; color: #0f172a !important; }
|
|
html.dark span.bg-slate-100 { background-color: var(--bg-secondary) !important; color: var(--text-muted) !important; }
|
|
html.dark .border-b { border-bottom-color: var(--border-secondary) !important; }
|
|
/* Gray variants (CategoryManager, BackupPanel) */
|
|
html.dark .bg-gray-50 { background-color: var(--bg-secondary) !important; }
|
|
html.dark .bg-gray-100 { background-color: var(--bg-secondary) !important; }
|
|
html.dark .border-gray-200, html.dark .border-gray-300 { border-color: var(--border-primary) !important; }
|
|
html.dark .border-gray-100 { border-color: var(--border-secondary) !important; }
|
|
html.dark .text-gray-900 { color: var(--text-primary) !important; }
|
|
html.dark .text-gray-700 { color: var(--text-secondary) !important; }
|
|
html.dark .text-gray-600 { color: var(--text-muted) !important; }
|
|
html.dark .text-gray-500 { color: var(--text-muted) !important; }
|
|
html.dark .text-gray-400 { color: var(--text-faint) !important; }
|
|
html.dark .text-gray-300 { color: var(--text-faint) !important; }
|
|
html.dark .hover\:bg-gray-50:hover, html.dark .hover\:bg-gray-200:hover { background-color: var(--bg-hover) !important; }
|
|
html.dark .hover\:border-gray-200:hover, html.dark .hover\:border-gray-400:hover { border-color: var(--border-primary) !important; }
|
|
html.dark input.bg-white, html.dark input[class*="bg-white"] { background: var(--bg-secondary) !important; color: var(--text-primary) !important; }
|
|
html.dark .bg-gray-200 { background-color: var(--border-primary) !important; }
|
|
html.dark .border-gray-300.border-t-slate-600 { border-color: var(--border-primary) !important; border-top-color: var(--text-primary) !important; }
|
|
/* Modal buttons */
|
|
html.dark button[class*="text-slate-600"][class*="border-slate-200"] { color: var(--text-muted) !important; border-color: var(--border-primary) !important; }
|
|
html.dark button[class*="text-slate-600"][class*="border-slate-200"]:hover { background: var(--bg-hover) !important; }
|
|
/* Dashed borders */
|
|
html.dark .border-dashed.border-gray-300 { border-color: var(--border-primary) !important; }
|
|
html.dark .bg-slate-50\/60, html.dark [class*="bg-slate-50/"] { background-color: transparent !important; }
|
|
|
|
/* Reorder buttons: desktop = original style; touch = always visible, larger touch targets.
|
|
Keyed on pointer as well as width — a tablet is a wide touch device, and drag reorder is
|
|
off wherever the pointer is coarse (#1432), so these buttons are the only affordance left. */
|
|
.reorder-buttons {
|
|
flex-direction: column;
|
|
opacity: 0;
|
|
/* Hidden means non-interactive. Opacity alone still hit-tests, which left an invisible
|
|
but fully clickable target on every row that silently reordered the itinerary. */
|
|
pointer-events: none;
|
|
}
|
|
.reorder-buttons button {
|
|
background: none !important;
|
|
padding: 1px 2px;
|
|
}
|
|
/* Reveal on row hover (and on keyboard focus, so the buttons are reachable by Tab). */
|
|
.dp-row:hover .reorder-buttons,
|
|
.dp-row:focus-within .reorder-buttons {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
@media (max-width: 767px), (pointer: coarse) {
|
|
.reorder-buttons {
|
|
flex-direction: row !important;
|
|
opacity: 1 !important;
|
|
/* There is no hover on touch, so these are always live — undo the base rule. */
|
|
pointer-events: auto !important;
|
|
align-items: center;
|
|
margin-left: auto;
|
|
}
|
|
.reorder-buttons button {
|
|
background: var(--bg-tertiary) !important;
|
|
border-radius: 6px;
|
|
width: 32px;
|
|
height: 32px;
|
|
padding: 0 !important;
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
justify-content: center !important;
|
|
}
|
|
.note-edit-buttons {
|
|
opacity: 1 !important;
|
|
}
|
|
}
|
|
|
|
/* Ladeanimation */
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* ── Press-Feedback + bessere Easings (Emil Kowalski) ─────────── */
|
|
/* Buttons sollen antworten wenn sie gedrückt werden. */
|
|
button:not(:disabled):not([data-no-press]),
|
|
[role="button"]:not([aria-disabled="true"]):not([data-no-press]) {
|
|
transition-property: transform, color, background-color, border-color, box-shadow, opacity, filter !important;
|
|
transition-duration: 180ms;
|
|
transition-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
|
|
}
|
|
button:not(:disabled):not([data-no-press]):active,
|
|
[role="button"]:not([aria-disabled="true"]):not([data-no-press]):active {
|
|
transform: scale(0.97);
|
|
transition-duration: 80ms;
|
|
}
|
|
|
|
/* Tailwind-Default-Easing durch ease-out-quint ersetzen.
|
|
Eingebaute CSS-Easings sind kraftlos; ease-out-quint hat Punch. */
|
|
.transition,
|
|
.transition-all,
|
|
.transition-colors,
|
|
.transition-opacity,
|
|
.transition-transform,
|
|
.transition-shadow {
|
|
transition-timing-function: cubic-bezier(0.23, 1, 0.32, 1);
|
|
}
|
|
|
|
/* Input-Focus transitions — border + ring faden weich ein */
|
|
input, textarea, select {
|
|
transition: border-color 150ms cubic-bezier(0.23, 1, 0.32, 1),
|
|
box-shadow 150ms cubic-bezier(0.23, 1, 0.32, 1),
|
|
background-color 150ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
}
|
|
|
|
/* Back-Button Icon-Slide on hover */
|
|
.trek-back-btn .trek-back-icon {
|
|
transition: transform 200ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
}
|
|
.trek-back-btn:hover .trek-back-icon {
|
|
transform: translateX(-2px);
|
|
}
|
|
|
|
/* Global focus-visible ring — konsistent überall */
|
|
:focus-visible {
|
|
outline: 2px solid var(--accent);
|
|
outline-offset: 2px;
|
|
border-radius: 4px;
|
|
}
|
|
button:focus-visible, [role="button"]:focus-visible, a:focus-visible {
|
|
outline-offset: 3px;
|
|
}
|
|
input:focus-visible, textarea:focus-visible, select:focus-visible {
|
|
outline: none;
|
|
}
|
|
|
|
/* Theme crossfade — beim Dark/Light switch, Hauptflächen + Text faden ihre Farben.
|
|
Sparingly: nur background-color und color bekommen eine Transition. */
|
|
html.trek-theme-transitioning,
|
|
html.trek-theme-transitioning body,
|
|
html.trek-theme-transitioning *:not(img):not(video):not(canvas):not([class*="trek-skeleton"]):not(.leaflet-layer) {
|
|
transition:
|
|
background-color 320ms cubic-bezier(0.23, 1, 0.32, 1),
|
|
color 320ms cubic-bezier(0.23, 1, 0.32, 1),
|
|
border-color 320ms cubic-bezier(0.23, 1, 0.32, 1),
|
|
fill 320ms cubic-bezier(0.23, 1, 0.32, 1) !important;
|
|
}
|
|
|
|
/* Touch-Geräte: iOS-Tap-Highlight weg (wir haben eigenes Press-Feedback) */
|
|
@media (hover: none) {
|
|
button, [role="button"], a {
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
}
|
|
html, body {
|
|
-webkit-tap-highlight-color: transparent;
|
|
}
|
|
|
|
/* Tabular-nums global für Time/Date/Currency/Counter */
|
|
time, .tabular-nums, [data-tabular],
|
|
input[type="number"], input[type="time"], input[type="date"], input[type="datetime-local"] {
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
/* Wenn Element explizit ease-in-out nutzt (z.B. Accordions), nicht überschreiben.
|
|
Tailwind setzt ease-in-out via eigener Klasse — die gewinnt durch letzte Deklaration. */
|
|
|
|
/* Press-Scale für clickbare Divs (Cards, Tiles) — sanfter als Buttons */
|
|
[data-press]:active {
|
|
transform: scale(0.985);
|
|
transition-duration: 80ms;
|
|
}
|
|
|
|
/* ── Popover/Dropdown Enter-Animationen ─────────────────────────
|
|
Emil: Popovers sollen von ihrem Trigger aus scalen, nicht vom Center.
|
|
Start bei scale(0.95) — nichts in der echten Welt poppt aus dem Nichts. */
|
|
@keyframes trek-menu-enter {
|
|
from { opacity: 0; transform: scale(0.95) translateY(-4px); }
|
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
|
}
|
|
@keyframes trek-popover-enter {
|
|
from { opacity: 0; transform: scale(0.96); }
|
|
to { opacity: 1; transform: scale(1); }
|
|
}
|
|
@keyframes trek-modal-enter {
|
|
from { opacity: 0; transform: scale(0.97); }
|
|
to { opacity: 1; transform: scale(1); }
|
|
}
|
|
@keyframes trek-backdrop-enter {
|
|
from { opacity: 0; }
|
|
to { opacity: 1; }
|
|
}
|
|
@keyframes trek-toast-enter {
|
|
from { opacity: 0; transform: translateY(8px) scale(0.96); }
|
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
}
|
|
@keyframes trek-progress-fill {
|
|
from { width: 0%; }
|
|
to { width: var(--trek-progress-to, 0%); }
|
|
}
|
|
|
|
/* Pie-Chart Reveal — rotate + fade-in, gibt dem Kreisdiagramm ein "Draw"-Gefühl */
|
|
@keyframes trek-pie-reveal {
|
|
from { opacity: 0; transform: rotate(-90deg) scale(0.85); }
|
|
to { opacity: 1; transform: rotate(0deg) scale(1); }
|
|
}
|
|
.trek-pie-reveal {
|
|
animation: trek-pie-reveal 900ms cubic-bezier(0.23, 1, 0.32, 1) both;
|
|
transform-origin: center;
|
|
will-change: transform, opacity;
|
|
}
|
|
|
|
/* Bar-Chart Reveal — horizontaler Fill von links */
|
|
@keyframes trek-bar-fill {
|
|
from { transform: scaleX(0); }
|
|
to { transform: scaleX(1); }
|
|
}
|
|
.trek-bar-fill {
|
|
animation: trek-bar-fill 700ms cubic-bezier(0.23, 1, 0.32, 1) both;
|
|
transform-origin: left center;
|
|
will-change: transform;
|
|
}
|
|
|
|
/* Page-Transition — subtiler Fade-Up beim Mount */
|
|
@keyframes trek-page-enter {
|
|
from { opacity: 0; transform: translateY(6px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
.trek-page-enter {
|
|
animation: trek-page-enter 220ms cubic-bezier(0.23, 1, 0.32, 1) both;
|
|
}
|
|
|
|
/* Skeleton shimmer — ein fließender Gradient-Strip überquert den Platzhalter */
|
|
@keyframes trek-shimmer {
|
|
from { background-position: -200% 0; }
|
|
to { background-position: 200% 0; }
|
|
}
|
|
.trek-skeleton {
|
|
background: linear-gradient(
|
|
90deg,
|
|
var(--bg-tertiary) 0%,
|
|
var(--bg-hover) 50%,
|
|
var(--bg-tertiary) 100%
|
|
);
|
|
background-size: 200% 100%;
|
|
animation: trek-shimmer 1.6s linear infinite;
|
|
border-radius: 8px;
|
|
color: transparent;
|
|
user-select: none;
|
|
}
|
|
.dark .trek-skeleton {
|
|
background: linear-gradient(
|
|
90deg,
|
|
rgba(255,255,255,0.04) 0%,
|
|
rgba(255,255,255,0.08) 50%,
|
|
rgba(255,255,255,0.04) 100%
|
|
);
|
|
background-size: 200% 100%;
|
|
}
|
|
.trek-menu-enter {
|
|
animation: trek-menu-enter 200ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
transform-origin: top right;
|
|
will-change: transform, opacity;
|
|
}
|
|
.trek-menu-enter-left {
|
|
animation: trek-menu-enter 200ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
transform-origin: top left;
|
|
will-change: transform, opacity;
|
|
}
|
|
.trek-popover-enter {
|
|
animation: trek-popover-enter 180ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
will-change: transform, opacity;
|
|
}
|
|
.trek-modal-enter {
|
|
animation: trek-modal-enter 220ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
will-change: transform, opacity;
|
|
}
|
|
|
|
/* Mobile-Drawer-Feel — Modal slidet von unten rein, wird unten am Screen angedockt */
|
|
@keyframes trek-drawer-enter {
|
|
from { opacity: 0; transform: translateY(100%); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
@media (max-width: 639px) {
|
|
.trek-modal-enter {
|
|
animation: trek-drawer-enter 320ms cubic-bezier(0.32, 0.72, 0, 1);
|
|
border-bottom-left-radius: 0 !important;
|
|
border-bottom-right-radius: 0 !important;
|
|
margin-top: auto !important;
|
|
align-self: flex-end;
|
|
}
|
|
}
|
|
.trek-backdrop-enter {
|
|
animation: trek-backdrop-enter 180ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
}
|
|
.trek-toast-enter {
|
|
animation: trek-toast-enter 260ms cubic-bezier(0.23, 1, 0.32, 1);
|
|
will-change: transform, opacity;
|
|
}
|
|
|
|
/* Stagger-Helpers für Listen — Enter-Animation mit Offset */
|
|
@keyframes trek-fade-up {
|
|
from { opacity: 0; transform: translateY(6px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
.trek-stagger > * {
|
|
animation: trek-fade-up 280ms cubic-bezier(0.23, 1, 0.32, 1) both;
|
|
}
|
|
.trek-stagger > *:nth-child(1) { animation-delay: 0ms; }
|
|
.trek-stagger > *:nth-child(2) { animation-delay: 40ms; }
|
|
.trek-stagger > *:nth-child(3) { animation-delay: 80ms; }
|
|
.trek-stagger > *:nth-child(4) { animation-delay: 120ms; }
|
|
.trek-stagger > *:nth-child(5) { animation-delay: 160ms; }
|
|
.trek-stagger > *:nth-child(6) { animation-delay: 200ms; }
|
|
.trek-stagger > *:nth-child(7) { animation-delay: 240ms; }
|
|
.trek-stagger > *:nth-child(8) { animation-delay: 280ms; }
|
|
.trek-stagger > *:nth-child(n+9) { animation-delay: 320ms; }
|
|
|
|
/* Reduced motion — Emil's Accessibility-Regel: fewer and gentler, not zero */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.trek-menu-enter, .trek-menu-enter-left, .trek-popover-enter,
|
|
.trek-modal-enter, .trek-toast-enter, .trek-stagger > * {
|
|
animation: trek-backdrop-enter 120ms ease-out;
|
|
}
|
|
.trek-skeleton {
|
|
animation: none;
|
|
background: var(--bg-tertiary);
|
|
}
|
|
button:not(:disabled):not([data-no-press]):active,
|
|
[role="button"]:not([aria-disabled="true"]):not([data-no-press]):active,
|
|
[data-press]:active {
|
|
transform: none;
|
|
}
|
|
/* Parallax & lift disablen */
|
|
.group:hover img,
|
|
.group:hover .cover-img { transform: none !important; }
|
|
*:hover { translate: none !important; }
|
|
}
|
|
|
|
/* ── Design tokens ─────────────────────────────── */
|
|
:root {
|
|
/* Easing curves — stärker als die CSS-Defaults, siehe easing.dev */
|
|
--ease-out-quint: cubic-bezier(0.23, 1, 0.32, 1);
|
|
--ease-in-out-quint: cubic-bezier(0.77, 0, 0.175, 1);
|
|
--ease-drawer: cubic-bezier(0.32, 0.72, 0, 1);
|
|
|
|
--safe-top: env(safe-area-inset-top, 0px);
|
|
--nav-h: 0px;
|
|
--bottom-nav-h: 0px;
|
|
--font-system: 'Poppins', -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Segoe UI', system-ui, sans-serif;
|
|
/* Secondary "subtext"/caption tier renders in Geist; primary text + headings stay Poppins. */
|
|
--font-subtext: 'Geist Sans', 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
|
--sp-1: 4px;
|
|
--sp-2: 8px;
|
|
--sp-3: 12px;
|
|
--sp-4: 16px;
|
|
--sp-6: 24px;
|
|
--radius-sm: 8px;
|
|
--radius-md: 12px;
|
|
--radius-lg: 16px;
|
|
--radius-xl: 20px;
|
|
--shadow-card: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);
|
|
--shadow-elevated: 0 4px 16px rgba(0,0,0,0.1);
|
|
|
|
/* Theme colors */
|
|
--bg-primary: #ffffff;
|
|
--bg-secondary: #f8fafc;
|
|
--bg-tertiary: #f1f5f9;
|
|
--bg-elevated: rgba(250,250,250,0.82);
|
|
--bg-card: #ffffff;
|
|
--bg-input: #ffffff;
|
|
--bg-hover: rgba(0,0,0,0.03);
|
|
--bg-selected: #e2e8f0;
|
|
--text-primary: #111827;
|
|
--text-secondary: #374151;
|
|
--text-muted: #6b7280;
|
|
--text-faint: #9ca3af;
|
|
--border-primary: #e5e7eb;
|
|
--border-secondary: #f3f4f6;
|
|
--border-faint: rgba(0,0,0,0.06);
|
|
--accent: #111827;
|
|
--accent-text: #ffffff;
|
|
--sidebar-bg: rgba(250,250,250,0.82);
|
|
--sidebar-shadow: 0 4px 32px rgba(0,0,0,0.10), 0 0 0 1px rgba(0,0,0,0.06);
|
|
--tooltip-bg: rgba(255,255,255,0.96);
|
|
--scrollbar-track: #f1f5f9;
|
|
--scrollbar-thumb: #d1d5db;
|
|
--scrollbar-hover: #9ca3af;
|
|
|
|
/* ── Extended semantic tokens (appearance platform) ──────────────
|
|
Accent variants + status/shadow/overlay/inverse + per-tier type scales.
|
|
Additive: nothing consumes these until components migrate to them, so
|
|
defining them is visually inert. Colored schemes override --accent* via
|
|
[data-scheme]; status/shadow/inverse stay scheme-independent. */
|
|
--accent-on: var(--accent); /* accent as text/icon/border on a surface */
|
|
--accent-hover: #1f2937; /* hover state of an accent fill */
|
|
--accent-subtle: #f1f5f9; /* faint accent-tinted surface (chips, selected) */
|
|
|
|
--fs-scale-title: 1;
|
|
--fs-scale-subtitle: 1;
|
|
--fs-scale-body: 1;
|
|
--fs-scale-caption: 1;
|
|
|
|
--success: #16a34a; --success-soft: #dcfce7;
|
|
--danger: #dc2626; --danger-soft: #fef2f2;
|
|
--warning: #d97706; --warning-soft: #fffbeb;
|
|
--info: #2563eb; --info-soft: #eff6ff;
|
|
|
|
--bg-inverse: #111827; --text-inverse: #ffffff;
|
|
--overlay: rgba(0, 0, 0, 0.5);
|
|
|
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.12);
|
|
--shadow-modal: 0 16px 48px rgba(0, 0, 0, 0.20);
|
|
--shadow-dropdown: 0 8px 24px rgba(0, 0, 0, 0.12);
|
|
--shadow-popover: 0 8px 32px rgba(0, 0, 0, 0.18);
|
|
|
|
/* Journey design tokens */
|
|
--journal-bg: #FAFAFA;
|
|
--journal-card: #FFFFFF;
|
|
--journal-border: #E4E4E7;
|
|
--journal-accent: #6366F1;
|
|
--journal-text: #09090B;
|
|
--journal-muted: #71717A;
|
|
--journal-faint: #A1A1AA;
|
|
--mood-amazing: #E8654A;
|
|
--mood-good: #EF9F27;
|
|
--mood-neutral: #94928C;
|
|
--mood-tired: #6B9BD2;
|
|
--mood-rough: #9B8EC4;
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
:root { --bottom-nav-h: calc(84px + env(safe-area-inset-bottom, 0px)); }
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
:root { --nav-h: calc(64px + env(safe-area-inset-top, 0px)); }
|
|
}
|
|
|
|
.dark {
|
|
--bg-primary: #121215;
|
|
--bg-secondary: #1a1a1e;
|
|
--bg-tertiary: #1c1c21;
|
|
--bg-elevated: rgba(19,19,22,0.82);
|
|
--bg-card: #131316;
|
|
--bg-input: #1c1c21;
|
|
--bg-hover: rgba(255,255,255,0.06);
|
|
--bg-selected: rgba(255,255,255,0.1);
|
|
--text-primary: #f4f4f5;
|
|
--text-secondary: #d4d4d8;
|
|
--text-muted: #a1a1aa;
|
|
--text-faint: #71717a;
|
|
--border-primary: #27272a;
|
|
--border-secondary: #1c1c21;
|
|
--border-faint: rgba(255,255,255,0.07);
|
|
--accent: #e4e4e7;
|
|
--accent-text: #09090b;
|
|
--sidebar-bg: rgba(19,19,22,0.82);
|
|
--sidebar-shadow: 0 4px 32px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.05);
|
|
--tooltip-bg: rgba(19,19,22,0.96);
|
|
--scrollbar-track: #131316;
|
|
--scrollbar-thumb: #3f3f46;
|
|
--scrollbar-hover: #52525b;
|
|
|
|
/* Extended semantic tokens (dark) */
|
|
--accent-on: var(--accent);
|
|
--accent-hover: #d4d4d8;
|
|
--accent-subtle: rgba(255, 255, 255, 0.08);
|
|
|
|
--success: #22c55e; --success-soft: rgba(34, 197, 94, 0.15);
|
|
--danger: #ef4444; --danger-soft: rgba(239, 68, 68, 0.15);
|
|
--warning: #f59e0b; --warning-soft: rgba(245, 158, 11, 0.15);
|
|
--info: #3b82f6; --info-soft: rgba(59, 130, 246, 0.15);
|
|
|
|
--bg-inverse: #e4e4e7; --text-inverse: #09090b;
|
|
--overlay: rgba(0, 0, 0, 0.6);
|
|
|
|
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
--shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4);
|
|
--shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.5);
|
|
--shadow-modal: 0 16px 48px rgba(0, 0, 0, 0.6);
|
|
--shadow-dropdown: 0 8px 24px rgba(0, 0, 0, 0.5);
|
|
--shadow-popover: 0 8px 32px rgba(0, 0, 0, 0.55);
|
|
|
|
/* Journey design tokens (dark) */
|
|
--journal-bg: #09090B;
|
|
--journal-card: #18181B;
|
|
--journal-border: #27272A;
|
|
--journal-accent: #818CF8;
|
|
--journal-text: #FAFAFA;
|
|
--journal-muted: #A1A1AA;
|
|
--journal-faint: #52525B;
|
|
--mood-amazing: #f28a6e;
|
|
--mood-good: #f5b84d;
|
|
--mood-neutral: #9a9a94;
|
|
--mood-tired: #6db3f0;
|
|
--mood-rough: #a9a3f0;
|
|
}
|
|
|
|
/* ── Color schemes (data-scheme) ──────────────────────────────────
|
|
Each non-default scheme overrides the accent family (High-Contrast also
|
|
raises neutral text/border contrast and solidifies surfaces). Light and dark
|
|
are tuned independently. The 'default' scheme sets NO attribute, so the
|
|
monochrome accent above is untouched — existing users see no change. */
|
|
|
|
:root[data-scheme="indigo"] {
|
|
--accent: #4f46e5; --accent-text: #ffffff; --accent-on: #4338ca;
|
|
--accent-hover: #4338ca; --accent-subtle: #eef2ff;
|
|
}
|
|
.dark[data-scheme="indigo"] {
|
|
--accent: #6366f1; --accent-text: #ffffff; --accent-on: #a5b4fc;
|
|
--accent-hover: #4f46e5; --accent-subtle: rgba(99, 102, 241, 0.18);
|
|
}
|
|
|
|
:root[data-scheme="teal"] {
|
|
--accent: #0d9488; --accent-text: #ffffff; --accent-on: #0f766e;
|
|
--accent-hover: #0f766e; --accent-subtle: #f0fdfa;
|
|
}
|
|
.dark[data-scheme="teal"] {
|
|
--accent: #14b8a6; --accent-text: #04211f; --accent-on: #5eead4;
|
|
--accent-hover: #0d9488; --accent-subtle: rgba(20, 184, 166, 0.18);
|
|
}
|
|
|
|
:root[data-scheme="rose"] {
|
|
--accent: #e11d48; --accent-text: #ffffff; --accent-on: #be123c;
|
|
--accent-hover: #be123c; --accent-subtle: #fff1f2;
|
|
}
|
|
.dark[data-scheme="rose"] {
|
|
--accent: #f43f5e; --accent-text: #ffffff; --accent-on: #fda4af;
|
|
--accent-hover: #e11d48; --accent-subtle: rgba(244, 63, 94, 0.18);
|
|
}
|
|
|
|
:root[data-scheme="amber"] {
|
|
--accent: #d97706; --accent-text: #ffffff; --accent-on: #b45309;
|
|
--accent-hover: #b45309; --accent-subtle: #fffbeb;
|
|
}
|
|
.dark[data-scheme="amber"] {
|
|
--accent: #f59e0b; --accent-text: #1c1917; --accent-on: #fcd34d;
|
|
--accent-hover: #d97706; --accent-subtle: rgba(245, 158, 11, 0.18);
|
|
}
|
|
|
|
:root[data-scheme="violet"] {
|
|
--accent: #7c3aed; --accent-text: #ffffff; --accent-on: #6d28d9;
|
|
--accent-hover: #6d28d9; --accent-subtle: #f5f3ff;
|
|
}
|
|
.dark[data-scheme="violet"] {
|
|
--accent: #8b5cf6; --accent-text: #ffffff; --accent-on: #c4b5fd;
|
|
--accent-hover: #7c3aed; --accent-subtle: rgba(139, 92, 246, 0.18);
|
|
}
|
|
|
|
/* High-Contrast — raises neutral text/border contrast and solidifies surfaces,
|
|
directly answering #951 / #1025 (grey-on-grey, transparency hurting legibility). */
|
|
:root[data-scheme="highContrast"] {
|
|
--accent: #1d4ed8; --accent-text: #ffffff; --accent-on: #1d4ed8;
|
|
--accent-hover: #1e40af; --accent-subtle: #dbeafe;
|
|
--text-primary: #000000; --text-secondary: #1f2937;
|
|
--text-muted: #374151; --text-faint: #4b5563;
|
|
--border-primary: #475569; --border-secondary: #94a3b8;
|
|
--border-faint: rgba(0, 0, 0, 0.35);
|
|
--bg-elevated: #ffffff; --bg-hover: #e2e8f0; --bg-selected: #cbd5e1;
|
|
--sidebar-bg: #ffffff; --tooltip-bg: #ffffff;
|
|
}
|
|
.dark[data-scheme="highContrast"] {
|
|
--accent: #60a5fa; --accent-text: #06182f; --accent-on: #93c5fd;
|
|
--accent-hover: #3b82f6; --accent-subtle: rgba(96, 165, 250, 0.2);
|
|
--text-primary: #ffffff; --text-secondary: #f1f5f9;
|
|
--text-muted: #e2e8f0; --text-faint: #cbd5e1;
|
|
--border-primary: #64748b; --border-secondary: #475569;
|
|
--border-faint: rgba(255, 255, 255, 0.35);
|
|
--bg-elevated: #131316; --bg-hover: #2a2a30; --bg-selected: #3a3a42;
|
|
--sidebar-bg: #131316; --tooltip-bg: #131316;
|
|
}
|
|
|
|
/* Custom — the user's picked accent (applyAppearance writes the inline vars;
|
|
color-mix derives hover/subtle so a single picked color yields a full set). */
|
|
:root[data-scheme="custom"] {
|
|
--accent: var(--accent-custom-light, var(--accent));
|
|
--accent-text: var(--accent-custom-text-light, #ffffff);
|
|
--accent-on: var(--accent-custom-light, var(--accent));
|
|
--accent-hover: color-mix(in srgb, var(--accent-custom-light, #4f46e5) 86%, #000);
|
|
--accent-subtle: color-mix(in srgb, var(--accent-custom-light, #4f46e5) 12%, transparent);
|
|
}
|
|
.dark[data-scheme="custom"] {
|
|
--accent: var(--accent-custom-dark, var(--accent));
|
|
--accent-text: var(--accent-custom-text-dark, #ffffff);
|
|
--accent-on: var(--accent-custom-dark, var(--accent));
|
|
--accent-hover: color-mix(in srgb, var(--accent-custom-dark, #6366f1) 84%, #fff);
|
|
--accent-subtle: color-mix(in srgb, var(--accent-custom-dark, #6366f1) 20%, transparent);
|
|
}
|
|
|
|
/* ── Legacy accent bridge (gated) ─────────────────────────────────
|
|
Under a NON-default scheme ONLY, redirect the hardcoded "primary/black"
|
|
action surfaces (and raw indigo accents) to the chosen accent so un-migrated
|
|
components follow the scheme without editing ~50 files. These selectors carry
|
|
higher specificity than the legacy dark shim, so they win under a scheme; on
|
|
the default scheme nothing here matches (zero change for existing users).
|
|
TEMPORARY scaffold — the Phase-8 codemod migrates these to bg-accent and this
|
|
block shrinks file-by-file until it is deleted. */
|
|
html[data-scheme]:not([data-scheme="default"]) :is(.bg-slate-900, .bg-gray-900, .bg-zinc-900, .bg-neutral-900, .bg-black) {
|
|
background-color: var(--accent) !important;
|
|
color: var(--accent-text) !important;
|
|
}
|
|
html[data-scheme]:not([data-scheme="default"]) :is(.bg-slate-900, .bg-gray-900, .bg-zinc-900, .bg-neutral-900, .bg-black):hover {
|
|
background-color: var(--accent-hover) !important;
|
|
}
|
|
html[data-scheme]:not([data-scheme="default"])
|
|
:is(.hover\:bg-slate-900, .hover\:bg-slate-800, .hover\:bg-slate-700, .hover\:bg-gray-900, .hover\:bg-gray-800, .hover\:bg-black):hover {
|
|
background-color: var(--accent-hover) !important;
|
|
color: var(--accent-text) !important;
|
|
}
|
|
html[data-scheme]:not([data-scheme="default"]) :is(.bg-indigo-500, .bg-indigo-600, .bg-indigo-700) {
|
|
background-color: var(--accent) !important;
|
|
color: var(--accent-text) !important;
|
|
}
|
|
html[data-scheme]:not([data-scheme="default"]) :is(.text-indigo-400, .text-indigo-500, .text-indigo-600, .text-indigo-700) {
|
|
color: var(--accent-on) !important;
|
|
}
|
|
html[data-scheme]:not([data-scheme="default"])
|
|
:is(.border-indigo-500, .border-indigo-600, .ring-indigo-500, .ring-indigo-600) {
|
|
border-color: var(--accent) !important;
|
|
}
|
|
|
|
/* ── Transparency off (data-no-transparency) ──────────────────────
|
|
Solidify the alpha-bearing surface tokens, flatten the dashboard liquid-glass,
|
|
and disable backdrop blur app-wide so content over the map is fully opaque
|
|
(#1025). Inert unless the attribute is set. Intentional dimming scrims (modal
|
|
backdrops) stay translucent on purpose — forcing them opaque blacks out the
|
|
screen. */
|
|
html[data-no-transparency] {
|
|
--bg-elevated: #fafafa;
|
|
--bg-hover: #f1f3f5;
|
|
--bg-selected: #e2e8f0;
|
|
--border-faint: #e5e7eb;
|
|
--sidebar-bg: #ffffff;
|
|
--tooltip-bg: #ffffff;
|
|
}
|
|
html.dark[data-no-transparency] {
|
|
--bg-elevated: #131316;
|
|
--bg-hover: #26262c;
|
|
--bg-selected: #2f2f37;
|
|
--border-faint: #27272a;
|
|
--sidebar-bg: #131316;
|
|
--tooltip-bg: #131316;
|
|
}
|
|
html[data-no-transparency] .trek-dash {
|
|
--glass-bg: var(--surface, var(--bg-card));
|
|
--glass-border: var(--line, var(--border-primary));
|
|
--glass-blur: none;
|
|
--glass-highlight: none;
|
|
}
|
|
/* The boarding-pass hero ticket uses its own translucent gradient (not the glass
|
|
tokens) — force it solid. */
|
|
html[data-no-transparency] .trek-dash .hero-pass {
|
|
background: var(--surface, var(--bg-card)) !important;
|
|
}
|
|
html[data-no-transparency] .trek-dash .hero-pass::before { display: none !important; }
|
|
html[data-no-transparency] *,
|
|
html[data-no-transparency] *::before,
|
|
html[data-no-transparency] *::after {
|
|
backdrop-filter: none !important;
|
|
-webkit-backdrop-filter: none !important;
|
|
}
|
|
/* Class-based translucent surfaces (Atlas tooltip, Leaflet controls, GL popups)
|
|
forced solid. Inline rgba() is handled per-component (it can't be reached by CSS). */
|
|
html[data-no-transparency] .atlas-tooltip {
|
|
background: var(--bg-card) !important;
|
|
color: var(--text-primary) !important;
|
|
border-color: var(--border-primary) !important;
|
|
}
|
|
html[data-no-transparency] .atlas-tooltip::before { border-top-color: var(--bg-card) !important; }
|
|
html[data-no-transparency] .leaflet-control-zoom a { background: var(--bg-card) !important; }
|
|
html[data-no-transparency] .map-tooltip,
|
|
html[data-no-transparency] .leaflet-tooltip.map-tooltip { background: var(--bg-card) !important; }
|
|
html[data-no-transparency] .trek-map-popup .mapboxgl-popup-content,
|
|
html[data-no-transparency] .trek-map-popup .maplibregl-popup-content { background: var(--bg-card) !important; }
|
|
|
|
/* ── Compact density ──────────────────────────────────────────────
|
|
Tightens the spacing scale; reach grows as components adopt the tokens. */
|
|
html[data-density="compact"] {
|
|
--sp-1: 3px;
|
|
--sp-2: 6px;
|
|
--sp-3: 9px;
|
|
--sp-4: 12px;
|
|
--sp-6: 18px;
|
|
}
|
|
html[data-density="compact"] .form-input { padding: 5px 11px; }
|
|
|
|
body {
|
|
font-family: var(--font-system);
|
|
background-color: var(--bg-primary);
|
|
color: var(--text-primary);
|
|
transition: background-color 0.2s, color 0.2s;
|
|
}
|
|
|
|
/* Subtext tier in Geist. The faint text token is TREK's caption/secondary tier;
|
|
a direct rule on the element beats the Poppins inherited from wrapper styles,
|
|
giving the design's "Geist text · Poppins numbers" hierarchy. */
|
|
.text-content-faint { font-family: var(--font-subtext); }
|
|
|
|
/* ── Marker cluster custom styling ────────────── */
|
|
.marker-cluster-wrapper {
|
|
background: transparent !important;
|
|
border: none !important;
|
|
}
|
|
|
|
.marker-cluster-custom {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border-radius: 50%;
|
|
background: #111827;
|
|
border: 2.5px solid rgba(255, 255, 255, 0.9);
|
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.25), 0 0 0 2px rgba(17, 24, 39, 0.15);
|
|
cursor: pointer;
|
|
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
|
}
|
|
|
|
.marker-cluster-custom:hover {
|
|
transform: scale(1.1);
|
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35), 0 0 0 3px rgba(17, 24, 39, 0.2);
|
|
}
|
|
|
|
.marker-cluster-custom span {
|
|
font-family:var(--font-system);
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
color: #ffffff;
|
|
line-height: 1;
|
|
}
|
|
|
|
/* Hide default markercluster styles we don't need */
|
|
.marker-cluster-small,
|
|
.marker-cluster-medium,
|
|
.marker-cluster-large {
|
|
background: transparent !important;
|
|
}
|
|
.marker-cluster-small div,
|
|
.marker-cluster-medium div,
|
|
.marker-cluster-large div {
|
|
background: transparent !important;
|
|
}
|
|
|
|
/* ── Leaflet z-index ───────────────────────────── */
|
|
.leaflet-container {
|
|
z-index: 0;
|
|
}
|
|
.leaflet-pane {
|
|
z-index: 0 !important;
|
|
}
|
|
.leaflet-top, .leaflet-bottom {
|
|
z-index: 1 !important;
|
|
}
|
|
|
|
/* ── iOS-style map tooltip ─────────────────────── */
|
|
.leaflet-tooltip.map-tooltip {
|
|
background: var(--tooltip-bg);
|
|
backdrop-filter: blur(8px);
|
|
-webkit-backdrop-filter: blur(8px);
|
|
border: none;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
|
|
padding: 5px 10px;
|
|
font-family: var(--font-system);
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
pointer-events: none;
|
|
color: var(--text-primary);
|
|
}
|
|
.leaflet-tooltip.map-tooltip::before,
|
|
.leaflet-tooltip-left.map-tooltip::before,
|
|
.leaflet-tooltip-top.map-tooltip::before {
|
|
display: none;
|
|
}
|
|
|
|
/* Scrollbars — styled on desktop, hidden on mobile */
|
|
::-webkit-scrollbar {
|
|
width: 6px;
|
|
height: 6px;
|
|
}
|
|
::-webkit-scrollbar-button {
|
|
display: none;
|
|
height: 0;
|
|
width: 0;
|
|
}
|
|
::-webkit-scrollbar-track {
|
|
background: var(--scrollbar-track);
|
|
border-radius: 3px;
|
|
}
|
|
::-webkit-scrollbar-thumb {
|
|
background: var(--scrollbar-thumb);
|
|
border-radius: 3px;
|
|
}
|
|
::-webkit-scrollbar-thumb:hover {
|
|
background: var(--scrollbar-hover);
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
* { scrollbar-width: none; }
|
|
::-webkit-scrollbar { width: 0; height: 0; }
|
|
}
|
|
|
|
.route-info-pill { background: none !important; border: none !important; box-shadow: none !important; width: auto !important; height: auto !important; margin: 0 !important; }
|
|
.chat-scroll { overflow-y: auto !important; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
|
|
.chat-scroll::-webkit-scrollbar { width: 0; background: transparent; }
|
|
|
|
|
|
/* Einheitliche Formular-Inputs */
|
|
.form-input {
|
|
width: 100%;
|
|
border: 1px solid var(--border-primary);
|
|
border-radius: 10px;
|
|
padding: 8px 14px;
|
|
font-size: 13px;
|
|
font-family: inherit;
|
|
outline: none;
|
|
box-sizing: border-box;
|
|
color: var(--text-primary);
|
|
background: var(--bg-input);
|
|
transition: border-color 0.15s;
|
|
}
|
|
.form-input:focus {
|
|
border-color: var(--text-faint);
|
|
}
|
|
.form-input::placeholder {
|
|
color: var(--text-faint);
|
|
}
|
|
|
|
/* Brand images: no save/copy/drag */
|
|
img[alt="TREK"] {
|
|
pointer-events: none;
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
-webkit-user-drag: none;
|
|
-webkit-touch-callout: none;
|
|
}
|
|
|
|
/* Weiche Übergänge */
|
|
.transition-smooth {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
|
|
|
|
/* Tagesliste */
|
|
.day-list-item {
|
|
transition: background-color 0.15s ease;
|
|
}
|
|
|
|
.day-list-item:hover {
|
|
background-color: var(--bg-secondary);
|
|
}
|
|
|
|
.day-list-item.active {
|
|
background-color: #4f46e5;
|
|
color: white;
|
|
}
|
|
|
|
.scroll-container {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
|
|
}
|
|
|
|
/* ── Dark-Mode: Tailwind-Klassen ─────────────── */
|
|
.dark .bg-slate-50 { background-color: #131316 !important; }
|
|
.dark .bg-slate-100 { background-color: #1c1c21 !important; }
|
|
.dark .bg-white { background-color: #09090b !important; }
|
|
.dark .bg-gray-50 { background-color: #131316 !important; }
|
|
.dark .bg-gray-100 { background-color: #1c1c21 !important; }
|
|
.dark .text-slate-900, .dark .text-gray-900, .dark .text-slate-800 { color: #f4f4f5 !important; }
|
|
.dark .text-slate-700, .dark .text-gray-700 { color: #d4d4d8 !important; }
|
|
.dark .text-slate-600, .dark .text-gray-600 { color: #a1a1aa !important; }
|
|
.dark .text-slate-500, .dark .text-gray-500 { color: #a1a1aa !important; }
|
|
.dark .text-slate-400, .dark .text-gray-400 { color: #71717a !important; }
|
|
.dark .border-slate-200, .dark .border-gray-200, .dark .border-slate-300 { border-color: #27272a !important; }
|
|
.dark .border-gray-100, .dark .border-slate-100 { border-color: #1c1c21 !important; }
|
|
.dark .hover\:bg-slate-50:hover, .dark .hover\:bg-gray-50:hover { background-color: #1c1c21 !important; }
|
|
.dark .bg-slate-50\/60 { background-color: transparent !important; }
|
|
.dark .divide-slate-100 > :not([hidden]) ~ :not([hidden]) { border-color: #27272a !important; }
|
|
.dark .bg-slate-100 { background-color: #27272a !important; }
|
|
.dark .bg-slate-900 { background-color: #e4e4e7 !important; color: #09090b !important; }
|
|
.dark .hover\:bg-slate-700:hover { background-color: #d4d4d8 !important; color: #09090b !important; }
|
|
.dark input, .dark textarea, .dark select {
|
|
background-color: #1c1c21;
|
|
color: #f4f4f5;
|
|
border-color: #27272a;
|
|
}
|
|
.dark input::placeholder, .dark textarea::placeholder { color: #71717a; }
|
|
.dark .bg-emerald-50 { background-color: rgba(16,185,129,0.15) !important; }
|
|
.dark .bg-red-50 { background-color: rgba(239,68,68,0.15) !important; }
|
|
|
|
/* Ladebildschirm */
|
|
.dark .min-h-screen { background-color: var(--bg-primary) !important; }
|
|
|
|
/* Modal-Hintergrund */
|
|
.dark .trek-modal-backdrop { background: rgba(0,0,0,0.6); }
|
|
|
|
/* ── Dark: Fallback für Komponenten die noch nicht auf CSS-Variablen umgestellt sind ── */
|
|
.dark {
|
|
color-scheme: dark;
|
|
}
|
|
|
|
|
|
/* Toast-Animationen */
|
|
@keyframes slideUp {
|
|
from { transform: translateY(100%); }
|
|
to { transform: translateY(0); }
|
|
}
|
|
|
|
@keyframes slide-in-right {
|
|
from { transform: translateX(100%); opacity: 0; }
|
|
to { transform: translateX(0); opacity: 1; }
|
|
}
|
|
|
|
@keyframes slide-out-right {
|
|
from { transform: translateX(0); opacity: 1; }
|
|
to { transform: translateX(100%); opacity: 0; }
|
|
}
|
|
|
|
.toast-enter {
|
|
animation: slide-in-right 0.3s ease-out forwards;
|
|
}
|
|
|
|
.toast-exit {
|
|
animation: slide-out-right 0.3s ease-in forwards;
|
|
}
|
|
|
|
/* Modal-Hintergrund (eigener Namespace, sonst blenden Content-Blocker .modal-backdrop aus) */
|
|
.trek-modal-backdrop {
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
|
|
/* Fortschrittsbalken */
|
|
@keyframes progress-fill {
|
|
from { width: 0; }
|
|
to { width: var(--progress); }
|
|
}
|
|
|
|
/* Anmeldeseiten-Gradient */
|
|
.auth-gradient {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}
|
|
|
|
/* Bildanzeige-Overlay */
|
|
.lightbox-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.95);
|
|
z-index: 1000;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
/* Markdown in Collab Notes */
|
|
.collab-note-md strong, .collab-note-md-full strong { font-weight: 700 !important; }
|
|
.collab-note-md em, .collab-note-md-full em { font-style: italic !important; }
|
|
.collab-note-md h1, .collab-note-md h2, .collab-note-md h3 { font-weight: 700 !important; margin: 0; }
|
|
.collab-note-md-full h1 { font-size: 1.3em !important; font-weight: 700 !important; margin: 0.8em 0 0.3em; }
|
|
.collab-note-md-full h2 { font-size: 1.15em !important; font-weight: 700 !important; margin: 0.8em 0 0.3em; }
|
|
.collab-note-md-full h3 { font-size: 1em !important; font-weight: 700 !important; margin: 0.8em 0 0.3em; }
|
|
.collab-note-md p, .collab-note-md-full p { margin: 0 0 0.3em; }
|
|
.collab-note-md ul, .collab-note-md-full ul { list-style-type: disc !important; padding-left: 1.4em !important; margin: 0.2em 0; }
|
|
.collab-note-md ol, .collab-note-md-full ol { list-style-type: decimal !important; padding-left: 1.4em !important; margin: 0.2em 0; }
|
|
.collab-note-md li, .collab-note-md-full li { display: list-item !important; margin: 0.1em 0; }
|
|
.collab-note-md code, .collab-note-md-full code { font-size: 0.9em; padding: 1px 5px; border-radius: 4px; background: var(--bg-secondary); }
|
|
.collab-note-md-full pre { padding: 10px 12px; border-radius: 8px; background: var(--bg-secondary); overflow-x: auto; margin: 0.5em 0; }
|
|
.collab-note-md-full pre code { padding: 0; background: none; }
|
|
.collab-note-md a, .collab-note-md-full a { color: #3b82f6; text-decoration: underline; word-break: break-all; }
|
|
.collab-note-md blockquote, .collab-note-md-full blockquote { border-left: 3px solid var(--border-primary); padding-left: 12px; margin: 0.5em 0; color: var(--text-muted); }
|
|
.collab-note-md-full table { border-collapse: collapse; width: 100%; margin: 0.5em 0; }
|
|
.collab-note-md-full th, .collab-note-md-full td { border: 1px solid var(--border-primary); padding: 4px 8px; font-size: 0.9em; }
|
|
.collab-note-md-full hr { border: none; border-top: 1px solid var(--border-primary); margin: 0.8em 0; }
|
|
|
|
/* Day-plan header action grid (edit / +transport / note / collapse) */
|
|
.dp-day-actions button {
|
|
color: var(--text-faint);
|
|
background: transparent;
|
|
transition: background-color 0.12s ease, color 0.12s ease;
|
|
}
|
|
.dp-day-actions button:hover {
|
|
background: var(--bg-hover);
|
|
color: var(--text-primary);
|
|
}
|
|
/* Reveal the action grid only when hovering the day row (pointer devices).
|
|
Touch devices (hover: none) keep it visible; the selected day stays visible too. */
|
|
@media (hover: hover) {
|
|
.dp-day-actions { opacity: 0; transition: opacity 0.12s ease; }
|
|
.dp-day-header:hover .dp-day-actions,
|
|
.dp-day-header[data-selected="true"] .dp-day-actions { opacity: 1; }
|
|
}
|
|
|
|
/* ── TREK range slider — thin filled track + soft round thumb ─────────────── */
|
|
.trek-range {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 100%;
|
|
height: 5px;
|
|
border-radius: 999px;
|
|
background: linear-gradient(to right, var(--accent) var(--fill, 50%), var(--border-faint) var(--fill, 50%));
|
|
outline: none;
|
|
cursor: pointer;
|
|
}
|
|
.trek-range::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
background: var(--bg-card, #fff);
|
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
|
transition: transform 0.12s ease;
|
|
}
|
|
.trek-range::-webkit-slider-thumb:hover { transform: scale(1.1); }
|
|
.trek-range::-webkit-slider-thumb:active { transform: scale(1.18); }
|
|
.trek-range::-moz-range-thumb {
|
|
width: 18px;
|
|
height: 18px;
|
|
border-radius: 50%;
|
|
background: var(--bg-card, #fff);
|
|
border: 1px solid rgba(0, 0, 0, 0.08);
|
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
|
}
|
|
.trek-range::-moz-range-track { height: 5px; border-radius: 999px; background: transparent; }
|