+5
-2
backend/src/backend/_internal/background.py
+5
-2
backend/src/backend/_internal/background.py
···
92
92
)
93
93
yield docket
94
94
finally:
95
-
# cancel the worker task and wait for it to finish
95
+
# cancel the worker task with timeout to avoid hanging on shutdown
96
96
if worker_task:
97
97
worker_task.cancel()
98
98
try:
99
-
await worker_task
99
+
# wait briefly for clean shutdown, but don't block forever
100
+
await asyncio.wait_for(worker_task, timeout=2.0)
101
+
except TimeoutError:
102
+
logger.warning("docket worker did not stop within timeout")
100
103
except asyncio.CancelledError:
101
104
logger.debug("docket worker task cancelled")
102
105
# clear global after worker is fully stopped
+19
-1
backend/src/backend/api/tracks/mutations.py
+19
-1
backend/src/backend/api/tracks/mutations.py
···
180
180
Form(description="JSON object for supporter gating, or 'null' to remove"),
181
181
] = None,
182
182
image: UploadFile | None = File(None),
183
+
remove_image: Annotated[
184
+
str | None,
185
+
Form(description="Set to 'true' to remove artwork"),
186
+
] = None,
183
187
) -> TrackResponse:
184
188
"""Update track metadata (only by owner)."""
185
189
result = await db.execute(
···
250
254
251
255
image_changed = False
252
256
image_url = None
253
-
if image and image.filename:
257
+
258
+
# handle image removal
259
+
if remove_image and remove_image.lower() == "true" and track.image_id:
260
+
# only delete image from R2 if album doesn't share it
261
+
album_shares_image = (
262
+
track.album_rel and track.album_rel.image_id == track.image_id
263
+
)
264
+
if not album_shares_image:
265
+
with contextlib.suppress(Exception):
266
+
await storage.delete(track.image_id)
267
+
track.image_id = None
268
+
track.image_url = None
269
+
image_changed = True
270
+
elif image and image.filename:
271
+
# handle image upload/replacement
254
272
image_id, image_url = await upload_track_image(image)
255
273
256
274
if track.image_id:
+10
-3
backend/src/backend/main.py
+10
-3
backend/src/backend/main.py
···
1
1
"""relay fastapi application."""
2
2
3
+
import asyncio
3
4
import logging
4
5
import re
5
6
import warnings
···
157
158
app.state.docket = docket
158
159
yield
159
160
160
-
# shutdown: cleanup resources
161
-
await notification_service.shutdown()
162
-
await queue_service.shutdown()
161
+
# shutdown: cleanup resources with timeouts to avoid hanging
162
+
try:
163
+
await asyncio.wait_for(notification_service.shutdown(), timeout=2.0)
164
+
except TimeoutError:
165
+
logging.warning("notification_service.shutdown() timed out")
166
+
try:
167
+
await asyncio.wait_for(queue_service.shutdown(), timeout=2.0)
168
+
except TimeoutError:
169
+
logging.warning("queue_service.shutdown() timed out")
163
170
164
171
165
172
app = FastAPI(
+359
-96
frontend/src/routes/portal/+page.svelte
+359
-96
frontend/src/routes/portal/+page.svelte
···
28
28
let editFeaturedArtists = $state<FeaturedArtist[]>([]);
29
29
let editTags = $state<string[]>([]);
30
30
let editImageFile = $state<File | null>(null);
31
+
let editImagePreviewUrl = $state<string | null>(null);
32
+
let editRemoveImage = $state(false);
31
33
let editSupportGate = $state(false);
32
34
let hasUnresolvedEditFeaturesInput = $state(false);
33
35
···
328
330
editFeaturedArtists = [];
329
331
editTags = [];
330
332
editImageFile = null;
333
+
if (editImagePreviewUrl) {
334
+
URL.revokeObjectURL(editImagePreviewUrl);
335
+
}
336
+
editImagePreviewUrl = null;
337
+
editRemoveImage = false;
331
338
editSupportGate = false;
332
339
}
333
340
···
351
358
} else {
352
359
formData.append('support_gate', 'null');
353
360
}
354
-
if (editImageFile) {
361
+
// handle artwork: remove, replace, or leave unchanged
362
+
if (editRemoveImage) {
363
+
formData.append('remove_image', 'true');
364
+
} else if (editImageFile) {
355
365
formData.append('image', editImageFile);
356
366
}
357
367
···
730
740
/>
731
741
</div>
732
742
<div class="edit-field-group">
733
-
<label for="edit-image" class="edit-label">artwork (optional)</label>
734
-
{#if track.image_url && !editImageFile}
735
-
<div class="current-image-preview">
736
-
<img src={track.image_url} alt="current artwork" />
737
-
<span class="current-image-label">current artwork</span>
738
-
</div>
739
-
{/if}
740
-
<input
741
-
id="edit-image"
742
-
type="file"
743
-
accept=".jpg,.jpeg,.png,.webp,.gif,image/jpeg,image/png,image/webp,image/gif"
744
-
onchange={(e) => {
745
-
const target = e.target as HTMLInputElement;
746
-
editImageFile = target.files?.[0] ?? null;
747
-
}}
748
-
class="edit-input"
749
-
/>
750
-
{#if editImageFile}
751
-
<p class="file-info">{editImageFile.name} (will replace current)</p>
752
-
{/if}
743
+
<span class="edit-label">artwork (optional)</span>
744
+
<div class="artwork-editor">
745
+
{#if editImagePreviewUrl}
746
+
<!-- New image selected - show preview -->
747
+
<div class="artwork-preview">
748
+
<img src={editImagePreviewUrl} alt="new artwork preview" />
749
+
<div class="artwork-preview-overlay">
750
+
<button
751
+
type="button"
752
+
class="artwork-action-btn"
753
+
onclick={() => {
754
+
editImageFile = null;
755
+
if (editImagePreviewUrl) {
756
+
URL.revokeObjectURL(editImagePreviewUrl);
757
+
}
758
+
editImagePreviewUrl = null;
759
+
}}
760
+
title="remove selection"
761
+
>
762
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
763
+
<line x1="18" y1="6" x2="6" y2="18"></line>
764
+
<line x1="6" y1="6" x2="18" y2="18"></line>
765
+
</svg>
766
+
</button>
767
+
</div>
768
+
</div>
769
+
<span class="artwork-status">new artwork selected</span>
770
+
{:else if editRemoveImage}
771
+
<!-- User chose to remove artwork -->
772
+
<div class="artwork-removed">
773
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
774
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
775
+
<line x1="9" y1="9" x2="15" y2="15"></line>
776
+
<line x1="15" y1="9" x2="9" y2="15"></line>
777
+
</svg>
778
+
<span>artwork will be removed</span>
779
+
<button
780
+
type="button"
781
+
class="undo-remove-btn"
782
+
onclick={() => { editRemoveImage = false; }}
783
+
>
784
+
undo
785
+
</button>
786
+
</div>
787
+
{:else if track.image_url}
788
+
<!-- Current artwork exists -->
789
+
<div class="artwork-preview">
790
+
<img src={track.image_url} alt="current artwork" />
791
+
<div class="artwork-preview-overlay">
792
+
<button
793
+
type="button"
794
+
class="artwork-action-btn"
795
+
onclick={() => { editRemoveImage = true; }}
796
+
title="remove artwork"
797
+
>
798
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
799
+
<polyline points="3 6 5 6 21 6"></polyline>
800
+
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
801
+
</svg>
802
+
</button>
803
+
</div>
804
+
</div>
805
+
<span class="artwork-status current">current artwork</span>
806
+
{:else}
807
+
<!-- No artwork -->
808
+
<div class="artwork-empty">
809
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
810
+
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
811
+
<circle cx="8.5" cy="8.5" r="1.5"></circle>
812
+
<polyline points="21 15 16 10 5 21"></polyline>
813
+
</svg>
814
+
<span>no artwork</span>
815
+
</div>
816
+
{/if}
817
+
{#if !editRemoveImage}
818
+
<label class="artwork-upload-btn">
819
+
<input
820
+
type="file"
821
+
accept=".jpg,.jpeg,.png,.webp,.gif,image/jpeg,image/png,image/webp,image/gif"
822
+
onchange={(e) => {
823
+
const target = e.target as HTMLInputElement;
824
+
const file = target.files?.[0];
825
+
if (file) {
826
+
editImageFile = file;
827
+
if (editImagePreviewUrl) {
828
+
URL.revokeObjectURL(editImagePreviewUrl);
829
+
}
830
+
editImagePreviewUrl = URL.createObjectURL(file);
831
+
}
832
+
}}
833
+
/>
834
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
835
+
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
836
+
<polyline points="17 8 12 3 7 8"></polyline>
837
+
<line x1="12" y1="3" x2="12" y2="15"></line>
838
+
</svg>
839
+
{track.image_url || editImagePreviewUrl ? 'replace' : 'upload'}
840
+
</label>
841
+
{/if}
842
+
</div>
753
843
</div>
754
844
{#if atprotofansEligible || track.support_gate}
755
845
<div class="edit-field-group">
···
771
861
</div>
772
862
<div class="edit-actions">
773
863
<button
774
-
class="action-btn save-btn"
775
-
onclick={() => saveTrackEdit(track.id)}
776
-
disabled={hasUnresolvedEditFeaturesInput}
777
-
title={hasUnresolvedEditFeaturesInput ? "please select or clear featured artist" : "save changes"}
864
+
type="button"
865
+
class="edit-cancel-btn"
866
+
onclick={cancelEdit}
778
867
>
779
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
780
-
<polyline points="20 6 9 17 4 12"></polyline>
781
-
</svg>
868
+
cancel
782
869
</button>
783
870
<button
784
-
class="action-btn cancel-btn"
785
-
onclick={cancelEdit}
786
-
title="cancel"
871
+
type="button"
872
+
class="edit-save-btn"
873
+
onclick={() => saveTrackEdit(track.id)}
874
+
disabled={hasUnresolvedEditFeaturesInput}
875
+
title={hasUnresolvedEditFeaturesInput ? "please select or clear featured artist" : "save changes"}
787
876
>
788
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
789
-
<line x1="18" y1="6" x2="6" y2="18"></line>
790
-
<line x1="6" y1="6" x2="18" y2="18"></line>
791
-
</svg>
877
+
save changes
792
878
</button>
793
879
</div>
794
880
</div>
···
880
966
</div>
881
967
<div class="track-actions">
882
968
<button
883
-
class="action-btn edit-btn"
969
+
type="button"
970
+
class="track-action-btn edit"
884
971
onclick={() => startEditTrack(track)}
885
-
title="edit track"
886
972
>
887
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
973
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
888
974
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path>
889
975
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path>
890
976
</svg>
977
+
edit
891
978
</button>
892
979
<button
893
-
class="action-btn delete-btn"
980
+
type="button"
981
+
class="track-action-btn delete"
894
982
onclick={() => deleteTrack(track.id, track.title)}
895
-
title="delete track"
896
983
>
897
-
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
984
+
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
898
985
<polyline points="3 6 5 6 21 6"></polyline>
899
986
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
900
987
</svg>
988
+
delete
901
989
</button>
902
990
</div>
903
991
{/if}
···
1451
1539
cursor: not-allowed;
1452
1540
}
1453
1541
1454
-
.file-info {
1455
-
margin-top: 0.5rem;
1456
-
font-size: var(--text-sm);
1457
-
color: var(--text-muted);
1458
-
}
1459
-
1460
-
button {
1542
+
/* form submit buttons only */
1543
+
form button[type="submit"] {
1461
1544
width: 100%;
1462
1545
padding: 0.75rem;
1463
1546
background: var(--accent);
···
1471
1554
transition: all 0.2s;
1472
1555
}
1473
1556
1474
-
button:hover:not(:disabled) {
1557
+
form button[type="submit"]:hover:not(:disabled) {
1475
1558
background: var(--accent-hover);
1476
1559
transform: translateY(-1px);
1477
1560
box-shadow: 0 4px 12px color-mix(in srgb, var(--accent) 30%, transparent);
1478
1561
}
1479
1562
1480
-
button:disabled {
1563
+
form button[type="submit"]:disabled {
1481
1564
opacity: 0.5;
1482
1565
cursor: not-allowed;
1483
1566
transform: none;
1484
1567
}
1485
1568
1486
-
button:active:not(:disabled) {
1569
+
form button[type="submit"]:active:not(:disabled) {
1487
1570
transform: translateY(0);
1488
1571
}
1489
1572
···
1787
1870
align-self: flex-start;
1788
1871
}
1789
1872
1790
-
.action-btn {
1791
-
display: flex;
1873
+
/* track action buttons (edit/delete in non-editing state) */
1874
+
.track-action-btn {
1875
+
display: inline-flex;
1792
1876
align-items: center;
1793
-
justify-content: center;
1794
-
width: 32px;
1795
-
height: 32px;
1796
-
padding: 0;
1877
+
gap: 0.35rem;
1878
+
padding: 0.4rem 0.65rem;
1797
1879
background: transparent;
1798
1880
border: 1px solid var(--border-default);
1799
-
border-radius: var(--radius-base);
1881
+
border-radius: var(--radius-full);
1800
1882
color: var(--text-tertiary);
1883
+
font-size: var(--text-sm);
1884
+
font-family: inherit;
1885
+
font-weight: 500;
1801
1886
cursor: pointer;
1802
1887
transition: all 0.15s;
1803
-
flex-shrink: 0;
1804
-
}
1805
-
1806
-
.action-btn svg {
1807
-
flex-shrink: 0;
1888
+
white-space: nowrap;
1889
+
width: auto;
1808
1890
}
1809
1891
1810
-
.action-btn:hover {
1892
+
.track-action-btn:hover {
1811
1893
transform: none;
1812
1894
box-shadow: none;
1895
+
border-color: var(--border-emphasis);
1896
+
color: var(--text-secondary);
1813
1897
}
1814
1898
1815
-
.edit-btn:hover {
1816
-
background: color-mix(in srgb, var(--accent) 12%, transparent);
1817
-
border-color: var(--accent);
1818
-
color: var(--accent);
1899
+
.track-action-btn.delete:hover {
1900
+
color: var(--text-secondary);
1819
1901
}
1820
1902
1821
-
.delete-btn:hover {
1822
-
background: color-mix(in srgb, var(--error) 12%, transparent);
1823
-
border-color: var(--error);
1824
-
color: var(--error);
1903
+
/* edit mode action buttons */
1904
+
.edit-actions {
1905
+
display: flex;
1906
+
gap: 0.75rem;
1907
+
justify-content: flex-end;
1908
+
padding-top: 0.75rem;
1909
+
border-top: 1px solid var(--border-subtle);
1910
+
margin-top: 0.5rem;
1825
1911
}
1826
1912
1827
-
.save-btn:hover {
1828
-
background: color-mix(in srgb, var(--success) 12%, transparent);
1829
-
border-color: var(--success);
1830
-
color: var(--success);
1913
+
.edit-cancel-btn {
1914
+
padding: 0.6rem 1.25rem;
1915
+
background: transparent;
1916
+
border: 1px solid var(--border-default);
1917
+
border-radius: var(--radius-base);
1918
+
color: var(--text-secondary);
1919
+
font-size: var(--text-base);
1920
+
font-weight: 500;
1921
+
font-family: inherit;
1922
+
cursor: pointer;
1923
+
transition: all 0.15s;
1924
+
width: auto;
1831
1925
}
1832
1926
1833
-
.cancel-btn:hover {
1834
-
background: color-mix(in srgb, var(--text-tertiary) 12%, transparent);
1927
+
.edit-cancel-btn:hover {
1835
1928
border-color: var(--text-tertiary);
1836
-
color: var(--text-secondary);
1929
+
background: var(--bg-hover);
1930
+
transform: none;
1931
+
box-shadow: none;
1932
+
}
1933
+
1934
+
.edit-save-btn {
1935
+
padding: 0.6rem 1.25rem;
1936
+
background: transparent;
1937
+
border: 1px solid var(--accent);
1938
+
border-radius: var(--radius-base);
1939
+
color: var(--accent);
1940
+
font-size: var(--text-base);
1941
+
font-weight: 500;
1942
+
font-family: inherit;
1943
+
cursor: pointer;
1944
+
transition: all 0.15s;
1945
+
width: auto;
1946
+
}
1947
+
1948
+
.edit-save-btn:hover:not(:disabled) {
1949
+
background: color-mix(in srgb, var(--accent) 8%, transparent);
1950
+
}
1951
+
1952
+
.edit-save-btn:disabled {
1953
+
opacity: 0.5;
1954
+
cursor: not-allowed;
1837
1955
}
1838
1956
1839
1957
.edit-input {
···
1847
1965
font-family: inherit;
1848
1966
}
1849
1967
1850
-
.current-image-preview {
1968
+
/* artwork editor */
1969
+
.artwork-editor {
1851
1970
display: flex;
1852
1971
align-items: center;
1853
-
gap: 0.75rem;
1854
-
padding: 0.5rem;
1972
+
gap: 1rem;
1973
+
padding: 0.75rem;
1855
1974
background: var(--bg-primary);
1856
1975
border: 1px solid var(--border-default);
1857
-
border-radius: var(--radius-sm);
1858
-
margin-bottom: 0.5rem;
1976
+
border-radius: var(--radius-base);
1859
1977
}
1860
1978
1861
-
.current-image-preview img {
1862
-
width: 48px;
1863
-
height: 48px;
1864
-
border-radius: var(--radius-sm);
1979
+
.artwork-preview {
1980
+
position: relative;
1981
+
width: 80px;
1982
+
height: 80px;
1983
+
border-radius: var(--radius-base);
1984
+
overflow: hidden;
1985
+
flex-shrink: 0;
1986
+
}
1987
+
1988
+
.artwork-preview img {
1989
+
width: 100%;
1990
+
height: 100%;
1865
1991
object-fit: cover;
1866
1992
}
1867
1993
1868
-
.current-image-label {
1994
+
.artwork-preview-overlay {
1995
+
position: absolute;
1996
+
inset: 0;
1997
+
background: rgba(0, 0, 0, 0.5);
1998
+
display: flex;
1999
+
align-items: center;
2000
+
justify-content: center;
2001
+
opacity: 0;
2002
+
transition: opacity 0.15s;
2003
+
}
2004
+
2005
+
.artwork-preview:hover .artwork-preview-overlay {
2006
+
opacity: 1;
2007
+
}
2008
+
2009
+
.artwork-action-btn {
2010
+
display: flex;
2011
+
align-items: center;
2012
+
justify-content: center;
2013
+
width: 32px;
2014
+
height: 32px;
2015
+
padding: 0;
2016
+
background: rgba(255, 255, 255, 0.15);
2017
+
border: none;
2018
+
border-radius: var(--radius-full);
2019
+
color: white;
2020
+
cursor: pointer;
2021
+
transition: all 0.15s;
2022
+
}
2023
+
2024
+
.artwork-action-btn:hover {
2025
+
background: var(--error);
2026
+
transform: scale(1.1);
2027
+
box-shadow: none;
2028
+
}
2029
+
2030
+
.artwork-status {
2031
+
font-size: var(--text-sm);
2032
+
color: var(--accent);
2033
+
font-weight: 500;
2034
+
}
2035
+
2036
+
.artwork-status.current {
2037
+
color: var(--text-tertiary);
2038
+
font-weight: 400;
2039
+
}
2040
+
2041
+
.artwork-removed {
2042
+
display: flex;
2043
+
flex-direction: column;
2044
+
align-items: center;
2045
+
gap: 0.5rem;
2046
+
padding: 0.75rem 1rem;
1869
2047
color: var(--text-tertiary);
2048
+
}
2049
+
2050
+
.artwork-removed span {
1870
2051
font-size: var(--text-sm);
1871
2052
}
1872
2053
2054
+
.undo-remove-btn {
2055
+
padding: 0.25rem 0.75rem;
2056
+
background: transparent;
2057
+
border: 1px solid var(--border-default);
2058
+
border-radius: var(--radius-full);
2059
+
color: var(--accent);
2060
+
font-size: var(--text-sm);
2061
+
font-family: inherit;
2062
+
cursor: pointer;
2063
+
transition: all 0.15s;
2064
+
width: auto;
2065
+
}
2066
+
2067
+
.undo-remove-btn:hover {
2068
+
border-color: var(--accent);
2069
+
background: color-mix(in srgb, var(--accent) 10%, transparent);
2070
+
transform: none;
2071
+
box-shadow: none;
2072
+
}
2073
+
2074
+
.artwork-empty {
2075
+
display: flex;
2076
+
flex-direction: column;
2077
+
align-items: center;
2078
+
gap: 0.5rem;
2079
+
padding: 0.75rem 1rem;
2080
+
color: var(--text-muted);
2081
+
}
2082
+
2083
+
.artwork-empty span {
2084
+
font-size: var(--text-sm);
2085
+
}
2086
+
2087
+
.artwork-upload-btn {
2088
+
display: inline-flex;
2089
+
align-items: center;
2090
+
gap: 0.4rem;
2091
+
padding: 0.5rem 0.85rem;
2092
+
background: transparent;
2093
+
border: 1px solid var(--accent);
2094
+
border-radius: var(--radius-full);
2095
+
color: var(--accent);
2096
+
font-size: var(--text-sm);
2097
+
font-weight: 500;
2098
+
cursor: pointer;
2099
+
transition: all 0.15s;
2100
+
margin-left: auto;
2101
+
}
2102
+
2103
+
.artwork-upload-btn:hover {
2104
+
background: color-mix(in srgb, var(--accent) 12%, transparent);
2105
+
}
2106
+
2107
+
.artwork-upload-btn input {
2108
+
display: none;
2109
+
}
2110
+
1873
2111
.edit-input:focus {
1874
2112
outline: none;
1875
2113
border-color: var(--accent);
···
2424
2662
.track-actions {
2425
2663
margin-left: 0.5rem;
2426
2664
gap: 0.35rem;
2665
+
flex-direction: column;
2427
2666
}
2428
2667
2429
-
.action-btn {
2430
-
width: 30px;
2431
-
height: 30px;
2668
+
.track-action-btn {
2669
+
padding: 0.35rem 0.55rem;
2670
+
font-size: var(--text-xs);
2432
2671
}
2433
2672
2434
-
.action-btn svg {
2435
-
width: 14px;
2436
-
height: 14px;
2673
+
.track-action-btn svg {
2674
+
width: 12px;
2675
+
height: 12px;
2437
2676
}
2438
2677
2439
2678
/* edit mode mobile */
···
2455
2694
}
2456
2695
2457
2696
.edit-actions {
2458
-
gap: 0.35rem;
2697
+
gap: 0.5rem;
2698
+
flex-direction: column;
2699
+
}
2700
+
2701
+
.edit-cancel-btn,
2702
+
.edit-save-btn {
2703
+
width: 100%;
2704
+
padding: 0.6rem;
2705
+
font-size: var(--text-sm);
2706
+
}
2707
+
2708
+
/* artwork editor mobile */
2709
+
.artwork-editor {
2710
+
flex-direction: column;
2711
+
gap: 0.75rem;
2712
+
padding: 0.65rem;
2713
+
}
2714
+
2715
+
.artwork-preview {
2716
+
width: 64px;
2717
+
height: 64px;
2718
+
}
2719
+
2720
+
.artwork-upload-btn {
2721
+
margin-left: 0;
2459
2722
}
2460
2723
2461
2724
/* data section mobile */