@rslsp1/fa-app-tools 6.1.0 → 7.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,9 +5,6 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __esm = (fn, res) => function __init() {
9
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
- };
11
8
  var __export = (target, all) => {
12
9
  for (var name in all)
13
10
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -30,7 +27,211 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
27
  ));
31
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
29
 
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ AvatarArchitectApp: () => AvatarArchitectApp,
34
+ CURRENT_TAG_EVENT_VERSION: () => CURRENT_TAG_EVENT_VERSION,
35
+ CollapsibleCard: () => CollapsibleCard,
36
+ CompactDropdown: () => CompactDropdown,
37
+ FaApp: () => FaApp,
38
+ FaToolsBadge: () => FaToolsBadge,
39
+ FlowWorkspacePicker: () => FlowWorkspacePicker,
40
+ GLOBAL_STYLES: () => GLOBAL_STYLES,
41
+ HfMediaSyncTab: () => HfMediaSyncTab,
42
+ HistoryPanel: () => HistoryPanel,
43
+ InspectPanel: () => InspectPanel,
44
+ LIBRARY_CUTOVER_MARKER: () => LIBRARY_CUTOVER_MARKER,
45
+ LIBRARY_SLICE_SCHEMA_VERSION: () => LIBRARY_SLICE_SCHEMA_VERSION,
46
+ LIB_VERSION: () => version,
47
+ LabBlend: () => LabBlend,
48
+ LabCompare: () => LabCompare,
49
+ LabFrameExtractor: () => LabFrameExtractor,
50
+ LabImagePicker: () => LabImagePicker,
51
+ LabLoop: () => LabLoop,
52
+ LabRemix: () => LabRemix,
53
+ LabsTab: () => LabsTab,
54
+ LibraryView: () => LibraryView,
55
+ ListView: () => ListView,
56
+ MediaLibrary: () => MediaLibrary,
57
+ PillButton: () => PillButton,
58
+ PromptTab: () => PromptTab,
59
+ SCOPE_MAP: () => SCOPE_MAP,
60
+ SectionLabel: () => SectionLabel,
61
+ ServerTab: () => ServerTab,
62
+ SetupPanel: () => SetupPanel,
63
+ TAGS_CUTOVER_MARKER: () => TAGS_CUTOVER_MARKER,
64
+ TAGS_SLICE_SCHEMA_VERSION: () => TAGS_SLICE_SCHEMA_VERSION,
65
+ TagBaseSection: () => TagBaseSection,
66
+ TagManager: () => TagManager,
67
+ TagManagerPanel: () => TagManagerPanel,
68
+ autoLabel: () => autoLabel,
69
+ buildBlendInstruction: () => buildBlendInstruction,
70
+ buildCompareInstruction: () => buildCompareInstruction,
71
+ buildFallbackPrompt: () => buildFallbackPrompt,
72
+ buildGenerationPrompt: () => buildGenerationPrompt,
73
+ buildImageGenerationOptions: () => buildImageGenerationOptions,
74
+ buildLoopInstruction: () => buildLoopInstruction,
75
+ buildPromptTabPayload: () => buildPromptTabPayload,
76
+ buildReferenceImageMediaIds: () => buildReferenceImageMediaIds,
77
+ buildRemixInstruction: () => buildRemixInstruction,
78
+ buildScanInstruction: () => buildScanInstruction,
79
+ cleanAiResponse: () => cleanAiResponse,
80
+ createFaStore: () => createFaStore,
81
+ createFlowServices: () => createFlowServices,
82
+ createInMemoryLibraryDataSource: () => createInMemoryLibraryDataSource,
83
+ createInMemoryWorkspacePersistence: () => createInMemoryWorkspacePersistence,
84
+ createLibraryDefault: () => createLibraryDefault,
85
+ createPersistenceCoordinator: () => createPersistenceCoordinator,
86
+ createTagCreatedEvent: () => createTagCreatedEvent,
87
+ createTagUpdatedEvent: () => createTagUpdatedEvent,
88
+ createTagsDefault: () => createTagsDefault,
89
+ deriveTagBaseWorkspaceTags: () => deriveTagBaseWorkspaceTags,
90
+ deriveTagViewModel: () => deriveTagViewModel,
91
+ formatTreeToMarkdown: () => formatTreeToMarkdown,
92
+ frameToGeneration: () => frameToGeneration,
93
+ getFormattedTimestamp: () => getFormattedTimestamp,
94
+ getHFToken: () => getHFToken,
95
+ getSessionClientId: () => getSessionClientId,
96
+ groupGenerationsToLabItems: () => groupGenerationsToLabItems,
97
+ hfBatchArchive: () => hfBatchArchive,
98
+ hfBootstrapFromLegacy: () => hfBootstrapFromLegacy,
99
+ hfListDir: () => hfListDir,
100
+ hfLoadImageAsBase64: () => hfLoadImageAsBase64,
101
+ hfUploadImage: () => hfUploadImage,
102
+ hfUploadSmallFile: () => hfUploadSmallFile,
103
+ injectXMPMetadata: () => injectXMPMetadata,
104
+ interpretSdkError: () => interpretSdkError,
105
+ libraryContract: () => libraryContract,
106
+ libraryReactDescriptor: () => libraryReactDescriptor,
107
+ listCategories: () => listCategories,
108
+ loadHFState: () => loadHFState,
109
+ loadPendingEvents: () => loadPendingEvents,
110
+ loadTagBase: () => loadTagBase,
111
+ parseLegacyLibraryFilters: () => parseLegacyLibraryFilters,
112
+ parseLegacyRecentTags: () => parseLegacyRecentTags,
113
+ parsePromptFile: () => parsePromptFile,
114
+ parsePromptResponse: () => parsePromptResponse,
115
+ runLegacyMigration: () => runLegacyMigration,
116
+ runLibraryLegacyImport: () => runLibraryLegacyImport,
117
+ runTagsLegacyImport: () => runTagsLegacyImport,
118
+ setHFToken: () => setHFToken,
119
+ tagsContract: () => tagsContract,
120
+ tagsReactDescriptor: () => tagsReactDescriptor,
121
+ tsFromEventPath: () => tsFromEventPath,
122
+ useFaSlice: () => useFaSlice,
123
+ useFaSliceMeta: () => useFaSliceMeta,
124
+ useHFState: () => useHFState,
125
+ useKeyboardNavigation: () => useKeyboardNavigation,
126
+ useOnClickOutside: () => useOnClickOutside,
127
+ useTagBase: () => useTagBase,
128
+ validateTagBaseSelection: () => validateTagBaseSelection,
129
+ writeHFEvent: () => writeHFEvent,
130
+ writeTagEvent: () => writeTagEvent
131
+ });
132
+ module.exports = __toCommonJS(index_exports);
133
+
134
+ // package.json
135
+ var version = "7.2.0";
136
+
137
+ // src/hooks/useOnClickOutside.ts
138
+ var import_react = require("react");
139
+ function useOnClickOutside(ref, handler) {
140
+ (0, import_react.useEffect)(() => {
141
+ const listener = (event) => {
142
+ if (!ref.current || ref.current.contains(event.target)) return;
143
+ handler(event);
144
+ };
145
+ document.addEventListener("mousedown", listener);
146
+ document.addEventListener("touchstart", listener);
147
+ return () => {
148
+ document.removeEventListener("mousedown", listener);
149
+ document.removeEventListener("touchstart", listener);
150
+ };
151
+ }, [ref, handler]);
152
+ }
153
+
154
+ // src/hooks/useKeyboardNavigation.ts
155
+ var import_react2 = require("react");
156
+ function useKeyboardNavigation(history, currentResult, setCurrentResult) {
157
+ (0, import_react2.useEffect)(() => {
158
+ const handleKeyDown = (e) => {
159
+ if (e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLInputElement) return;
160
+ if (!currentResult || history.length === 0) return;
161
+ const currentIndex = history.findIndex((h) => h.id === currentResult.id);
162
+ if (e.key === "ArrowRight" && currentIndex < history.length - 1) {
163
+ setCurrentResult(history[currentIndex + 1]);
164
+ } else if (e.key === "ArrowLeft" && currentIndex > 0) {
165
+ setCurrentResult(history[currentIndex - 1]);
166
+ }
167
+ };
168
+ window.addEventListener("keydown", handleKeyDown);
169
+ return () => window.removeEventListener("keydown", handleKeyDown);
170
+ }, [currentResult, history, setCurrentResult]);
171
+ }
172
+
173
+ // src/features/szenen/szenenViewStyles.generated.ts
174
+ var SZENEN_VIEW_STYLES = '/* ==========================================================================\n SZENEN VIEW\n ========================================================================== */\n\n.szenen-view {\n display: grid;\n gap: var(--space-md);\n width: 100%;\n min-width: 0;\n font-family: var(--font-sans);\n padding: var(--space-md);\n}\n\n@media (max-width: 760px) {\n .szenen-view {\n padding: var(--space-sm);\n gap: var(--space-sm);\n }\n}\n\n.szenen-view__intro {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-sm);\n padding: clamp(0.6rem, 1.2vw, 0.9rem) clamp(0.75rem, 1.6vw, 1.1rem);\n color: var(--color-text-primary);\n background:\n radial-gradient(circle at 85% 15%, rgba(45, 212, 191, 0.16), transparent 34%),\n linear-gradient(135deg, var(--color-bg-surface), var(--color-bg-elevated));\n border: 1px solid var(--color-border);\n border-radius: var(--radius-lg);\n}\n\n.szenen-view__intro h2,\n.szenen-view__intro p {\n margin: 0;\n}\n\n.szenen-view__intro h2 {\n margin-bottom: 0.1rem;\n font-size: clamp(1rem, 1.9vw, 1.3rem);\n line-height: 1.2;\n}\n\n.szenen-view__intro > div > p:last-child {\n max-width: 44rem;\n color: var(--color-text-secondary);\n font-size: 0.82rem;\n line-height: 1.35;\n}\n\n.szenen-view__eyebrow {\n margin-bottom: 0.1rem !important;\n color: var(--color-accent-teal, #2dd4bf);\n font-size: 0.62rem;\n font-weight: 800;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n}\n\n.szenen-view__step {\n flex: none;\n padding: 0.25rem 0.6rem;\n color: var(--color-text-primary);\n background: rgba(45, 212, 191, 0.12);\n border: 1px solid rgba(45, 212, 191, 0.35);\n border-radius: var(--radius-full);\n font-size: 0.72rem;\n font-weight: 700;\n}\n\n.szenen-view__nav {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-view__nav button,\n.szenen-view button {\n min-height: 2.25rem;\n padding: 0.45rem 0.75rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-view button:hover:not(:disabled) {\n color: var(--color-text-primary);\n background: var(--color-bg-surface-hover);\n border-color: var(--color-border-hover);\n}\n\n.szenen-view button:focus-visible,\n.szenen-view input:focus-visible,\n.szenen-view select:focus-visible,\n.szenen-view textarea:focus-visible {\n outline: 2px solid var(--color-accent-blue);\n outline-offset: 2px;\n}\n\n.szenen-view button:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.szenen-view__nav button.is-active,\n.szenen-view__nav button[aria-selected="true"] {\n color: var(--color-text-primary);\n background: var(--color-accent-blue-dim);\n border-color: var(--color-accent-blue);\n}\n\n.szenen-view__area {\n min-width: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-view__capability-note,\n.szenen-area__lead {\n margin: 0;\n padding: var(--space-sm) var(--space-md);\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border-left: 3px solid var(--color-accent-purple);\n border-radius: var(--radius-sm);\n}\n\n.szenen-view__host-notes {\n color: var(--color-text-secondary);\n}\n\n.szenen-view__host-notes > summary {\n cursor: pointer;\n font-size: 0.8rem;\n}\n\n/* `box-sizing` steht hier und nicht beim Host: Die React-App bringt in\n * `master.css` einen globalen `* { box-sizing: border-box }` mit, Flow **nicht**\n * \u2014 dort ragte `width: 100%` plus Innenabstand um die Randbreite aus dem\n * Elternelement heraus und erzeugte auf ~390 px einen horizontalen Scroll. Die\n * gemeinsame Ansicht darf sich auf keinen Host-Reset verlassen. */\n.szenen-view input:not([type="checkbox"]),\n.szenen-view select,\n.szenen-view textarea {\n box-sizing: border-box;\n width: 100%;\n min-height: 2.25rem;\n padding: 0.5rem 0.65rem;\n color: var(--color-text-primary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n font: inherit;\n}\n\n.szenen-view textarea {\n min-height: 7rem;\n resize: vertical;\n font-family: var(--font-mono);\n font-size: 0.8rem;\n}\n\n.szenen-view label {\n display: grid;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-size: 0.85rem;\n}\n\n.szenen-view code,\n.szenen-view pre {\n font-family: var(--font-mono);\n overflow-wrap: anywhere;\n}\n\n.szenen-view h3,\n.szenen-view h4,\n.szenen-view h5 {\n margin-top: 0;\n}\n\n.szenen-view ul,\n.szenen-view ol {\n padding-left: 1.25rem;\n}\n\n.szenen-elements,\n.szenen-detail,\n.szenen-scene,\n.szenen-setup,\n.szenen-shot,\n.szenen-media,\n.szenen-runs {\n display: grid;\n gap: var(--space-md);\n min-width: 0;\n}\n\n/* Arbeitsoberflaeche: Liste links, Bearbeitung rechts.\n *\n * Breit stehen beide Spalten nebeneinander \u2014 die Auswahl bleibt sichtbar,\n * waehrend bearbeitet wird. Schmal rendert die Komponente nur eine der beiden;\n * hier steht deshalb keine Regel, die die andere versteckt. */\n@media (min-width: 761px) {\n .szenen-elements {\n grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);\n align-items: start;\n }\n\n .szenen-elements__master,\n .szenen-elements__detail {\n min-width: 0;\n }\n}\n\n.szenen-elements__master,\n.szenen-elements__detail {\n display: grid;\n gap: var(--space-sm);\n align-content: start;\n min-width: 0;\n}\n\n.szenen-elements__toolbar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-sm);\n}\n\n.szenen-elements__heading {\n margin: 0;\n font-size: 1.05rem;\n}\n\n.szenen-elements__scope {\n display: flex;\n align-items: center;\n gap: var(--space-xs);\n min-width: 0;\n color: var(--color-text-secondary);\n font-size: 0.8rem;\n}\n\n.szenen-elements__scope > select {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.szenen-elements__new {\n flex: none;\n font-weight: 700;\n}\n\n.szenen-elements__create {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-accent-blue);\n border-radius: var(--radius-sm);\n}\n\n.szenen-elements__create label {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-elements__create-actions {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n}\n\n.szenen-elements__options > summary {\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n cursor: pointer;\n}\n\n.szenen-elements__back {\n justify-self: start;\n font-weight: 700;\n}\n\n.szenen-elements__empty-detail {\n margin: 0;\n color: var(--color-text-tertiary);\n}\n\n.szenen-elements__filter,\n.szenen-detail__actions,\n.szenen-runs__start,\n.szenen-scene-editor__add,\n.szenen-scene-editor__add-relation {\n display: flex;\n flex-wrap: wrap;\n align-items: end;\n gap: var(--space-sm);\n}\n\n.szenen-elements__filter > label,\n.szenen-runs__start > label,\n.szenen-scene-editor__add > input {\n flex: 1 1 16rem;\n}\n\n.szenen-elements__filter > label:has(input[type="checkbox"]) {\n display: flex;\n flex: 1 1 16rem;\n flex-direction: row;\n align-items: center;\n}\n\n.szenen-elements__filter input[type="checkbox"] {\n width: 1rem;\n height: 1rem;\n accent-color: var(--color-accent-blue);\n}\n\n.szenen-elements__list {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-elements__list > li > button {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs) var(--space-sm);\n width: 100%;\n height: 100%;\n text-align: left;\n}\n\n.szenen-elements__list > li > button > .szenen-elements__title {\n flex: 1 1 100%;\n}\n\n.szenen-elements__list > li > button.is-selected,\n.szenen-elements__list > li > button[aria-current="true"] {\n background: var(--color-accent-blue-dim);\n border-color: var(--color-accent-blue);\n}\n\n.szenen-elements__type {\n color: var(--color-accent-blue);\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n}\n\n.szenen-elements__title {\n color: var(--color-text-primary);\n font-size: 0.95rem;\n}\n\n.szenen-elements__subtype,\n.szenen-elements__retired {\n color: var(--color-text-secondary);\n font-size: 0.75rem;\n}\n\n.szenen-elements__retired {\n color: var(--color-warning);\n}\n\n.szenen-detail {\n margin-top: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n@media (min-width: 761px) {\n .szenen-detail {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n }\n}\n\n.szenen-detail > header,\n.szenen-detail__heads,\n.szenen-detail__draft,\n.szenen-capability {\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n/* .szenen-runs ist ein eigenst\xE4ndiger sichtbarer Bereich, beh\xE4lt Styling auf Desktop */\n@media (min-width: 761px) {\n .szenen-runs {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n }\n}\n\n@media (max-width: 760px) {\n .szenen-runs {\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n }\n}\n\n.szenen-detail__ids {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin-bottom: 0;\n color: var(--color-text-secondary);\n}\n\n.szenen-detail__heads ul,\n.szenen-scene-editor ul,\n.szenen-runs > ol,\n.szenen-capability > ul {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-detail__heads li,\n.szenen-scene-editor li,\n.szenen-runs > ol > li,\n.szenen-capability > ul > li {\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n\n/* Stufe 6.7.2 (D-126) \u2014 Advanced-/Debug-Fl\xE4che f\xFCr die rohe, schema-getriebene\n * Feldliste einer Szene. Gestrichelt wie die anderen \u201EAngebot statt\n * Auskunft"-Bereiche, aber ohne Akzentfarbe: sie ist kein Vorschlag, sondern\n * ein Eskalationsweg. */\n.szenen-detail__technical {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__technical > summary {\n margin-bottom: var(--space-sm);\n color: var(--color-text-secondary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-detail__technical[open] > summary {\n margin-bottom: var(--space-md);\n}\n\n.szenen-detail__field small,\n.szenen-capability small {\n color: var(--color-text-tertiary);\n}\n\n.szenen-detail__preview-errors,\n.szenen-detail__notice--error,\n.szenen-view [role="alert"] {\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-color: var(--color-accent-red);\n}\n\n.szenen-detail__notice {\n padding: var(--space-sm) var(--space-md);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__notice--success,\n.szenen-view [data-tone="success"] {\n color: #bbf7d0;\n background: var(--color-accent-green-dim);\n border-color: var(--color-accent-green);\n}\n\n.szenen-scene-editor {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: var(--space-md);\n margin-bottom: 0;\n}\n\n.szenen-scene-editor > section {\n min-width: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n/* \u2500\u2500 Stufe 6.7.2 (D-126): Node-/Relationskarten \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Nodes und Beziehungen sind `<details>` \u2014 dieselbe Interaktionssprache wie\n * \u201ERohantwort (Diagnose)" und \u201EBilder zu diesem Lauf" in RunsPanel.tsx. Kopf\n * (immer sichtbar) tr\xE4gt Inhalt zuerst, Technik zuletzt; Body (nur bei\n * ge\xF6ffneter Karte) tr\xE4gt die Bearbeitungscontrols. Padding wandert von der\n * `<li>` in Kopf/Body, damit die Karte keinen doppelten Rand bekommt. */\n.szenen-scene-editor li {\n padding: 0;\n overflow: hidden;\n}\n\n.szenen-node-karte__kopf,\n.szenen-relation-karte__kopf {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-xs) var(--space-sm);\n padding: var(--space-sm);\n cursor: pointer;\n list-style: none;\n}\n\n.szenen-node-karte__kopf::-webkit-details-marker,\n.szenen-relation-karte__kopf::-webkit-details-marker {\n display: none;\n}\n\n.szenen-node-karte__titel {\n flex: 1 1 12rem;\n min-width: 0;\n color: var(--color-text-primary);\n font-weight: 600;\n overflow-wrap: anywhere;\n}\n\n.szenen-node-karte__typ {\n flex: none;\n padding: 0.15rem 0.55rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-full);\n font-size: 0.72rem;\n white-space: nowrap;\n}\n\n/* node_id klein, monospace, sekund\xE4r \u2014 sichtbar auch bei zugeklappter Karte,\n * aber optisch klar hinter Beschreibung und Typ. */\n.szenen-node-karte__meta {\n flex: 1 1 100%;\n display: flex;\n flex-wrap: wrap;\n gap: 0.25rem;\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n}\n\n.szenen-node-karte__meta code {\n font-size: inherit;\n}\n\n.szenen-node-karte__body,\n.szenen-relation-karte__body {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm) var(--space-sm) var(--space-sm);\n border-top: 1px solid var(--color-border);\n}\n\n/* \xA74 \u2014 Prim\xE4raktion und destruktive Aktion optisch getrennt: \u201ENode/Beziehung\n * entfernen" bekommt einen eigenen, warnenden Ton statt gleichgewichtig neben\n * der normalen Bearbeitung zu stehen. */\n.szenen-node-karte__entfernen,\n.szenen-relation-karte__entfernen {\n justify-self: start;\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-color: var(--color-accent-red);\n}\n\n.szenen-relation-karte__kopf {\n font-size: 0.9rem;\n}\n\n.szenen-relation-karte__kopf code {\n overflow-wrap: anywhere;\n}\n\n.szenen-relation-karte__pfeil {\n flex: none;\n color: var(--color-text-secondary);\n}\n\n/* Die Composer-\xD6ffner (\u201E+ Node hinzuf\xFCgen", \u201E+ Beziehung hinzuf\xFCgen") sind\n * Angebote, keine Ausk\xFCnfte \u2014 dasselbe gestrichelte Muster wie beim Folgelauf\n * und der Fehlerrettung (\xA73.3/\xA73.5). */\n.szenen-scene-editor__oeffnen {\n width: 100%;\n border-style: dashed;\n}\n\n.szenen-refpicker {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n margin: var(--space-sm) 0;\n padding: var(--space-sm) 0;\n border: none;\n border-radius: 0;\n}\n\n.szenen-refpicker legend {\n padding: 0 var(--space-xs);\n color: var(--color-text-secondary);\n font-weight: 600;\n}\n\n.szenen-capability > ul > li {\n display: grid;\n grid-template-columns: minmax(10rem, 1fr) auto minmax(14rem, 2fr);\n align-items: start;\n gap: var(--space-sm);\n}\n\n.szenen-capability__state {\n padding: 0.1rem 0.5rem;\n border-radius: var(--radius-full);\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n}\n\n.szenen-capability__state--yes {\n color: #bbf7d0;\n background: var(--color-accent-green-dim);\n}\n\n.szenen-capability__state--no {\n color: #fecaca;\n background: var(--color-accent-red-dim);\n}\n\n.szenen-capability__state--unknown {\n color: #fef08a;\n background: rgba(234, 179, 8, 0.2);\n}\n\n.szenen-runs > ol > li {\n display: grid;\n gap: var(--space-sm);\n}\n\n/* B1 \u2014 die geschlossene Runde. Vorher stand die `run_id` als erstes und war\n die einzige Information, die niemand liest; jetzt beantwortet die Kopfzeile\n welche Runde, ob sie durchging, mit welchem Modell und was ungef\xE4hr\n herauskam. */\n.szenen-runs__kopf {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-sm);\n width: 100%;\n padding: var(--space-xs) 0;\n text-align: left;\n color: var(--color-text-secondary);\n background: transparent;\n border: none;\n cursor: pointer;\n}\n\n.szenen-runs__marke {\n font-weight: 700;\n color: var(--color-text-primary);\n}\n\n.szenen-runs__status {\n padding: 0.05rem 0.4rem;\n font-size: 0.6875rem;\n font-weight: 600;\n text-transform: uppercase;\n border-radius: var(--radius-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n}\n\n.szenen-runs__status[data-status="succeeded"] {\n border-color: var(--color-success);\n}\n\n.szenen-runs__status[data-status="failed"] {\n border-color: var(--color-danger);\n}\n\n.szenen-runs__modell {\n font-size: 0.75rem;\n}\n\n/* Der Auszug ist die Inhaltsvorschau der Runde. Er bekommt eine eigene Zeile,\n damit er nicht mit Marke und Modell um dieselbe Zeile ringt. */\n.szenen-runs__auszug {\n flex: 1 1 100%;\n min-width: 0;\n color: var(--color-text-tertiary);\n font-size: 0.8125rem;\n}\n\n/* B2 \u2014 ein Fehler steht ausserhalb der aufklappbaren Fl\xE4che: Wer eine rote\n Runde sieht, soll nicht erst klicken m\xFCssen, um zu erfahren, woran sie\n gescheitert ist. */\n.szenen-runs__fehler {\n display: grid;\n gap: var(--space-xs);\n padding: var(--space-sm);\n border-radius: var(--radius-sm);\n}\n\n.szenen-runs__fehler p {\n margin: 0;\n}\n\n.szenen-runs__inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n}\n\n.szenen-runs__prompt > h5,\n.szenen-runs__sekundaer > h5 {\n margin: 0 0 var(--space-xs);\n}\n\n.szenen-runs__prompt > p {\n margin: 0;\n white-space: pre-wrap;\n}\n\n.szenen-runs__prompt--negativ > p,\n.szenen-runs__sekundaer > p {\n margin: 0;\n color: var(--color-text-secondary);\n white-space: pre-wrap;\n}\n\n.szenen-runs__mehr {\n justify-self: start;\n margin-top: var(--space-xs);\n padding: 0;\n color: var(--color-accent-teal, var(--color-text-secondary));\n background: transparent;\n border: none;\n font-size: 0.75rem;\n text-decoration: underline;\n cursor: pointer;\n}\n\n.szenen-runs > ol > li[data-status="succeeded"] {\n border-left: 3px solid var(--color-success);\n}\n\n.szenen-runs > ol > li[data-status="failed"] {\n border-left: 3px solid var(--color-danger);\n}\n\n.szenen-runs details pre {\n max-height: 20rem;\n overflow: auto;\n padding: var(--space-sm);\n white-space: pre-wrap;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border-radius: var(--radius-sm);\n}\n\n.szenen-media > section {\n display: grid;\n gap: var(--space-sm);\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-media__lookup,\n.szenen-media__attach {\n display: flex;\n flex-wrap: wrap;\n align-items: end;\n gap: var(--space-sm);\n}\n\n.szenen-media__lookup > label,\n.szenen-media__attach > label:first-child {\n flex: 1 1 18rem;\n}\n\n.szenen-media__list {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-media__list > li {\n display: grid;\n grid-template-columns: minmax(12rem, 1fr) auto auto auto;\n align-items: center;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n@media (max-width: 760px) {\n .szenen-view {\n gap: var(--space-sm);\n }\n\n /* C1 \u2014 auf 390 px bleiben drei Kacheln je Reihe gross genug zum Erkennen\n und klein genug, dass vier Bilder eine Reihe sind und keine Seite. */\n .szenen-bildgalerie {\n grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr));\n }\n\n /* Der Viewer bekommt den ganzen Bildschirm; die Bl\xE4tterkn\xF6pfe bleiben am\n Rand, wo der Daumen sie erreicht. */\n .szenen-bildviewer {\n padding: var(--space-xs);\n }\n\n .szenen-bildviewer__blaettern {\n width: 2.25rem;\n height: 4rem;\n }\n\n .szenen-view__intro {\n align-items: start;\n flex-direction: column;\n gap: var(--space-xs);\n padding: var(--space-sm);\n }\n\n .szenen-view__intro h2 {\n font-size: 1.05rem;\n }\n\n .szenen-view__intro > div > p:last-child {\n font-size: 0.78rem;\n }\n\n .szenen-view__nav {\n position: sticky;\n top: 0;\n z-index: 2;\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: var(--space-xs);\n padding: var(--space-xs);\n }\n\n .szenen-view__nav button {\n width: 100%;\n min-width: 0;\n min-height: 2.5rem;\n padding: 0.35rem 0.5rem;\n }\n\n\n .szenen-scene-editor {\n grid-template-columns: 1fr;\n }\n\n .szenen-capability > ul > li {\n grid-template-columns: 1fr;\n }\n\n .szenen-derive,\n .szenen-elements,\n .szenen-scene,\n .szenen-setup,\n .szenen-shot,\n .szenen-media,\n .szenen-runs {\n gap: var(--space-sm);\n }\n\n .szenen-elements__filter {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n align-items: center;\n gap: var(--space-sm);\n }\n\n .szenen-elements__filter > label {\n width: 100%;\n min-width: 0;\n flex: none;\n }\n\n .szenen-elements__filter > label:has(input[type="checkbox"]) {\n flex: none;\n grid-column: 1 / -1;\n }\n\n /* Schmal ist die Liste eine Spalte, und die Auswahl fuehrt weiter \u2014 der\n * Eintrag ist deshalb ein Ziel fuer den Finger, keine Kachel. */\n .szenen-elements__list {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .szenen-elements__list > li > button {\n min-height: 2.75rem;\n }\n\n .szenen-elements__back {\n justify-self: stretch;\n min-height: 2.5rem;\n }\n\n .szenen-detail__actions,\n .szenen-runs__start,\n .szenen-scene-editor__add,\n .szenen-scene-editor__add-relation {\n align-items: stretch;\n flex-direction: column;\n }\n\n .szenen-runs__start > *,\n .szenen-scene-editor__add > *,\n .szenen-scene-editor__add-relation > * {\n width: 100%;\n flex: none;\n }\n}\n\n/* \u2500\u2500 Stufe 6.5: Rekonstruktion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Bereich zeigt drei Dinge nebeneinander: den Vorschlag, das nicht\n * Ableitbare und die Widerspr\xFCche. Die Diagnosen sind deshalb bewusst nicht\n * kleiner gesetzt als der Vorschlag \u2014 wer sie \xFCbersieht, \xFCbernimmt Geratenes.\n */\n\n.szenen-derive {\n display: grid;\n gap: var(--space-md);\n}\n\n.szenen-form {\n display: grid;\n gap: var(--space-sm);\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-field {\n display: grid;\n gap: var(--space-xs);\n}\n\n.szenen-field > span {\n color: var(--color-text-secondary);\n font-size: 0.875rem;\n}\n\n.szenen-error {\n padding: var(--space-sm) var(--space-md);\n color: var(--color-danger);\n background: var(--color-accent-red-dim);\n border: 1px solid var(--color-accent-red);\n border-radius: var(--radius-sm);\n}\n\n.szenen-empty {\n padding: var(--space-md);\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-diagnose {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-warning);\n border-radius: var(--radius-sm);\n}\n\n/* Ein Widerspruch ist der schwerste der drei Befunde: dort steht eine\n * Entscheidung aus, die das System bewusst nicht getroffen hat (D-74). */\n.szenen-diagnose--konflikt {\n border-left-color: var(--color-danger);\n}\n\n.szenen-diagnose > h4 {\n margin: 0 0 var(--space-sm);\n font-size: 0.9375rem;\n}\n\n.szenen-diagnose ul {\n display: grid;\n gap: var(--space-xs);\n margin: 0;\n padding-left: var(--space-md);\n}\n\n.szenen-proposal {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-proposal > h4 {\n margin: 0 0 var(--space-sm);\n font-family: var(--font-mono);\n font-size: 0.9375rem;\n}\n\n.szenen-proposal__payload {\n max-height: 18rem;\n margin: 0 0 var(--space-sm);\n padding: var(--space-sm);\n overflow: auto;\n /* Ein erzeugter Prompt ist ein einziger langer Satz \u2014 ohne Umbruch\n verschwindet er nach rechts aus dem Bild. JSON-Payloads bringen ihre\n eigenen Zeilenumbrueche mit und leiden nicht darunter. */\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n font-family: var(--font-mono);\n font-size: 0.8125rem;\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-proposal__done {\n margin: var(--space-sm) 0 0;\n color: var(--color-success);\n}\n\n/* Die Belege eines Laufs: Rohantwort und Systemprompt. Zum Lesen und Kopieren,\n nicht zum Bedienen \u2014 deshalb voll markierbar und in Monospace. */\n.szenen-beleg__text {\n width: 100%;\n margin: var(--space-sm) 0 0;\n padding: var(--space-sm);\n font-family: var(--font-mono);\n font-size: 0.8125rem;\n line-height: 1.5;\n white-space: pre;\n background: var(--color-bg-app);\n color: var(--color-text);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n resize: vertical;\n}\n\n/* Der Folgelauf (D-87): abgesetzt, damit er nicht wie Teil des Ergebnisses\n aussieht \u2014 er ist eine Frage, keine Auskunft. */\n.szenen-folgelauf {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-folgelauf > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-run__herkunft {\n margin: 0 0 var(--space-sm);\n font-size: 0.875rem;\n color: var(--color-text-muted, inherit);\n}\n\n/* Widerspricht das Modell, ist das das Wichtigste auf der Seite. */\n.szenen-diagnose--antwort {\n border-left: 3px solid var(--color-accent, var(--color-border));\n}\n\n/* Der Bildw\xE4hler (D-92) \u2014 Browser und Inspector, dem Labs-Tab nachgebildet,\n aber mit dem CSS dieser Ansicht: eine gemeinsame Komponente \xFCber zwei\n Dom\xE4nengrenzen w\xE4re eine Kopplung, die keiner gewollt hat.\n\n Der Review vom 2026-08-22 (R12.1/R12.2) hat den Ma\xDFstab gesetzt: Der Bestand\n ist ein **Browser**, kein Seiteninhalt. Vierzig Werke d\xFCrfen die Szenenseite\n nicht meterlang machen, und zw\xF6lf Varianten eines Werkes keine zw\xF6lf\n Bildschirmseiten. Deshalb steht hier alles unter zwei Regeln \u2014 begrenzte\n Fl\xE4che mit eigenem Scroll, und Kacheln statt Karten. */\n\n/* \u2500\u2500 Ebene 1: der Browser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl {\n /* Dieselbe Begr\xFCndung wie beim Inspector: Als Grid-Element w\xFCrde der\n W\xE4hler sonst so breit wie sein breitester Inhalt. */\n min-width: 0;\n}\n\n.szenen-bildwahl__browser {\n /* Die eigene Scrollfl\xE4che ist der ganze Punkt: Der Bestand w\xE4chst nach\n innen, nicht nach unten. Dieselbe Gr\xF6\xDFenordnung wie `max-h-[300px]` im\n Labs-Browser. */\n max-height: 19rem;\n overflow-y: auto;\n overscroll-behavior: contain;\n margin-top: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__raster {\n display: grid;\n /* Vier Spalten auf schmalen Displays \u2014 der gemeldete Screenshot zeigte eine\n einzige Spalte gro\xDFer Karten. Breiter wird automatisch dichter. */\n grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr));\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildwahl__karte {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n overflow: hidden;\n background: var(--color-bg-surface);\n text-align: left;\n cursor: pointer;\n}\n\n/* Das fokussierte Werk ist klar markiert \u2014 sonst wei\xDF niemand, wessen Galerie\n der Inspector unten zeigt. */\n.szenen-bildwahl__karte.is-fokus {\n border-color: var(--color-accent, var(--color-text-primary));\n box-shadow: 0 0 0 2px var(--color-accent, var(--color-text-primary));\n}\n\n/* \u2500\u2500 Die Kachel selbst \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Der Platz steht, bevor das Bild da ist \u2014 sonst springt das Raster, sobald\n die erste Vorschau eintrifft. */\n.szenen-bildkachel {\n display: block;\n width: 100%;\n aspect-ratio: 1;\n overflow: hidden;\n background: var(--color-bg-app);\n}\n\n.szenen-bildkachel img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n\n.szenen-bildkachel__leer {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n background: linear-gradient(135deg, var(--color-bg-app), var(--color-bg-surface));\n}\n\n/* R12.3: Eine fehlende Vorschau bleibt kachelgro\xDF. Vorher fiel die Karte auf\n ihren Prompttext zur\xFCck \u2014 und ein Prompt hat im echten Bestand mehrere\n hundert Zeichen. */\n.szenen-bildkachel__zeichen {\n font-size: 0.875rem;\n font-weight: 700;\n color: var(--color-text-secondary);\n opacity: 0.6;\n}\n\n/* Wie viele Varianten das Werk hat \u2014 klein und \xFCberlagernd, nie als\n dominanter Text. */\n.szenen-bildwahl__anzahl {\n position: absolute;\n top: 0.15rem;\n right: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n font-weight: 700;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* Eine Zeile, dann Schluss. Der Prompt eines Werkes ist oft mehrere hundert\n Zeichen lang und w\xE4re als Prim\xE4rdarstellung genau die Textw\xFCste, gegen die\n der Review antritt. */\n.szenen-bildwahl__kacheltitel {\n display: block;\n padding: 0.15rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.3;\n color: var(--color-text-secondary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 Ebene 2: der Inspector \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl__inspector {\n /* Ohne `min-width: 0` gewinnt der Inhalt: Der W\xE4hler ist Element eines\n Grids (`.szenen-form`), und dort ist `min-width: auto` die Vorgabe. Der\n Streifen unten ist rund 1000 px breit \u2014 gemessen am 2026-08-22 im echten\n Browser zog er die Auswahl von 374 px auf 1088 px auf und schob die\n ganze Seite waagerecht heraus, statt selbst zu scrollen. */\n min-width: 0;\n margin-top: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n /* Sichtbar abgesetzt vom Browser: zwei Ebenen, nicht eine lange Liste. */\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-accent, var(--color-border));\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__inspektorkopf {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-sm);\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bildwahl__werktitel {\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--color-text-primary);\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.szenen-bildwahl__zurueck {\n flex: 0 0 auto;\n font-size: 0.6875rem;\n padding: 0.15rem 0.5rem;\n}\n\n/* Der Streifen: waagerecht scrollend, feste kleine Kacheln \u2014 zw\xF6lf Varianten\n bleiben ein \xDCberblick statt zw\xF6lf Bildschirmseiten. */\n.szenen-bildwahl__streifen {\n /* Die Breite kommt vom Elter, nie vom Inhalt \u2014 sonst scrollt hier nichts. */\n max-width: 100%;\n display: flex;\n gap: var(--space-sm);\n margin: 0;\n padding: 0 0 0.25rem;\n list-style: none;\n overflow-x: auto;\n overscroll-behavior-x: contain;\n}\n\n.szenen-bildwahl__streifen > li {\n flex: 0 0 auto;\n width: 5rem;\n}\n\n.szenen-bildwahl__variante {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n overflow: hidden;\n background: var(--color-bg-app);\n cursor: pointer;\n}\n\n.szenen-bildwahl__variante.is-gewaehlt {\n border-color: var(--color-accent, var(--color-text-primary));\n box-shadow: 0 0 0 2px var(--color-accent, var(--color-text-primary));\n}\n\n.szenen-bildwahl__nummer {\n position: absolute;\n bottom: 0.15rem;\n left: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border-radius: var(--radius-sm);\n}\n\n/* \u2500\u2500 Das Gew\xE4hlte \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl__gewaehlt {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: var(--space-sm) 0 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildwahl__gewaehlt li {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.25rem 0.5rem;\n font-size: 0.8125rem;\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n max-width: 100%;\n}\n\n/* Das gew\xE4hlte Bild wird best\xE4tigt, nicht nur benannt: eine kleine Vorschau\n neben dem Titel. */\n.szenen-bildwahl__gewaehlt .szenen-bildkachel {\n width: 2.25rem;\n flex: 0 0 auto;\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__gewaehlt .szenen-bildwahl__kacheltitel {\n min-width: 0;\n max-width: 12rem;\n font-size: 0.75rem;\n}\n\n.szenen-beleg__kopf {\n display: flex;\n align-items: center;\n gap: var(--space-sm);\n margin-top: var(--space-sm);\n}\n\n.szenen-diagnose > details + details {\n margin-top: var(--space-sm);\n}\n\n.szenen-diagnose > details > summary {\n cursor: pointer;\n font-weight: 600;\n}\n\n/* \u2500\u2500 Die Fehlerrettung (Stufe 6.6, A5) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Sie steht im Fehlerblock und muss trotzdem als Werkzeug lesbar sein, nicht\n * als weitere Fehlermeldung. Deshalb abgesetzt wie der Folgelauf \u2014 und aus\n * demselben Grund: Beides sind Angebote, keine Ausk\xFCnfte.\n *\n * Der wichtigste optische Punkt ist die Trennung von Original und\n * Arbeitskopie. Das Original bekommt den ruhigen, \u201Efesten" Hintergrund der\n * Belege; die Kopie sieht aus wie ein normales Eingabefeld, weil sie eines\n * ist. */\n.szenen-rettung {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-rettung > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-rettung textarea[readonly] {\n background: var(--color-bg-surface);\n color: var(--color-text-secondary);\n font-family: var(--font-mono, monospace);\n font-size: 0.8125rem;\n}\n\n.szenen-rettung textarea:not([readonly]) {\n font-family: var(--font-mono, monospace);\n font-size: 0.8125rem;\n}\n\n.szenen-rettung__aktionen {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: var(--space-sm) 0;\n}\n\n/* Ein Befund tr\xE4gt Code, Bezug und Meldung. Der Code steht klein und\n * monospaced davor: Er ist die Kennung, nicht die Aussage. */\n.szenen-diagnose__liste {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding-left: var(--space-md);\n}\n\n.szenen-diagnose__liste code {\n font-size: 0.8125rem;\n}\n\n.szenen-diagnose__liste > li[data-severity=\'warning\'] {\n color: var(--color-text-secondary);\n}\n\n/* \u2500\u2500 Die Draft-Kette (Stufe 6.6, C4) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Hauptweg. Er steht nach den Einzel\xFCbernahmen und muss als das erkennbar\n * sein, was er ist: nicht eine weitere Option, sondern der k\xFCrzere Weg zum\n * selben Ziel. Deshalb durchgezogener Rahmen statt gestricheltem \u2014 er ist\n * Angebot **und** Empfehlung. */\n.szenen-kette {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-accent, var(--color-border));\n border-radius: var(--radius-sm);\n}\n\n.szenen-kette > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-kette ul {\n display: grid;\n gap: var(--space-xs);\n margin: 0 0 var(--space-sm);\n padding-left: var(--space-md);\n}\n\n.szenen-kette code {\n font-size: 0.8125rem;\n}\n\n/* Ein Hinweis, der eine Bedingung erklaert \u2014 nicht rot, weil er kein Fehler\n ist, aber abgesetzt, weil er eine Folge hat. */\n.szenen-detail__hint {\n margin: var(--space-sm) 0 0;\n padding: var(--space-sm);\n font-size: 0.875rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border-left: 3px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* \u2500\u2500 Stufe 6.7: Bilder am Prompt-Lauf (H0/H1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Bereich tr\xE4gt zwei Dinge, die man nie verwechseln darf: den **Eingang**\n * (woraus der Prompt entstand) und den **Ausgang** (was daraus wurde). Sie\n * bekommen deshalb verschiedene Akzentkanten statt nur verschiedener\n * \xDCberschriften \u2014 wer schnell liest, sieht die Trennung, bevor er sie liest.\n */\n.szenen-bilder {\n display: grid;\n gap: var(--space-md);\n margin-top: var(--space-sm);\n}\n\n.szenen-bilder__eingang,\n.szenen-bilder__ausgang {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__eingang {\n border-left: 3px solid var(--color-accent-purple, var(--color-border));\n}\n\n.szenen-bilder__ausgang {\n border-left: 3px solid var(--color-accent-teal, var(--color-border));\n}\n\n.szenen-bilder h5 {\n margin: 0;\n}\n\n.szenen-bilder__ids {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: 0;\n align-items: baseline;\n color: var(--color-text-secondary);\n font-size: 0.875rem;\n}\n\n.szenen-bilder__eingangsliste,\n.szenen-bilder__batchliste,\n.szenen-bilder__auftraege {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bilder__eingangsliste > li,\n.szenen-bilder__batchliste > li,\n.szenen-bilder__auftraege > li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n font-size: 0.8125rem;\n}\n\n/* Ein unterbrochener Auftrag ist der Fall, f\xFCr den der Auftragssatz existiert.\n Er wird deshalb markiert \u2014 nicht rot, denn er ist kein Fehler, sondern eine\n offene Entscheidung. */\n.szenen-bilder__auftraege > li[data-status="started"] {\n border-left: 3px solid var(--color-warning);\n}\n\n.szenen-bilder__auftraege > li[data-status="succeeded"] {\n border-left: 3px solid var(--color-success);\n}\n\n.szenen-bilder__auftraege > li[data-status="partial"] {\n border-left: 3px solid var(--color-accent-blue, var(--color-border));\n}\n\n.szenen-bilder__auftraege > li[data-status="failed"] {\n border-left: 3px solid var(--color-danger);\n}\n\n/* Das Eingangsbild benutzt seit C3 dieselbe Kachel wie W\xE4hler und Galerie.\n Sie ist von Haus aus so breit wie ihr Platz \u2014 in dieser Zeile bekommt sie\n deshalb eine feste Gr\xF6sse, sonst sch\xF6be sie die Metadaten heraus. */\n.szenen-bilder__eingangsliste .szenen-bildkachel {\n flex: 0 0 auto;\n width: 4rem;\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__batch {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__batchkopf {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--space-sm);\n}\n\n.szenen-bilder__herkunft {\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n}\n\n/* Review-Befund \u2014 ein transienter Bildfehler ist wiederholbar, kein\n endg\xFCltiges \u201Efehlt". Dezent wie ein Angebot, nicht wie ein Fehlerzustand. */\n.szenen-bilder__retry {\n justify-self: start;\n padding: 0.15rem 0.5rem;\n font-size: 0.75rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n/* C4 \u2014 die Provenienz vollst\xE4ndig, aber sekund\xE4r. */\n.szenen-bilder__technik > summary {\n color: var(--color-text-secondary);\n font-size: 0.75rem;\n cursor: pointer;\n}\n\n.szenen-bilder__meta {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n font-size: 0.75rem;\n color: var(--color-text-secondary);\n}\n\n/* C5 \u2014 der Verlauf verdichtet, seine Ausreisser nicht. */\n.szenen-bilder__verlauf > summary {\n color: var(--color-text-secondary);\n font-size: 0.8125rem;\n cursor: pointer;\n}\n\n.szenen-bilder__verlauf[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bilder__auffaellig {\n margin: 0;\n padding: var(--space-sm);\n border-radius: var(--radius-sm);\n font-size: 0.8125rem;\n}\n\n.szenen-bilder__auftragskennung {\n flex-basis: 100%;\n color: var(--color-text-tertiary);\n font-size: 0.6875rem;\n}\n\n/* \u2500\u2500 C1/C2 \u2014 Galerie und grosse Ansicht \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Das Raster w\xE4chst mit dem Platz: auf 390 px drei bis vier Kacheln je Reihe,\n auf dem Desktop entsprechend mehr. Vorher war jedes Bild eine Zeile aus\n Daumennagel plus vier Metadatenfeldern \u2014 vier Bilder ergaben vier Textzeilen\n und das Protokoll war die Hauptsache. */\n.szenen-bildgalerie {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr));\n gap: var(--space-sm);\n min-width: 0;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildgalerie__kachel {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-bildgalerie__kachel:hover,\n.szenen-bildgalerie__kachel:focus-visible {\n border-color: var(--color-accent-teal, var(--color-text-primary));\n}\n\n.szenen-bildgalerie__nummer {\n position: absolute;\n bottom: 0.15rem;\n left: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border-radius: var(--radius-sm);\n}\n\n/* Die grosse Ansicht liegt \xFCber allem und bringt ihren eigenen Grund mit \u2014\n `position: fixed` bezieht sich im Flow-Canvas auf dessen Rahmen, in React auf\n das Fenster; in beiden F\xE4llen genau das Gew\xFCnschte. */\n.szenen-bildviewer {\n position: fixed;\n inset: 0;\n z-index: 1000;\n display: grid;\n grid-template-rows: auto 1fr auto;\n gap: var(--space-sm);\n padding: var(--space-sm);\n /* Deckend, nicht get\xF6nt: Bei 0.92 schimmerte der Laufbereich durch und man\n las Prompttext quer \xFCber dem Bild \u2014 im Browser gemessen. */\n background: #000;\n}\n\n/* Titel, Z\xE4hlung, Schliessen \u2014 drei Spalten, damit ein langer Werktitel die\n Z\xE4hlung nicht in die n\xE4chste Zeile und \xFCber das Bild schiebt. */\n.szenen-bildviewer__kopf {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto auto;\n align-items: center;\n gap: var(--space-sm);\n color: var(--color-text-primary);\n}\n\n.szenen-bildviewer__titel {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-weight: 600;\n}\n\n.szenen-bildviewer__zaehler {\n color: var(--color-text-secondary);\n font-size: 0.8125rem;\n}\n\n.szenen-bildviewer__buehne {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--space-sm);\n min-height: 0;\n min-width: 0;\n}\n\n/* Der Rahmen nimmt den Platz, der zwischen den Bl\xE4tterkn\xF6pfen \xFCbrig ist \u2014\n `min-width: 0`, damit er auch wirklich schrumpft statt sie hinauszuschieben. */\n.szenen-bildviewer__rahmen {\n flex: 1 1 auto;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n min-height: 0;\n height: 100%;\n}\n\n.szenen-bildviewer__bild {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n}\n\n.szenen-bildviewer__leer {\n color: var(--color-text-secondary);\n text-align: center;\n}\n\n/* Die Bl\xE4tterkn\xF6pfe sind daumengross und liegen an den R\xE4ndern \u2014 auf einem\n Telefon ist der Rand erreichbar, die Bildmitte nicht. */\n.szenen-bildviewer__blaettern {\n flex: 0 0 auto;\n width: 2.75rem;\n height: 3.5rem;\n font-size: 1.75rem;\n line-height: 1;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-bildviewer__details {\n max-height: 8rem;\n overflow-y: auto;\n color: var(--color-text-secondary);\n}\n\n/* Die Aktionen stehen abgesetzt: Sie sind Angebote, keine Ausk\xFCnfte \u2014 dasselbe\n Muster wie beim Folgelauf und bei der Fehlerrettung. */\n/* D \u2014 die Aktionen stehen abgesetzt und standardm\xE4ssig kompakt: Sie sind\n Angebote, keine Ausk\xFCnfte. Vorher hielten beide Formulare dauerhaft offen und\n machten aus jedem Lauf zwei zus\xE4tzliche Bildschirmseiten. */\n.szenen-bilder__aktion {\n min-width: 0;\n padding: var(--space-sm) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__aktion > summary {\n color: var(--color-text-primary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-bilder__aktion[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bilder__aktion-inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding-bottom: var(--space-sm);\n}\n\n.szenen-bilder__aktion h5 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-bilder__aktion ol {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding-left: var(--space-md);\n font-size: 0.8125rem;\n}\n\n/* Regelwerk-UX Schritt 16 \u2014 der ZIP-Sammelimport.\n *\n * \xA78: Der gemeldete Einsatz ist mobil. Deshalb **ein** Seedfeld statt einer\n * gestapelten Eingabe je Bild, und die Zuordnung Bild -> Seed als umbrechende\n * Zeile statt als Tabelle: Eine Tabelle waere auf 390 px abgeschnitten, und\n * abgeschnitten heisst hier, dass der Nutzer die Zuordnung nicht mehr pruefen\n * kann \u2014 genau die Pruefung, wegen der die Vorschau existiert. */\n.szenen-bilder__zip {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n}\n\n.szenen-bilder__zipkopf {\n margin: 0;\n overflow-wrap: anywhere;\n}\n\n.szenen-bilder__zipvorlagen {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n margin: 0;\n}\n\n/* Eine laengere Liste bleibt scrollbar, statt die Sektion und damit den\n * Import-Knopf aus dem Blick zu schieben (\xA78, letzter Punkt). */\n.szenen-bilder__zipliste {\n max-height: 16rem;\n overflow-y: auto;\n}\n\n.szenen-bilder__zipliste > li {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-between;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-bilder__zipliste > li > span:first-child {\n overflow-wrap: anywhere;\n}\n\n.szenen-bilder__zipliste > li > span:last-child {\n color: var(--color-text-secondary);\n white-space: nowrap;\n}\n\n/* ==========================================================================\n STUFE 6.10 \u2014 ELEMENTEDITOR UND VERSIONSNAVIGATION\n ========================================================================== */\n\n/* \xA74.2 \u2014 vier Sachverhalte, vier Marken. Die Marken stehen in einer eigenen\n * Spalte, damit ANGEZEIGT + BEVORZUGT nebeneinander lesbar bleiben, statt sich\n * durch den Text zu schieben. */\n.szenen-detail__heads li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-sm);\n}\n\n.szenen-version__titel {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n min-width: 0;\n margin-right: auto;\n}\n\n/* V17 \u2014 Kennungen bleiben vollst\xE4ndig sichtbar, aber optisch sekund\xE4r. */\n.szenen-version__id,\n.szenen-ref__id,\n.szenen-detail__field-key {\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n overflow-wrap: anywhere;\n}\n\n.szenen-version__marken {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n}\n\n.szenen-version__marken > span {\n padding: 0.1rem 0.5rem;\n color: var(--color-text-primary);\n background: rgba(45, 212, 191, 0.12);\n border: 1px solid rgba(45, 212, 191, 0.35);\n border-radius: var(--radius-full);\n font-size: 0.7rem;\n font-weight: 800;\n letter-spacing: 0.08em;\n}\n\n.szenen-version--angezeigt {\n border-color: var(--color-accent-teal, #2dd4bf) !important;\n}\n\n/* V9 \u2014 der Basiswechsel ist eine Entscheidung, keine Nebenwirkung. */\n.szenen-detail__confirm {\n display: grid;\n gap: var(--space-sm);\n margin: var(--space-md) 0;\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-accent-amber, #f59e0b);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__confirm p {\n margin: 0;\n}\n\n/* Untergruppen von Objektfeldern (`prompt_render_settings`, `overrides`,\n `blender_render_settings`) \u2014 seit A1/A2 auf- und zuklappbar. Eine optionale\n Gruppe startet zu: Blender und `overrides` f\xFCllten auf einem 390-px-Display\n sonst mehrere Bildschirmseiten, obwohl die meisten Shots sie nie anfassen. */\n.szenen-detail__group {\n min-width: 0;\n margin: var(--space-md) 0;\n padding: var(--space-sm) var(--space-md);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__group > summary {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-detail__group[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n/* A2 \u2014 dass eine zugeklappte Gruppe schon Werte tr\xE4gt, muss man sehen, ohne\n sie zu \xF6ffnen. Sonst \xFCbersieht man eine gesetzte Blender-Ausgabe genau so\n lange, bis sie im Ergebnis auftaucht. */\n.szenen-detail__group-status {\n padding: 0.05rem 0.4rem;\n font-size: 0.6875rem;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--color-text-tertiary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__group[data-konfiguriert="ja"] > summary .szenen-detail__group-status {\n color: var(--color-text-primary);\n border-color: var(--color-accent-teal, var(--color-border));\n}\n\n.szenen-detail__group-inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding-bottom: var(--space-sm);\n}\n\n/* A3 \u2014 die Kernaussage steht am Feld, die Begr\xFCndung dahinter. Nicht als\n Tooltip: Auf einem Telefon gibt es kein Hover, und was dort unerreichbar ist,\n ist gestrichen \u2014 nur ohne dass es jemand entschieden h\xE4tte. */\n.szenen-detail__warum > summary {\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n cursor: pointer;\n}\n\n.szenen-detail__warum > small {\n display: block;\n padding-top: var(--space-xs);\n color: var(--color-text-tertiary);\n}\n\n.szenen-detail__fieldrow {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n margin-bottom: var(--space-md);\n}\n\n/* A4 \u2014 der technische Feldname bleibt lesbar, tritt aber hinter das fachliche\n Label zur\xFCck. */\n.szenen-detail__field-label {\n color: var(--color-text-primary);\n}\n\n.szenen-detail__field {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-detail__field > span {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n}\n\n/* Mehrfachauswahl und Listeneditoren. */\n.szenen-feld__mehrfach,\n.szenen-feld__liste,\n.szenen-feld__reflist,\n.szenen-feld__ref,\n.szenen-feld__zielmodell,\n.szenen-feld__kamera {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-feld__mehrfach > label,\n.szenen-feld__null {\n display: flex;\n align-items: center;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-size: 0.85rem;\n}\n\n.szenen-feld__liste-zeile {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n}\n\n.szenen-feld__reflist ol {\n display: grid;\n gap: var(--space-xs);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-feld__reflist li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-xs);\n padding: var(--space-xs) var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* \xA76.1 \u2014 Titel und Typ zuerst, Kennungen daneben. */\n.szenen-ref__anzeige {\n display: inline-flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-ref__typ {\n color: var(--color-text-secondary);\n font-size: 0.8rem;\n}\n\n.szenen-ref__warn {\n padding: 0.1rem 0.5rem;\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-radius: var(--radius-full);\n font-size: 0.72rem;\n font-weight: 700;\n}\n\n/* \xA76.5 \u2014 abgeleitete Angaben, ausdr\xFCcklich nur Anzeige. */\n.szenen-detail__derived {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__derived dl {\n display: grid;\n grid-template-columns: minmax(10rem, auto) 1fr;\n gap: var(--space-xs) var(--space-sm);\n margin: 0;\n}\n\n.szenen-detail__derived dt {\n color: var(--color-text-secondary);\n}\n\n.szenen-detail__derived dd {\n margin: 0;\n}\n\n.szenen-feld__abgeleitet {\n margin: 0;\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n}\n';
175
+
176
+ // src/lib/utils.ts
177
+ var getFormattedTimestamp = () => {
178
+ const d = /* @__PURE__ */ new Date();
179
+ const pad = (n) => n.toString().padStart(2, "0");
180
+ return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
181
+ };
182
+ var GLOBAL_STYLES = `
183
+ .dark-scrollbar::-webkit-scrollbar { width: 4px; height: 4px; }
184
+ .dark-scrollbar::-webkit-scrollbar-track { background: transparent; }
185
+ .dark-scrollbar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
186
+ html, body, #root { margin: 0; padding: 0; width: 100%; height: 100%; background: #0e0e0e; font-family: 'Google Sans Text', sans-serif; overflow: hidden; }
187
+ @keyframes prompt-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
188
+ .prompt-loading { background: linear-gradient(90deg, rgba(255,255,255,0.02) 25%, rgba(255,255,255,0.05) 50%, rgba(255,255,255,0.02) 75%); background-size: 200% 100%; animation: prompt-shimmer 2s infinite linear; }
189
+ @keyframes dropdown-enter { from { opacity: 0; transform: scale(0.95) translateY(-5px); } to { opacity: 1; transform: scale(1) translateY(0); } }
190
+ .animate-dropdown { animation: dropdown-enter 0.1s ease-out forwards; }
191
+ .flow-workspace-picker { position: fixed; inset: 0; display: grid; place-items: center; padding: 16px; color: #f5f5f5; background: #050505; overflow: auto; }
192
+ .flow-workspace-picker__card { box-sizing: border-box; display: grid; gap: 16px; width: min(100%, 448px); padding: 24px; background: #171717; border: 1px solid #404040; border-radius: 12px; box-shadow: 0 25px 50px rgba(0,0,0,.55); }
193
+ .flow-workspace-picker__card header { display: grid; gap: 4px; }
194
+ .flow-workspace-picker__card header strong { font-size: 22px; }
195
+ .flow-workspace-picker__card header small, .flow-workspace-picker__choices small { color: #a3a3a3; }
196
+ .flow-workspace-picker__choices { display: grid; gap: 8px; }
197
+ .flow-workspace-picker__choices button { display: flex; justify-content: space-between; align-items: center; min-height: 46px; padding: 10px 12px; color: #f5f5f5; background: #050505; border: 1px solid #404040; border-radius: 8px; font: inherit; cursor: pointer; }
198
+ .flow-workspace-picker__choices button:hover { border-color: #3b82f6; background: #262626; }
199
+ .flow-workspace-picker__back { justify-self: start; padding: 0; color: #a3a3a3; background: transparent; border: 0; font: inherit; cursor: pointer; }
200
+ .flow-workspace-picker__warning { margin: 0; padding: 10px; color: #fbbf24; background: rgba(146,64,14,.2); border: 1px solid rgba(217,119,6,.4); border-radius: 8px; font-size: 12px; }
201
+ .flow-szenen-pane {
202
+ --sz-bg: #0e0e0e; --sz-surface: #171717; --sz-elevated: #262626;
203
+ --sz-border: #404040; --sz-text: #f5f5f5; --sz-muted: #a3a3a3;
204
+ --color-bg-app: #000; --color-bg-surface: #0a0a0a; --color-bg-surface-hover: #171717; --color-bg-elevated: #171717;
205
+ --color-border: #262626; --color-border-hover: #404040;
206
+ --color-text-primary: #fff; --color-text-secondary: #a3a3a3; --color-text-tertiary: #525252;
207
+ --color-accent-blue: #3b82f6; --color-accent-blue-dim: rgba(59,130,246,.2);
208
+ --color-accent-purple: #a855f7; --color-accent-green: #22c55e; --color-accent-green-dim: rgba(34,197,94,.2);
209
+ --color-accent-red: #ef4444; --color-accent-red-dim: rgba(239,68,68,.2);
210
+ --color-success: #22c55e; --color-warning: #eab308; --color-danger: #ef4444;
211
+ --space-xs: .25rem; --space-sm: .5rem; --space-md: 1rem;
212
+ --radius-sm: .25rem; --radius-md: .5rem; --radius-lg: .75rem; --radius-full: 9999px;
213
+ --font-sans: system-ui, -apple-system, sans-serif;
214
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
215
+ flex: 1; min-height: 0; display: flex; flex-direction: column; padding: 16px;
216
+ color: var(--sz-text); background: var(--sz-bg); overflow: auto;
217
+ }
218
+ .flow-szenen-pane--empty { align-items: center; justify-content: center; text-align: center; color: var(--sz-muted); font-size: 12px; }
219
+ @media (max-width: 760px) { .flow-szenen-pane { padding: 8px; } }
220
+ ` + SZENEN_VIEW_STYLES;
221
+
33
222
  // src/lib/metadata.ts
