@joewinke/jatui 0.1.11 → 0.1.20

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.
Files changed (100) hide show
  1. package/README.md +123 -0
  2. package/package.json +2 -1
  3. package/src/lib/actions/railNav.ts +473 -0
  4. package/src/lib/components/AnnotationLayer.svelte +108 -0
  5. package/src/lib/components/AnnotationPanel.svelte +319 -0
  6. package/src/lib/components/AudioWaveform.svelte +9 -5
  7. package/src/lib/components/AvailabilityModal.svelte +7 -3
  8. package/src/lib/components/AvatarUpload.svelte +27 -4
  9. package/src/lib/components/BookingForm.svelte +11 -9
  10. package/src/lib/components/BurndownChart.svelte +778 -0
  11. package/src/lib/components/Button.svelte +10 -1
  12. package/src/lib/components/CalendarPicker.svelte +3 -3
  13. package/src/lib/components/Card.svelte +2 -2
  14. package/src/lib/components/ChipInput.svelte +8 -3
  15. package/src/lib/components/ColorSelector.svelte +17 -13
  16. package/src/lib/components/CommentThread.svelte +773 -0
  17. package/src/lib/components/ConfirmDialog.svelte +348 -0
  18. package/src/lib/components/ConfirmModal.svelte +78 -11
  19. package/src/lib/components/ContextMenu.svelte +59 -19
  20. package/src/lib/components/CountdownTimer.svelte +1 -1
  21. package/src/lib/components/DateRangePicker.svelte +6 -4
  22. package/src/lib/components/Drawer.svelte +36 -3
  23. package/src/lib/components/EntityPreviewCard.svelte +104 -0
  24. package/src/lib/components/FileDropzone.svelte +493 -0
  25. package/src/lib/components/FilePicker.svelte +83 -14
  26. package/src/lib/components/FileThumbnail.svelte +80 -0
  27. package/src/lib/components/FilterDropdown.svelte +11 -11
  28. package/src/lib/components/GPSTracker.svelte +202 -0
  29. package/src/lib/components/HunkDiffView.svelte +348 -0
  30. package/src/lib/components/ImageLightbox.svelte +274 -0
  31. package/src/lib/components/ImageUpload.svelte +58 -9
  32. package/src/lib/components/InlineEdit.svelte +6 -2
  33. package/src/lib/components/InputDialog.svelte +327 -0
  34. package/src/lib/components/KeyboardShortcutsOverlay.svelte +296 -0
  35. package/src/lib/components/LazyImage.svelte +1 -0
  36. package/src/lib/components/LinkShortener.svelte +1 -1
  37. package/src/lib/components/LoadingSpinner.svelte +6 -2
  38. package/src/lib/components/LocationMap.svelte +186 -0
  39. package/src/lib/components/MapView.svelte +341 -0
  40. package/src/lib/components/MarkupEditor.svelte +485 -0
  41. package/src/lib/components/MarkupOverlay.svelte +55 -0
  42. package/src/lib/components/MediaWorkbench.svelte +871 -0
  43. package/src/lib/components/MilestoneCard.svelte +1 -1
  44. package/src/lib/components/MilestoneTimeline.svelte +1 -1
  45. package/src/lib/components/Modal.svelte +39 -4
  46. package/src/lib/components/PDFViewer.svelte +105 -0
  47. package/src/lib/components/PdfThumbnail.svelte +3 -1
  48. package/src/lib/components/PhoneInput.svelte +1 -1
  49. package/src/lib/components/ResizablePanel.svelte +4 -4
  50. package/src/lib/components/SearchDropdown.svelte +26 -13
  51. package/src/lib/components/SelectInput.svelte +26 -4
  52. package/src/lib/components/SidebarUserFooter.svelte +1 -1
  53. package/src/lib/components/SignaturePad.svelte +8 -4
  54. package/src/lib/components/SmartImageEditor.svelte +720 -0
  55. package/src/lib/components/SortDropdown.svelte +9 -3
  56. package/src/lib/components/Sparkline.svelte +9 -0
  57. package/src/lib/components/StatusBadge.svelte +20 -18
  58. package/src/lib/components/TextArea.svelte +24 -5
  59. package/src/lib/components/TextInput.svelte +29 -6
  60. package/src/lib/components/ThemeSelector.svelte +15 -4
  61. package/src/lib/components/TimeSlotPicker.svelte +7 -7
  62. package/src/lib/components/UserAvatar.svelte +14 -1
  63. package/src/lib/components/VariablePicker.svelte +170 -0
  64. package/src/lib/components/VoicePlayer.svelte +4 -3
  65. package/src/lib/components/linked-columns/LinkedColumns.svelte +520 -0
  66. package/src/lib/components/markup.ts +287 -0
  67. package/src/lib/components/messaging/ChannelInfoModal.svelte +9 -9
  68. package/src/lib/components/messaging/ChannelList.svelte +1 -1
  69. package/src/lib/components/messaging/ChannelMembersModal.svelte +1 -1
  70. package/src/lib/components/messaging/CreateChannelModal.svelte +1 -1
  71. package/src/lib/components/messaging/DirectMessageList.svelte +1 -1
  72. package/src/lib/components/messaging/EmojiSelector.svelte +2 -1
  73. package/src/lib/components/messaging/MentionAutocomplete.svelte +1 -1
  74. package/src/lib/components/messaging/MessageAttachment.svelte +3 -3
  75. package/src/lib/components/messaging/MessageAttachmentUpload.svelte +3 -3
  76. package/src/lib/components/messaging/MessageInput.svelte +1 -1
  77. package/src/lib/components/messaging/MessageItem.svelte +6 -3
  78. package/src/lib/components/messaging/NotificationSettingsModal.svelte +1 -1
  79. package/src/lib/components/messaging/QuotedMessageDisplay.svelte +6 -1
  80. package/src/lib/components/messaging/StartDMModal.svelte +1 -1
  81. package/src/lib/components/pipeline/Pipeline.svelte +4 -4
  82. package/src/lib/components/pipeline/PipelineCard.svelte +1 -1
  83. package/src/lib/components/pipeline/PipelineColumn.svelte +8 -3
  84. package/src/lib/components/replay/ChapterTimeline.svelte +326 -0
  85. package/src/lib/components/session-nav/transcriptModel.ts +352 -0
  86. package/src/lib/index.ts +138 -0
  87. package/src/lib/stores/confirmDialog.svelte.ts +48 -0
  88. package/src/lib/stores/inputDialog.svelte.ts +51 -0
  89. package/src/lib/styles/rail.css +63 -0
  90. package/src/lib/types/annotation.ts +38 -0
  91. package/src/lib/types/comments.ts +97 -0
  92. package/src/lib/types/entityPreview.ts +45 -0
  93. package/src/lib/types/filePicker.ts +2 -0
  94. package/src/lib/types/googleMaps.d.ts +51 -0
  95. package/src/lib/types/maps.ts +43 -0
  96. package/src/lib/types/smartImageEditor.ts +39 -0
  97. package/src/lib/types/templateVars.ts +36 -0
  98. package/src/lib/utils/dateFormatters.ts +12 -10
  99. package/src/lib/utils/googleMapsLoader.ts +84 -0
  100. package/src/lib/utils/taskUtils.ts +21 -7