223
+ var crcTable = (() => {
224
+ let c;
225
+ const table = new Uint32Array(256);
226
+ for (let n = 0; n < 256; n++) {
227
+ c = n;
228
+ for (let k = 0; k < 8; k++) {
229
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
230
+ }
231
+ table[n] = c;
232
+ }
233
+ return table;
234
+ })();
34
235
  function calculateCRC(bytes) {
35
236
  let crc = 4294967295;
36
237
  for (let i = 0; i < bytes.length; i++) {
@@ -136,817 +337,47 @@ function injectXMPMetadata(base64, prompt, seed, model, id, tags = [], hierarchy
136
337
  return base64;
137
338
  }
138
339
  }
139
- var crcTable;
140
- var init_metadata = __esm({
141
- "src/lib/metadata.ts"() {
142
- "use strict";
143
- crcTable = (() => {
144
- let c;
145
- const table = new Uint32Array(256);
146
- for (let n = 0; n < 256; n++) {
147
- c = n;
148
- for (let k = 0; k < 8; k++) {
149
- c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
150
- }
151
- table[n] = c;
152
- }
153
- return table;
154
- })();
155
- }
156
- });
157
340
 
158
- // src/lib/project.ts
159
- var project_exports = {};
160
- __export(project_exports, {
161
- exportProjectToZip: () => exportProjectToZip,
162
- importProjectFromZip: () => importProjectFromZip
163
- });
164
- function generateMarkdownReport(nodes, history) {
165
- let md = "# Avatar Architect - Projekt Report\n\n";
166
- md += `Exportiert: ${(/* @__PURE__ */ new Date()).toLocaleString()}
341
+ // src/lib/aiHelpers.ts
342
+ function buildGenerationPrompt(hierarchyText, mode = "creative") {
343
+ const safe = hierarchyText.length > 2500 ? hierarchyText.slice(0, 2500) + "..." : hierarchyText;
344
+ if (mode === "literal") {
345
+ return `Translate this hierarchy into a literal image generation prompt. Output ONLY the raw prompt string. No chat.
167
346
 
168
- `;
169
- md += "## Hierarchie\n";
170
- nodes.forEach((n) => {
171
- md += `- **${n.data?.label || "Unbenannt"}**
172
- `;
173
- });
174
- md += "\n## Historie der Generationen\n";
175
- history.forEach((h, i) => {
176
- md += `### ${i + 1}. Generation (${h.model || "AI"})
177
- `;
178
- md += `- **Zeitstempel**: ${new Date(h.timestamp).toLocaleString()}
179
- `;
180
- md += `- **Seed**: ${h.seed}
181
- `;
182
- md += `- **Prompt**:
183
- > ${h.prompt}
347
+ HIERARCHY:
348
+ ${safe}`;
349
+ }
350
+ return `Create a cinematic, high-quality image generation prompt based on this hierarchy. Translate everything to English. Output ONLY the prompt string. No chat.
184
351
 
185
- `;
186
- });
187
- return md;
352
+ HIERARCHY:
353
+ ${safe}`;
188
354
  }
189
- async function exportProjectToZip(nodes, edges, history, galleryItems, settings, workspaceTags, recentLabItemIds) {
190
- const zip = new import_jszip.default();
191
- const projectData = {
192
- nodes,
193
- edges,
194
- history: history.map((h) => ({ ...h, base64: void 0 })),
195
- galleryItems: galleryItems.map((g) => ({ ...g, base64: void 0 })),
196
- settings,
197
- workspaceTags: workspaceTags || null,
198
- recentLabItemIds: recentLabItemIds || [],
199
- version: "1.3.0"
200
- };
201
- zip.file("project.json", JSON.stringify(projectData, null, 2));
202
- zip.file("projekt_bericht.md", generateMarkdownReport(nodes, history));
203
- const imgFolder = zip.folder("images");
204
- if (imgFolder) {
205
- const allItems = [...history, ...galleryItems];
206
- for (const item of allItems) {
207
- if (item.base64) {
208
- try {
209
- const cleanB64 = item.base64.includes(",") ? item.base64.split(",")[1] : item.base64;
210
- const withMeta = injectXMPMetadata(cleanB64, item.prompt || "", item.seed, item.model, item.id, item.tags || []);
211
- imgFolder.file(`${item.id}.png`, withMeta, { base64: true });
212
- } catch (e) {
213
- console.error("Fehler beim Packen eines Bildes:", e);
214
- }
215
- }
216
- }
217
- }
218
- const blob = await zip.generateAsync({ type: "blob", compression: "STORE" });
219
- return blobToBase64(blob);
355
+ function buildFallbackPrompt(hierarchyText) {
356
+ const lines = hierarchyText.split("\n");
357
+ const firstConcept = lines.find((l) => l.includes("-"))?.replace(/^[-\s]+/, "") || "character concept";
358
+ return `Cinematic professional photograph of ${firstConcept}, masterpiece, highly detailed, fine art style`;
220
359
  }
221
- async function blobToBase64(blob) {
222
- return new Promise((resolve, reject) => {
223
- const reader = new FileReader();
224
- reader.onloadend = () => {
225
- const result = reader.result;
226
- resolve({ base64: result.split(",")[1] });
227
- };
228
- reader.onerror = reject;
229
- reader.readAsDataURL(blob);
230
- });
360
+ function cleanAiResponse(text) {
361
+ return text.replace(/^["']|["']$/g, "").replace(/```/g, "").trim();
231
362
  }
232
- async function importProjectFromZip(file) {
233
- const zip = await import_jszip.default.loadAsync(file);
234
- const jsonStr = await zip.file("project.json")?.async("string");
235
- if (!jsonStr) throw new Error("Keine project.json gefunden.");
236
- const data = JSON.parse(jsonStr);
237
- const imgFolder = zip.folder("images");
238
- if (imgFolder) {
239
- const loadImgs = async (items) => {
240
- if (!items) return [];
241
- return Promise.all(items.map(async (h) => {
242
- const imgFile = zip.file(`images/${h.id}.png`);
243
- if (!imgFile) return h;
244
- const b64 = await imgFile.async("base64");
245
- return { ...h, base64: `data:image/png;base64,${b64}` };
246
- }));
247
- };
248
- data.history = await loadImgs(data.history || []);
249
- data.galleryItems = await loadImgs(data.galleryItems || []);
363
+ function buildImageGenerationOptions(prompt, aspectRatio, model, referenceMediaId) {
364
+ const options = {
365
+ prompt: prompt.replace(/\n/g, " ").trim().slice(0, 1500),
366
+ aspectRatio,
367
+ modelDisplayName: model
368
+ };
369
+ if (referenceMediaId && referenceMediaId.length > 5) {
370
+ options.referenceImageMediaIds = [referenceMediaId];
250
371
  }
251
- return data;
372
+ return options;
252
373
  }
253
- var import_jszip;
254
- var init_project = __esm({
255
- "src/lib/project.ts"() {
256
- "use strict";
257
- import_jszip = __toESM(require("jszip"));
258
- init_metadata();
259
- }
260
- });
261
-
262
- // src/lib/hfEventTypes.ts
263
- function bildEndungFuer(mimeType) {
264
- if (mimeType === "image/png") return "png";
265
- if (mimeType === "image/webp") return "webp";
266
- return "jpg";
267
- }
268
- var HF_BASE, HF_REPO, CURRENT_EVENT_VERSION, SPEICHERBARE_BILDTYPEN;
269
- var init_hfEventTypes = __esm({
270
- "src/lib/hfEventTypes.ts"() {
271
- "use strict";
272
- HF_BASE = "https://huggingface.co";
273
- HF_REPO = "RolandSch/fa-app-state";
274
- CURRENT_EVENT_VERSION = { major: 1, minor: 0 };
275
- SPEICHERBARE_BILDTYPEN = ["image/png", "image/jpeg", "image/webp"];
276
- }
277
- });
278
-
279
- // src/lib/hfEventLog.ts
280
- function treeUrl(namespace, subdir = "") {
281
- const parts = [namespace.replace(/\/$/, ""), subdir].filter(Boolean).join("/");
282
- return `${HF_BASE}/api/datasets/${HF_REPO}/tree/main${parts ? "/" + parts : ""}`;
283
- }
284
- async function holeMitZeitlimit(url, init) {
285
- const abbruch = new AbortController();
286
- const timer = setTimeout(() => abbruch.abort(), HF_LESE_ZEITLIMIT_MS);
287
- try {
288
- return await fetch(url, { ...init, signal: abbruch.signal });
289
- } catch (error) {
290
- if (abbruch.signal.aborted) {
291
- throw new Error(`HF-Anfrage ohne Antwort nach ${HF_LESE_ZEITLIMIT_MS}ms abgebrochen: ${url}`);
292
- }
293
- throw error;
294
- } finally {
295
- clearTimeout(timer);
296
- }
297
- }
298
- async function mitGeduld(anfrage) {
299
- let letzte = null;
300
- for (let versuch = 0; versuch < DROSSEL_VERSUCHE; versuch += 1) {
301
- const res = await anfrage();
302
- if (res.status !== 429 && res.status < 500) return res;
303
- letzte = res;
304
- if (versuch === DROSSEL_VERSUCHE - 1) break;
305
- const angesagt = Number(res.headers.get("retry-after"));
306
- const warten2 = Number.isFinite(angesagt) && angesagt > 0 ? angesagt * 1e3 : DROSSEL_BASIS_MS * 2 ** versuch;
307
- await schlafen(warten2);
308
- }
309
- return letzte;
310
- }
311
- async function hfListDir(namespace, subdir, token) {
312
- const res = await mitGeduld(() => holeMitZeitlimit(treeUrl(namespace, subdir), {
313
- headers: { Authorization: `Bearer ${token}` }
314
- }));
315
- if (res.status === 404) return [];
316
- if (!res.ok) throw new Error(`HF list failed: ${res.status} ${res.statusText}`);
317
- return res.json();
318
- }
319
- async function hfDownloadJsonByPath(repoPath, token) {
320
- const res = await mitGeduld(() => holeMitZeitlimit(
321
- `${HF_BASE}/datasets/${HF_REPO}/resolve/main/${repoPath}?download=true`,
322
- { headers: { Authorization: `Bearer ${token}` } }
323
- ));
324
- if (!res.ok) throw new Error(`HF download failed: ${res.status}`);
325
- return res.json();
326
- }
327
- async function hfDownloadBinaryByPath(repoPath, token) {
328
- const res = await mitGeduld(() => holeMitZeitlimit(
329
- `${HF_BASE}/datasets/${HF_REPO}/resolve/main/${repoPath}?download=true`,
330
- { headers: { Authorization: `Bearer ${token}` } }
331
- ));
332
- if (!res.ok) throw new Error(`HF download binary failed: ${res.status}`);
333
- return new Uint8Array(await res.arrayBuffer());
334
- }
335
- function toBase64(text) {
336
- const bytes = new TextEncoder().encode(text);
337
- let binary = "";
338
- bytes.forEach((b) => binary += String.fromCharCode(b));
339
- return btoa(binary);
340
- }
341
- async function hfUploadSmallFile(repoPath, content, token, summary = `Update ${repoPath}`) {
342
- const res = await fetch(`${HF_BASE}/api/datasets/${HF_REPO}/commit/main`, {
343
- method: "POST",
344
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/x-ndjson" },
345
- body: [
346
- JSON.stringify({ key: "header", value: { summary, description: "" } }),
347
- JSON.stringify({ key: "file", value: { path: repoPath, encoding: "base64", content: toBase64(content) } })
348
- ].join("\n")
349
- });
350
- if (!res.ok) {
351
- const err = await res.text().catch(() => "");
352
- throw new Error(`HF upload failed: ${res.status} \u2014 ${err.slice(0, 200)}`);
353
- }
354
- }
355
- async function hfBatchArchive(moves, token, summary) {
356
- const lines = [
357
- JSON.stringify({ key: "header", value: { summary, description: "" } }),
358
- ...moves.flatMap(({ from, to, content }) => [
359
- JSON.stringify({ key: "file", value: { path: to, encoding: "base64", content: toBase64(content) } }),
360
- JSON.stringify({ key: "deletedFile", value: { path: from } })
361
- ])
362
- ];
363
- const res = await fetch(`${HF_BASE}/api/datasets/${HF_REPO}/commit/main`, {
364
- method: "POST",
365
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/x-ndjson" },
366
- body: lines.join("\n")
367
- });
368
- if (!res.ok) {
369
- const err = await res.text().catch(() => "");
370
- throw new Error(`HF batch archive failed: ${res.status} \u2014 ${err.slice(0, 200)}`);
371
- }
372
- }
373
- function tsFromEventPath(repoPath) {
374
- const filename = repoPath.split("/").pop() || "";
375
- const base = filename.replace(/_[^_]+\.json$/, "");
376
- const iso = base.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/, "T$1:$2:$3.$4Z");
377
- const ts = Date.parse(iso);
378
- return isNaN(ts) ? 0 : ts;
379
- }
380
- async function mitBegrenzterParallelitaet(count, limit, task) {
381
- let cursor = 0;
382
- let ersterFehler;
383
- let fehlerAufgetreten = false;
384
- const worker = async () => {
385
- while (cursor < count) {
386
- const index = cursor++;
387
- try {
388
- await task(index);
389
- } catch (error) {
390
- if (!fehlerAufgetreten) {
391
- fehlerAufgetreten = true;
392
- ersterFehler = error;
393
- }
394
- }
395
- }
396
- };
397
- await Promise.all(Array.from({ length: Math.min(limit, count) }, worker));
398
- if (fehlerAufgetreten) throw ersterFehler;
399
- }
400
- async function loadPendingEvents(namespace, token, sinceTs, knownPaths) {
401
- const files = await hfListDir(namespace, "events", token);
402
- const pending = files.filter((f) => f.type === "file" && tsFromEventPath(f.path) > sinceTs && !knownPaths?.has(f.path)).sort((a, b) => a.path.localeCompare(b.path));
403
- if (!pending.length) return [];
404
- const raw = new Array(pending.length);
405
- await mitBegrenzterParallelitaet(pending.length, EVENTDOWNLOAD_PARALLELITAET, async (index) => {
406
- raw[index] = await hfDownloadJsonByPath(pending[index].path, token);
407
- });
408
- for (const f of pending) knownPaths?.add(f.path);
409
- return raw.flatMap((e) => Array.isArray(e) ? e : [e]);
410
- }
411
- async function listEventPaths(namespace, token) {
412
- const files = await hfListDir(namespace, "events", token);
413
- return files.filter((f) => f.type === "file").map((f) => f.path).sort((a, b) => a.localeCompare(b));
414
- }
415
- function getSessionClientId() {
416
- return SESSION_CLIENT_ID;
417
- }
418
- function tsToFilename(ts) {
419
- return new Date(ts).toISOString().replace(/:/g, "-").replace(".", "-");
420
- }
421
- async function writeHFEvent(namespace, token, type, payload, prevTs, options) {
422
- const wallTs = Date.now();
423
- const ts = options?.ordered ? Math.max(wallTs, lastOrderedTs) : wallTs;
424
- if (options?.ordered) lastOrderedTs = ts;
425
- const uuid = crypto.randomUUID().slice(0, 8);
426
- const event = {
427
- v: CURRENT_EVENT_VERSION,
428
- type,
429
- ts,
430
- prevTs,
431
- clientId: SESSION_CLIENT_ID,
432
- payload
433
- };
434
- const suffix = options?.ordered ? `${String(++writeSeq % 1e6).padStart(6, "0")}${uuid}` : uuid;
435
- const filename = `${tsToFilename(ts)}_${suffix}.json`;
436
- const repoPath = `${namespace}events/${filename}`;
437
- await hfUploadSmallFile(repoPath, JSON.stringify(event, null, 2), token, `Event: ${type}`);
438
- return event;
439
- }
440
- var DROSSEL_VERSUCHE, DROSSEL_BASIS_MS, schlafen, HF_LESE_ZEITLIMIT_MS, EVENTDOWNLOAD_PARALLELITAET, SESSION_CLIENT_ID, writeSeq, lastOrderedTs;
441
- var init_hfEventLog = __esm({
442
- "src/lib/hfEventLog.ts"() {
443
- "use strict";
444
- init_hfEventTypes();
445
- DROSSEL_VERSUCHE = 4;
446
- DROSSEL_BASIS_MS = 400;
447
- schlafen = (ms) => new Promise((r) => setTimeout(r, ms));
448
- HF_LESE_ZEITLIMIT_MS = 2e4;
449
- EVENTDOWNLOAD_PARALLELITAET = 3;
450
- SESSION_CLIENT_ID = `client-${crypto.randomUUID().slice(0, 8)}`;
451
- writeSeq = 0;
452
- lastOrderedTs = 0;
453
- }
454
- });
455
-
456
- // src/lib/hfStateService.ts
457
- var hfStateService_exports = {};
458
- __export(hfStateService_exports, {
459
- HF_TOKEN_KEY: () => HF_TOKEN_KEY,
460
- getHFToken: () => getHFToken,
461
- getSessionClientId: () => getSessionClientId,
462
- hfBatchArchive: () => hfBatchArchive,
463
- hfBootstrapFromLegacy: () => hfBootstrapFromLegacy,
464
- hfDeleteProject: () => hfDeleteProject,
465
- hfDownloadBinaryByPath: () => hfDownloadBinaryByPath,
466
- hfDownloadJsonByPath: () => hfDownloadJsonByPath,
467
- hfDownloadProject: () => hfDownloadProject,
468
- hfListDir: () => hfListDir,
469
- hfListProjects: () => hfListProjects,
470
- hfLoadImageAsBase64: () => hfLoadImageAsBase64,
471
- hfLoadMetadata: () => hfLoadMetadata,
472
- hfLoadTags: () => hfLoadTags,
473
- hfSaveMetadata: () => hfSaveMetadata,
474
- hfSaveTags: () => hfSaveTags,
475
- hfUploadImage: () => hfUploadImage,
476
- hfUploadProject: () => hfUploadProject,
477
- hfUploadProjectForm: () => hfUploadProjectForm,
478
- hfUploadSmallFile: () => hfUploadSmallFile,
479
- listEventPaths: () => listEventPaths,
480
- loadHFState: () => loadHFState,
481
- loadPendingEvents: () => loadPendingEvents,
482
- setHFToken: () => setHFToken,
483
- tsFromEventPath: () => tsFromEventPath,
484
- writeHFEvent: () => writeHFEvent
485
- });
486
- function getHFToken() {
487
- try {
488
- return localStorage.getItem(HF_TOKEN_KEY);
489
- } catch {
490
- return null;
491
- }
492
- }
493
- function setHFToken(token) {
494
- try {
495
- localStorage.setItem(HF_TOKEN_KEY, token);
496
- } catch {
497
- }
498
- }
499
- async function loadHFState(namespace, token) {
500
- const files = await hfListDir(namespace, "", token);
501
- const stateFiles = files.filter((f) => f.type === "file" && /^state-[\dT\-]+Z\.zip$/.test(f.path.split("/").pop() || "")).sort((a, b) => b.path.localeCompare(a.path));
502
- if (!stateFiles.length) return null;
503
- const zipBytes = await hfDownloadBinaryByPath(stateFiles[0].path, token);
504
- const zip = await import_jszip2.default.loadAsync(zipBytes);
505
- const [metadataStr, tagsStr, metaStr, openTasksStr] = await Promise.all([
506
- zip.file("metadata.json")?.async("string"),
507
- zip.file("tags.json")?.async("string"),
508
- zip.file("state_meta.json")?.async("string"),
509
- zip.file("open_tasks.json")?.async("string")
510
- // optional — ältere Snapshots haben es nicht
511
- ]);
512
- if (!metadataStr || !tagsStr || !metaStr) throw new Error("state.zip: fehlende Pflicht-Dateien");
513
- return {
514
- metadata: JSON.parse(metadataStr),
515
- tags: JSON.parse(tagsStr),
516
- meta: JSON.parse(metaStr),
517
- openTasks: openTasksStr ? JSON.parse(openTasksStr) : {}
518
- };
519
- }
520
- async function hfBootstrapFromLegacy(namespace, token, onProgress) {
521
- const log = (msg) => {
522
- if (onProgress) onProgress(msg);
523
- };
524
- log("Lese tags.json \u2026");
525
- const tags = await fetch(
526
- `${HF_BASE}/datasets/${HF_REPO}/resolve/main/tags.json?download=true`,
527
- { headers: { Authorization: `Bearer ${token}` } }
528
- ).then((r) => r.ok ? r.json() : null).catch(() => null);
529
- log("Lese metadata.json \u2026");
530
- const metadata = await fetch(
531
- `${HF_BASE}/datasets/${HF_REPO}/resolve/main/metadata.json?download=true`,
532
- { headers: { Authorization: `Bearer ${token}` } }
533
- ).then((r) => r.ok ? r.json() : []).catch(() => []);
534
- const snapshot = {
535
- metadata: Array.isArray(metadata) ? metadata : [],
536
- tags: tags?.by_category ? tags : { by_category: {}, all: [] },
537
- meta: { consolidatedAt: Date.now(), version: 1 }
538
- };
539
- log(`Erstelle state.zip (${snapshot.metadata.length} Bilder, ${snapshot.tags.all?.length ?? 0} Tags) \u2026`);
540
- const zip = new import_jszip2.default();
541
- zip.file("metadata.json", JSON.stringify(snapshot.metadata, null, 2));
542
- zip.file("tags.json", JSON.stringify(snapshot.tags, null, 2));
543
- zip.file("state_meta.json", JSON.stringify(snapshot.meta, null, 2));
544
- const zipBytes = await zip.generateAsync({ type: "uint8array", compression: "DEFLATE" });
545
- const ts = snapshot.meta.consolidatedAt;
546
- const stateFilename = `state-${new Date(ts).toISOString().replace(/:/g, "-").replace(".", "-")}.zip`;
547
- const repoPath = `${namespace}${stateFilename}`;
548
- log("Lade state.zip hoch \u2026");
549
- await (0, import_hub.uploadFile)({
550
- repo: { type: "dataset", name: HF_REPO },
551
- credentials: { accessToken: token },
552
- file: { path: repoPath, content: new Blob([zipBytes.buffer], { type: "application/zip" }) },
553
- branch: "main",
554
- commitTitle: `Bootstrap: initialer State aus Legacy-Daten`
555
- });
556
- log(`Fertig \u2014 ${stateFilename}`);
557
- }
558
- async function hfListProjects(token) {
559
- const res = await fetch(`${HF_BASE}/api/datasets/${HF_REPO}/tree/main`, {
560
- headers: { Authorization: `Bearer ${token}` }
561
- });
562
- if (!res.ok) throw new Error(`HF list failed: ${res.status} ${res.statusText}`);
563
- const files = await res.json();
564
- return files.filter((f) => f.type === "file" && f.path.endsWith(".zip")).map((f) => ({
565
- id: f.path.replace(/\.zip$/, ""),
566
- name: f.path.replace(/\.zip$/, ""),
567
- path: f.path,
568
- size: f.size,
569
- isLfs: !!f.lfs
570
- }));
571
- }
572
- async function hfDownloadProject(path, token) {
573
- const res = await fetch(
574
- `${HF_BASE}/datasets/${HF_REPO}/resolve/main/${path}?download=true`,
575
- { headers: { Authorization: `Bearer ${token}` } }
576
- );
577
- if (!res.ok) throw new Error(`HF download failed: ${res.status} ${res.statusText}`);
578
- const blob = await res.blob();
579
- return new File([blob], path, { type: "application/zip" });
580
- }
581
- async function hfUploadProject(zipBase64, name, token) {
582
- const filename = name.endsWith(".zip") ? name : `${name}.zip`;
583
- const binary = atob(zipBase64);
584
- const bytes = new Uint8Array(binary.length);
585
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
586
- await (0, import_hub.uploadFile)({
587
- repo: { type: "dataset", name: HF_REPO },
588
- credentials: { accessToken: token },
589
- file: { path: filename, content: new Blob([bytes], { type: "application/zip" }) },
590
- branch: "main",
591
- commitTitle: `Upload ${filename}`
592
- });
593
- return { id: filename.replace(/\.zip$/, ""), name: filename.replace(/\.zip$/, ""), path: filename, size: bytes.length, isLfs: true };
594
- }
595
- async function hfUploadProjectForm(_zipBase64, _name, _token) {
596
- throw new Error("hfUploadProjectForm is deprecated. Use hfUploadProject instead.");
597
- }
598
- async function hfDeleteProject(path, token) {
599
- const res = await fetch(`${HF_BASE}/api/datasets/${HF_REPO}/commit/main`, {
600
- method: "POST",
601
- headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/x-ndjson" },
602
- body: [
603
- JSON.stringify({ key: "header", value: { summary: `Delete ${path}`, description: "" } }),
604
- JSON.stringify({ key: "deletedFile", value: { path } })
605
- ].join("\n")
606
- });
607
- if (!res.ok) throw new Error(`HF delete failed: ${res.status} ${res.statusText}`);
608
- }
609
- async function hfSaveTags(_workspaceTags, _token) {
610
- throw new Error("hfSaveTags is deprecated. Use writeHFEvent instead.");
611
- }
612
- async function hfLoadTags(token) {
613
- try {
614
- const res = await fetch(
615
- `${HF_BASE}/datasets/${HF_REPO}/resolve/main/tags.json?download=true`,
616
- { headers: { Authorization: `Bearer ${token}` } }
617
- );
618
- if (!res.ok) return null;
619
- return res.json();
620
- } catch {
621
- return null;
622
- }
623
- }
624
- async function hfSaveMetadata(_entries, _token) {
625
- throw new Error("hfSaveMetadata is deprecated. Use writeHFEvent instead.");
626
- }
627
- async function hfLoadMetadata(token) {
628
- try {
629
- const res = await fetch(
630
- `${HF_BASE}/datasets/${HF_REPO}/resolve/main/metadata.json?download=true`,
631
- { headers: { Authorization: `Bearer ${token}` } }
632
- );
633
- if (!res.ok) return [];
634
- return res.json();
635
- } catch {
636
- return [];
637
- }
638
- }
639
- async function hfUploadImage(base64, id, token, mimeType = "image/jpeg") {
640
- const ext = bildEndungFuer(mimeType);
641
- const binary = atob(base64);
642
- const bytes = new Uint8Array(binary.length);
643
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
644
- await (0, import_hub.uploadFile)({
645
- repo: { type: "dataset", name: HF_REPO },
646
- credentials: { accessToken: token },
647
- file: { path: `images/${id}.${ext}`, content: new Blob([bytes], { type: mimeType }) },
648
- branch: "main",
649
- commitTitle: `Add image ${id}`
650
- });
651
- }
652
- async function hfLoadImageAsBase64(id, token, namespace, filename, on404, mimeType, hasThumb) {
653
- const fetchBase64 = async (repoPath) => {
654
- try {
655
- const res = await fetch(
656
- `${HF_BASE}/datasets/${HF_REPO}/resolve/main/${repoPath}?download=true`,
657
- { headers: { Authorization: `Bearer ${token}` } }
658
- );
659
- if (!res.ok) {
660
- if (on404 && on404()) return "ABORT";
661
- return null;
662
- }
663
- const blob = await res.blob();
664
- return new Promise((resolve, reject) => {
665
- const reader = new FileReader();
666
- reader.onload = () => resolve(reader.result.split(",")[1]);
667
- reader.onerror = reject;
668
- reader.readAsDataURL(blob);
669
- });
670
- } catch {
671
- return null;
672
- }
673
- };
674
- if (filename) {
675
- if (hasThumb) {
676
- const thumb = await fetchBase64(`thumbs/${filename}`);
677
- if (thumb && thumb !== "ABORT") return thumb;
678
- }
679
- const result2 = await fetchBase64(`images/${filename}`);
680
- return result2 === "ABORT" ? null : result2;
681
- }
682
- const ext = bildEndungFuer(mimeType);
683
- const result = await fetchBase64(`images/${id}.${ext}`);
684
- return result === "ABORT" ? null : result;
685
- }
686
- var import_jszip2, import_hub, HF_TOKEN_KEY;
687
- var init_hfStateService = __esm({
688
- "src/lib/hfStateService.ts"() {
689
- "use strict";
690
- import_jszip2 = __toESM(require("jszip"));
691
- import_hub = require("@huggingface/hub");
692
- init_hfEventTypes();
693
- init_hfEventLog();
694
- init_hfEventLog();
695
- HF_TOKEN_KEY = "hf-token";
696
- }
697
- });
698
-
699
- // src/index.ts
700
- var index_exports = {};
701
- __export(index_exports, {
702
- AvatarArchitectApp: () => AvatarArchitectApp,
703
- CURRENT_TAG_EVENT_VERSION: () => CURRENT_TAG_EVENT_VERSION,
704
- CollapsibleCard: () => CollapsibleCard,
705
- CompactDropdown: () => CompactDropdown,
706
- FaApp: () => FaApp,
707
- FaToolsBadge: () => FaToolsBadge,
708
- FlowWorkspacePicker: () => FlowWorkspacePicker,
709
- GLOBAL_STYLES: () => GLOBAL_STYLES,
710
- HistoryPanel: () => HistoryPanel,
711
- InspectPanel: () => InspectPanel,
712
- LIBRARY_CUTOVER_MARKER: () => LIBRARY_CUTOVER_MARKER,
713
- LIBRARY_SLICE_SCHEMA_VERSION: () => LIBRARY_SLICE_SCHEMA_VERSION,
714
- LIB_VERSION: () => version,
715
- LabBlend: () => LabBlend,
716
- LabCompare: () => LabCompare,
717
- LabFrameExtractor: () => LabFrameExtractor,
718
- LabImagePicker: () => LabImagePicker,
719
- LabLoop: () => LabLoop,
720
- LabRemix: () => LabRemix,
721
- LabsTab: () => LabsTab,
722
- LibraryView: () => LibraryView,
723
- ListView: () => ListView,
724
- MediaLibrary: () => MediaLibrary,
725
- PillButton: () => PillButton,
726
- ProjectSyncTab: () => ProjectSyncTab,
727
- PromptTab: () => PromptTab,
728
- SCOPE_MAP: () => SCOPE_MAP,
729
- SectionLabel: () => SectionLabel,
730
- ServerTab: () => ServerTab,
731
- SetupPanel: () => SetupPanel,
732
- TAGS_CUTOVER_MARKER: () => TAGS_CUTOVER_MARKER,
733
- TAGS_SLICE_SCHEMA_VERSION: () => TAGS_SLICE_SCHEMA_VERSION,
734
- TagBaseSection: () => TagBaseSection,
735
- TagManager: () => TagManager,
736
- TagManagerPanel: () => TagManagerPanel,
737
- autoLabel: () => autoLabel,
738
- buildBlendInstruction: () => buildBlendInstruction,
739
- buildCompareInstruction: () => buildCompareInstruction,
740
- buildFallbackPrompt: () => buildFallbackPrompt,
741
- buildGenerationPrompt: () => buildGenerationPrompt,
742
- buildImageGenerationOptions: () => buildImageGenerationOptions,
743
- buildLoopInstruction: () => buildLoopInstruction,
744
- buildPromptTabPayload: () => buildPromptTabPayload,
745
- buildReferenceImageMediaIds: () => buildReferenceImageMediaIds,
746
- buildRemixInstruction: () => buildRemixInstruction,
747
- buildScanInstruction: () => buildScanInstruction,
748
- cleanAiResponse: () => cleanAiResponse,
749
- createFaStore: () => createFaStore,
750
- createFlowServices: () => createFlowServices,
751
- createInMemoryLibraryDataSource: () => createInMemoryLibraryDataSource,
752
- createInMemoryWorkspacePersistence: () => createInMemoryWorkspacePersistence,
753
- createLibraryDefault: () => createLibraryDefault,
754
- createPersistenceCoordinator: () => createPersistenceCoordinator,
755
- createTagCreatedEvent: () => createTagCreatedEvent,
756
- createTagUpdatedEvent: () => createTagUpdatedEvent,
757
- createTagsDefault: () => createTagsDefault,
758
- deriveTagBaseWorkspaceTags: () => deriveTagBaseWorkspaceTags,
759
- deriveTagViewModel: () => deriveTagViewModel,
760
- exportProjectToZip: () => exportProjectToZip,
761
- formatTreeToMarkdown: () => formatTreeToMarkdown,
762
- frameToGeneration: () => frameToGeneration,
763
- getFormattedTimestamp: () => getFormattedTimestamp,
764
- getHFToken: () => getHFToken,
765
- getSessionClientId: () => getSessionClientId,
766
- groupGenerationsToLabItems: () => groupGenerationsToLabItems,
767
- hfBatchArchive: () => hfBatchArchive,
768
- hfBootstrapFromLegacy: () => hfBootstrapFromLegacy,
769
- hfDeleteProject: () => hfDeleteProject,
770
- hfDownloadProject: () => hfDownloadProject,
771
- hfListDir: () => hfListDir,
772
- hfListProjects: () => hfListProjects,
773
- hfLoadImageAsBase64: () => hfLoadImageAsBase64,
774
- hfUploadImage: () => hfUploadImage,
775
- hfUploadProjectForm: () => hfUploadProjectForm,
776
- hfUploadSmallFile: () => hfUploadSmallFile,
777
- importProjectFromZip: () => importProjectFromZip,
778
- injectXMPMetadata: () => injectXMPMetadata,
779
- interpretSdkError: () => interpretSdkError,
780
- libraryContract: () => libraryContract,
781
- libraryReactDescriptor: () => libraryReactDescriptor,
782
- listCategories: () => listCategories,
783
- loadHFState: () => loadHFState,
784
- loadPendingEvents: () => loadPendingEvents,
785
- loadTagBase: () => loadTagBase,
786
- parseLegacyLibraryFilters: () => parseLegacyLibraryFilters,
787
- parseLegacyRecentTags: () => parseLegacyRecentTags,
788
- parsePromptFile: () => parsePromptFile,
789
- parsePromptResponse: () => parsePromptResponse,
790
- runLegacyMigration: () => runLegacyMigration,
791
- runLibraryLegacyImport: () => runLibraryLegacyImport,
792
- runTagsLegacyImport: () => runTagsLegacyImport,
793
- setHFToken: () => setHFToken,
794
- tagsContract: () => tagsContract,
795
- tagsReactDescriptor: () => tagsReactDescriptor,
796
- tsFromEventPath: () => tsFromEventPath,
797
- useFaSlice: () => useFaSlice,
798
- useFaSliceMeta: () => useFaSliceMeta,
799
- useHFState: () => useHFState,
800
- useKeyboardNavigation: () => useKeyboardNavigation,
801
- useOnClickOutside: () => useOnClickOutside,
802
- useTagBase: () => useTagBase,
803
- validateTagBaseSelection: () => validateTagBaseSelection,
804
- writeHFEvent: () => writeHFEvent,
805
- writeTagEvent: () => writeTagEvent
806
- });
807
- module.exports = __toCommonJS(index_exports);
808
-
809
- // package.json
810
- var version = "6.1.0";
811
-
812
- // src/hooks/useOnClickOutside.ts
813
- var import_react = require("react");
814
- function useOnClickOutside(ref, handler) {
815
- (0, import_react.useEffect)(() => {
816
- const listener = (event) => {
817
- if (!ref.current || ref.current.contains(event.target)) return;
818
- handler(event);
819
- };
820
- document.addEventListener("mousedown", listener);
821
- document.addEventListener("touchstart", listener);
822
- return () => {
823
- document.removeEventListener("mousedown", listener);
824
- document.removeEventListener("touchstart", listener);
825
- };
826
- }, [ref, handler]);
827
- }
828
-
829
- // src/hooks/useKeyboardNavigation.ts
830
- var import_react2 = require("react");
831
- function useKeyboardNavigation(history, currentResult, setCurrentResult) {
832
- (0, import_react2.useEffect)(() => {
833
- const handleKeyDown = (e) => {
834
- if (e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLInputElement) return;
835
- if (!currentResult || history.length === 0) return;
836
- const currentIndex = history.findIndex((h) => h.id === currentResult.id);
837
- if (e.key === "ArrowRight" && currentIndex < history.length - 1) {
838
- setCurrentResult(history[currentIndex + 1]);
839
- } else if (e.key === "ArrowLeft" && currentIndex > 0) {
840
- setCurrentResult(history[currentIndex - 1]);
841
- }
842
- };
843
- window.addEventListener("keydown", handleKeyDown);
844
- return () => window.removeEventListener("keydown", handleKeyDown);
845
- }, [currentResult, history, setCurrentResult]);
846
- }
847
-
848
- // src/features/szenen/szenenViewStyles.generated.ts
849
- var SZENEN_VIEW_STYLES = '/* ==========================================================================\n SZENEN VIEW\n ========================================================================== */\n\n.szenen-view {\n display: grid;\n gap: var(--space-md);\n width: 100%;\n min-width: 0;\n font-family: var(--font-sans);\n padding: var(--space-md);\n}\n\n@media (max-width: 760px) {\n .szenen-view {\n padding: var(--space-sm);\n gap: var(--space-sm);\n }\n}\n\n.szenen-view__intro {\n display: flex;\n align-items: end;\n justify-content: space-between;\n gap: var(--space-md);\n padding: clamp(1rem, 3vw, 1.75rem);\n color: var(--color-text-primary);\n background:\n radial-gradient(circle at 85% 15%, rgba(45, 212, 191, 0.16), transparent 34%),\n linear-gradient(135deg, var(--color-bg-surface), var(--color-bg-elevated));\n border: 1px solid var(--color-border);\n border-radius: var(--radius-lg);\n}\n\n.szenen-view__intro h2,\n.szenen-view__intro p {\n margin: 0;\n}\n\n.szenen-view__intro h2 {\n margin-bottom: var(--space-xs);\n font-size: clamp(1.25rem, 4vw, 2rem);\n}\n\n.szenen-view__intro > div > p:last-child {\n max-width: 44rem;\n color: var(--color-text-secondary);\n}\n\n.szenen-view__eyebrow {\n margin-bottom: var(--space-xs) !important;\n color: var(--color-accent-teal, #2dd4bf);\n font-size: 0.72rem;\n font-weight: 800;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n}\n\n.szenen-view__step {\n flex: none;\n padding: 0.35rem 0.65rem;\n color: var(--color-text-primary);\n background: rgba(45, 212, 191, 0.12);\n border: 1px solid rgba(45, 212, 191, 0.35);\n border-radius: var(--radius-full);\n font-size: 0.78rem;\n font-weight: 700;\n}\n\n.szenen-view__nav {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-view__nav button,\n.szenen-view button {\n min-height: 2.25rem;\n padding: 0.45rem 0.75rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-view button:hover:not(:disabled) {\n color: var(--color-text-primary);\n background: var(--color-bg-surface-hover);\n border-color: var(--color-border-hover);\n}\n\n.szenen-view button:focus-visible,\n.szenen-view input:focus-visible,\n.szenen-view select:focus-visible,\n.szenen-view textarea:focus-visible {\n outline: 2px solid var(--color-accent-blue);\n outline-offset: 2px;\n}\n\n.szenen-view button:disabled {\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.szenen-view__nav button.is-active,\n.szenen-view__nav button[aria-selected="true"] {\n color: var(--color-text-primary);\n background: var(--color-accent-blue-dim);\n border-color: var(--color-accent-blue);\n}\n\n.szenen-view__area {\n min-width: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-view__capability-note,\n.szenen-area__lead {\n margin: 0;\n padding: var(--space-sm) var(--space-md);\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border-left: 3px solid var(--color-accent-purple);\n border-radius: var(--radius-sm);\n}\n\n.szenen-view__host-notes {\n color: var(--color-text-secondary);\n}\n\n.szenen-view__host-notes > summary {\n cursor: pointer;\n font-size: 0.8rem;\n}\n\n/* `box-sizing` steht hier und nicht beim Host: Die React-App bringt in\n * `master.css` einen globalen `* { box-sizing: border-box }` mit, Flow **nicht**\n * \u2014 dort ragte `width: 100%` plus Innenabstand um die Randbreite aus dem\n * Elternelement heraus und erzeugte auf ~390 px einen horizontalen Scroll. Die\n * gemeinsame Ansicht darf sich auf keinen Host-Reset verlassen. */\n.szenen-view input:not([type="checkbox"]),\n.szenen-view select,\n.szenen-view textarea {\n box-sizing: border-box;\n width: 100%;\n min-height: 2.25rem;\n padding: 0.5rem 0.65rem;\n color: var(--color-text-primary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n font: inherit;\n}\n\n.szenen-view textarea {\n min-height: 7rem;\n resize: vertical;\n font-family: var(--font-mono);\n font-size: 0.8rem;\n}\n\n.szenen-view label {\n display: grid;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-size: 0.85rem;\n}\n\n.szenen-view code,\n.szenen-view pre {\n font-family: var(--font-mono);\n overflow-wrap: anywhere;\n}\n\n.szenen-view h3,\n.szenen-view h4,\n.szenen-view h5 {\n margin-top: 0;\n}\n\n.szenen-view ul,\n.szenen-view ol {\n padding-left: 1.25rem;\n}\n\n.szenen-elements,\n.szenen-detail,\n.szenen-scene,\n.szenen-setup,\n.szenen-shot,\n.szenen-media,\n.szenen-runs {\n display: grid;\n gap: var(--space-md);\n min-width: 0;\n}\n\n/* Arbeitsoberflaeche: Liste links, Bearbeitung rechts.\n *\n * Breit stehen beide Spalten nebeneinander \u2014 die Auswahl bleibt sichtbar,\n * waehrend bearbeitet wird. Schmal rendert die Komponente nur eine der beiden;\n * hier steht deshalb keine Regel, die die andere versteckt. */\n@media (min-width: 761px) {\n .szenen-elements {\n grid-template-columns: minmax(0, 22rem) minmax(0, 1fr);\n align-items: start;\n }\n\n .szenen-elements__master,\n .szenen-elements__detail {\n min-width: 0;\n }\n}\n\n.szenen-elements__master,\n.szenen-elements__detail {\n display: grid;\n gap: var(--space-sm);\n align-content: start;\n min-width: 0;\n}\n\n.szenen-elements__toolbar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-sm);\n}\n\n.szenen-elements__heading {\n margin: 0;\n font-size: 1.05rem;\n}\n\n.szenen-elements__scope {\n display: flex;\n align-items: center;\n gap: var(--space-xs);\n min-width: 0;\n color: var(--color-text-secondary);\n font-size: 0.8rem;\n}\n\n.szenen-elements__scope > select {\n flex: 1 1 auto;\n min-width: 0;\n}\n\n.szenen-elements__new {\n flex: none;\n font-weight: 700;\n}\n\n.szenen-elements__create {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-accent-blue);\n border-radius: var(--radius-sm);\n}\n\n.szenen-elements__create label {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-elements__create-actions {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n}\n\n.szenen-elements__options > summary {\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n cursor: pointer;\n}\n\n.szenen-elements__back {\n justify-self: start;\n font-weight: 700;\n}\n\n.szenen-elements__empty-detail {\n margin: 0;\n color: var(--color-text-tertiary);\n}\n\n.szenen-elements__filter,\n.szenen-detail__actions,\n.szenen-runs__start,\n.szenen-scene-editor__add,\n.szenen-scene-editor__add-relation {\n display: flex;\n flex-wrap: wrap;\n align-items: end;\n gap: var(--space-sm);\n}\n\n.szenen-elements__filter > label,\n.szenen-runs__start > label,\n.szenen-scene-editor__add > input {\n flex: 1 1 16rem;\n}\n\n.szenen-elements__filter > label:has(input[type="checkbox"]) {\n display: flex;\n flex: 1 1 16rem;\n flex-direction: row;\n align-items: center;\n}\n\n.szenen-elements__filter input[type="checkbox"] {\n width: 1rem;\n height: 1rem;\n accent-color: var(--color-accent-blue);\n}\n\n.szenen-elements__list {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-elements__list > li > button {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs) var(--space-sm);\n width: 100%;\n height: 100%;\n text-align: left;\n}\n\n.szenen-elements__list > li > button > .szenen-elements__title {\n flex: 1 1 100%;\n}\n\n.szenen-elements__list > li > button.is-selected,\n.szenen-elements__list > li > button[aria-current="true"] {\n background: var(--color-accent-blue-dim);\n border-color: var(--color-accent-blue);\n}\n\n.szenen-elements__type {\n color: var(--color-accent-blue);\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n}\n\n.szenen-elements__title {\n color: var(--color-text-primary);\n font-size: 0.95rem;\n}\n\n.szenen-elements__subtype,\n.szenen-elements__retired {\n color: var(--color-text-secondary);\n font-size: 0.75rem;\n}\n\n.szenen-elements__retired {\n color: var(--color-warning);\n}\n\n.szenen-detail {\n margin-top: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n@media (min-width: 761px) {\n .szenen-detail {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n }\n}\n\n.szenen-detail > header,\n.szenen-detail__heads,\n.szenen-detail__draft,\n.szenen-capability {\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n/* .szenen-runs ist ein eigenst\xE4ndiger sichtbarer Bereich, beh\xE4lt Styling auf Desktop */\n@media (min-width: 761px) {\n .szenen-runs {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n }\n}\n\n@media (max-width: 760px) {\n .szenen-runs {\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n }\n}\n\n.szenen-detail__ids {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin-bottom: 0;\n color: var(--color-text-secondary);\n}\n\n.szenen-detail__heads ul,\n.szenen-scene-editor ul,\n.szenen-runs > ol,\n.szenen-capability > ul {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-detail__heads li,\n.szenen-scene-editor li,\n.szenen-runs > ol > li,\n.szenen-capability > ul > li {\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n\n/* Stufe 6.7.2 (D-126) \u2014 Advanced-/Debug-Fl\xE4che f\xFCr die rohe, schema-getriebene\n * Feldliste einer Szene. Gestrichelt wie die anderen \u201EAngebot statt\n * Auskunft"-Bereiche, aber ohne Akzentfarbe: sie ist kein Vorschlag, sondern\n * ein Eskalationsweg. */\n.szenen-detail__technical {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__technical > summary {\n margin-bottom: var(--space-sm);\n color: var(--color-text-secondary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-detail__technical[open] > summary {\n margin-bottom: var(--space-md);\n}\n\n.szenen-detail__field small,\n.szenen-capability small {\n color: var(--color-text-tertiary);\n}\n\n.szenen-detail__preview-errors,\n.szenen-detail__notice--error,\n.szenen-view [role="alert"] {\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-color: var(--color-accent-red);\n}\n\n.szenen-detail__notice {\n padding: var(--space-sm) var(--space-md);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__notice--success,\n.szenen-view [data-tone="success"] {\n color: #bbf7d0;\n background: var(--color-accent-green-dim);\n border-color: var(--color-accent-green);\n}\n\n.szenen-scene-editor {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: var(--space-md);\n margin-bottom: 0;\n}\n\n.szenen-scene-editor > section {\n min-width: 0;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n/* \u2500\u2500 Stufe 6.7.2 (D-126): Node-/Relationskarten \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Nodes und Beziehungen sind `<details>` \u2014 dieselbe Interaktionssprache wie\n * \u201ERohantwort (Diagnose)" und \u201EBilder zu diesem Lauf" in RunsPanel.tsx. Kopf\n * (immer sichtbar) tr\xE4gt Inhalt zuerst, Technik zuletzt; Body (nur bei\n * ge\xF6ffneter Karte) tr\xE4gt die Bearbeitungscontrols. Padding wandert von der\n * `<li>` in Kopf/Body, damit die Karte keinen doppelten Rand bekommt. */\n.szenen-scene-editor li {\n padding: 0;\n overflow: hidden;\n}\n\n.szenen-node-karte__kopf,\n.szenen-relation-karte__kopf {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-xs) var(--space-sm);\n padding: var(--space-sm);\n cursor: pointer;\n list-style: none;\n}\n\n.szenen-node-karte__kopf::-webkit-details-marker,\n.szenen-relation-karte__kopf::-webkit-details-marker {\n display: none;\n}\n\n.szenen-node-karte__titel {\n flex: 1 1 12rem;\n min-width: 0;\n color: var(--color-text-primary);\n font-weight: 600;\n overflow-wrap: anywhere;\n}\n\n.szenen-node-karte__typ {\n flex: none;\n padding: 0.15rem 0.55rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-full);\n font-size: 0.72rem;\n white-space: nowrap;\n}\n\n/* node_id klein, monospace, sekund\xE4r \u2014 sichtbar auch bei zugeklappter Karte,\n * aber optisch klar hinter Beschreibung und Typ. */\n.szenen-node-karte__meta {\n flex: 1 1 100%;\n display: flex;\n flex-wrap: wrap;\n gap: 0.25rem;\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n}\n\n.szenen-node-karte__meta code {\n font-size: inherit;\n}\n\n.szenen-node-karte__body,\n.szenen-relation-karte__body {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm) var(--space-sm) var(--space-sm);\n border-top: 1px solid var(--color-border);\n}\n\n/* \xA74 \u2014 Prim\xE4raktion und destruktive Aktion optisch getrennt: \u201ENode/Beziehung\n * entfernen" bekommt einen eigenen, warnenden Ton statt gleichgewichtig neben\n * der normalen Bearbeitung zu stehen. */\n.szenen-node-karte__entfernen,\n.szenen-relation-karte__entfernen {\n justify-self: start;\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-color: var(--color-accent-red);\n}\n\n.szenen-relation-karte__kopf {\n font-size: 0.9rem;\n}\n\n.szenen-relation-karte__kopf code {\n overflow-wrap: anywhere;\n}\n\n.szenen-relation-karte__pfeil {\n flex: none;\n color: var(--color-text-secondary);\n}\n\n/* Die Composer-\xD6ffner (\u201E+ Node hinzuf\xFCgen", \u201E+ Beziehung hinzuf\xFCgen") sind\n * Angebote, keine Ausk\xFCnfte \u2014 dasselbe gestrichelte Muster wie beim Folgelauf\n * und der Fehlerrettung (\xA73.3/\xA73.5). */\n.szenen-scene-editor__oeffnen {\n width: 100%;\n border-style: dashed;\n}\n\n.szenen-refpicker {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n margin: var(--space-sm) 0;\n padding: var(--space-sm) 0;\n border: none;\n border-radius: 0;\n}\n\n.szenen-refpicker legend {\n padding: 0 var(--space-xs);\n color: var(--color-text-secondary);\n font-weight: 600;\n}\n\n.szenen-capability > ul > li {\n display: grid;\n grid-template-columns: minmax(10rem, 1fr) auto minmax(14rem, 2fr);\n align-items: start;\n gap: var(--space-sm);\n}\n\n.szenen-capability__state {\n padding: 0.1rem 0.5rem;\n border-radius: var(--radius-full);\n font-size: 0.75rem;\n font-weight: 700;\n text-transform: uppercase;\n}\n\n.szenen-capability__state--yes {\n color: #bbf7d0;\n background: var(--color-accent-green-dim);\n}\n\n.szenen-capability__state--no {\n color: #fecaca;\n background: var(--color-accent-red-dim);\n}\n\n.szenen-capability__state--unknown {\n color: #fef08a;\n background: rgba(234, 179, 8, 0.2);\n}\n\n.szenen-runs > ol > li {\n display: grid;\n gap: var(--space-sm);\n}\n\n/* B1 \u2014 die geschlossene Runde. Vorher stand die `run_id` als erstes und war\n die einzige Information, die niemand liest; jetzt beantwortet die Kopfzeile\n welche Runde, ob sie durchging, mit welchem Modell und was ungef\xE4hr\n herauskam. */\n.szenen-runs__kopf {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-sm);\n width: 100%;\n padding: var(--space-xs) 0;\n text-align: left;\n color: var(--color-text-secondary);\n background: transparent;\n border: none;\n cursor: pointer;\n}\n\n.szenen-runs__marke {\n font-weight: 700;\n color: var(--color-text-primary);\n}\n\n.szenen-runs__status {\n padding: 0.05rem 0.4rem;\n font-size: 0.6875rem;\n font-weight: 600;\n text-transform: uppercase;\n border-radius: var(--radius-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n}\n\n.szenen-runs__status[data-status="succeeded"] {\n border-color: var(--color-success);\n}\n\n.szenen-runs__status[data-status="failed"] {\n border-color: var(--color-danger);\n}\n\n.szenen-runs__modell {\n font-size: 0.75rem;\n}\n\n/* Der Auszug ist die Inhaltsvorschau der Runde. Er bekommt eine eigene Zeile,\n damit er nicht mit Marke und Modell um dieselbe Zeile ringt. */\n.szenen-runs__auszug {\n flex: 1 1 100%;\n min-width: 0;\n color: var(--color-text-tertiary);\n font-size: 0.8125rem;\n}\n\n/* B2 \u2014 ein Fehler steht ausserhalb der aufklappbaren Fl\xE4che: Wer eine rote\n Runde sieht, soll nicht erst klicken m\xFCssen, um zu erfahren, woran sie\n gescheitert ist. */\n.szenen-runs__fehler {\n display: grid;\n gap: var(--space-xs);\n padding: var(--space-sm);\n border-radius: var(--radius-sm);\n}\n\n.szenen-runs__fehler p {\n margin: 0;\n}\n\n.szenen-runs__inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n}\n\n.szenen-runs__prompt > h5,\n.szenen-runs__sekundaer > h5 {\n margin: 0 0 var(--space-xs);\n}\n\n.szenen-runs__prompt > p {\n margin: 0;\n white-space: pre-wrap;\n}\n\n.szenen-runs__prompt--negativ > p,\n.szenen-runs__sekundaer > p {\n margin: 0;\n color: var(--color-text-secondary);\n white-space: pre-wrap;\n}\n\n.szenen-runs__mehr {\n justify-self: start;\n margin-top: var(--space-xs);\n padding: 0;\n color: var(--color-accent-teal, var(--color-text-secondary));\n background: transparent;\n border: none;\n font-size: 0.75rem;\n text-decoration: underline;\n cursor: pointer;\n}\n\n.szenen-runs > ol > li[data-status="succeeded"] {\n border-left: 3px solid var(--color-success);\n}\n\n.szenen-runs > ol > li[data-status="failed"] {\n border-left: 3px solid var(--color-danger);\n}\n\n.szenen-runs details pre {\n max-height: 20rem;\n overflow: auto;\n padding: var(--space-sm);\n white-space: pre-wrap;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border-radius: var(--radius-sm);\n}\n\n.szenen-media > section {\n display: grid;\n gap: var(--space-sm);\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-media__lookup,\n.szenen-media__attach {\n display: flex;\n flex-wrap: wrap;\n align-items: end;\n gap: var(--space-sm);\n}\n\n.szenen-media__lookup > label,\n.szenen-media__attach > label:first-child {\n flex: 1 1 18rem;\n}\n\n.szenen-media__list {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-media__list > li {\n display: grid;\n grid-template-columns: minmax(12rem, 1fr) auto auto auto;\n align-items: center;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n@media (max-width: 760px) {\n .szenen-view {\n gap: var(--space-sm);\n }\n\n /* C1 \u2014 auf 390 px bleiben drei Kacheln je Reihe gross genug zum Erkennen\n und klein genug, dass vier Bilder eine Reihe sind und keine Seite. */\n .szenen-bildgalerie {\n grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr));\n }\n\n /* Der Viewer bekommt den ganzen Bildschirm; die Bl\xE4tterkn\xF6pfe bleiben am\n Rand, wo der Daumen sie erreicht. */\n .szenen-bildviewer {\n padding: var(--space-xs);\n }\n\n .szenen-bildviewer__blaettern {\n width: 2.25rem;\n height: 4rem;\n }\n\n .szenen-view__intro {\n align-items: start;\n flex-direction: column;\n gap: var(--space-sm);\n padding: var(--space-md);\n }\n\n .szenen-view__intro h2 {\n font-size: 1.25rem;\n }\n\n .szenen-view__intro > div > p:last-child {\n font-size: 0.9rem;\n }\n\n .szenen-view__nav {\n position: sticky;\n top: 0;\n z-index: 2;\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: var(--space-xs);\n padding: var(--space-xs);\n }\n\n .szenen-view__nav button {\n width: 100%;\n min-width: 0;\n min-height: 2.5rem;\n padding: 0.35rem 0.5rem;\n }\n\n\n .szenen-scene-editor {\n grid-template-columns: 1fr;\n }\n\n .szenen-capability > ul > li {\n grid-template-columns: 1fr;\n }\n\n .szenen-derive,\n .szenen-elements,\n .szenen-scene,\n .szenen-setup,\n .szenen-shot,\n .szenen-media,\n .szenen-runs {\n gap: var(--space-sm);\n }\n\n .szenen-elements__filter {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto;\n align-items: center;\n gap: var(--space-sm);\n }\n\n .szenen-elements__filter > label {\n width: 100%;\n min-width: 0;\n flex: none;\n }\n\n .szenen-elements__filter > label:has(input[type="checkbox"]) {\n flex: none;\n grid-column: 1 / -1;\n }\n\n /* Schmal ist die Liste eine Spalte, und die Auswahl fuehrt weiter \u2014 der\n * Eintrag ist deshalb ein Ziel fuer den Finger, keine Kachel. */\n .szenen-elements__list {\n grid-template-columns: minmax(0, 1fr);\n }\n\n .szenen-elements__list > li > button {\n min-height: 2.75rem;\n }\n\n .szenen-elements__back {\n justify-self: stretch;\n min-height: 2.5rem;\n }\n\n .szenen-detail__actions,\n .szenen-runs__start,\n .szenen-scene-editor__add,\n .szenen-scene-editor__add-relation {\n align-items: stretch;\n flex-direction: column;\n }\n\n .szenen-runs__start > *,\n .szenen-scene-editor__add > *,\n .szenen-scene-editor__add-relation > * {\n width: 100%;\n flex: none;\n }\n}\n\n/* \u2500\u2500 Stufe 6.5: Rekonstruktion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Bereich zeigt drei Dinge nebeneinander: den Vorschlag, das nicht\n * Ableitbare und die Widerspr\xFCche. Die Diagnosen sind deshalb bewusst nicht\n * kleiner gesetzt als der Vorschlag \u2014 wer sie \xFCbersieht, \xFCbernimmt Geratenes.\n */\n\n.szenen-derive {\n display: grid;\n gap: var(--space-md);\n}\n\n.szenen-form {\n display: grid;\n gap: var(--space-sm);\n padding: 0;\n background: transparent;\n border: none;\n border-radius: 0;\n}\n\n.szenen-field {\n display: grid;\n gap: var(--space-xs);\n}\n\n.szenen-field > span {\n color: var(--color-text-secondary);\n font-size: 0.875rem;\n}\n\n.szenen-error {\n padding: var(--space-sm) var(--space-md);\n color: var(--color-danger);\n background: var(--color-accent-red-dim);\n border: 1px solid var(--color-accent-red);\n border-radius: var(--radius-sm);\n}\n\n.szenen-empty {\n padding: var(--space-md);\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-diagnose {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-warning);\n border-radius: var(--radius-sm);\n}\n\n/* Ein Widerspruch ist der schwerste der drei Befunde: dort steht eine\n * Entscheidung aus, die das System bewusst nicht getroffen hat (D-74). */\n.szenen-diagnose--konflikt {\n border-left-color: var(--color-danger);\n}\n\n.szenen-diagnose > h4 {\n margin: 0 0 var(--space-sm);\n font-size: 0.9375rem;\n}\n\n.szenen-diagnose ul {\n display: grid;\n gap: var(--space-xs);\n margin: 0;\n padding-left: var(--space-md);\n}\n\n.szenen-proposal {\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-md);\n}\n\n.szenen-proposal > h4 {\n margin: 0 0 var(--space-sm);\n font-family: var(--font-mono);\n font-size: 0.9375rem;\n}\n\n.szenen-proposal__payload {\n max-height: 18rem;\n margin: 0 0 var(--space-sm);\n padding: var(--space-sm);\n overflow: auto;\n /* Ein erzeugter Prompt ist ein einziger langer Satz \u2014 ohne Umbruch\n verschwindet er nach rechts aus dem Bild. JSON-Payloads bringen ihre\n eigenen Zeilenumbrueche mit und leiden nicht darunter. */\n white-space: pre-wrap;\n overflow-wrap: anywhere;\n font-family: var(--font-mono);\n font-size: 0.8125rem;\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-proposal__done {\n margin: var(--space-sm) 0 0;\n color: var(--color-success);\n}\n\n/* Die Belege eines Laufs: Rohantwort und Systemprompt. Zum Lesen und Kopieren,\n nicht zum Bedienen \u2014 deshalb voll markierbar und in Monospace. */\n.szenen-beleg__text {\n width: 100%;\n margin: var(--space-sm) 0 0;\n padding: var(--space-sm);\n font-family: var(--font-mono);\n font-size: 0.8125rem;\n line-height: 1.5;\n white-space: pre;\n background: var(--color-bg-app);\n color: var(--color-text);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n resize: vertical;\n}\n\n/* Der Folgelauf (D-87): abgesetzt, damit er nicht wie Teil des Ergebnisses\n aussieht \u2014 er ist eine Frage, keine Auskunft. */\n.szenen-folgelauf {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-folgelauf > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-run__herkunft {\n margin: 0 0 var(--space-sm);\n font-size: 0.875rem;\n color: var(--color-text-muted, inherit);\n}\n\n/* Widerspricht das Modell, ist das das Wichtigste auf der Seite. */\n.szenen-diagnose--antwort {\n border-left: 3px solid var(--color-accent, var(--color-border));\n}\n\n/* Der Bildw\xE4hler (D-92) \u2014 Browser und Inspector, dem Labs-Tab nachgebildet,\n aber mit dem CSS dieser Ansicht: eine gemeinsame Komponente \xFCber zwei\n Dom\xE4nengrenzen w\xE4re eine Kopplung, die keiner gewollt hat.\n\n Der Review vom 2026-08-22 (R12.1/R12.2) hat den Ma\xDFstab gesetzt: Der Bestand\n ist ein **Browser**, kein Seiteninhalt. Vierzig Werke d\xFCrfen die Szenenseite\n nicht meterlang machen, und zw\xF6lf Varianten eines Werkes keine zw\xF6lf\n Bildschirmseiten. Deshalb steht hier alles unter zwei Regeln \u2014 begrenzte\n Fl\xE4che mit eigenem Scroll, und Kacheln statt Karten. */\n\n/* \u2500\u2500 Ebene 1: der Browser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl {\n /* Dieselbe Begr\xFCndung wie beim Inspector: Als Grid-Element w\xFCrde der\n W\xE4hler sonst so breit wie sein breitester Inhalt. */\n min-width: 0;\n}\n\n.szenen-bildwahl__browser {\n /* Die eigene Scrollfl\xE4che ist der ganze Punkt: Der Bestand w\xE4chst nach\n innen, nicht nach unten. Dieselbe Gr\xF6\xDFenordnung wie `max-h-[300px]` im\n Labs-Browser. */\n max-height: 19rem;\n overflow-y: auto;\n overscroll-behavior: contain;\n margin-top: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__raster {\n display: grid;\n /* Vier Spalten auf schmalen Displays \u2014 der gemeldete Screenshot zeigte eine\n einzige Spalte gro\xDFer Karten. Breiter wird automatisch dichter. */\n grid-template-columns: repeat(auto-fill, minmax(4.5rem, 1fr));\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildwahl__karte {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n overflow: hidden;\n background: var(--color-bg-surface);\n text-align: left;\n cursor: pointer;\n}\n\n/* Das fokussierte Werk ist klar markiert \u2014 sonst wei\xDF niemand, wessen Galerie\n der Inspector unten zeigt. */\n.szenen-bildwahl__karte.is-fokus {\n border-color: var(--color-accent, var(--color-text-primary));\n box-shadow: 0 0 0 2px var(--color-accent, var(--color-text-primary));\n}\n\n/* \u2500\u2500 Die Kachel selbst \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Der Platz steht, bevor das Bild da ist \u2014 sonst springt das Raster, sobald\n die erste Vorschau eintrifft. */\n.szenen-bildkachel {\n display: block;\n width: 100%;\n aspect-ratio: 1;\n overflow: hidden;\n background: var(--color-bg-app);\n}\n\n.szenen-bildkachel img {\n width: 100%;\n height: 100%;\n object-fit: cover;\n display: block;\n}\n\n.szenen-bildkachel__leer {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n background: linear-gradient(135deg, var(--color-bg-app), var(--color-bg-surface));\n}\n\n/* R12.3: Eine fehlende Vorschau bleibt kachelgro\xDF. Vorher fiel die Karte auf\n ihren Prompttext zur\xFCck \u2014 und ein Prompt hat im echten Bestand mehrere\n hundert Zeichen. */\n.szenen-bildkachel__zeichen {\n font-size: 0.875rem;\n font-weight: 700;\n color: var(--color-text-secondary);\n opacity: 0.6;\n}\n\n/* Wie viele Varianten das Werk hat \u2014 klein und \xFCberlagernd, nie als\n dominanter Text. */\n.szenen-bildwahl__anzahl {\n position: absolute;\n top: 0.15rem;\n right: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n font-weight: 700;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* Eine Zeile, dann Schluss. Der Prompt eines Werkes ist oft mehrere hundert\n Zeichen lang und w\xE4re als Prim\xE4rdarstellung genau die Textw\xFCste, gegen die\n der Review antritt. */\n.szenen-bildwahl__kacheltitel {\n display: block;\n padding: 0.15rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.3;\n color: var(--color-text-secondary);\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* \u2500\u2500 Ebene 2: der Inspector \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl__inspector {\n /* Ohne `min-width: 0` gewinnt der Inhalt: Der W\xE4hler ist Element eines\n Grids (`.szenen-form`), und dort ist `min-width: auto` die Vorgabe. Der\n Streifen unten ist rund 1000 px breit \u2014 gemessen am 2026-08-22 im echten\n Browser zog er die Auswahl von 374 px auf 1088 px auf und schob die\n ganze Seite waagerecht heraus, statt selbst zu scrollen. */\n min-width: 0;\n margin-top: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-surface);\n /* Sichtbar abgesetzt vom Browser: zwei Ebenen, nicht eine lange Liste. */\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-accent, var(--color-border));\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__inspektorkopf {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--space-sm);\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bildwahl__werktitel {\n font-size: 0.75rem;\n font-weight: 600;\n color: var(--color-text-primary);\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.szenen-bildwahl__zurueck {\n flex: 0 0 auto;\n font-size: 0.6875rem;\n padding: 0.15rem 0.5rem;\n}\n\n/* Der Streifen: waagerecht scrollend, feste kleine Kacheln \u2014 zw\xF6lf Varianten\n bleiben ein \xDCberblick statt zw\xF6lf Bildschirmseiten. */\n.szenen-bildwahl__streifen {\n /* Die Breite kommt vom Elter, nie vom Inhalt \u2014 sonst scrollt hier nichts. */\n max-width: 100%;\n display: flex;\n gap: var(--space-sm);\n margin: 0;\n padding: 0 0 0.25rem;\n list-style: none;\n overflow-x: auto;\n overscroll-behavior-x: contain;\n}\n\n.szenen-bildwahl__streifen > li {\n flex: 0 0 auto;\n width: 5rem;\n}\n\n.szenen-bildwahl__variante {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n overflow: hidden;\n background: var(--color-bg-app);\n cursor: pointer;\n}\n\n.szenen-bildwahl__variante.is-gewaehlt {\n border-color: var(--color-accent, var(--color-text-primary));\n box-shadow: 0 0 0 2px var(--color-accent, var(--color-text-primary));\n}\n\n.szenen-bildwahl__nummer {\n position: absolute;\n bottom: 0.15rem;\n left: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border-radius: var(--radius-sm);\n}\n\n/* \u2500\u2500 Das Gew\xE4hlte \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.szenen-bildwahl__gewaehlt {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: var(--space-sm) 0 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildwahl__gewaehlt li {\n display: flex;\n align-items: center;\n gap: 0.375rem;\n padding: 0.25rem 0.5rem;\n font-size: 0.8125rem;\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n max-width: 100%;\n}\n\n/* Das gew\xE4hlte Bild wird best\xE4tigt, nicht nur benannt: eine kleine Vorschau\n neben dem Titel. */\n.szenen-bildwahl__gewaehlt .szenen-bildkachel {\n width: 2.25rem;\n flex: 0 0 auto;\n border-radius: var(--radius-sm);\n}\n\n.szenen-bildwahl__gewaehlt .szenen-bildwahl__kacheltitel {\n min-width: 0;\n max-width: 12rem;\n font-size: 0.75rem;\n}\n\n.szenen-beleg__kopf {\n display: flex;\n align-items: center;\n gap: var(--space-sm);\n margin-top: var(--space-sm);\n}\n\n.szenen-diagnose > details + details {\n margin-top: var(--space-sm);\n}\n\n.szenen-diagnose > details > summary {\n cursor: pointer;\n font-weight: 600;\n}\n\n/* \u2500\u2500 Die Fehlerrettung (Stufe 6.6, A5) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Sie steht im Fehlerblock und muss trotzdem als Werkzeug lesbar sein, nicht\n * als weitere Fehlermeldung. Deshalb abgesetzt wie der Folgelauf \u2014 und aus\n * demselben Grund: Beides sind Angebote, keine Ausk\xFCnfte.\n *\n * Der wichtigste optische Punkt ist die Trennung von Original und\n * Arbeitskopie. Das Original bekommt den ruhigen, \u201Efesten" Hintergrund der\n * Belege; die Kopie sieht aus wie ein normales Eingabefeld, weil sie eines\n * ist. */\n.szenen-rettung {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-rettung > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-rettung textarea[readonly] {\n background: var(--color-bg-surface);\n color: var(--color-text-secondary);\n font-family: var(--font-mono, monospace);\n font-size: 0.8125rem;\n}\n\n.szenen-rettung textarea:not([readonly]) {\n font-family: var(--font-mono, monospace);\n font-size: 0.8125rem;\n}\n\n.szenen-rettung__aktionen {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: var(--space-sm) 0;\n}\n\n/* Ein Befund tr\xE4gt Code, Bezug und Meldung. Der Code steht klein und\n * monospaced davor: Er ist die Kennung, nicht die Aussage. */\n.szenen-diagnose__liste {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding-left: var(--space-md);\n}\n\n.szenen-diagnose__liste code {\n font-size: 0.8125rem;\n}\n\n.szenen-diagnose__liste > li[data-severity=\'warning\'] {\n color: var(--color-text-secondary);\n}\n\n/* \u2500\u2500 Die Draft-Kette (Stufe 6.6, C4) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Hauptweg. Er steht nach den Einzel\xFCbernahmen und muss als das erkennbar\n * sein, was er ist: nicht eine weitere Option, sondern der k\xFCrzere Weg zum\n * selben Ziel. Deshalb durchgezogener Rahmen statt gestricheltem \u2014 er ist\n * Angebot **und** Empfehlung. */\n.szenen-kette {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-left: 3px solid var(--color-accent, var(--color-border));\n border-radius: var(--radius-sm);\n}\n\n.szenen-kette > h4 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-kette ul {\n display: grid;\n gap: var(--space-xs);\n margin: 0 0 var(--space-sm);\n padding-left: var(--space-md);\n}\n\n.szenen-kette code {\n font-size: 0.8125rem;\n}\n\n/* Ein Hinweis, der eine Bedingung erklaert \u2014 nicht rot, weil er kein Fehler\n ist, aber abgesetzt, weil er eine Folge hat. */\n.szenen-detail__hint {\n margin: var(--space-sm) 0 0;\n padding: var(--space-sm);\n font-size: 0.875rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-surface);\n border-left: 3px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* \u2500\u2500 Stufe 6.7: Bilder am Prompt-Lauf (H0/H1) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n *\n * Der Bereich tr\xE4gt zwei Dinge, die man nie verwechseln darf: den **Eingang**\n * (woraus der Prompt entstand) und den **Ausgang** (was daraus wurde). Sie\n * bekommen deshalb verschiedene Akzentkanten statt nur verschiedener\n * \xDCberschriften \u2014 wer schnell liest, sieht die Trennung, bevor er sie liest.\n */\n.szenen-bilder {\n display: grid;\n gap: var(--space-md);\n margin-top: var(--space-sm);\n}\n\n.szenen-bilder__eingang,\n.szenen-bilder__ausgang {\n display: grid;\n gap: var(--space-sm);\n padding: var(--space-sm) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__eingang {\n border-left: 3px solid var(--color-accent-purple, var(--color-border));\n}\n\n.szenen-bilder__ausgang {\n border-left: 3px solid var(--color-accent-teal, var(--color-border));\n}\n\n.szenen-bilder h5 {\n margin: 0;\n}\n\n.szenen-bilder__ids {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-sm);\n margin: 0;\n align-items: baseline;\n color: var(--color-text-secondary);\n font-size: 0.875rem;\n}\n\n.szenen-bilder__eingangsliste,\n.szenen-bilder__batchliste,\n.szenen-bilder__auftraege {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bilder__eingangsliste > li,\n.szenen-bilder__batchliste > li,\n.szenen-bilder__auftraege > li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-sm);\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n font-size: 0.8125rem;\n}\n\n/* Ein unterbrochener Auftrag ist der Fall, f\xFCr den der Auftragssatz existiert.\n Er wird deshalb markiert \u2014 nicht rot, denn er ist kein Fehler, sondern eine\n offene Entscheidung. */\n.szenen-bilder__auftraege > li[data-status="started"] {\n border-left: 3px solid var(--color-warning);\n}\n\n.szenen-bilder__auftraege > li[data-status="succeeded"] {\n border-left: 3px solid var(--color-success);\n}\n\n.szenen-bilder__auftraege > li[data-status="partial"] {\n border-left: 3px solid var(--color-accent-blue, var(--color-border));\n}\n\n.szenen-bilder__auftraege > li[data-status="failed"] {\n border-left: 3px solid var(--color-danger);\n}\n\n/* Das Eingangsbild benutzt seit C3 dieselbe Kachel wie W\xE4hler und Galerie.\n Sie ist von Haus aus so breit wie ihr Platz \u2014 in dieser Zeile bekommt sie\n deshalb eine feste Gr\xF6sse, sonst sch\xF6be sie die Metadaten heraus. */\n.szenen-bilder__eingangsliste .szenen-bildkachel {\n flex: 0 0 auto;\n width: 4rem;\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__batch {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding: var(--space-sm);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__batchkopf {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--space-sm);\n}\n\n.szenen-bilder__herkunft {\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n}\n\n/* Review-Befund \u2014 ein transienter Bildfehler ist wiederholbar, kein\n endg\xFCltiges \u201Efehlt". Dezent wie ein Angebot, nicht wie ein Fehlerzustand. */\n.szenen-bilder__retry {\n justify-self: start;\n padding: 0.15rem 0.5rem;\n font-size: 0.75rem;\n color: var(--color-text-secondary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n/* C4 \u2014 die Provenienz vollst\xE4ndig, aber sekund\xE4r. */\n.szenen-bilder__technik > summary {\n color: var(--color-text-secondary);\n font-size: 0.75rem;\n cursor: pointer;\n}\n\n.szenen-bilder__meta {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n font-size: 0.75rem;\n color: var(--color-text-secondary);\n}\n\n/* C5 \u2014 der Verlauf verdichtet, seine Ausreisser nicht. */\n.szenen-bilder__verlauf > summary {\n color: var(--color-text-secondary);\n font-size: 0.8125rem;\n cursor: pointer;\n}\n\n.szenen-bilder__verlauf[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bilder__auffaellig {\n margin: 0;\n padding: var(--space-sm);\n border-radius: var(--radius-sm);\n font-size: 0.8125rem;\n}\n\n.szenen-bilder__auftragskennung {\n flex-basis: 100%;\n color: var(--color-text-tertiary);\n font-size: 0.6875rem;\n}\n\n/* \u2500\u2500 C1/C2 \u2014 Galerie und grosse Ansicht \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n/* Das Raster w\xE4chst mit dem Platz: auf 390 px drei bis vier Kacheln je Reihe,\n auf dem Desktop entsprechend mehr. Vorher war jedes Bild eine Zeile aus\n Daumennagel plus vier Metadatenfeldern \u2014 vier Bilder ergaben vier Textzeilen\n und das Protokoll war die Hauptsache. */\n.szenen-bildgalerie {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(5.5rem, 1fr));\n gap: var(--space-sm);\n min-width: 0;\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-bildgalerie__kachel {\n position: relative;\n display: block;\n width: 100%;\n padding: 0;\n overflow: hidden;\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-bildgalerie__kachel:hover,\n.szenen-bildgalerie__kachel:focus-visible {\n border-color: var(--color-accent-teal, var(--color-text-primary));\n}\n\n.szenen-bildgalerie__nummer {\n position: absolute;\n bottom: 0.15rem;\n left: 0.15rem;\n padding: 0.05rem 0.3rem;\n font-size: 0.625rem;\n line-height: 1.4;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border-radius: var(--radius-sm);\n}\n\n/* Die grosse Ansicht liegt \xFCber allem und bringt ihren eigenen Grund mit \u2014\n `position: fixed` bezieht sich im Flow-Canvas auf dessen Rahmen, in React auf\n das Fenster; in beiden F\xE4llen genau das Gew\xFCnschte. */\n.szenen-bildviewer {\n position: fixed;\n inset: 0;\n z-index: 1000;\n display: grid;\n grid-template-rows: auto 1fr auto;\n gap: var(--space-sm);\n padding: var(--space-sm);\n /* Deckend, nicht get\xF6nt: Bei 0.92 schimmerte der Laufbereich durch und man\n las Prompttext quer \xFCber dem Bild \u2014 im Browser gemessen. */\n background: #000;\n}\n\n/* Titel, Z\xE4hlung, Schliessen \u2014 drei Spalten, damit ein langer Werktitel die\n Z\xE4hlung nicht in die n\xE4chste Zeile und \xFCber das Bild schiebt. */\n.szenen-bildviewer__kopf {\n display: grid;\n grid-template-columns: minmax(0, 1fr) auto auto;\n align-items: center;\n gap: var(--space-sm);\n color: var(--color-text-primary);\n}\n\n.szenen-bildviewer__titel {\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-weight: 600;\n}\n\n.szenen-bildviewer__zaehler {\n color: var(--color-text-secondary);\n font-size: 0.8125rem;\n}\n\n.szenen-bildviewer__buehne {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: var(--space-sm);\n min-height: 0;\n min-width: 0;\n}\n\n/* Der Rahmen nimmt den Platz, der zwischen den Bl\xE4tterkn\xF6pfen \xFCbrig ist \u2014\n `min-width: 0`, damit er auch wirklich schrumpft statt sie hinauszuschieben. */\n.szenen-bildviewer__rahmen {\n flex: 1 1 auto;\n display: flex;\n align-items: center;\n justify-content: center;\n min-width: 0;\n min-height: 0;\n height: 100%;\n}\n\n.szenen-bildviewer__bild {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n}\n\n.szenen-bildviewer__leer {\n color: var(--color-text-secondary);\n text-align: center;\n}\n\n/* Die Bl\xE4tterkn\xF6pfe sind daumengross und liegen an den R\xE4ndern \u2014 auf einem\n Telefon ist der Rand erreichbar, die Bildmitte nicht. */\n.szenen-bildviewer__blaettern {\n flex: 0 0 auto;\n width: 2.75rem;\n height: 3.5rem;\n font-size: 1.75rem;\n line-height: 1;\n color: var(--color-text-primary);\n background: var(--color-bg-elevated);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n cursor: pointer;\n}\n\n.szenen-bildviewer__details {\n max-height: 8rem;\n overflow-y: auto;\n color: var(--color-text-secondary);\n}\n\n/* Die Aktionen stehen abgesetzt: Sie sind Angebote, keine Ausk\xFCnfte \u2014 dasselbe\n Muster wie beim Folgelauf und bei der Fehlerrettung. */\n/* D \u2014 die Aktionen stehen abgesetzt und standardm\xE4ssig kompakt: Sie sind\n Angebote, keine Ausk\xFCnfte. Vorher hielten beide Formulare dauerhaft offen und\n machten aus jedem Lauf zwei zus\xE4tzliche Bildschirmseiten. */\n.szenen-bilder__aktion {\n min-width: 0;\n padding: var(--space-sm) var(--space-md);\n border: 1px dashed var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-bilder__aktion > summary {\n color: var(--color-text-primary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-bilder__aktion[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n.szenen-bilder__aktion-inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding-bottom: var(--space-sm);\n}\n\n.szenen-bilder__aktion h5 {\n margin: var(--space-sm) 0 0;\n}\n\n.szenen-bilder__aktion ol {\n display: grid;\n gap: var(--space-sm);\n margin: 0;\n padding-left: var(--space-md);\n font-size: 0.8125rem;\n}\n\n/* Regelwerk-UX Schritt 16 \u2014 der ZIP-Sammelimport.\n *\n * \xA78: Der gemeldete Einsatz ist mobil. Deshalb **ein** Seedfeld statt einer\n * gestapelten Eingabe je Bild, und die Zuordnung Bild -> Seed als umbrechende\n * Zeile statt als Tabelle: Eine Tabelle waere auf 390 px abgeschnitten, und\n * abgeschnitten heisst hier, dass der Nutzer die Zuordnung nicht mehr pruefen\n * kann \u2014 genau die Pruefung, wegen der die Vorschau existiert. */\n.szenen-bilder__zip {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n}\n\n.szenen-bilder__zipkopf {\n margin: 0;\n overflow-wrap: anywhere;\n}\n\n.szenen-bilder__zipvorlagen {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n margin: 0;\n}\n\n/* Eine laengere Liste bleibt scrollbar, statt die Sektion und damit den\n * Import-Knopf aus dem Blick zu schieben (\xA78, letzter Punkt). */\n.szenen-bilder__zipliste {\n max-height: 16rem;\n overflow-y: auto;\n}\n\n.szenen-bilder__zipliste > li {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-between;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-bilder__zipliste > li > span:first-child {\n overflow-wrap: anywhere;\n}\n\n.szenen-bilder__zipliste > li > span:last-child {\n color: var(--color-text-secondary);\n white-space: nowrap;\n}\n\n/* ==========================================================================\n STUFE 6.10 \u2014 ELEMENTEDITOR UND VERSIONSNAVIGATION\n ========================================================================== */\n\n/* \xA74.2 \u2014 vier Sachverhalte, vier Marken. Die Marken stehen in einer eigenen\n * Spalte, damit ANGEZEIGT + BEVORZUGT nebeneinander lesbar bleiben, statt sich\n * durch den Text zu schieben. */\n.szenen-detail__heads li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-sm);\n}\n\n.szenen-version__titel {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n min-width: 0;\n margin-right: auto;\n}\n\n/* V17 \u2014 Kennungen bleiben vollst\xE4ndig sichtbar, aber optisch sekund\xE4r. */\n.szenen-version__id,\n.szenen-ref__id,\n.szenen-detail__field-key {\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n overflow-wrap: anywhere;\n}\n\n.szenen-version__marken {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n}\n\n.szenen-version__marken > span {\n padding: 0.1rem 0.5rem;\n color: var(--color-text-primary);\n background: rgba(45, 212, 191, 0.12);\n border: 1px solid rgba(45, 212, 191, 0.35);\n border-radius: var(--radius-full);\n font-size: 0.7rem;\n font-weight: 800;\n letter-spacing: 0.08em;\n}\n\n.szenen-version--angezeigt {\n border-color: var(--color-accent-teal, #2dd4bf) !important;\n}\n\n/* V9 \u2014 der Basiswechsel ist eine Entscheidung, keine Nebenwirkung. */\n.szenen-detail__confirm {\n display: grid;\n gap: var(--space-sm);\n margin: var(--space-md) 0;\n padding: var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-accent-amber, #f59e0b);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__confirm p {\n margin: 0;\n}\n\n/* Untergruppen von Objektfeldern (`prompt_render_settings`, `overrides`,\n `blender_render_settings`) \u2014 seit A1/A2 auf- und zuklappbar. Eine optionale\n Gruppe startet zu: Blender und `overrides` f\xFCllten auf einem 390-px-Display\n sonst mehrere Bildschirmseiten, obwohl die meisten Shots sie nie anfassen. */\n.szenen-detail__group {\n min-width: 0;\n margin: var(--space-md) 0;\n padding: var(--space-sm) var(--space-md);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__group > summary {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-weight: 600;\n cursor: pointer;\n}\n\n.szenen-detail__group[open] > summary {\n margin-bottom: var(--space-sm);\n}\n\n/* A2 \u2014 dass eine zugeklappte Gruppe schon Werte tr\xE4gt, muss man sehen, ohne\n sie zu \xF6ffnen. Sonst \xFCbersieht man eine gesetzte Blender-Ausgabe genau so\n lange, bis sie im Ergebnis auftaucht. */\n.szenen-detail__group-status {\n padding: 0.05rem 0.4rem;\n font-size: 0.6875rem;\n font-weight: 600;\n letter-spacing: 0.02em;\n text-transform: uppercase;\n color: var(--color-text-tertiary);\n background: var(--color-bg-app);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__group[data-konfiguriert="ja"] > summary .szenen-detail__group-status {\n color: var(--color-text-primary);\n border-color: var(--color-accent-teal, var(--color-border));\n}\n\n.szenen-detail__group-inhalt {\n display: grid;\n gap: var(--space-sm);\n min-width: 0;\n padding-bottom: var(--space-sm);\n}\n\n/* A3 \u2014 die Kernaussage steht am Feld, die Begr\xFCndung dahinter. Nicht als\n Tooltip: Auf einem Telefon gibt es kein Hover, und was dort unerreichbar ist,\n ist gestrichen \u2014 nur ohne dass es jemand entschieden h\xE4tte. */\n.szenen-detail__warum > summary {\n color: var(--color-text-tertiary);\n font-size: 0.75rem;\n cursor: pointer;\n}\n\n.szenen-detail__warum > small {\n display: block;\n padding-top: var(--space-xs);\n color: var(--color-text-tertiary);\n}\n\n.szenen-detail__fieldrow {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n margin-bottom: var(--space-md);\n}\n\n/* A4 \u2014 der technische Feldname bleibt lesbar, tritt aber hinter das fachliche\n Label zur\xFCck. */\n.szenen-detail__field-label {\n color: var(--color-text-primary);\n}\n\n.szenen-detail__field {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-detail__field > span {\n display: flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n}\n\n/* Mehrfachauswahl und Listeneditoren. */\n.szenen-feld__mehrfach,\n.szenen-feld__liste,\n.szenen-feld__reflist,\n.szenen-feld__ref,\n.szenen-feld__zielmodell,\n.szenen-feld__kamera {\n display: grid;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-feld__mehrfach > label,\n.szenen-feld__null {\n display: flex;\n align-items: center;\n gap: var(--space-xs);\n color: var(--color-text-secondary);\n font-size: 0.85rem;\n}\n\n.szenen-feld__liste-zeile {\n display: flex;\n flex-wrap: wrap;\n gap: var(--space-xs);\n}\n\n.szenen-feld__reflist ol {\n display: grid;\n gap: var(--space-xs);\n margin: 0;\n padding: 0;\n list-style: none;\n}\n\n.szenen-feld__reflist li {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--space-xs);\n padding: var(--space-xs) var(--space-sm);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n/* \xA76.1 \u2014 Titel und Typ zuerst, Kennungen daneben. */\n.szenen-ref__anzeige {\n display: inline-flex;\n flex-wrap: wrap;\n align-items: baseline;\n gap: var(--space-xs);\n min-width: 0;\n}\n\n.szenen-ref__typ {\n color: var(--color-text-secondary);\n font-size: 0.8rem;\n}\n\n.szenen-ref__warn {\n padding: 0.1rem 0.5rem;\n color: #fecaca;\n background: var(--color-accent-red-dim);\n border-radius: var(--radius-full);\n font-size: 0.72rem;\n font-weight: 700;\n}\n\n/* \xA76.5 \u2014 abgeleitete Angaben, ausdr\xFCcklich nur Anzeige. */\n.szenen-detail__derived {\n margin-top: var(--space-md);\n padding: var(--space-sm) var(--space-md);\n background: var(--color-bg-surface);\n border: 1px solid var(--color-border);\n border-radius: var(--radius-sm);\n}\n\n.szenen-detail__derived dl {\n display: grid;\n grid-template-columns: minmax(10rem, auto) 1fr;\n gap: var(--space-xs) var(--space-sm);\n margin: 0;\n}\n\n.szenen-detail__derived dt {\n color: var(--color-text-secondary);\n}\n\n.szenen-detail__derived dd {\n margin: 0;\n}\n\n.szenen-feld__abgeleitet {\n margin: 0;\n color: var(--color-text-tertiary);\n font-size: 0.78rem;\n}\n';
850
-
851
- // src/lib/utils.ts
852
- var getFormattedTimestamp = () => {
853
- const d = /* @__PURE__ */ new Date();
854
- const pad = (n) => n.toString().padStart(2, "0");
855
- return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`;
856
- };
857
- var GLOBAL_STYLES = `
858
- .dark-scrollbar::-webkit-scrollbar { width: 4px; height: 4px; }
859
- .dark-scrollbar::-webkit-scrollbar-track { background: transparent; }
860
- .dark-scrollbar::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 10px; }
861
- html, body, #root { margin: 0; padding: 0; width: 100%; height: 100%; background: #0e0e0e; font-family: 'Google Sans Text', sans-serif; overflow: hidden; }
862
- @keyframes prompt-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
863
- .prompt-loading { background: linear-gradient(90deg, rgba(255,255,255,0.02) 25%, rgba(255,255,255,0.05) 50%, rgba(255,255,255,0.02) 75%); background-size: 200% 100%; animation: prompt-shimmer 2s infinite linear; }
864
- @keyframes dropdown-enter { from { opacity: 0; transform: scale(0.95) translateY(-5px); } to { opacity: 1; transform: scale(1) translateY(0); } }
865
- .animate-dropdown { animation: dropdown-enter 0.1s ease-out forwards; }
866
- .flow-szenen-entry { display: grid; gap: 6px; width: 100%; max-width: 280px; text-align: center; }
867
- .flow-szenen-entry button, .flow-szenen-shell__header button { min-height: 44px; padding: 0 16px; color: #e7e5e4; background: #1d4ed8; border: 1px solid #3b82f6; border-radius: 12px; font: inherit; font-weight: 700; cursor: pointer; }
868
- .flow-szenen-entry button { display: flex; align-items: center; justify-content: center; gap: 8px; }
869
- .flow-szenen-entry button:disabled { color: rgba(255,255,255,.3); background: transparent; border-color: rgba(255,255,255,.1); cursor: not-allowed; }
870
- .flow-szenen-entry small { color: rgba(255,255,255,.35); font-size: 10px; }
871
- .flow-workspace-picker { position: fixed; inset: 0; display: grid; place-items: center; padding: 16px; color: #f5f5f5; background: #050505; overflow: auto; }
872
- .flow-workspace-picker__card { box-sizing: border-box; display: grid; gap: 16px; width: min(100%, 448px); padding: 24px; background: #171717; border: 1px solid #404040; border-radius: 12px; box-shadow: 0 25px 50px rgba(0,0,0,.55); }
873
- .flow-workspace-picker__card header { display: grid; gap: 4px; }
874
- .flow-workspace-picker__card header strong { font-size: 22px; }
875
- .flow-workspace-picker__card header small, .flow-workspace-picker__choices small { color: #a3a3a3; }
876
- .flow-workspace-picker__choices { display: grid; gap: 8px; }
877
- .flow-workspace-picker__choices button { display: flex; justify-content: space-between; align-items: center; min-height: 46px; padding: 10px 12px; color: #f5f5f5; background: #050505; border: 1px solid #404040; border-radius: 8px; font: inherit; cursor: pointer; }
878
- .flow-workspace-picker__choices button:hover { border-color: #3b82f6; background: #262626; }
879
- .flow-workspace-picker__back { justify-self: start; padding: 0; color: #a3a3a3; background: transparent; border: 0; font: inherit; cursor: pointer; }
880
- .flow-workspace-picker__warning { margin: 0; padding: 10px; color: #fbbf24; background: rgba(146,64,14,.2); border: 1px solid rgba(217,119,6,.4); border-radius: 8px; font-size: 12px; }
881
- .flow-szenen-shell {
882
- --sz-bg: #0e0e0e; --sz-surface: #171717; --sz-elevated: #262626;
883
- --sz-border: #404040; --sz-text: #f5f5f5; --sz-muted: #a3a3a3;
884
- --color-bg-app: #000; --color-bg-surface: #0a0a0a; --color-bg-surface-hover: #171717; --color-bg-elevated: #171717;
885
- --color-border: #262626; --color-border-hover: #404040;
886
- --color-text-primary: #fff; --color-text-secondary: #a3a3a3; --color-text-tertiary: #525252;
887
- --color-accent-blue: #3b82f6; --color-accent-blue-dim: rgba(59,130,246,.2);
888
- --color-accent-purple: #a855f7; --color-accent-green: #22c55e; --color-accent-green-dim: rgba(34,197,94,.2);
889
- --color-accent-red: #ef4444; --color-accent-red-dim: rgba(239,68,68,.2);
890
- --color-success: #22c55e; --color-warning: #eab308; --color-danger: #ef4444;
891
- --space-xs: .25rem; --space-sm: .5rem; --space-md: 1rem;
892
- --radius-sm: .25rem; --radius-md: .5rem; --radius-lg: .75rem; --radius-full: 9999px;
893
- --font-sans: system-ui, -apple-system, sans-serif;
894
- --font-mono: ui-monospace, SFMono-Regular, Menlo, monospace;
895
- position: fixed; inset: 0; display: flex; flex-direction: column;
896
- color: var(--sz-text); background: var(--sz-bg); overflow: hidden;
897
- }
898
- .flow-szenen-shell__header { display: flex; align-items: center; gap: 16px; min-height: 58px; padding: 8px 16px; background: #050505; border-bottom: 1px solid var(--sz-border); }
899
- .flow-szenen-shell__header button { min-height: 36px; background: var(--sz-elevated); border-color: var(--sz-border); }
900
- .flow-szenen-shell__header div { display: grid; gap: 2px; }
901
- .flow-szenen-shell__header small { color: var(--sz-muted); font-family: monospace; }
902
- .flow-szenen-shell__content { flex: 1; min-height: 0; padding: 16px; overflow: auto; }
903
- @media (max-width: 760px) { .flow-szenen-shell__content { padding: 8px; } }
904
- ` + SZENEN_VIEW_STYLES;
905
-
906
- // src/index.ts
907
- init_metadata();
908
- init_project();
909
-
910
- // src/lib/aiHelpers.ts
911
- function buildGenerationPrompt(hierarchyText, mode = "creative") {
912
- const safe = hierarchyText.length > 2500 ? hierarchyText.slice(0, 2500) + "..." : hierarchyText;
913
- if (mode === "literal") {
914
- return `Translate this hierarchy into a literal image generation prompt. Output ONLY the raw prompt string. No chat.
915
-
916
- HIERARCHY:
917
- ${safe}`;
918
- }
919
- return `Create a cinematic, high-quality image generation prompt based on this hierarchy. Translate everything to English. Output ONLY the prompt string. No chat.
920
-
921
- HIERARCHY:
922
- ${safe}`;
923
- }
924
- function buildFallbackPrompt(hierarchyText) {
925
- const lines = hierarchyText.split("\n");
926
- const firstConcept = lines.find((l) => l.includes("-"))?.replace(/^[-\s]+/, "") || "character concept";
927
- return `Cinematic professional photograph of ${firstConcept}, masterpiece, highly detailed, fine art style`;
928
- }
929
- function cleanAiResponse(text) {
930
- return text.replace(/^["']|["']$/g, "").replace(/```/g, "").trim();
931
- }
932
- function buildImageGenerationOptions(prompt, aspectRatio, model, referenceMediaId) {
933
- const options = {
934
- prompt: prompt.replace(/\n/g, " ").trim().slice(0, 1500),
935
- aspectRatio,
936
- modelDisplayName: model
937
- };
938
- if (referenceMediaId && referenceMediaId.length > 5) {
939
- options.referenceImageMediaIds = [referenceMediaId];
940
- }
941
- return options;
942
- }
943
- var CONTENT_FIRST = ["subject", "clothing", "environment", "mood", "action", "content", "general"];
944
- var RULES_LAST = ["system", "rules", "safety", "generated", "source", "status", "type", "model"];
945
- function categoryZone(cat) {
946
- const c = cat.toLowerCase();
947
- if (CONTENT_FIRST.some((k) => c.includes(k))) return 0;
948
- if (RULES_LAST.some((k) => c.includes(k))) return 2;
949
- return 1;
374
+ var CONTENT_FIRST = ["subject", "clothing", "environment", "mood", "action", "content", "general"];
375
+ var RULES_LAST = ["system", "rules", "safety", "generated", "source", "status", "type", "model"];
376
+ function categoryZone(cat) {
377
+ const c = cat.toLowerCase();
378
+ if (CONTENT_FIRST.some((k) => c.includes(k))) return 0;
379
+ if (RULES_LAST.some((k) => c.includes(k))) return 2;
380
+ return 1;
950
381
  }
951
382
  var ZONE_SEPARATORS = { 1: "\u2500\u2500\u2500\u2500 STYLE \u2500\u2500\u2500\u2500", 2: "\u2500\u2500\u2500\u2500 SYSTEM / RULES \u2500\u2500\u2500\u2500" };
952
383
  function buildPromptTabPayload(selectedTags, instructions, rules, treeText) {
@@ -2812,383 +2243,54 @@ var TaskExecutor = ({ openTasks, onExecute, onTaskCompleted }) => {
2812
2243
  ] });
2813
2244
  };
2814
2245
 
2815
- // src/components/ProjectSyncTab.tsx
2246
+ // src/components/HfMediaSyncTab.tsx
2816
2247
  var import_react17 = require("react");
2817
- init_hfStateService();
2818
2248
  var import_jsx_runtime15 = require("react/jsx-runtime");
2819
- var ProjectSyncTab = ({
2820
- topSlot,
2821
- onProjectExport,
2822
- onProjectImport,
2823
- onWorkspaceImport,
2824
- projectActionState,
2825
- serverProjects,
2826
- onServerSave,
2827
- onServerLoad,
2828
- onServerDelete,
2829
- onRefreshServerProjects,
2830
- onComputeSyncDiff,
2831
- onExecuteSync,
2832
- hfToken,
2833
- onHfLoad,
2834
- onProjectExportBase64,
2835
- onHfInitialSync
2836
- }) => {
2837
- const projectInputRef = (0, import_react17.useRef)(null);
2838
- const workspaceInputRef = (0, import_react17.useRef)(null);
2839
- const [saveName, setSaveName] = (0, import_react17.useState)("");
2840
- const [isSaving, setIsSaving] = (0, import_react17.useState)(false);
2841
- const [isExporting, setIsExporting] = (0, import_react17.useState)(false);
2842
- const [syncState, setSyncState] = (0, import_react17.useState)("idle");
2843
- const [syncDiff, setSyncDiff] = (0, import_react17.useState)(null);
2844
- const [selectedLocalIds, setSelectedLocalIds] = (0, import_react17.useState)(/* @__PURE__ */ new Set());
2845
- const handleExport = async () => {
2846
- if (!onProjectExport) return;
2847
- setIsExporting(true);
2848
- try {
2849
- await onProjectExport();
2850
- } finally {
2851
- setIsExporting(false);
2852
- }
2853
- };
2854
- const handleComputeDiff = async () => {
2855
- if (!onComputeSyncDiff) return;
2856
- setSyncState("computing");
2857
- setSyncDiff(null);
2858
- try {
2859
- const diff = await onComputeSyncDiff();
2860
- setSyncDiff(diff);
2861
- setSelectedLocalIds(new Set(diff.localOnly.map((g) => g.id)));
2862
- setSyncState("ready");
2863
- } catch {
2864
- setSyncState("error");
2865
- setTimeout(() => setSyncState("idle"), 4e3);
2866
- }
2867
- };
2868
- const handleExecuteSync = async () => {
2869
- if (!onExecuteSync || !syncDiff) return;
2870
- setSyncState("executing");
2871
- try {
2872
- await onExecuteSync([...selectedLocalIds]);
2873
- setSyncState("done");
2874
- setSyncDiff(null);
2875
- setTimeout(() => setSyncState("idle"), 3e3);
2876
- } catch {
2877
- setSyncState("error");
2878
- setTimeout(() => setSyncState("idle"), 4e3);
2879
- }
2880
- };
2881
- const toggleLocalId = (id) => {
2882
- setSelectedLocalIds((prev) => {
2883
- const next = new Set(prev);
2884
- if (next.has(id)) next.delete(id);
2885
- else next.add(id);
2886
- return next;
2887
- });
2888
- };
2889
- const isWorking = projectActionState === "working" || projectActionState === "working-full";
2890
- const [hfProjects, setHfProjects] = (0, import_react17.useState)([]);
2891
- const [hfLoading, setHfLoading] = (0, import_react17.useState)(false);
2892
- const [hfSaving, setHfSaving] = (0, import_react17.useState)(false);
2893
- const [hfError, setHfError] = (0, import_react17.useState)(null);
2894
- const [hfSaveName, setHfSaveName] = (0, import_react17.useState)("");
2895
- const [hfSyncProgress, setHfSyncProgress] = (0, import_react17.useState)(null);
2896
- const [hfSyncing, setHfSyncing] = (0, import_react17.useState)(false);
2897
- const loadHfProjects = async (token) => {
2898
- setHfLoading(true);
2899
- setHfError(null);
2900
- try {
2901
- setHfProjects(await hfListProjects(token));
2902
- } catch (e) {
2903
- setHfError(e.message);
2904
- } finally {
2905
- setHfLoading(false);
2906
- }
2907
- };
2908
- (0, import_react17.useEffect)(() => {
2909
- if (hfToken) loadHfProjects(hfToken);
2910
- }, [hfToken]);
2249
+ var HfMediaSyncTab = ({ topSlot, onHfInitialSync }) => {
2250
+ const [error, setError] = (0, import_react17.useState)(null);
2251
+ const [progress, setProgress] = (0, import_react17.useState)(null);
2252
+ const [syncing, setSyncing] = (0, import_react17.useState)(false);
2911
2253
  return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "absolute inset-0 overflow-y-auto dark-scrollbar", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "p-6 flex flex-col gap-8", children: [
2912
2254
  topSlot && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { children: topSlot }),
2913
- (onProjectExport || onProjectImport || onWorkspaceImport) && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-4", children: [
2914
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SectionLabel, { children: "Projekt-ZIP" }),
2255
+ onHfInitialSync && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-4", children: [
2256
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SectionLabel, { children: "Hugging Face" }),
2915
2257
  /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-2", children: [
2916
- onProjectExport && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PillButton, { variant: "filled", icon: "download", loading: isExporting || isWorking, onClick: handleExport, children: "Exportieren (.zip)" }),
2917
- onProjectImport && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
2918
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PillButton, { variant: "outline", icon: "upload", onClick: () => projectInputRef.current?.click(), children: "ZIP laden" }),
2919
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2920
- "input",
2921
- {
2922
- ref: projectInputRef,
2923
- type: "file",
2924
- accept: ".zip",
2925
- className: "hidden",
2926
- onChange: (e) => {
2927
- const f = e.target.files?.[0];
2928
- if (f) onProjectImport(f);
2929
- e.target.value = "";
2930
- }
2931
- }
2932
- )
2933
- ] })
2934
- ] }),
2935
- onWorkspaceImport && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
2936
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PillButton, { variant: "outline", icon: "upload_file", onClick: () => workspaceInputRef.current?.click(), children: "Tags laden (.json)" }),
2937
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2938
- "input",
2939
- {
2940
- ref: workspaceInputRef,
2941
- type: "file",
2942
- accept: ".json",
2943
- className: "hidden",
2944
- onChange: (e) => {
2945
- const f = e.target.files?.[0];
2946
- if (f) onWorkspaceImport(f);
2947
- e.target.value = "";
2948
- }
2949
- }
2950
- )
2951
- ] })
2952
- ] }),
2953
- (onServerSave || serverProjects || onServerDelete) && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-4", children: [
2954
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SectionLabel, { children: "Server-Speicher" }),
2955
- onServerSave && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex gap-2", children: [
2956
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2957
- "input",
2958
- {
2959
- type: "text",
2960
- value: saveName,
2961
- onChange: (e) => setSaveName(e.target.value),
2962
- placeholder: "Name (optional)",
2963
- className: "flex-1 bg-white/5 border border-white/10 rounded-xl px-3 text-[12px] text-white/70 outline-none placeholder:text-white/20",
2964
- style: { height: 40 }
2965
- }
2966
- ),
2967
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2968
- PillButton,
2969
- {
2970
- variant: "filled",
2971
- icon: "cloud_upload",
2972
- loading: isSaving,
2973
- onClick: async () => {
2974
- setIsSaving(true);
2975
- try {
2976
- await onServerSave(saveName || void 0);
2977
- setSaveName("");
2978
- await onRefreshServerProjects?.();
2979
- } finally {
2980
- setIsSaving(false);
2981
- }
2982
- },
2983
- children: "Speichern"
2984
- }
2985
- )
2986
- ] }),
2987
- (!serverProjects || serverProjects.length === 0) && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[10px] text-white/20 px-2", children: "Noch nichts gespeichert." }),
2988
- serverProjects && serverProjects.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "flex flex-col gap-1", children: serverProjects.map((p) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-2 px-3 py-2 rounded-xl bg-white/5 border border-white/5", children: [
2989
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex-1 min-w-0", children: [
2990
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[11px] text-white/70 font-bold truncate", children: p.name }),
2991
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("p", { className: "text-[9px] text-white/25", children: [
2992
- new Date(p.saved_at).toLocaleString("de-DE", { dateStyle: "short", timeStyle: "short" }),
2993
- " \xB7 ",
2994
- (p.size / 1024).toFixed(0),
2995
- " KB"
2996
- ] })
2997
- ] }),
2998
- onServerLoad && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { onClick: () => onServerLoad(p.id), className: "w-8 h-8 flex items-center justify-center text-white/30 active:text-white transition-colors", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "material-symbols-outlined text-[18px]", children: "cloud_download" }) }),
2999
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { onClick: () => onServerDelete?.(p.id), className: "w-8 h-8 flex items-center justify-center text-white/20 active:text-red-400 transition-colors", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "material-symbols-outlined text-[18px]", children: "delete" }) })
3000
- ] }, p.id)) })
3001
- ] }),
3002
- hfToken && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-4", children: [
3003
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center justify-between", children: [
3004
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SectionLabel, { children: "Hugging Face" }),
3005
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("button", { onClick: () => loadHfProjects(hfToken), className: "text-white/30 hover:text-white/60 transition", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: `material-symbols-outlined text-[18px]${hfLoading ? " animate-spin" : ""}`, children: "refresh" }) })
3006
- ] }),
3007
- onProjectExportBase64 && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex gap-2", children: [
3008
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3009
- "input",
3010
- {
3011
- type: "text",
3012
- value: hfSaveName,
3013
- onChange: (e) => setHfSaveName(e.target.value),
3014
- placeholder: "Name (optional)",
3015
- className: "flex-1 bg-white/5 border border-white/10 rounded-xl px-3 text-[12px] text-white/70 outline-none placeholder:text-white/20",
3016
- style: { height: 40 }
3017
- }
3018
- ),
3019
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3020
- PillButton,
3021
- {
3022
- variant: "filled",
3023
- icon: "cloud_upload",
3024
- loading: hfSaving,
3025
- onClick: async () => {
3026
- if (!onProjectExportBase64) return;
3027
- setHfSaving(true);
3028
- setHfError(null);
3029
- try {
3030
- const base64 = await onProjectExportBase64();
3031
- const name = hfSaveName.trim() || `project_${(/* @__PURE__ */ new Date()).toISOString().slice(0, 16).replace("T", "_").replace(":", "")}`;
3032
- await hfUploadProjectForm(base64, name, hfToken);
3033
- setHfSaveName("");
3034
- await loadHfProjects(hfToken);
3035
- } catch (e) {
3036
- setHfError(e.message);
3037
- } finally {
3038
- setHfSaving(false);
3039
- }
3040
- },
3041
- children: "Speichern"
3042
- }
3043
- )
3044
- ] }),
3045
- onHfInitialSync && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-2", children: [
3046
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3047
- PillButton,
3048
- {
3049
- variant: "outline",
3050
- icon: "cloud_sync",
3051
- loading: hfSyncing,
3052
- onClick: async () => {
3053
- setHfSyncing(true);
3054
- setHfError(null);
3055
- setHfSyncProgress({ done: 0, total: 0 });
3056
- try {
3057
- await onHfInitialSync((done, total) => setHfSyncProgress({ done, total }));
3058
- setHfSyncProgress(null);
3059
- } catch (e) {
3060
- setHfError(e.message);
3061
- setHfSyncProgress(null);
3062
- } finally {
3063
- setHfSyncing(false);
3064
- }
3065
- },
3066
- children: hfSyncing && hfSyncProgress ? `${hfSyncProgress.done} / ${hfSyncProgress.total} Bilder` : "Initial-Sync nach HF"
3067
- }
3068
- ),
3069
- hfSyncing && hfSyncProgress && hfSyncProgress.total > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "w-full h-1 bg-white/10 rounded-full overflow-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3070
- "div",
3071
- {
3072
- className: "h-full bg-blue-400 rounded-full transition-all",
3073
- style: { width: `${Math.round(hfSyncProgress.done / hfSyncProgress.total * 100)}%` }
3074
- }
3075
- ) })
3076
- ] }),
3077
- hfError && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[10px] text-red-400 font-mono px-1", children: hfError }),
3078
- !hfLoading && hfProjects.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[10px] text-white/20 px-2", children: "Noch nichts auf HF gespeichert." }),
3079
- hfProjects.map((p) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-2 px-3 py-2 rounded-xl bg-white/5 border border-white/5", children: [
3080
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex-1 min-w-0", children: [
3081
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[11px] text-white/70 font-bold truncate", children: p.name }),
3082
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("p", { className: "text-[9px] text-white/25", children: [
3083
- (p.size / 1024 / 1024).toFixed(1),
3084
- " MB",
3085
- p.isLfs ? " \xB7 LFS" : ""
3086
- ] })
3087
- ] }),
3088
- onHfLoad && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3089
- "button",
3090
- {
3091
- onClick: async () => {
3092
- try {
3093
- const { hfDownloadProject: hfDownloadProject2 } = await Promise.resolve().then(() => (init_hfStateService(), hfStateService_exports));
3094
- const file = await hfDownloadProject2(p.path, hfToken);
3095
- onHfLoad(file);
3096
- } catch (e) {
3097
- setHfError(e.message);
3098
- }
3099
- },
3100
- className: "w-8 h-8 flex items-center justify-center text-white/30 active:text-white transition-colors",
3101
- children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "material-symbols-outlined text-[18px]", children: "cloud_download" })
3102
- }
3103
- ),
3104
2258
  /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3105
- "button",
3106
- {
3107
- onClick: async () => {
3108
- if (!confirm(`"${p.name}" auf HF l\xF6schen?`)) return;
3109
- try {
3110
- await hfDeleteProject(p.path, hfToken);
3111
- await loadHfProjects(hfToken);
3112
- } catch (e) {
3113
- setHfError(e.message);
3114
- }
3115
- },
3116
- className: "w-8 h-8 flex items-center justify-center text-white/20 active:text-red-400 transition-colors",
3117
- children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "material-symbols-outlined text-[18px]", children: "delete" })
3118
- }
3119
- )
3120
- ] }, p.id))
3121
- ] }),
3122
- onComputeSyncDiff && onExecuteSync && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-4", children: [
3123
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(SectionLabel, { children: "Sync" }),
3124
- (syncState === "idle" || syncState === "computing") && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(PillButton, { variant: "outline", icon: "compare_arrows", loading: syncState === "computing", onClick: handleComputeDiff, children: "Diff berechnen" }),
3125
- syncState === "done" && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-2 px-2 text-[11px] text-green-400", children: [
3126
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "material-symbols-outlined text-[16px]", children: "check_circle" }),
3127
- "Sync abgeschlossen"
3128
- ] }),
3129
- syncState === "error" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[11px] text-red-400 px-2", children: "Fehler beim Sync." }),
3130
- syncDiff && (syncState === "ready" || syncState === "executing") && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
3131
- syncDiff.localOnly.length === 0 && syncDiff.serverOnly.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[10px] text-white/30 px-2", children: "Alles synchron \u2014 keine Unterschiede." }),
3132
- syncDiff.localOnly.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-2", children: [
3133
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("p", { className: "text-[9px] text-white/30 uppercase tracking-widest px-1 font-bold", children: [
3134
- "Nur lokal (",
3135
- syncDiff.localOnly.length,
3136
- ")"
3137
- ] }),
3138
- syncDiff.localOnly.map((gen) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3139
- "button",
3140
- {
3141
- onClick: () => toggleLocalId(gen.id),
3142
- className: `flex items-center gap-3 px-3 py-2 rounded-xl border transition-all text-left ${selectedLocalIds.has(gen.id) ? "bg-blue-500/10 border-blue-500/30" : "bg-white/3 border-white/5 opacity-60"}`,
3143
- children: [
3144
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: `w-4 h-4 rounded border flex items-center justify-center flex-shrink-0 ${selectedLocalIds.has(gen.id) ? "bg-blue-500 border-blue-500" : "border-white/20"}`, children: selectedLocalIds.has(gen.id) && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "material-symbols-outlined text-[11px] text-white", children: "check" }) }),
3145
- gen.base64 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("img", { src: gen.base64, className: "w-10 h-10 rounded object-cover flex-shrink-0" }),
3146
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex-1 min-w-0", children: [
3147
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[10px] text-white/60 truncate", children: gen.prompt || "Kein Prompt" }),
3148
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[9px] text-white/25", children: new Date(gen.timestamp).toLocaleString("de-DE", { dateStyle: "short", timeStyle: "short" }) })
3149
- ] })
3150
- ]
3151
- },
3152
- gen.id
3153
- ))
3154
- ] }),
3155
- syncDiff.serverOnly.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex flex-col gap-2", children: [
3156
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("p", { className: "text-[9px] text-white/30 uppercase tracking-widest px-1 font-bold", children: [
3157
- "Nur auf Server (",
3158
- syncDiff.serverOnly.length,
3159
- ")"
3160
- ] }),
3161
- syncDiff.serverOnly.map((gen) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-3 px-3 py-2 rounded-xl bg-white/3 border border-white/5 opacity-40", children: [
3162
- gen.base64 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("img", { src: gen.base64, className: "w-10 h-10 rounded object-cover flex-shrink-0" }),
3163
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex-1 min-w-0", children: [
3164
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[10px] text-white/60 truncate", children: gen.prompt || "Kein Prompt" }),
3165
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[9px] text-white/25", children: new Date(gen.timestamp).toLocaleString("de-DE", { dateStyle: "short", timeStyle: "short" }) })
3166
- ] })
3167
- ] }, gen.id))
3168
- ] }),
3169
- (syncDiff.localOnly.length > 0 || syncDiff.serverOnly.length > 0) && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3170
2259
  PillButton,