@@ -14,6 +14,38 @@
14
14
  }>()
15
15
 
16
16
  let error = $state("")
17
+ let isDragging = $state(false)
18
+
19
+ function handleDragOver(event: DragEvent) {
20
+ event.preventDefault()
21
+ isDragging = true
22
+ }
23
+
24
+ function handleDragLeave(event: DragEvent) {
25
+ // Only clear if leaving the label entirely (not entering a child)
26
+ if (!(event.currentTarget as HTMLElement).contains(event.relatedTarget as Node)) {
27
+ isDragging = false
28
+ }
29
+ }
30
+
31
+ function handleDrop(event: DragEvent) {
32
+ event.preventDefault()
33
+ isDragging = false
34
+ error = ""
35
+ const file = event.dataTransfer?.files[0]
36
+ if (!file) return
37
+ if (!file.type.startsWith("image/")) {
38
+ error = "Please drop an image file."
39
+ return
40
+ }
41
+ if (file.size > maxSize) {
42
+ error = `File too large! Maximum size is ${formatFileSize(maxSize)}.`
43
+ return
44
+ }
45
+ const url = URL.createObjectURL(file)
46
+ imageUrl = url
47
+ onUpload?.(url)
48
+ }
17
49
 
18
50
  async function handleFileInput(event: Event) {
19
51
  error = ""
@@ -71,17 +103,34 @@
71
103
  </button>
72
104
  </div>
73
105
  {:else}
106
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
74
107
  <label
75
- class="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-md cursor-pointer bg-base-200 hover:bg-base-300"
108
+ class="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-md cursor-pointer transition-all duration-200
109
+ {isDragging
110
+ ? 'border-primary bg-primary/8 scale-[1.01]'
111
+ : 'border-base-content/20 bg-base-200 hover:bg-base-300 hover:border-base-content/30'}"
112
+ ondragover={handleDragOver}
113
+ ondragleave={handleDragLeave}
114
+ ondrop={handleDrop}
76
115
  >
77
- <div class="flex flex-col items-center justify-center pt-5 pb-6">
78
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mb-2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/></svg>
79
- <p class="mb-2 text-sm">
80
- <span class="font-semibold">Click to upload</span> or drag and drop
81
- </p>
82
- <p class="text-xs opacity-70">
83
- PNG, JPG or JPEG (max {formatFileSize(maxSize)})
84
- </p>
116
+ <div class="flex flex-col items-center justify-center pt-5 pb-6 pointer-events-none">
117
+ {#if isDragging}
118
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mb-2 text-primary">
119
+ <path d="M12 15V3"/><path d="M7 10l5 5 5-5"/>
120
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
121
+ </svg>
122
+ <p class="mb-2 text-sm font-semibold text-primary">Drop to upload</p>
123
+ {:else}
124
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mb-2 text-base-content/40">
125
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/>
126
+ </svg>
127
+ <p class="mb-2 text-sm">
128
+ <span class="font-semibold">Click to upload</span> or drag and drop
129
+ </p>
130
+ <p class="text-xs text-base-content/45">
131
+ PNG, JPG or JPEG (max {formatFileSize(maxSize)})
132
+ </p>
133
+ {/if}
85
134
  </div>
86
135
  <input
87
136
  type="file"
@@ -62,6 +62,7 @@
62
62
 
63
63
  // Internal state
64
64
  let isEditing = $state(false);
65
+ // svelte-ignore state_referenced_locally
65
66
  let editValue = $state(value);
66
67
  let isSaving = $state(false);
67
68
  let inputElement = $state<HTMLInputElement | HTMLTextAreaElement | null>(null);
@@ -136,6 +137,8 @@
136
137
  });