3171
2260
  {
3172
- variant: "solid",
3173
- icon: "sync",
3174
- loading: syncState === "executing",
3175
- disabled: selectedLocalIds.size === 0,
3176
- onClick: handleExecuteSync,
3177
- children: [
3178
- selectedLocalIds.size,
3179
- " \xFCbertragen"
3180
- ]
2261
+ variant: "outline",
2262
+ icon: "cloud_sync",
2263
+ loading: syncing,
2264
+ onClick: async () => {
2265
+ setSyncing(true);
2266
+ setError(null);
2267
+ setProgress({ done: 0, total: 0 });
2268
+ try {
2269
+ await onHfInitialSync((done, total) => setProgress({ done, total }));
2270
+ setProgress(null);
2271
+ } catch (e) {
2272
+ setError(e instanceof Error ? e.message : String(e));
2273
+ setProgress(null);
2274
+ } finally {
2275
+ setSyncing(false);
2276
+ }
2277
+ },
2278
+ children: syncing && progress ? `${progress.done} / ${progress.total} Bilder` : "Initial-Sync nach HF"
3181
2279
  }
3182
- )
3183
- ] })
2280
+ ),
2281
+ syncing && progress && progress.total > 0 && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "w-full h-1 bg-white/10 rounded-full overflow-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
2282
+ "div",
2283
+ {
2284
+ className: "h-full bg-blue-400 rounded-full transition-all",
2285
+ style: { width: `${Math.round(progress.done / progress.total * 100)}%` }
2286
+ }
2287
+ ) })
2288
+ ] }),
2289
+ error && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { className: "text-[10px] text-red-400 font-mono px-1", children: error })
3184
2290
  ] })
3185
2291
  ] }) });
3186
2292
  };
3187
2293
 
3188
- // src/components/AvatarArchitectApp.tsx
3189
- init_project();
3190
- init_metadata();
3191
-
3192
2294
  // src/lib/labHelpers.ts
3193
2295
  function groupGenerationsToLabItems(generations) {
3194
2296
  const map = /* @__PURE__ */ new Map();
@@ -3262,65 +2364,436 @@ Create a creative variation of this prompt based on the instruction. Output ONLY
3262
2364
  systemInstruction: "You are an expert prompt engineer. Return ONLY the new generation prompt, no headers, no quotes, no explanations."
3263
2365
  };
3264
2366
  }
3265
- function buildBlendInstruction(images, userInstruction) {
3266
- const imgContext = buildImageContext(images);
3267
- const promptList = images.map((i) => `${i.label}: "${i.item.prompt}"`).join("\n");
3268
- return {
3269
- text: `Blend these prompts into one new prompt:
3270
- ${promptList}
3271
-
3272
- Instruction: ${userInstruction}${imgContext}
2367
+ function buildBlendInstruction(images, userInstruction) {
2368
+ const imgContext = buildImageContext(images);
2369
+ const promptList = images.map((i) => `${i.label}: "${i.item.prompt}"`).join("\n");
2370
+ return {
2371
+ text: `Blend these prompts into one new prompt:
2372
+ ${promptList}
2373
+
2374
+ Instruction: ${userInstruction}${imgContext}
2375
+
2376
+ Output ONLY the blended prompt string.`,
2377
+ systemInstruction: "You are an expert prompt engineer. Blend the given prompts into one coherent generation prompt. Return ONLY the prompt string."
2378
+ };
2379
+ }
2380
+ function buildCompareInstruction(images, userInstruction) {
2381
+ const imgContext = buildImageContext(images);
2382
+ return {
2383
+ text: `${userInstruction}${imgContext}
2384
+
2385
+ Analyze the provided images according to the instruction above.`,
2386
+ systemInstruction: "You are an expert image analyst. Provide a clear, structured analysis. Be concise and actionable."
2387
+ };
2388
+ }
2389
+ function buildLoopInstruction(rounds, newInstruction) {
2390
+ const history = rounds.map(
2391
+ (r, i) => `Round ${i + 1} prompt: "${r.prompt}"
2392
+ Instruction given: "${r.instruction}"`
2393
+ ).join("\n\n");
2394
+ const activeImages = rounds.flatMap((r) => r.images.filter((i) => i.sendToPromptAI));
2395
+ const imgContext = activeImages.length ? buildImageContext(activeImages) : "";
2396
+ return {
2397
+ text: `Previous rounds:
2398
+ ${history}
2399
+
2400
+ New instruction: ${newInstruction}${imgContext}
2401
+
2402
+ Improve the prompt cumulatively based on all previous rounds and the new instruction. Output ONLY the new prompt string.`,
2403
+ systemInstruction: "You are an expert prompt engineer refining prompts iteratively. Each round the prompt should improve. Return ONLY the new prompt string."
2404
+ };
2405
+ }
2406
+ function autoLabel(index) {
2407
+ return `Img${String.fromCharCode(65 + index)}`;
2408
+ }
2409
+ function buildReferenceImageMediaIds(images) {
2410
+ return images.filter((i) => i.sendToImageGen && i.frame.mediaId).map((i) => i.frame.mediaId);
2411
+ }
2412
+ function toPromptImages(images) {
2413
+ return images.filter((i) => i.sendToPromptAI && i.frame.base64).map((i) => {
2414
+ const raw = i.frame.base64.includes(",") ? i.frame.base64.split(",")[1] : i.frame.base64;
2415
+ const mimeType = i.frame.base64.startsWith("data:image/png") ? "image/png" : "image/jpeg";
2416
+ return { base64: raw, mimeType };
2417
+ });
2418
+ }
2419
+
2420
+ // src/lib/hfStateService.ts
2421
+ var import_jszip = __toESM(require("jszip"));
2422
+ var import_hub = require("@huggingface/hub");
2423
+
2424
+ // src/lib/hfEventTypes.ts
2425
+ var HF_BASE = "https://huggingface.co";
2426
+ var HF_REPO = "RolandSch/fa-app-state";
2427
+ var CURRENT_EVENT_VERSION = { major: 1, minor: 0 };
2428
+ var SPEICHERBARE_BILDTYPEN = ["image/png", "image/jpeg", "image/webp"];
2429
+ function bildEndungFuer(mimeType) {
2430
+ if (mimeType === "image/png") return "png";
2431
+ if (mimeType === "image/webp") return "webp";
2432
+ return "jpg";
2433
+ }
2434
+
2435
+ // src/lib/szenen/szenenSnapshot.ts
2436
+ var SZENEN_SNAPSHOT_SCHEMA_VERSION = 1;
2437
+ var SZENEN_SNAPSHOT_PREFIX = "szenen-state-";
2438
+ function istSzenenSnapshotDatei(name) {
2439
+ return name.startsWith(SZENEN_SNAPSHOT_PREFIX) && name.endsWith(".json");
2440
+ }
2441
+ var maxTsOf = (events) => {
2442
+ let max = 0;
2443
+ for (const event of events) if (event.ts > max) max = event.ts;
2444
+ return max;
2445
+ };
2446
+ function aufgenommeneDateien(snapshot) {
2447
+ return new Set(snapshot ? snapshot.consumed.paths : []);
2448
+ }
2449
+ function parseSzenenSnapshot(raw, herkunft) {
2450
+ if (!raw || typeof raw !== "object") {
2451
+ throw new Error(`Szenen-Snapshot ${herkunft}: kein Objekt, sondern ${typeof raw}.`);
2452
+ }
2453
+ const obj = raw;
2454
+ if (obj.schema_version !== SZENEN_SNAPSHOT_SCHEMA_VERSION) {
2455
+ throw new Error(
2456
+ `Szenen-Snapshot ${herkunft}: schema_version ${String(obj.schema_version)} wird nicht gelesen, erwartet ${SZENEN_SNAPSHOT_SCHEMA_VERSION}.`
2457
+ );
2458
+ }
2459
+ if (!Array.isArray(obj.events)) {
2460
+ throw new Error(`Szenen-Snapshot ${herkunft}: "events" fehlt oder ist kein Array.`);
2461
+ }
2462
+ const events = obj.events;
2463
+ for (const [i, event] of events.entries()) {
2464
+ if (!event || typeof event.type !== "string" || typeof event.ts !== "number") {
2465
+ throw new Error(
2466
+ `Szenen-Snapshot ${herkunft}: Ereignis ${i} hat keinen Typ oder keinen Zeitstempel.`
2467
+ );
2468
+ }
2469
+ }
2470
+ const consumed = obj.consumed;
2471
+ if (!consumed || typeof consumed.count !== "number" || typeof consumed.max_ts !== "number") {
2472
+ throw new Error(`Szenen-Snapshot ${herkunft}: "consumed" fehlt oder ist unvollstaendig.`);
2473
+ }
2474
+ if (!Array.isArray(consumed.paths)) {
2475
+ throw new Error(
2476
+ `Szenen-Snapshot ${herkunft}: "consumed.paths" fehlt. Ohne die aufgenommenen Dateinamen laesst sich nicht entscheiden, welche Datei in events/ noch aussteht.`
2477
+ );
2478
+ }
2479
+ if (consumed.count !== events.length) {
2480
+ throw new Error(
2481
+ `Szenen-Snapshot ${herkunft}: consumed.count ${consumed.count} passt nicht zu ${events.length} Ereignis(sen) in der Datei \u2014 die Datei ist unvollstaendig.`
2482
+ );
2483
+ }
2484
+ const echt = maxTsOf(events);
2485
+ if (consumed.max_ts !== echt) {
2486
+ throw new Error(
2487
+ `Szenen-Snapshot ${herkunft}: consumed.max_ts ${consumed.max_ts} passt nicht zum groessten Zeitstempel ${echt} in der Datei.`
2488
+ );
2489
+ }
2490
+ return { schema_version: SZENEN_SNAPSHOT_SCHEMA_VERSION, consumed, events };
2491
+ }
2492
+ function mergeSnapshotAndPending(snapshot, pending, bereitsImSnapshot = 0) {
2493
+ if (!snapshot) return { events: pending, bereitsImSnapshot };
2494
+ return { events: [...snapshot.events, ...pending], bereitsImSnapshot };
2495
+ }
2496
+
2497
+ // src/lib/hfEventLog.ts
2498
+ function treeUrl(namespace, subdir = "") {
2499
+ const parts = [namespace.replace(/\/$/, ""), subdir].filter(Boolean).join("/");
2500
+ return `${HF_BASE}/api/datasets/${HF_REPO}/tree/main${parts ? "/" + parts : ""}`;
2501
+ }
2502
+ var DROSSEL_VERSUCHE = 4;
2503
+ var DROSSEL_BASIS_MS = 400;
2504
+ var schlafen = (ms) => new Promise((r) => setTimeout(r, ms));
2505
+ var HF_LESE_ZEITLIMIT_MS = 2e4;
2506
+ async function holeMitZeitlimit(url, init) {
2507
+ const abbruch = new AbortController();
2508
+ const timer = setTimeout(() => abbruch.abort(), HF_LESE_ZEITLIMIT_MS);
2509
+ try {
2510
+ return await fetch(url, { ...init, signal: abbruch.signal });
2511
+ } catch (error) {
2512
+ if (abbruch.signal.aborted) {
2513
+ throw new Error(`HF-Anfrage ohne Antwort nach ${HF_LESE_ZEITLIMIT_MS}ms abgebrochen: ${url}`);
2514
+ }
2515
+ throw error;
2516
+ } finally {
2517
+ clearTimeout(timer);
2518
+ }
2519
+ }
2520
+ async function mitGeduld(anfrage) {
2521
+ let letzte = null;
2522
+ for (let versuch = 0; versuch < DROSSEL_VERSUCHE; versuch += 1) {
2523
+ const res = await anfrage();
2524
+ if (res.status !== 429 && res.status < 500) return res;
2525
+ letzte = res;
2526
+ if (versuch === DROSSEL_VERSUCHE - 1) break;
2527
+ const angesagt = Number(res.headers.get("retry-after"));
2528
+ const warten2 = Number.isFinite(angesagt) && angesagt > 0 ? angesagt * 1e3 : DROSSEL_BASIS_MS * 2 ** versuch;
2529
+ await schlafen(warten2);
2530
+ }
2531
+ return letzte;
2532
+ }
2533
+ async function hfListDir(namespace, subdir, token) {
2534
+ const res = await mitGeduld(() => holeMitZeitlimit(treeUrl(namespace, subdir), {
2535
+ headers: { Authorization: `Bearer ${token}` }
2536
+ }));
2537
+ if (res.status === 404) return [];
2538
+ if (!res.ok) throw new Error(`HF list failed: ${res.status} ${res.statusText}`);
2539
+ return res.json();
2540
+ }
2541
+ async function hfDownloadJsonByPath(repoPath, token) {
2542
+ const res = await mitGeduld(() => holeMitZeitlimit(
2543
+ `${HF_BASE}/datasets/${HF_REPO}/resolve/main/${repoPath}?download=true`,
2544
+ { headers: { Authorization: `Bearer ${token}` } }
2545
+ ));
2546
+ if (!res.ok) throw new Error(`HF download failed: ${res.status}`);
2547
+ return res.json();
2548
+ }
2549
+ async function hfDownloadBinaryByPath(repoPath, token) {
2550
+ const res = await mitGeduld(() => holeMitZeitlimit(
2551
+ `${HF_BASE}/datasets/${HF_REPO}/resolve/main/${repoPath}?download=true`,
2552
+ { headers: { Authorization: `Bearer ${token}` } }
2553
+ ));
2554
+ if (!res.ok) throw new Error(`HF download binary failed: ${res.status}`);
2555
+ return new Uint8Array(await res.arrayBuffer());
2556
+ }
2557
+ function toBase64(text) {
2558
+ const bytes = new TextEncoder().encode(text);
2559
+ let binary = "";
2560
+ bytes.forEach((b) => binary += String.fromCharCode(b));
2561
+ return btoa(binary);
2562
+ }
2563
+ async function hfUploadSmallFile(repoPath, content, token, summary = `Update ${repoPath}`) {
2564
+ const res = await fetch(`${HF_BASE}/api/datasets/${HF_REPO}/commit/main`, {
2565
+ method: "POST",
2566
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/x-ndjson" },
2567
+ body: [
2568
+ JSON.stringify({ key: "header", value: { summary, description: "" } }),
2569
+ JSON.stringify({ key: "file", value: { path: repoPath, encoding: "base64", content: toBase64(content) } })
2570
+ ].join("\n")
2571
+ });
2572
+ if (!res.ok) {
2573
+ const err = await res.text().catch(() => "");
2574
+ throw new Error(`HF upload failed: ${res.status} \u2014 ${err.slice(0, 200)}`);
2575
+ }
2576
+ }
2577
+ async function hfBatchArchive(moves, token, summary) {
2578
+ const lines = [
2579
+ JSON.stringify({ key: "header", value: { summary, description: "" } }),
2580
+ ...moves.flatMap(({ from, to, content }) => [
2581
+ JSON.stringify({ key: "file", value: { path: to, encoding: "base64", content: toBase64(content) } }),
2582
+ JSON.stringify({ key: "deletedFile", value: { path: from } })
2583
+ ])
2584
+ ];
2585
+ const res = await fetch(`${HF_BASE}/api/datasets/${HF_REPO}/commit/main`, {
2586
+ method: "POST",
2587
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/x-ndjson" },
2588
+ body: lines.join("\n")
2589
+ });
2590
+ if (!res.ok) {
2591
+ const err = await res.text().catch(() => "");
2592
+ throw new Error(`HF batch archive failed: ${res.status} \u2014 ${err.slice(0, 200)}`);
2593
+ }
2594
+ }
2595
+ function tsFromEventPath(repoPath) {
2596
+ const filename = repoPath.split("/").pop() || "";
2597
+ const base = filename.replace(/_[^_]+\.json$/, "");
2598
+ const iso = base.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z$/, "T$1:$2:$3.$4Z");
2599
+ const ts = Date.parse(iso);
2600
+ return isNaN(ts) ? 0 : ts;
2601
+ }
2602
+ var EVENTDOWNLOAD_PARALLELITAET = 3;
2603
+ async function mitBegrenzterParallelitaet(count, limit, task) {
2604
+ let cursor = 0;
2605
+ let ersterFehler;
2606
+ let fehlerAufgetreten = false;
2607
+ const worker = async () => {
2608
+ while (cursor < count) {
2609
+ const index = cursor++;
2610
+ try {
2611
+ await task(index);
2612
+ } catch (error) {
2613
+ if (!fehlerAufgetreten) {
2614
+ fehlerAufgetreten = true;
2615
+ ersterFehler = error;
2616
+ }
2617
+ }
2618
+ }
2619
+ };
2620
+ await Promise.all(Array.from({ length: Math.min(limit, count) }, worker));
2621
+ if (fehlerAufgetreten) throw ersterFehler;
2622
+ }
2623
+ async function loadPendingEvents(namespace, token, sinceTs, knownPaths) {
2624
+ const files = await hfListDir(namespace, "events", token);
2625
+ const pending = files.filter((f) => f.type === "file" && tsFromEventPath(f.path) > sinceTs && !knownPaths?.has(f.path)).sort((a, b) => a.path.localeCompare(b.path));
2626
+ if (!pending.length) return [];
2627
+ const raw = new Array(pending.length);
2628
+ await mitBegrenzterParallelitaet(pending.length, EVENTDOWNLOAD_PARALLELITAET, async (index) => {
2629
+ raw[index] = await hfDownloadJsonByPath(pending[index].path, token);
2630
+ });
2631
+ for (const f of pending) knownPaths?.add(f.path);
2632
+ return raw.flatMap((e) => Array.isArray(e) ? e : [e]);
2633
+ }
2634
+ async function findLatestSzenenSnapshotPath(namespace, token) {
2635
+ const files = await hfListDir(namespace, "", token);
2636
+ const treffer = files.filter((f) => f.type === "file" && istSzenenSnapshotDatei(f.path.split("/").pop() || "")).map((f) => f.path).sort((a, b) => b.localeCompare(a));
2637
+ return treffer[0] ?? null;
2638
+ }
2639
+ async function loadLatestSzenenSnapshot(namespace, token) {
2640
+ const pfad = await findLatestSzenenSnapshotPath(namespace, token);
2641
+ if (!pfad) return null;
2642
+ return parseSzenenSnapshot(await hfDownloadJsonByPath(pfad, token), pfad);
2643
+ }
2644
+ var SESSION_CLIENT_ID = `client-${crypto.randomUUID().slice(0, 8)}`;
2645
+ function getSessionClientId() {
2646
+ return SESSION_CLIENT_ID;
2647
+ }
2648
+ function tsToFilename(ts) {
2649
+ return new Date(ts).toISOString().replace(/:/g, "-").replace(".", "-");
2650
+ }
2651
+ var writeSeq = 0;
2652
+ var lastOrderedTs = 0;
2653
+ async function writeHFEvent(namespace, token, type, payload, prevTs, options) {
2654
+ const wallTs = Date.now();
2655
+ const ts = options?.ordered ? Math.max(wallTs, lastOrderedTs) : wallTs;
2656
+ if (options?.ordered) lastOrderedTs = ts;
2657
+ const uuid = crypto.randomUUID().slice(0, 8);
2658
+ const event = {
2659
+ v: CURRENT_EVENT_VERSION,
2660
+ type,
2661
+ ts,
2662
+ prevTs,
2663
+ clientId: SESSION_CLIENT_ID,
2664
+ payload
2665
+ };
2666
+ const suffix = options?.ordered ? `${String(++writeSeq % 1e6).padStart(6, "0")}${uuid}` : uuid;
2667
+ const filename = `${tsToFilename(ts)}_${suffix}.json`;
2668
+ const repoPath = `${namespace}events/${filename}`;
2669
+ await hfUploadSmallFile(repoPath, JSON.stringify(event, null, 2), token, `Event: ${type}`);
2670
+ return event;
2671
+ }
3273
2672
 
3274
- Output ONLY the blended prompt string.`,
3275
- systemInstruction: "You are an expert prompt engineer. Blend the given prompts into one coherent generation prompt. Return ONLY the prompt string."
3276
- };
2673
+ // src/lib/hfStateService.ts
2674
+ var HF_TOKEN_KEY = "hf-token";
2675
+ function getHFToken() {
2676
+ try {
2677
+ return localStorage.getItem(HF_TOKEN_KEY);
2678
+ } catch {
2679
+ return null;
2680
+ }
3277
2681
  }
3278
- function buildCompareInstruction(images, userInstruction) {
3279
- const imgContext = buildImageContext(images);
3280
- return {
3281
- text: `${userInstruction}${imgContext}
3282
-
3283
- Analyze the provided images according to the instruction above.`,
3284
- systemInstruction: "You are an expert image analyst. Provide a clear, structured analysis. Be concise and actionable."
3285
- };
2682
+ function setHFToken(token) {
2683
+ try {
2684
+ localStorage.setItem(HF_TOKEN_KEY, token);
2685
+ } catch {
2686
+ }
3286
2687
  }
3287
- function buildLoopInstruction(rounds, newInstruction) {
3288
- const history = rounds.map(
3289
- (r, i) => `Round ${i + 1} prompt: "${r.prompt}"
3290
- Instruction given: "${r.instruction}"`
3291
- ).join("\n\n");
3292
- const activeImages = rounds.flatMap((r) => r.images.filter((i) => i.sendToPromptAI));
3293
- const imgContext = activeImages.length ? buildImageContext(activeImages) : "";
2688
+ async function loadHFState(namespace, token) {
2689
+ const files = await hfListDir(namespace, "", token);
2690
+ const stateFiles = files.filter((f) => f.type === "file" && /^state-[\dT\-]+Z\.zip$/.test(f.path.split("/").pop() || "")).sort((a, b) => b.path.localeCompare(a.path));
2691
+ if (!stateFiles.length) return null;
2692
+ const zipBytes = await hfDownloadBinaryByPath(stateFiles[0].path, token);
2693
+ const zip = await import_jszip.default.loadAsync(zipBytes);
2694
+ const [metadataStr, tagsStr, metaStr, openTasksStr] = await Promise.all([
2695
+ zip.file("metadata.json")?.async("string"),
2696
+ zip.file("tags.json")?.async("string"),
2697
+ zip.file("state_meta.json")?.async("string"),
2698
+ zip.file("open_tasks.json")?.async("string")
2699
+ // optional — ältere Snapshots haben es nicht
2700
+ ]);
2701
+ if (!metadataStr || !tagsStr || !metaStr) throw new Error("state.zip: fehlende Pflicht-Dateien");
3294
2702
  return {
3295
- text: `Previous rounds:
3296
- ${history}
3297
-
3298
- New instruction: ${newInstruction}${imgContext}
3299
-
3300
- Improve the prompt cumulatively based on all previous rounds and the new instruction. Output ONLY the new prompt string.`,
3301
- systemInstruction: "You are an expert prompt engineer refining prompts iteratively. Each round the prompt should improve. Return ONLY the new prompt string."
2703
+ metadata: JSON.parse(metadataStr),
2704
+ tags: JSON.parse(tagsStr),
2705
+ meta: JSON.parse(metaStr),
2706
+ openTasks: openTasksStr ? JSON.parse(openTasksStr) : {}
3302
2707
  };
3303
2708
  }
3304
- function autoLabel(index) {
3305
- return `Img${String.fromCharCode(65 + index)}`;
3306
- }
3307
- function buildReferenceImageMediaIds(images) {
3308
- return images.filter((i) => i.sendToImageGen && i.frame.mediaId).map((i) => i.frame.mediaId);
2709
+ async function hfBootstrapFromLegacy(namespace, token, onProgress) {
2710
+ const log = (msg) => {
2711
+ if (onProgress) onProgress(msg);
2712
+ };
2713
+ log("Lese tags.json \u2026");
2714
+ const tags = await fetch(
2715
+ `${HF_BASE}/datasets/${HF_REPO}/resolve/main/tags.json?download=true`,
2716
+ { headers: { Authorization: `Bearer ${token}` } }
2717
+ ).then((r) => r.ok ? r.json() : null).catch(() => null);
2718
+ log("Lese metadata.json \u2026");
2719
+ const metadata = await fetch(
2720
+ `${HF_BASE}/datasets/${HF_REPO}/resolve/main/metadata.json?download=true`,
2721
+ { headers: { Authorization: `Bearer ${token}` } }
2722
+ ).then((r) => r.ok ? r.json() : []).catch(() => []);
2723
+ const snapshot = {
2724
+ metadata: Array.isArray(metadata) ? metadata : [],
2725
+ tags: tags?.by_category ? tags : { by_category: {}, all: [] },
2726
+ meta: { consolidatedAt: Date.now(), version: 1 }
2727
+ };
2728
+ log(`Erstelle state.zip (${snapshot.metadata.length} Bilder, ${snapshot.tags.all?.length ?? 0} Tags) \u2026`);
2729
+ const zip = new import_jszip.default();
2730
+ zip.file("metadata.json", JSON.stringify(snapshot.metadata, null, 2));
2731
+ zip.file("tags.json", JSON.stringify(snapshot.tags, null, 2));
2732
+ zip.file("state_meta.json", JSON.stringify(snapshot.meta, null, 2));
2733
+ const zipBytes = await zip.generateAsync({ type: "uint8array", compression: "DEFLATE" });
2734
+ const ts = snapshot.meta.consolidatedAt;
2735
+ const stateFilename = `state-${new Date(ts).toISOString().replace(/:/g, "-").replace(".", "-")}.zip`;
2736
+ const repoPath = `${namespace}${stateFilename}`;
2737
+ log("Lade state.zip hoch \u2026");
2738
+ await (0, import_hub.uploadFile)({
2739
+ repo: { type: "dataset", name: HF_REPO },
2740
+ credentials: { accessToken: token },
2741
+ file: { path: repoPath, content: new Blob([zipBytes.buffer], { type: "application/zip" }) },
2742
+ branch: "main",
2743
+ commitTitle: `Bootstrap: initialer State aus Legacy-Daten`
2744
+ });
2745
+ log(`Fertig \u2014 ${stateFilename}`);
3309
2746
  }
3310
- function toPromptImages(images) {
3311
- return images.filter((i) => i.sendToPromptAI && i.frame.base64).map((i) => {
3312
- const raw = i.frame.base64.includes(",") ? i.frame.base64.split(",")[1] : i.frame.base64;
3313
- const mimeType = i.frame.base64.startsWith("data:image/png") ? "image/png" : "image/jpeg";
3314
- return { base64: raw, mimeType };
2747
+ async function hfUploadImage(base64, id, token, mimeType = "image/jpeg") {
2748
+ const ext = bildEndungFuer(mimeType);
2749
+ const binary = atob(base64);
2750
+ const bytes = new Uint8Array(binary.length);
2751
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
2752
+ await (0, import_hub.uploadFile)({
2753
+ repo: { type: "dataset", name: HF_REPO },
2754
+ credentials: { accessToken: token },
2755
+ file: { path: `images/${id}.${ext}`, content: new Blob([bytes], { type: mimeType }) },
2756
+ branch: "main",
2757
+ commitTitle: `Add image ${id}`
3315
2758
  });
3316
2759
  }
3317
-
3318
- // src/components/AvatarArchitectApp.tsx
3319
- init_hfStateService();
2760
+ async function hfLoadImageAsBase64(id, token, namespace, filename, on404, mimeType, hasThumb) {
2761
+ const fetchBase64 = async (repoPath) => {
2762
+ try {
2763
+ const res = await fetch(
2764
+ `${HF_BASE}/datasets/${HF_REPO}/resolve/main/${repoPath}?download=true`,
2765
+ { headers: { Authorization: `Bearer ${token}` } }
2766
+ );
2767
+ if (!res.ok) {
2768
+ if (on404 && on404()) return "ABORT";
2769
+ return null;
2770
+ }
2771
+ const blob = await res.blob();
2772
+ return new Promise((resolve, reject) => {
2773
+ const reader = new FileReader();
2774
+ reader.onload = () => resolve(reader.result.split(",")[1]);
2775
+ reader.onerror = reject;
2776
+ reader.readAsDataURL(blob);
2777
+ });
2778
+ } catch {
2779
+ return null;
2780
+ }
2781
+ };
2782
+ if (filename) {
2783
+ if (hasThumb) {
2784
+ const thumb = await fetchBase64(`thumbs/${filename}`);
2785
+ if (thumb && thumb !== "ABORT") return thumb;
2786
+ }
2787
+ const result2 = await fetchBase64(`images/${filename}`);
2788
+ return result2 === "ABORT" ? null : result2;
2789
+ }
2790
+ const ext = bildEndungFuer(mimeType);
2791
+ const result = await fetchBase64(`images/${id}.${ext}`);
2792
+ return result === "ABORT" ? null : result;
2793
+ }
3320
2794
 
3321
2795
  // src/hooks/useHFState.ts
3322
2796
  var import_react18 = require("react");
3323
- init_hfStateService();
3324
2797
 
3325
2798
  // src/lib/hfReducer.ts
3326
2799
  function applyEvent(state, event) {
@@ -3619,8 +3092,7 @@ function useHFState(token, namespace) {
3619
3092
  var import_react19 = require("react");
3620
3093
 
3621
3094
  // src/lib/hfTagBaseService.ts
3622
- var import_jszip3 = __toESM(require("jszip"));
3623
- init_hfStateService();
3095
+ var import_jszip2 = __toESM(require("jszip"));
3624
3096
  var CURRENT_TAG_EVENT_VERSION = { major: 1, minor: 0 };
3625
3097
  async function loadTagBase(namespace, token) {
3626
3098
  const files = await hfListDir(namespace, "", token);
@@ -3628,7 +3100,7 @@ async function loadTagBase(namespace, token) {
3628
3100
  if (!tagBaseFiles.length) return null;
3629
3101
  const newestPath = tagBaseFiles[0].path;
3630
3102
  const zipBytes = await hfDownloadBinaryByPath(newestPath, token);
3631
- const zip = await import_jszip3.default.loadAsync(zipBytes);
3103
+ const zip = await import_jszip2.default.loadAsync(zipBytes);
3632
3104
  const [usersStr, contextsStr, tagsStr, metaStr] = await Promise.all([
3633
3105
  zip.file("users.json")?.async("string"),
3634
3106
  zip.file("contexts.json")?.async("string"),
@@ -13943,8 +13415,8 @@ async function leseBilderAusZip(datei) {
13943
13415
  `${name} ist ${mb(datei.size)} MB gro\xDF \u2014 die Grenze liegt bei ${UPLOAD_MAX_BYTES / 1024 / 1024} MB.`
13944
13416
  );
13945
13417
  }
13946
- const { default: JSZip4 } = await import("jszip");
13947
- const archiv = await JSZip4.loadAsync(datei).catch((fehler4) => {
13418
+ const { default: JSZip3 } = await import("jszip");
13419
+ const archiv = await JSZip3.loadAsync(datei).catch((fehler4) => {
13948
13420
  throw new Error(
13949
13421
  `${name} konnte nicht als ZIP gelesen werden: ${fehler4 instanceof Error ? fehler4.message : String(fehler4)}`
13950
13422
  );
@@ -19774,13 +19246,21 @@ function createHfElementStore(options) {
19774
19246
  }
19775
19247
 
19776
19248
  // src/lib/szenen/hfEventChannel.ts
19777
- init_hfEventLog();
19778
19249
  function createHfEventChannel(options) {
19779
- const { namespace, token, sinceTs = 0 } = options;
19250
+ const { namespace, token, sinceTs = 0, ohneSnapshot = false } = options;
19780
19251
  const knownPaths = /* @__PURE__ */ new Set();
19781
19252
  let cachedRawEvents = [];
19782
19253
  let syncInFlight = null;
19254
+ let snapshot = null;
19255
+ let snapshotGeladen = ohneSnapshot;
19783
19256
  const refresh = async () => {
19257
+ if (!snapshotGeladen) {
19258
+ snapshot = await loadLatestSzenenSnapshot(namespace, token);
19259
+ snapshotGeladen = true;
19260
+ for (const name of aufgenommeneDateien(snapshot)) {
19261
+ knownPaths.add(`${namespace}events/${name}`);
19262
+ }
19263
+ }
19784
19264
  const neue = await loadPendingEvents(namespace, token, sinceTs, knownPaths);
19785
19265
  if (neue.length) cachedRawEvents = [...cachedRawEvents, ...neue];
19786
19266
  };
@@ -19795,12 +19275,13 @@ function createHfEventChannel(options) {
19795
19275
  return {
19796
19276
  async readEvents() {
19797
19277
  await sync();
19798
- return cachedRawEvents.filter((event) => typeof event.type === "string" && event.type.startsWith(SZENEN_EVENT_PREFIX)).map((event) => ({
19278
+ const pending = cachedRawEvents.filter((event) => typeof event.type === "string" && event.type.startsWith(SZENEN_EVENT_PREFIX)).map((event) => ({
19799
19279
  type: event.type,
19800
19280
  payload: event.payload,
19801
19281
  clientId: event.clientId,
19802
19282
  ts: event.ts
19803
19283
  }));
19284
+ return mergeSnapshotAndPending(snapshot, pending).events;
19804
19285
  },
19805
19286
  async appendEvent(type, payload) {
19806
19287
  if (!type.startsWith(SZENEN_EVENT_PREFIX)) {
@@ -21341,9 +20822,6 @@ function createClientRunPort(options) {
21341
20822
  }
21342
20823
 
21343
20824
  // src/lib/szenen/hfImageStore.ts
21344
- init_hfStateService();
21345
- init_hfEventLog();
21346
- init_hfEventTypes();
21347
20825
  var LEERER_BESTAND = {
21348
20826
  metadata: [],
21349
20827
  tags: { by_category: {}, all: [] },
@@ -22338,7 +21816,7 @@ function resolveBootstrapTarget(input) {
22338
21816
 
22339
21817
  // src/components/AvatarArchitectApp.tsx
22340
21818
  var import_jsx_runtime48 = require("react/jsx-runtime");
22341
- function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onSelectMedia, buildInfo, initialHfToken, hfNamespace, allowDevNamespace, requireWorkspaceSelection = false, serverBaseUrl, onFetchServerProjects, onServerSave, onServerLoad, onServerDelete }) {
21819
+ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onSelectMedia, buildInfo, initialHfToken, hfNamespace, allowDevNamespace, requireWorkspaceSelection = false, serverBaseUrl }) {
22342
21820
  (0, import_react50.useEffect)(() => {
22343
21821
  const id = "flow-styles";
22344
21822
  if (!document.getElementById(id)) {
@@ -22349,7 +21827,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
22349
21827
  }
22350
21828
  }, []);
22351
21829
  const [showStart, setShowStart] = (0, import_react50.useState)(true);
22352
- const [showSzenen, setShowSzenen] = (0, import_react50.useState)(false);
22353
21830
  const [layoutChoice, setLayoutChoice] = (0, import_react50.useState)(() => {
22354
21831
  try {
22355
21832
  return localStorage.getItem("aa-layout") || null;
@@ -22357,10 +21834,8 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
22357
21834
  return null;
22358
21835
  }
22359
21836
  });
22360
- const [projectLoaded, setProjectLoaded] = (0, import_react50.useState)(false);
22361
21837
  const [hfToken, setHfToken] = (0, import_react50.useState)(initialHfToken || "");
22362
21838
  const [hfTokenInput, setHfTokenInput] = (0, import_react50.useState)(initialHfToken || "");
22363
- const [isLoadingFromHF, setIsLoadingFromHF] = (0, import_react50.useState)(false);
22364
21839
  const [hfNamespaceLocal, setHfNamespaceLocal] = (0, import_react50.useState)(() => {
22365
21840
  try {
22366
21841
  return normalizeSelectableNamespace(localStorage.getItem("aa-hf-namespace"));
@@ -22464,7 +21939,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
22464
21939
  bootstrapLog.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { style: { marginTop: 6, fontFamily: "monospace", fontSize: 10, color: "#78716c", lineHeight: 1.6 }, children: bootstrapLog.map((l, i) => /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { children: l }, i)) })
22465
21940
  ] })
22466
21941
  ] });
22467
- const wsInputRef = (0, import_react50.useRef)(null);
22468
21942
  const startApp = (choice) => {
22469
21943
  try {
22470
21944
  localStorage.setItem("aa-layout", choice);
@@ -22599,11 +22073,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
22599
22073
  }
22600
22074
  });
22601
22075
  const [isPromptCollapsed, setIsPromptCollapsed] = (0, import_react50.useState)(false);
22602
- const [projectActionState, setProjectActionState] = (0, import_react50.useState)("idle");
22603
- const syncServerDataRef = (0, import_react50.useRef)(null);
22604
22076
  const [workspaceTags, setWorkspaceTags] = (0, import_react50.useState)(null);
22605
- const [serverProjects, setServerProjects] = (0, import_react50.useState)([]);
22606
- const [isLoadingFromServer, setIsLoadingFromServer] = (0, import_react50.useState)(false);
22607
22077
  const [highContrast, setHighContrast] = (0, import_react50.useState)(() => {
22608
22078
  try {
22609
22079
  return localStorage.getItem("aa-contrast") === "high";
@@ -23051,51 +22521,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
23051
22521
  console.error("Download Error", e);
23052
22522
  }
23053
22523
  };
23054
- const handleProjectExport = async () => {
23055
- setProjectActionState("working-full");
23056
- try {
23057
- const { base64 } = await exportProjectToZip(nodes, edges, history, galleryItems, { aspectRatio, selectedModel, seed, seedMode }, workspaceTags, recentLabItems.map((i) => i.id));
23058
- await onDownload(base64, "application/zip", `avatar_project_${getFormattedTimestamp()}.zip`);
23059
- setProjectActionState("done");
23060
- setTimeout(() => setProjectActionState("idle"), 3e3);
23061
- } catch {
23062
- setProjectActionState("error");
23063
- }
23064
- };
23065
- const handleProjectImport = async (input) => {
23066
- const file = input instanceof FileList ? input[0] : input;
23067
- if (!file) return;
23068
- setProjectActionState("working-full");
23069
- try {
23070
- const data = await importProjectFromZip(file);
23071
- if (data.nodes) setNodes(data.nodes);
23072
- if (data.edges) setEdges(data.edges);
23073
- if (data.history) {
23074
- setHistory(data.history);
23075
- if (data.history.length > 0) setCurrentResult(data.history[0]);
23076
- }
23077
- if (data.galleryItems) setGalleryItems(data.galleryItems);
23078
- if (data.settings) {
23079
- setAspectRatio(data.settings.aspectRatio || "1:1");
23080
- setSelectedModel(data.settings.selectedModel || "\u{1F34C} Nano Banana Pro");
23081
- setSeed(data.settings.seed || 0);
23082
- setSeedMode(data.settings.seedMode || "random");
23083
- }
23084
- if (data.workspaceTags?.by_category) setWorkspaceTags(data.workspaceTags);
23085
- if (data.recentLabItemIds?.length) {
23086
- const allGens = [...data.history || [], ...data.galleryItems || []];
23087
- const labItems = groupGenerationsToLabItems(allGens);
23088
- const recent = data.recentLabItemIds.map((id) => labItems.find((item) => item.id === id)).filter(Boolean);
23089
- setRecentLabItems(recent);
23090
- }
23091
- setProjectActionState("done");
23092
- setProjectLoaded(true);
23093
- setTimeout(() => setProjectActionState("idle"), 2e3);
23094
- } catch {
23095
- setProjectActionState("error");
23096
- setTimeout(() => setProjectActionState("idle"), 4e3);
23097
- }
23098
- };
23099
22524
  const handleWorkspaceImport = (file) => {
23100
22525
  const reader = new FileReader();
23101
22526
  reader.onload = (ev) => {
@@ -23117,37 +22542,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
23117
22542
  };
23118
22543
  reader.readAsText(file);
23119
22544
  };
23120
- const fetchServerProjects = async () => {
23121
- if (!onFetchServerProjects) return;
23122
- try {
23123
- setServerProjects(await onFetchServerProjects());
23124
- } catch {
23125
- }
23126
- };
23127
- const handleServerSave = async (name) => {
23128
- if (!onServerSave) return;
23129
- setProjectActionState("working-full");
23130
- try {
23131
- const { base64 } = await exportProjectToZip(nodes, edges, history, galleryItems, { aspectRatio, selectedModel, seed, seedMode }, workspaceTags, recentLabItems.map((i) => i.id));
23132
- await onServerSave(base64, name || `avatar_project_${getFormattedTimestamp()}`);
23133
- await fetchServerProjects();
23134
- setProjectActionState("done");
23135
- setTimeout(() => setProjectActionState("idle"), 2e3);
23136
- } catch {
23137
- setProjectActionState("error");
23138
- setTimeout(() => setProjectActionState("idle"), 4e3);
23139
- }
23140
- };
23141
- const handleServerLoad = async (id) => {
23142
- if (!onServerLoad) return;
23143
- const file = await onServerLoad(id);
23144
- await handleProjectImport(file);
23145
- };
23146
- const handleServerDelete = async (id) => {
23147
- if (!onServerDelete) return;
23148
- await onServerDelete(id);
23149
- await fetchServerProjects();
23150
- };
23151
22545
  const handleHfInitialSync = async (onProgress) => {
23152
22546
  if (!hfToken || !effectiveNamespace) return;
23153
22547
  const existingIds = new Set((hfState?.metadata || []).map((m) => m.id));
@@ -23175,45 +22569,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
23175
22569
  onProgress(done, total);
23176
22570
  }
23177
22571
  };
23178
- const handleComputeSyncDiff = async () => {
23179
- if (!onFetchServerProjects || !onServerLoad) throw new Error("Server nicht konfiguriert");
23180
- const projects = await onFetchServerProjects();
23181
- if (projects.length === 0) return { localOnly: [...history], serverOnly: [] };
23182
- const file = await onServerLoad(projects[0].id);
23183
- const serverData = await importProjectFromZip(file);
23184
- syncServerDataRef.current = serverData;
23185
- const localOnly = history.filter((h) => !(serverData.history || []).find((s) => s.id === h.id));
23186
- const serverOnly = (serverData.history || []).filter((s) => !history.find((h) => h.id === s.id));
23187
- return { localOnly, serverOnly };
23188
- };
23189
- const handleExecuteSync = async (selectedLocalIds) => {
23190
- const serverData = syncServerDataRef.current;
23191
- if (!serverData || !onServerSave) return;
23192
- setProjectActionState("working-full");
23193
- try {
23194
- const selectedLocal = history.filter((h) => selectedLocalIds.includes(h.id));
23195
- const mergedHistory = [...serverData.history || [], ...selectedLocal];
23196
- const { base64 } = await exportProjectToZip(
23197
- serverData.nodes || nodes,
23198
- serverData.edges || edges,
23199
- mergedHistory,
23200
- serverData.galleryItems || galleryItems,
23201
- serverData.settings || { aspectRatio, selectedModel, seed, seedMode },
23202
- serverData.workspaceTags || workspaceTags,
23203
- serverData.recentLabItemIds || recentLabItems.map((i) => i.id)
23204
- );
23205
- await onServerSave(base64, `sync_${getFormattedTimestamp()}`);
23206
- await fetchServerProjects();
23207
- setProjectActionState("done");
23208
- setTimeout(() => setProjectActionState("idle"), 2e3);
23209
- } catch {
23210
- setProjectActionState("error");
23211
- setTimeout(() => setProjectActionState("idle"), 4e3);
23212
- }
23213
- };
23214
- (0, import_react50.useEffect)(() => {
23215
- if (activeTab === "setup" || activeTab === "sync") fetchServerProjects();
23216
- }, [activeTab]);
23217
22572
  const mergeWorkspaceTags = (local, remote) => {
23218
22573
  if (!remote?.by_category) return local;
23219
22574
  const merged = {};
@@ -23284,31 +22639,8 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
23284
22639
  }
23285
22640
  );
23286
22641
  }
23287
- if (showSzenen && szenenPort) {
23288
- return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("main", { className: "flow-szenen-shell", children: [
23289
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("header", { className: "flow-szenen-shell__header", children: [
23290
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { type: "button", onClick: () => setShowSzenen(false), children: "Zur\xFCck" }),
23291
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { children: [
23292
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("strong", { children: "Szenen" }),
23293
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("small", { children: [
23294
- effectiveNamespace,
23295
- " \xB7 User ",
23296
- tagBaseResult.selectedUserId,
23297
- " \xB7 Context ",
23298
- tagBaseResult.selectedContextId
23299
- ] })
23300
- ] })
23301
- ] }),
23302
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "flow-szenen-shell__content", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(SzenenView, { port: szenenPort, contextId: szenenKontextId }) })
23303
- ] });
23304
- }
23305
22642
  if (showStart) {
23306
22643
  return /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "fixed inset-0 bg-[#0e0e0e] flex flex-col items-center justify-center p-6", style: { gap: 28, ...hcStyle }, children: [
23307
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("input", { ref: wsInputRef, type: "file", accept: ".zip", className: "hidden", onChange: (e) => {
23308
- const f = e.target.files?.[0];
23309
- if (f) handleProjectImport(f);
23310
- e.target.value = "";
23311
- } }),
23312
22644
  /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex flex-col items-center gap-1", children: [
23313
22645
  /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined text-white/15 text-[44px]", children: "palette" }),
23314
22646
  /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-white/25 text-[10px] font-bold uppercase tracking-[0.25em]", children: "Avatar Architect" }),
@@ -23332,21 +22664,6 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
23332
22664
  ]
23333
22665
  }
23334
22666
  ),
23335
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex flex-col items-center gap-2 w-full max-w-[280px]", children: [
23336
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
23337
- "button",
23338
- {
23339
- onClick: () => wsInputRef.current?.click(),
23340
- className: "w-full flex items-center justify-center gap-3 rounded-2xl font-bold text-[14px] uppercase tracking-wide text-white active:scale-95 transition-transform",
23341
- style: { height: 56, background: projectLoaded ? "#16a34a" : "#0284c7" },
23342
- children: [
23343
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined text-[22px]", children: projectLoaded ? "check_circle" : "folder_zip" }),
23344
- projectLoaded ? "Projekt geladen \u2713" : "Projekt laden (.zip)"
23345
- ]
23346
- }
23347
- ),
23348
- !projectLoaded && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-white/20 text-[10px] text-center", children: "Baum, Bilder und Einstellungen wiederherstellen" })
23349
- ] }),
23350
22667
  /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex flex-col items-center gap-2 w-full max-w-[280px]", children: [
23351
22668
  !initialHfToken && /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex gap-2 w-full", children: [
23352
22669
  /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
@@ -23397,79 +22714,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
23397
22714
  },
23398
22715
  ns
23399
22716
  ))
23400
- ] }),
23401
- hfToken && /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
23402
- "button",
23403
- {
23404
- disabled: isLoadingFromHF,
23405
- onClick: async () => {
23406
- setIsLoadingFromHF(true);
23407
- try {
23408
- const { hfListProjects: hfListProjects2, hfDownloadProject: hfDownloadProject2 } = await Promise.resolve().then(() => (init_hfStateService(), hfStateService_exports));
23409
- const projects = await hfListProjects2(hfToken);
23410
- if (projects.length > 0) {
23411
- const file = await hfDownloadProject2(projects[0].path, hfToken);
23412
- await handleProjectImport(file);
23413
- }
23414
- } catch (e) {
23415
- alert("HF Fehler: " + e.message);
23416
- } finally {
23417
- setIsLoadingFromHF(false);
23418
- }
23419
- },
23420
- className: "w-full flex items-center justify-center gap-3 rounded-2xl font-bold text-[14px] uppercase tracking-wide text-white active:scale-95 transition-transform disabled:opacity-50",
23421
- style: { height: 56, background: "#f59e0b" },
23422
- children: [
23423
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: `material-symbols-outlined text-[22px]${isLoadingFromHF ? " animate-spin" : ""}`, children: isLoadingFromHF ? "sync" : "cloud_download" }),
23424
- isLoadingFromHF ? "Laden\u2026" : "Von HF laden"
23425
- ]
23426
- }
23427
- ),
23428
- hfToken && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-white/20 text-[10px] text-center", children: "Letzten Stand von Hugging Face laden" })
23429
- ] }),
23430
- onFetchServerProjects && /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex flex-col items-center gap-2 w-full max-w-[280px]", children: [
23431
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
23432
- "button",
23433
- {
23434
- disabled: isLoadingFromServer,
23435
- onClick: async () => {
23436
- setIsLoadingFromServer(true);
23437
- try {
23438
- const projects = await onFetchServerProjects();
23439
- setServerProjects(projects);
23440
- if (projects.length > 0 && onServerLoad) {
23441
- const file = await onServerLoad(projects[0].id);
23442
- await handleProjectImport(file);
23443
- }
23444
- } catch {
23445
- } finally {
23446
- setIsLoadingFromServer(false);
23447
- }
23448
- },
23449
- className: "w-full flex items-center justify-center gap-3 rounded-2xl font-bold text-[14px] uppercase tracking-wide text-white active:scale-95 transition-transform disabled:opacity-50",
23450
- style: { height: 56, background: "#7c3aed" },
23451
- children: [
23452
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: `material-symbols-outlined text-[22px]${isLoadingFromServer ? " animate-spin" : ""}`, children: isLoadingFromServer ? "sync" : "cloud_download" }),
23453
- isLoadingFromServer ? "Laden\u2026" : "Vom Server laden"
23454
- ]
23455
- }
23456
- ),
23457
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-white/20 text-[10px] text-center", children: "Letzten Stand vom Server wiederherstellen" })
23458
- ] }),
23459
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flow-szenen-entry", children: [
23460
- /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)(
23461
- "button",
23462
- {
23463
- type: "button",
23464
- disabled: !szenenPort || !bootstrapTarget.ready,
23465
- onClick: () => setShowSzenen(true),
23466
- children: [
23467
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined", children: "movie" }),
23468
- "Szenen \xF6ffnen"
23469
- ]
23470
- }
23471
- ),
23472
- !szenenPort && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("small", { children: "F\xFCr Szenen zuerst den HF-Token setzen." })
22717
+ ] })
23473
22718
  ] }),