137
138
  }
138
139
 
140
+ import { fade } from 'svelte/transition';
141
+
139
142
  // Auto-resize textarea to fit content
140
143
  function autoresize(node: HTMLTextAreaElement) {
141
144
  function resize() {
@@ -157,14 +160,14 @@
157
160
 
158
161
  {#if isEditing}
159
162
  <!-- Edit mode -->
160
- <div class="inline-edit-container w-full {className}">
163
+ <div class="inline-edit-container w-full {className}" in:fade={{ duration: 120 }}>
161
164
  {#if type === 'textarea'}
162
165
  <textarea
163
166
  bind:this={inputElement}
164
167
  bind:value={editValue}
165
168
  {placeholder}
166
169
  {rows}
167
- class="textarea textarea-bordered w-full text-sm resize-none overflow-hidden"
170
+ class="textarea textarea-bordered w-full text-[0.9375rem] resize-none overflow-hidden"
168
171
  style="min-height: {rows * 1.5}rem;"
169
172
  disabled={isSaving}
170
173
  onkeydown={handleKeyDown}
@@ -217,6 +220,7 @@
217
220
  <!-- Display mode -->
218
221
  <button
219
222
  class="inline-edit-display text-left w-full rounded px-2 py-1 transition-colors {disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer hover:bg-base-200'} {truncate ? 'inline-edit-truncate' : ''} {className}"
223
+ in:fade={{ duration: 120 }}
220
224
  onclick={startEditing}
221
225
  disabled={disabled}
222
226
  type="button"
@@ -0,0 +1,327 @@
1
+ <script lang="ts">
2
+ import { fly, fade } from 'svelte/transition';
3
+ import { cubicOut } from 'svelte/easing';
4
+ import { tick } from 'svelte';
5
+ import { getPendingInput, submitInput, cancelInput } from '../stores/inputDialog.svelte';
6
+
7
+ const pending = $derived(getPendingInput());
8
+ const opts = $derived(pending?.options ?? null);
9
+ const visible = $derived(pending !== null);
10
+
11
+ let value = $state('');
12
+ let error = $state<string | null>(null);
13
+ let inputRef = $state<HTMLInputElement | null>(null);
14
+
15
+ $effect(() => {
16
+ if (visible && opts) {
17
+ value = '';
18
+ error = null;
19
+ tick().then(() => inputRef?.focus());
20
+ }
21
+ });
22
+
23
+ function handleSubmit() {
24
+ if (opts?.validate) {
25
+ const msg = opts.validate(value);
26
+ if (msg !== null) { error = msg; return; }
27
+ }
28
+ error = null;
29
+ submitInput(value);
30
+ }
31
+
32
+ function handleKeydown(e: KeyboardEvent) {
33
+ if (!visible) return;
34
+ if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); cancelInput(); }
35
+ if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); handleSubmit(); }
36
+ }
37
+
38
+ $effect(() => {
39
+ window.addEventListener('keydown', handleKeydown, true);
40
+ return () => window.removeEventListener('keydown', handleKeydown, true);
41
+ });
42
+ </script>
43
+
44
+ {#if visible && opts}
45
+ <!-- Scrim (above ConfirmDialog z-9990/9991) -->
46
+ <div
47
+ class="id-scrim"
48
+ transition:fade={{ duration: 120 }}
49
+ role="presentation"
50
+ onclick={cancelInput}
51
+ ></div>
52
+
53
+ <!-- Dialog -->
54
+ <div
55
+ class="id-wrap"
56
+ role="dialog"
57
+ aria-modal="true"
58
+ aria-label="{opts.title}. Press Enter to confirm or Escape to cancel."
59
+ in:fly={{ y: -20, duration: 200, easing: cubicOut }}
60
+ out:fade={{ duration: 120 }}
61
+ >
62
+ <div class="id-box">
63
+ <!-- Icon + title -->
64
+ <div class="id-header">
65
+ <div class="id-icon">
66
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
67
+ <path d="M12 20h9"/>
68
+ <path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
69
+ </svg>
70
+ </div>
71
+ <div class="id-title-group">
72
+ <h3 class="id-title">{opts.title}</h3>
73
+ {#if opts.body}
74
+ <p class="id-body">{opts.body}</p>
75
+ {/if}
76
+ </div>
77
+ </div>
78
+
79
+ <!-- Input field -->
80
+ <input
81
+ bind:this={inputRef}
82
+ bind:value
83
+ oninput={() => { if (error) error = null; }}
84
+ type="text"
85
+ class="id-input"
86
+ class:id-input-error={error !== null}
87
+ placeholder={opts.placeholder ?? ''}
88
+ maxlength={opts.maxLength}
89
+ autocomplete="off"
90
+ spellcheck="false"
91
+ />
92
+ {#if error !== null || opts.maxLength}
93
+ <div class="id-field-footer">
94
+ {#if error !== null}
95
+ <p class="id-error">{error}</p>
96
+ {:else}
97
+ <span></span>
98
+ {/if}
99
+ {#if opts.maxLength}
100
+ <span class="id-char-count">{value.length}/{opts.maxLength}</span>
101
+ {/if}
102
+ </div>
103
+ {/if}
104
+
105
+ <!-- Actions -->
106
+ <div class="id-actions">
107
+ <button
108
+ type="button"
109
+ class="id-btn id-btn-cancel"
110
+ onclick={cancelInput}
111
+ >
112
+ <kbd class="id-kbd">Esc</kbd>
113
+ <span>{opts.cancelLabel ?? 'Cancel'}</span>
114
+ </button>
115
+
116
+ <button
117
+ type="button"
118
+ class="id-btn id-btn-confirm"
119
+ onclick={handleSubmit}
120
+ >
121
+ <kbd class="id-kbd">⏎</kbd>
122
+ <span>{opts.confirmLabel ?? 'OK'}</span>
123
+ </button>
124
+ </div>
125
+ </div>
126
+ </div>
127
+ {/if}
128
+
129
+ <style>
130
+ .id-scrim {
131
+ position: fixed;
132
+ inset: 0;
133
+ background: oklch(0 0 0 / 0.45);
134
+ z-index: 9991;
135
+ backdrop-filter: blur(2px);
136
+ }
137
+
138
+ .id-wrap {
139
+ position: fixed;
140
+ top: 5rem;
141
+ left: 50%;
142
+ transform: translateX(-50%);
143
+ z-index: 9992;
144
+ pointer-events: auto;
145
+ width: max-content;
146
+ max-width: min(92vw, 460px);
147
+ }
148
+
149
+ .id-box {
150
+ display: flex;
151
+ flex-direction: column;
152
+ gap: 0.625rem;
153
+ padding: 1rem 1.125rem 0.625rem;
154
+ background: oklch(0.17 0.02 250 / 0.98);
155
+ border: 1px solid oklch(0.32 0.03 250);
156
+ border-radius: 0.875rem;
157
+ box-shadow:
158
+ 0 20px 60px oklch(0 0 0 / 0.55),
159
+ 0 0 0 1px oklch(0.28 0.03 250 / 0.5);
160
+ backdrop-filter: blur(16px);
161
+ }
162
+
163
+ .id-header {
164
+ display: flex;
165
+ align-items: flex-start;
166
+ gap: 0.875rem;
167
+ }
168
+
169
+ .id-icon {
170
+ flex-shrink: 0;
171
+ width: 2.25rem;
172
+ height: 2.25rem;
173
+ border-radius: 0.5rem;
174
+ display: flex;
175
+ align-items: center;
176
+ justify-content: center;
177
+ background: oklch(0.30 0.06 200 / 0.35);
178
+ border: 1px solid oklch(0.45 0.10 200 / 0.4);
179
+ color: oklch(0.75 0.12 200);
180
+ }
181
+
182
+ .id-icon svg {
183
+ width: 1.125rem;
184
+ height: 1.125rem;
185
+ }
186
+
187
+ .id-title-group {
188
+ display: flex;
189
+ flex-direction: column;
190
+ gap: 0.25rem;
191
+ flex: 1;
192
+ min-width: 0;
193
+ }
194
+
195
+ .id-title {
196
+ margin: 0;
197
+ font-size: 0.9375rem;
198
+ font-weight: 600;
199
+ color: oklch(0.92 0.04 250);
200
+ line-height: 1.3;
201
+ }
202
+
203
+ .id-body {
204
+ margin: 0;
205
+ font-size: 0.8rem;
206
+ color: oklch(0.60 0.03 250);
207
+ line-height: 1.5;
208
+ }
209
+
210
+ .id-input {
211
+ width: 100%;
212
+ padding: 0.4375rem 0.75rem;
213
+ font-size: 0.875rem;
214
+ background: oklch(0.13 0.02 250);
215
+ border: 1px solid oklch(0.32 0.03 250);
216
+ border-radius: 0.5rem;
217
+ color: oklch(0.92 0.04 250);
218
+ outline: none;
219
+ transition: border-color 0.1s ease;
220
+ box-sizing: border-box;
221
+ }
222
+
223
+ .id-input::placeholder {
224
+ color: oklch(0.45 0.03 250);
225
+ }
226
+
227
+ .id-input:focus {
228
+ border-color: oklch(0.55 0.12 250);
229
+ box-shadow: 0 0 0 2px oklch(0.55 0.12 250 / 0.2);
230
+ }
231
+
232
+ .id-input-error,
233
+ .id-input-error:focus {
234
+ border-color: oklch(0.55 0.20 25);
235
+ box-shadow: 0 0 0 2px oklch(0.55 0.20 25 / 0.2);
236
+ }
237
+
238
+ .id-field-footer {
239
+ display: flex;
240
+ align-items: center;
241
+ justify-content: space-between;
242
+ gap: 0.5rem;
243
+ margin-top: -0.125rem;
244
+ min-height: 1rem;
245
+ }
246
+
247
+ .id-error {
248
+ margin: 0;
249
+ font-size: 0.75rem;
250
+ color: oklch(0.65 0.18 25);
251
+ line-height: 1.4;
252
+ }
253
+
254
+ .id-char-count {
255
+ margin-left: auto;
256
+ font-size: 0.75rem;
257
+ color: oklch(0.50 0.03 250);
258
+ font-variant-numeric: tabular-nums;
259
+ white-space: nowrap;
260
+ flex-shrink: 0;
261
+ }
262
+
263
+ .id-actions {
264
+ display: flex;
265
+ align-items: center;
266
+ gap: 0.375rem;
267
+ padding-top: 0.125rem;
268
+ }
269
+
270
+ .id-btn {
271
+ display: inline-flex;
272
+ align-items: center;
273
+ gap: 0.375rem;
274
+ padding: 0.3125rem 0.75rem;
275
+ font-size: 0.8rem;
276
+ font-weight: 500;
277
+ border-radius: 0.5rem;
278
+ border: 1px solid transparent;
279
+ background: transparent;
280
+ color: oklch(0.80 0.04 250);
281
+ cursor: pointer;
282
+ transition: background 0.1s ease, border-color 0.1s ease, color 0.1s ease;
283
+ white-space: nowrap;
284
+ }
285
+
286
+ .id-btn:hover {
287
+ background: oklch(0.24 0.03 250);
288
+ border-color: oklch(0.32 0.04 250);
289
+ }
290
+
291
+ .id-btn-cancel:focus-visible {
292
+ outline: 2px solid oklch(0.60 0.10 250);
293
+ outline-offset: 2px;
294
+ }
295
+
296
+ .id-btn-confirm {
297
+ background: oklch(0.28 0.04 250 / 0.6);
298
+ border-color: oklch(0.40 0.05 250);
299
+ }
300
+
301
+ .id-btn-confirm:hover {
302
+ background: oklch(0.34 0.06 250);
303
+ border-color: oklch(0.50 0.08 250);
304
+ }
305
+
306
+ .id-btn-confirm:focus-visible {
307
+ outline: 2px solid oklch(0.60 0.10 250);
308
+ outline-offset: 2px;
309
+ }
310
+
311
+ .id-kbd {
312
+ font-family: ui-monospace, monospace;
313
+ font-size: 0.65rem;
314
+ padding: 0.0625rem 0.3125rem;
315
+ background: oklch(0.22 0.02 250);
316
+ border: 1px solid oklch(0.30 0.03 250);
317
+ border-bottom-width: 2px;
318
+ border-radius: 0.25rem;
319
+ color: oklch(0.75 0.04 250);
320
+ line-height: 1.6;
321
+ }
322
+
323
+ @media (prefers-reduced-motion: reduce) {
324
+ .id-input { transition: none; }
325
+ .id-btn { transition: none; }
326
+ }
327
+ </style>