23474
22719
  /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex flex-col items-center gap-2 w-full max-w-[280px]", children: [
23475
22720
  /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-white/25 text-[10px] uppercase tracking-widest font-bold", children: "Layout w\xE4hlen & starten" }),
@@ -23538,6 +22783,11 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
23538
22783
  );
23539
22784
  }
23540
22785
  }
22786
+ const szenenPane = szenenPort ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "flow-szenen-pane", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(SzenenView, { port: szenenPort, contextId: szenenKontextId }) }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "flow-szenen-pane flow-szenen-pane--empty", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("p", { children: [
22787
+ "Szenen brauchen einen HF-Token f\xFCr Namespace ",
22788
+ effectiveNamespace || "(noch nicht bestimmt)",
22789
+ " \u2014 auf der Flow-Startseite setzen."
22790
+ ] }) });
23541
22791
  if (layoutChoice === "mobile" || layoutChoice === "mobile-desktop") {
23542
22792
  const mdMode = layoutChoice === "mobile-desktop";
23543
22793
  const mdScale = mdMode ? window.innerWidth / 430 : 1;
@@ -23569,6 +22819,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
23569
22819
  onTagMove: handleTagMove
23570
22820
  }
23571
22821
  ) }),
22822
+ mobileTab === "szenen" && szenenPane,
23572
22823
  mobileTab === "stage" && /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex flex-col flex-1 min-h-0", children: [
23573
22824
  /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex items-center gap-2 px-3 border-b border-white/5 bg-black/30 shrink-0", style: { height: 52 }, children: [
23574
22825
  /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(CompactDropdown, { value: aspectRatio, onChange: setAspectRatio, options: [{ label: "1:1", value: "1:1" }, { label: "16:9", value: "16:9" }, { label: "9:16", value: "9:16" }] }),
@@ -23812,26 +23063,9 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
23812
23063
  }, onTagCreate: handleTagCreate, onTagUpdate: handleTagUpdate, onTagDelete: handleTagDelete, onScanImage: handleScanImage, isScanning: isScanningImage }) }),
23813
23064
  activeTab === "setup" && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(SetupPanel, { onWorkspaceImport: handleWorkspaceImport, buildInfo, tagBase: tagBaseResult }),
23814
23065
  activeTab === "sync" && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
23815
- ProjectSyncTab,
23066
+ HfMediaSyncTab,
23816
23067
  {
23817
23068
  topSlot: syncTopSlot,
23818
- onProjectExport: handleProjectExport,
23819
- onProjectImport: (f) => handleProjectImport(f),
23820
- onWorkspaceImport: handleWorkspaceImport,
23821
- projectActionState,
23822
- serverProjects: onFetchServerProjects ? serverProjects : void 0,
23823
- onServerSave: onServerSave ? handleServerSave : void 0,
23824
- onServerLoad: onServerLoad ? handleServerLoad : void 0,
23825
- onServerDelete: onServerDelete ? handleServerDelete : void 0,
23826
- onRefreshServerProjects: onFetchServerProjects ? fetchServerProjects : void 0,
23827
- onComputeSyncDiff: onFetchServerProjects && onServerLoad && onServerSave ? handleComputeSyncDiff : void 0,
23828
- onExecuteSync: onFetchServerProjects && onServerLoad && onServerSave ? handleExecuteSync : void 0,
23829
- hfToken: hfToken || void 0,
23830
- onHfLoad: (f) => handleProjectImport(f),
23831
- onProjectExportBase64: async () => {
23832
- const { base64 } = await exportProjectToZip(nodes, edges, history, galleryItems, { aspectRatio, selectedModel, seed, seedMode }, workspaceTags, recentLabItems.map((i) => i.id));
23833
- return base64;
23834
- },
23835
23069
  onHfInitialSync: hfToken ? handleHfInitialSync : void 0
23836
23070
  }
23837
23071
  ),
@@ -23869,7 +23103,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
23869
23103
  ...workspaceTags ? [{ id: "tags", icon: "label", label: "Tags" }] : [],
23870
23104
  { id: "browse", icon: "photo_library", label: "Galerie" },
23871
23105
  ...serverBaseUrl ? [{ id: "server", icon: "storage", label: "Server" }] : []
23872
- ].map((tab) => /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("button", { onClick: () => tab.id === "szenen" ? setShowSzenen(true) : setMobileTab(tab.id), className: `flex-none min-w-[64px] flex flex-col items-center justify-center gap-0.5 transition-colors ${mobileTab === tab.id ? "text-white" : "text-white/30"}`, children: [
23106
+ ].map((tab) => /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("button", { onClick: () => setMobileTab(tab.id), className: `flex-none min-w-[64px] flex flex-col items-center justify-center gap-0.5 transition-colors ${mobileTab === tab.id ? "text-white" : "text-white/30"}`, children: [
23873
23107
  /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined text-[24px]", children: tab.icon }),
23874
23108
  /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "text-[10px] font-bold uppercase tracking-wide", children: tab.label })
23875
23109
  ] }, tab.id)) })
@@ -24064,6 +23298,14 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
24064
23298
  className: `px-3 h-7 rounded-lg text-[9px] font-bold uppercase tracking-wide transition-colors ${middlePanel === "labs" ? "bg-white/10 text-white" : "text-white/30 hover:text-white/50"}`,
24065
23299
  children: "Labs"
24066
23300
  }
23301
+ ),
23302
+ /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
23303
+ "button",
23304
+ {
23305
+ onClick: () => setMiddlePanel("szenen"),
23306
+ className: `px-3 h-7 rounded-lg text-[9px] font-bold uppercase tracking-wide transition-colors ${middlePanel === "szenen" ? "bg-white/10 text-white" : "text-white/30 hover:text-white/50"}`,
23307
+ children: "Szenen"
23308
+ }
24067
23309
  )
24068
23310
  ] }),
24069
23311
  /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex items-center gap-2", children: [
@@ -24086,7 +23328,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
24086
23328
  /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("textarea", { value: activePrompt, onChange: (e) => setActivePrompt(e.target.value), className: "w-full bg-transparent border-none outline-none text-[12px] leading-relaxed text-white/80 resize-none h-20 dark-scrollbar", placeholder: "W\xE4hle einen Knoten oder tippe einen Prompt..." }),
24087
23329
  activePrompt && !isSynthesizing && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { onClick: () => setActivePrompt(""), className: "absolute top-2 right-2 w-6 h-6 rounded-full bg-white/5 hover:bg-white/10 flex items-center justify-center transition-colors text-white/20 hover:text-white", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined text-[14px]", children: "close" }) })
24088
23330
  ] }) }),
24089
- middlePanel === "labs" ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "flex-1 overflow-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(LabsTab, { services: labServices, onResult: (item) => {
23331
+ middlePanel === "szenen" ? szenenPane : middlePanel === "labs" ? /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("div", { className: "flex-1 overflow-hidden", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(LabsTab, { services: labServices, onResult: (item) => {
24090
23332
  const frame = item.frames[0];
24091
23333
  if (frame?.base64) setCurrentResult(frameToGeneration(frame, item));
24092
23334
  } }) }) : /* @__PURE__ */ (0, import_jsx_runtime48.jsxs)("div", { className: "flex-1 overflow-hidden flex flex-col", children: [
@@ -24171,8 +23413,7 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
24171
23413
  serverBaseUrl && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { onClick: () => {
24172
23414
  setActiveTab("server");
24173
23415
  setIsRightCollapsed(false);
24174
- }, className: `w-10 flex items-center justify-center relative transition-colors ${activeTab === "server" ? "text-white" : "text-white/20"}`, children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined text-[20px]", children: "storage" }) }),
24175
- /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { onClick: () => setShowSzenen(true), className: "w-10 flex items-center justify-center relative text-white/20 hover:text-white", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined text-[20px]", children: "movie" }) })
23416
+ }, className: `w-10 flex items-center justify-center relative transition-colors ${activeTab === "server" ? "text-white" : "text-white/20"}`, children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined text-[20px]", children: "storage" }) })
24176
23417
  ] }),
24177
23418
  hfToken && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { onClick: () => refreshHF(), disabled: isHfRefreshing, className: "w-10 flex items-center justify-center text-white/20 hover:text-white/60 transition-colors disabled:opacity-30", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: `material-symbols-outlined text-[18px]${isHfRefreshing ? " animate-spin" : ""}`, children: "sync" }) }),
24178
23419
  /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("button", { onClick: () => setIsRightCollapsed(!isRightCollapsed), className: "w-10 flex items-center justify-center text-white/20 hover:text-white", children: /* @__PURE__ */ (0, import_jsx_runtime48.jsx)("span", { className: "material-symbols-outlined text-[18px]", children: isRightCollapsed ? "chevron_left" : "chevron_right" }) })
@@ -24213,26 +23454,9 @@ function AvatarArchitectApp({ onGenerateImage, onGeneratePrompt, onDownload, onS
24213
23454
  activeTab === "inspect" && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(InspectPanel, { currentResult, history, onSelect: setCurrentResult }),
24214
23455
  activeTab === "setup" && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(SetupPanel, { onWorkspaceImport: handleWorkspaceImport, buildInfo, tagBase: tagBaseResult }),
24215
23456
  activeTab === "sync" && /* @__PURE__ */ (0, import_jsx_runtime48.jsx)(
24216
- ProjectSyncTab,
23457
+ HfMediaSyncTab,
24217
23458
  {
24218
23459
  topSlot: syncTopSlot,
24219
- onProjectExport: handleProjectExport,
24220
- onProjectImport: (f) => handleProjectImport(f),
24221
- onWorkspaceImport: handleWorkspaceImport,
24222
- projectActionState,
24223
- serverProjects: onFetchServerProjects ? serverProjects : void 0,
24224
- onServerSave: onServerSave ? handleServerSave : void 0,
24225
- onServerLoad: onServerLoad ? handleServerLoad : void 0,
24226
- onServerDelete: onServerDelete ? handleServerDelete : void 0,
24227
- onRefreshServerProjects: onFetchServerProjects ? fetchServerProjects : void 0,
24228
- onComputeSyncDiff: onFetchServerProjects && onServerLoad && onServerSave ? handleComputeSyncDiff : void 0,
24229
- onExecuteSync: onFetchServerProjects && onServerLoad && onServerSave ? handleExecuteSync : void 0,
24230
- hfToken: hfToken || void 0,
24231
- onHfLoad: (f) => handleProjectImport(f),
24232
- onProjectExportBase64: async () => {
24233
- const { base64 } = await Promise.resolve().then(() => (init_project(), project_exports)).then((m) => m.exportProjectToZip(nodes, edges, history, galleryItems, { aspectRatio, selectedModel, seed, seedMode }, workspaceTags, recentLabItems.map((i) => i.id)));
24234
- return base64;
24235
- },
24236
23460
  onHfInitialSync: hfToken ? handleHfInitialSync : void 0
24237
23461
  }
24238
23462
  ),
@@ -24257,10 +23481,6 @@ function FaApp({
24257
23481
  libToken,
24258
23482
  allowDevNamespace,
24259
23483
  serverBaseUrl,
24260
- onFetchServerProjects,
24261
- onServerSave,
24262
- onServerLoad,
24263
- onServerDelete,
24264
23484
  buildInfo
24265
23485
  }) {
24266
23486
  const [hfNamespace, setHfNamespace] = (0, import_react51.useState)(void 0);
@@ -24287,10 +23507,6 @@ function FaApp({
24287
23507
  allowDevNamespace: !hfNamespace && allowDevNamespace,
24288
23508
  requireWorkspaceSelection: true,
24289
23509
  serverBaseUrl,
24290
- onFetchServerProjects,
24291
- onServerSave,
24292
- onServerLoad,
24293
- onServerDelete,
24294
23510
  buildInfo
24295
23511
  }
24296
23512
  );
@@ -24855,10 +24071,6 @@ async function runTagsLegacyImport(options) {
24855
24071
  writeMarker: options.writeMarker
24856
24072
  });
24857
24073
  }
24858
-
24859
- // src/index.ts
24860
- init_hfStateService();
24861
- init_hfStateService();
24862
24074
  // Annotate the CommonJS export names for ESM import in node:
24863
24075
  0 && (module.exports = {
24864
24076
  AvatarArchitectApp,
@@ -24869,6 +24081,7 @@ init_hfStateService();
24869
24081
  FaToolsBadge,
24870
24082
  FlowWorkspacePicker,
24871
24083
  GLOBAL_STYLES,
24084
+ HfMediaSyncTab,
24872
24085
  HistoryPanel,
24873
24086
  InspectPanel,
24874
24087
  LIBRARY_CUTOVER_MARKER,
@@ -24885,7 +24098,6 @@ init_hfStateService();
24885
24098
  ListView,
24886
24099
  MediaLibrary,
24887
24100
  PillButton,
24888
- ProjectSyncTab,
24889
24101
  PromptTab,
24890
24102
  SCOPE_MAP,
24891
24103
  SectionLabel,
@@ -24919,7 +24131,6 @@ init_hfStateService();
24919
24131
  createTagsDefault,
24920
24132
  deriveTagBaseWorkspaceTags,
24921
24133
  deriveTagViewModel,
24922
- exportProjectToZip,
24923
24134
  formatTreeToMarkdown,
24924
24135
  frameToGeneration,
24925
24136
  getFormattedTimestamp,
@@ -24928,15 +24139,10 @@ init_hfStateService();
24928
24139
  groupGenerationsToLabItems,
24929
24140
  hfBatchArchive,
24930
24141
  hfBootstrapFromLegacy,
24931
- hfDeleteProject,
24932
- hfDownloadProject,
24933
24142
  hfListDir,
24934
- hfListProjects,
24935
24143
  hfLoadImageAsBase64,
24936
24144
  hfUploadImage,
24937
- hfUploadProjectForm,
24938
24145
  hfUploadSmallFile,
24939
- importProjectFromZip,
24940
24146
  injectXMPMetadata,
24941
24147
  interpretSdkError,
24942
24148
  libraryContract,