⏱ {mm}:{ss} {t('dub.elapsed')}
diff --git a/frontend/src/components/dub/dub.css b/frontend/src/components/dub/dub.css
new file mode 100644
index 00000000..311f3411
--- /dev/null
+++ b/frontend/src/components/dub/dub.css
@@ -0,0 +1,269 @@
+/* Dub Studio — irreducible CSS that can't live as Tailwind utilities:
+ keyframe-driven motion (stepper spin, idle-drop pulse, skeleton shimmer),
+ ::before connectors, and a few rules that must override other *global*
+ design-system classes (.studio-panel / .label-row / .override-toggle /
+ .segment-del) where a utility would lose on cascade. Everything else in the
+ dub feature is plain Tailwind utilities on the components. */
+
+/* ── Title strip — coexists with the global .label-row it sits on ── */
+.dub-head__title {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ margin-bottom: 0;
+ min-width: 0;
+ flex: 1;
+}
+
+/* ── Column flex that fills height — overrides .studio-panel overflow ── */
+.dub-panel-col {
+ margin-bottom: 0;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+}
+
+/* ── Pipeline stepper (top spine) ─────────────────────────────────────── */
+.dub-stepper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-wrap: wrap;
+ gap: 2px 0;
+ padding: 8px 12px;
+ flex: 0 0 auto;
+ border-bottom: 1px solid var(--chrome-border);
+}
+.dub-stepper__step {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 0.7rem;
+ font-weight: 500;
+ white-space: nowrap;
+ color: var(--chrome-fg-dim, #665c54);
+}
+.dub-stepper__step:not(:first-child)::before {
+ content: '';
+ width: 24px;
+ height: 2px;
+ margin: 0 8px;
+ border-radius: 1px;
+ background: var(--chrome-border-strong, rgba(255, 255, 255, 0.12));
+}
+.dub-stepper__step.is-reached:not(:first-child)::before {
+ background: var(--chrome-accent, #d3869b);
+}
+.dub-stepper__icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 22px;
+ height: 22px;
+ flex: 0 0 auto;
+ border-radius: 50%;
+ border: 1.5px solid var(--chrome-border-strong, rgba(255, 255, 255, 0.15));
+ color: var(--chrome-fg-dim, #665c54);
+}
+.dub-stepper__step.is-done .dub-stepper__icon {
+ border-color: var(--chrome-accent, #d3869b);
+ color: var(--chrome-accent, #d3869b);
+ background: color-mix(in srgb, var(--chrome-accent, #d3869b) 16%, transparent);
+}
+.dub-stepper__step.is-active .dub-stepper__icon {
+ border-color: var(--chrome-accent, #d3869b);
+ color: var(--chrome-accent, #d3869b);
+ box-shadow: 0 0 0 3px color-mix(in srgb, var(--chrome-accent, #d3869b) 14%, transparent);
+}
+.dub-stepper__step.is-done .dub-stepper__label,
+.dub-stepper__step.is-active .dub-stepper__label {
+ color: var(--chrome-fg);
+}
+.dub-stepper__spin { animation: dub-stepper-spin 1s linear infinite; }
+@keyframes dub-stepper-spin { to { transform: rotate(360deg); } }
+
+/* ── Idle / drop-zone ─────────────────────────────────────────────────── */
+.dub-idle-drop {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 10px;
+ cursor: pointer;
+ border: 2px dashed rgba(255, 255, 255, 0.06);
+ border-radius: 8px;
+ transition: background 0.3s, border-color 0.3s;
+ margin: 2px;
+ background: transparent;
+}
+@keyframes dub-pulse {
+ 0%, 100% { border-color: rgba(211, 134, 155, 0.3); background: rgba(211, 134, 155, 0.04); }
+ 50% { border-color: rgba(211, 134, 155, 0.6); background: rgba(211, 134, 155, 0.08); }
+}
+.dub-idle-drop.is-dragging {
+ border-color: #d3869b;
+ background: rgba(211, 134, 155, 0.05);
+ animation: dub-pulse 1.2s ease-in-out infinite;
+}
+.dub-idle-drop__puck {
+ width: 60px; height: 60px; border-radius: 50%;
+ display: flex; align-items: center; justify-content: center;
+ background: rgba(211, 134, 155, 0.06);
+ border: 1px solid rgba(211, 134, 155, 0.1);
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+}
+.dub-idle-drop:hover .dub-idle-drop__puck {
+ transform: scale(1.08);
+ box-shadow: 0 0 24px rgba(211, 134, 155, 0.15);
+}
+.dub-ingest-row__cta {
+ padding: 3px 10px;
+ border-radius: 4px;
+ font-size: 0.7rem;
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid rgba(255, 255, 255, 0.06);
+ color: #665c54;
+ cursor: default;
+}
+.dub-ingest-row__cta.is-ready {
+ background: rgba(211, 134, 155, 0.15);
+ border-color: rgba(211, 134, 155, 0.3);
+ color: var(--color-brand);
+ cursor: pointer;
+}
+
+/* ── Landing options — descendants override global element styling ── */
+.dub-landing-opts__lang select { max-width: 220px; }
+.dub-landing-adv__field--grow { flex: 1; min-width: 200px; }
+.dub-landing-adv__field--grow input { flex: 1; }
+
+/* ── Transcript toggle — sits on the global .override-toggle ── */
+.dub-transcript-toggle__inner {
+ margin-top: 0;
+ padding: 3px var(--space-3);
+ font-family: var(--chrome-font-mono);
+ font-size: var(--chrome-label-size);
+ color: var(--chrome-fg-muted);
+ letter-spacing: var(--chrome-label-track);
+ text-transform: uppercase;
+ background: var(--chrome-bg);
+ border: 1px solid var(--chrome-border);
+ border-radius: var(--chrome-radius-pill);
+}
+.dub-transcript-toggle__inner:hover { color: var(--chrome-fg); }
+.dub-skel-transcript-toggle__inner { margin-top: 0; padding: 2px 6px; font-size: 0.65rem; opacity: 0.3; cursor: default; }
+
+/* ── Prep / Transcribe overlays ───────────────────────────────────────── */
+.dub-prep-overlay--large {
+ flex: 1;
+ justify-content: center;
+ gap: var(--space-6);
+ padding: var(--space-5);
+ margin: var(--space-2);
+ border: 2px dashed rgba(211, 134, 155, 0.2);
+ border-radius: var(--radius-xl);
+ background: rgba(211, 134, 155, 0.03);
+}
+.dub-prep-bar {
+ width: min(240px, 85vw);
+ height: 4px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.06);
+ overflow: hidden;
+}
+.dub-prep-overlay--large .dub-prep-bar { width: 320px; height: 5px; }
+.dub-prep-bar__fill {
+ height: 100%;
+ background: linear-gradient(90deg, rgba(211, 134, 155, 0.75), rgba(211, 134, 155, 1));
+ border-radius: inherit;
+ transition: width 220ms ease-out;
+}
+.dub-prep-chip {
+ padding: 1px 6px;
+ border-radius: var(--radius-xs);
+ background: rgba(255, 255, 255, 0.03);
+ border: 1px solid rgba(255, 255, 255, 0.05);
+ color: var(--color-fg-subtle);
+}
+.dub-prep-chip.is-active {
+ background: rgba(211, 134, 155, 0.20);
+ border-color: rgba(211, 134, 155, 0.30);
+ color: var(--color-brand);
+}
+.dub-prep-chip.is-active.is-cached {
+ background: rgba(142, 192, 124, 0.20);
+ border-color: rgba(142, 192, 124, 0.30);
+ color: var(--color-success);
+}
+.dub-prep-chips--lg .dub-prep-chip { padding: 2px 8px; border-radius: var(--radius-sm); font-weight: var(--weight-regular); }
+.dub-prep-chips--lg .dub-prep-chip.is-active { font-weight: var(--weight-semibold); }
+
+/* ── Cast strip — --muted overrides descend onto __label/__kicker ── */
+.dub-cast__pair { display: flex; align-items: center; gap: 3px; padding: 1px 5px; border-radius: var(--radius-sm); transition: background 0.2s ease; }
+.dub-cast__pair:hover { background: rgba(255, 255, 255, 0.03); }
+.dub-cast__select { width: 95px; padding: 1px 3px; font-size: 0.60rem; }
+.dub-cast--muted { background: transparent; border-color: var(--chrome-border); }
+.dub-cast--muted .dub-cast__label,
+.dub-cast--muted .dub-cast__kicker { color: var(--chrome-fg-dim); }
+
+/* ── Multi-language preview switcher ──────────────────────────────────── */
+.dub-lang-switch {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 6px;
+ padding: 6px 0 8px;
+}
+.dub-lang-pill {
+ padding: 5px 14px;
+ font-size: 0.72rem;
+ font-weight: 600;
+ color: var(--chrome-fg-muted);
+ background: var(--chrome-hover-bg, rgba(255, 255, 255, 0.05));
+ border: 1px solid var(--chrome-border);
+ border-radius: var(--chrome-radius-pill, 999px);
+ cursor: pointer;
+ transition: background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast);
+}
+.dub-lang-pill:hover {
+ color: var(--chrome-fg);
+ border-color: var(--chrome-border-strong);
+}
+.dub-lang-pill.is-active {
+ color: #1b1b1b;
+ background: var(--chrome-fg, #ebdbb2);
+ border-color: var(--chrome-fg, #ebdbb2);
+}
+.dub-lang-pill:focus-visible {
+ outline: 2px solid var(--chrome-accent);
+ outline-offset: 1px;
+}
+
+/* ── Skeleton segment rows (shimmer bars) ─────────────────────────────── */
+.dub-skel-table { flex: 1; max-height: none; overflow-y: auto; min-height: 0; }
+.dub-skel-row { gap: 6px; }
+.dub-skel-bar {
+ height: 11px;
+ border-radius: 3px;
+ background: linear-gradient(90deg,
+ rgba(255, 255, 255, 0.04) 25%,
+ rgba(255, 255, 255, 0.09) 37%,
+ rgba(255, 255, 255, 0.04) 63%);
+ background-size: 400% 100%;
+ animation: dub-skel-shimmer 1.4s ease-in-out infinite;
+}
+.dub-skel-cell-acts__icon { opacity: 0.3; }
+@keyframes dub-skel-shimmer {
+ 0% { background-position: 100% 0; }
+ 100% { background-position: 0 0; }
+}
+
+/* ── Reduced motion: shimmer/pulse die; stepper spinner holds a frame ── */
+@media (prefers-reduced-motion: reduce) {
+ .dub-skel-bar,
+ .dub-idle-drop.is-dragging,
+ .dub-stepper__spin {
+ animation: none;
+ }
+}
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 6c2edf7f..e8bc004c 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -216,8 +216,6 @@ h1, h2, h3, h4 {
.segment-time,
.segment-table code,
.ui-table-toolbar__meta,
-.dub-gen-overlay__stats,
-.dub-trans-overlay__stats,
.logs-footer__count,
[data-tabular] {
font-variant-numeric: tabular-nums slashed-zero;
@@ -809,7 +807,6 @@ select:focus:not(:focus-visible) { outline: none; box-shadow: none; }
.personality-chip:focus-visible,
.chip:focus-visible,
.wh__chip:focus-visible,
-.dub-lang-pill:focus-visible,
.wf-player__btn:focus-visible,
.studio-action-bar__overrides:focus-visible {
outline: 2px solid var(--chrome-accent);
@@ -1465,15 +1462,8 @@ div[role="dialog"].audio-trimmer {
RESPONSIVE — make every component adapt to viewport width
════════════════════════════════════════════════════════════ */
-/* ── DubTab + CloneDesignTab split grids → single column ── */
-@media (max-width: 1000px) {
- .dub-split-grid {
- grid-template-columns: 1fr !important;
- grid-template-rows: auto 1fr !important;
- }
- /* .clone-split-grid is single-column at every width now (spec §1 restack —
- see CloneDesignTab.css); no narrow-viewport override needed. */
-}
+/* DubTab split grid → single column is handled inline in DubTab.jsx /
+ IdleSkeleton.jsx via Tailwind max-[1000px]:grid-cols-1 utilities. */
/* .grid-2 / .grid-4 responsive collapse → Tailwind max-[700px]/max-[500px]
variants inline at the usage sites (P4). .grid-3 was already dead (no base
@@ -1502,30 +1492,8 @@ div[role="dialog"].audio-trimmer {
.studio-panel { padding: 8px 6px; }
}
-/* ── DubTab settings bar wrap ── */
-@media (max-width: 900px) {
- .dub-settings-bar {
- flex-direction: column !important;
- gap: 6px !important;
- }
- .dub-settings-bar > div { min-width: 0 !important; width: 100%; }
-}
-
-/* ── DubTab footer buttons — wrap on narrow ── */
-@media (max-width: 800px) {
- .dub-footer-btns {
- flex-wrap: wrap !important;
- }
- .dub-footer-btns .btn-primary {
- min-width: 80px;
- flex: 1 1 calc(50% - 4px) !important;
- }
-}
-@media (max-width: 500px) {
- .dub-footer-btns .btn-primary {
- flex: 1 1 100% !important;
- }
-}
+/* DubTab settings-bar wrap is handled inline in DubLeftColumn.jsx via Tailwind
+ max-[900px]:gap utilities (the bar is already a flex-column at every width). */
/* ── Segment table — tighter columns ── */
@media (max-width: 800px) {
diff --git a/frontend/src/pages/DubTab.css b/frontend/src/pages/DubTab.css
deleted file mode 100644
index d11a4991..00000000
--- a/frontend/src/pages/DubTab.css
+++ /dev/null
@@ -1,800 +0,0 @@
-/* DubTab — page-specific styles layered over the design system. */
-
-/* ── Header strip ────────────────────────────────────────────── */
-/* Top-bar — anchors to the top of the workspace, claims the full width
- above the video + segment-table split. Drop the card chrome so it reads
- as a header strip, not a floating panel. */
-/* .dub-head strip + filename/meta/project/actions/primary → Tailwind utilities
- (DubHeader.jsx, IdleSkeleton.jsx). .dub-head__title stays: it coexists with
- the unlayered global .label-row rule (which it overrides), so a utility would
- lose to it. */
-.dub-head__title { display: flex; align-items: center; gap: var(--space-3); margin-bottom: 0; min-width: 0; flex: 1; }
-
-/* ── Preview toggle wrapper (above waveform) ─────────────────── */
-.dub-preview-toggle {
- display: flex;
- gap: var(--space-2);
- align-items: center;
- padding: 4px var(--space-3);
- background: var(--chrome-bg);
- border: 1px solid var(--chrome-border);
- border-radius: var(--chrome-radius-pill);
- margin-bottom: var(--space-2);
- flex-shrink: 0;
-}
-.dub-preview-toggle__kicker {
- font-family: var(--chrome-font-mono);
- font-size: var(--chrome-label-size);
- color: var(--chrome-fg-muted);
- letter-spacing: var(--chrome-label-track);
- text-transform: uppercase;
- margin-right: var(--space-2);
-}
-.dub-preview-toggle__hint {
- font-family: var(--font-sans);
- font-size: var(--chrome-label-size);
- color: var(--chrome-fg-dim);
- margin-left: auto;
-}
-
-/* ── Prep stage chips (upload/preparation flow) ──────────────── */
-/* .dub-prep-chips container → utilities (PrepOverlay.jsx). The --lg modifier and
- the .dub-prep-chip items below stay (state/modifier combinators). */
-.dub-prep-chip {
- padding: 1px 6px;
- border-radius: var(--radius-xs);
- background: rgba(255, 255, 255, 0.03);
- border: 1px solid rgba(255, 255, 255, 0.05);
- color: var(--color-fg-subtle);
-}
-.dub-prep-chip.is-active {
- background: rgba(211, 134, 155, 0.20);
- border-color: rgba(211, 134, 155, 0.30);
- color: var(--color-brand);
-}
-.dub-prep-chip.is-active.is-cached {
- background: rgba(142, 192, 124, 0.20);
- border-color: rgba(142, 192, 124, 0.30);
- color: var(--color-success);
-}
-.dub-prep-chips--lg .dub-prep-chip { padding: 2px 8px; border-radius: var(--radius-sm); font-weight: var(--weight-regular); }
-.dub-prep-chips--lg .dub-prep-chip.is-active { font-weight: var(--weight-semibold); }
-
-/* ── Prep / Transcribe overlays ──────────────────────────────── */
-/* .dub-prep-overlay base + __title/__note/__detail → utilities (PrepOverlay.jsx).
- The --large modifier (and its descendant .dub-prep-bar override) stays. */
-.dub-prep-overlay--large {
- flex: 1;
- justify-content: center;
- gap: var(--space-6);
- padding: var(--space-5);
- margin: var(--space-2);
- border: 2px dashed rgba(211, 134, 155, 0.2);
- border-radius: var(--radius-xl);
- background: rgba(211, 134, 155, 0.03);
-}
-.dub-prep-bar {
- width: min(240px, 85vw);
- height: 4px;
- border-radius: 999px;
- background: rgba(255, 255, 255, 0.06);
- overflow: hidden;
-}
-.dub-prep-overlay--large .dub-prep-bar { width: 320px; height: 5px; }
-.dub-prep-bar__fill {
- height: 100%;
- background: linear-gradient(90deg, rgba(211, 134, 155, 0.75), rgba(211, 134, 155, 1));
- border-radius: inherit;
- transition: width 220ms ease-out;
-}
-
-/* .dub-trans-overlay base + __head/__title/__bar → utilities (TranscribeOverlay.jsx).
- __stats stays: it's targeted by the unlayered global tabular-nums rule in index.css. */
-.dub-trans-overlay__stats {
- display: flex;
- gap: var(--space-6);
- font-size: var(--text-md);
- color: var(--color-fg-muted);
-}
-
-/* ── Bulk selection + Apply-voice rows ───────────────────────── */
-.dub-bulk-row {
- display: flex;
- align-items: center;
- gap: var(--space-3);
- padding: 3px 6px;
- border-radius: var(--radius-md);
- margin-bottom: var(--space-2);
- font-size: var(--text-xs);
-}
-.dub-bulk-row--apply { background: rgba(142, 192, 124, 0.06); border: 1px solid rgba(142, 192, 124, 0.12); }
-.dub-bulk-row--select { background: rgba(211, 134, 155, 0.08); border: 1px solid rgba(211, 134, 155, 0.25); }
-
-.dub-bulk-row__label-success { color: var(--color-success); font-weight: var(--weight-bold); white-space: nowrap; }
-/* .dub-bulk-row__label-brand → utilities (DubRightColumn.jsx). */
-
-/* ── Footer status banner ────────────────────────────────────── */
-/* .dub-footer-banner → utilities (DubFooter.jsx, IdleSkeleton.jsx). */
-.dub-footer-banner__badge { display: inline-flex; }
-
-/* ── Output options row (checkboxes + default track) ─────────── */
-.dub-outputs-row {
- display: flex;
- align-items: center;
- gap: var(--space-3);
- margin-bottom: 2px;
- padding: 0 var(--space-2);
- font-size: var(--text-xs);
- color: var(--chrome-fg-muted);
- font-family: var(--font-sans);
- flex-wrap: wrap;
-}
-.dub-outputs-row label { display: flex; align-items: center; gap: var(--space-2); cursor: pointer; }
-.dub-outputs-row label:hover { color: var(--chrome-fg); }
-.dub-outputs-row input[type="checkbox"] { accent-color: var(--chrome-accent); }
-
-/* ── Export track checkbox row ───────────────────────────────── */
-.dub-tracks-row {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- margin-bottom: 2px;
- padding: 3px var(--space-3);
- font-size: var(--text-xs);
- color: var(--chrome-fg-muted);
- font-family: var(--font-sans);
- background: var(--chrome-bg);
- border-radius: var(--chrome-radius-pill);
- border: 1px solid var(--chrome-border);
- flex-wrap: wrap;
-}
-.dub-tracks-row__title {
- font-family: var(--chrome-font-mono);
- font-size: var(--chrome-label-size);
- letter-spacing: var(--chrome-label-track);
- text-transform: uppercase;
- color: var(--chrome-fg-muted);
- font-weight: 600;
-}
-.dub-tracks-row label {
- display: inline-flex;
- align-items: center;
- gap: 4px;
- padding: 2px 8px;
- border: 1px solid transparent;
- border-radius: var(--chrome-radius-pill);
- cursor: pointer;
- transition: border-color var(--dur-fast), background var(--dur-fast), color var(--dur-fast);
-}
-.dub-tracks-row label input { accent-color: var(--chrome-accent); }
-.dub-tracks-row label.is-on.is-success {
- color: var(--chrome-severity-ok);
- border-color: color-mix(in srgb, var(--chrome-severity-ok) 45%, transparent);
- background: color-mix(in srgb, var(--chrome-severity-ok) 10%, transparent);
-}
-.dub-tracks-row label.is-off { color: var(--chrome-fg-dim); }
-.dub-tracks-row label.is-on { color: var(--chrome-fg); border-color: var(--chrome-border-strong); background: var(--chrome-hover-bg); }
-.dub-tracks-row label span.code { text-transform: uppercase; letter-spacing: 0.04em; }
-
-/* Pre-generation compression-ratio warning surfaced above Generate Dub. */
-.dub-compression-warn {
- display: flex;
- align-items: flex-start;
- gap: 8px;
- padding: 6px 10px;
- margin: 4px 0;
- background: color-mix(in srgb, #fabd2f 12%, transparent);
- border: 1px solid color-mix(in srgb, #fabd2f 35%, transparent);
- border-left: 2px solid #fabd2f;
- border-radius: var(--chrome-radius-pill);
- font-size: 0.72rem;
- color: var(--chrome-fg);
- line-height: 1.35;
-}
-.dub-compression-warn__icon {
- color: #fabd2f;
- font-size: 0.9rem;
- line-height: 1;
- flex-shrink: 0;
-}
-.dub-compression-warn__body { flex: 1; }
-.dub-compression-warn strong { color: #fabd2f; font-weight: var(--weight-semibold); }
-
-/* ── Utility: column flex that fills remaining height ────────── */
-/* .dub-col + .dub-split-1/.dub-split-2 → utilities (DubTab.jsx, IdleSkeleton.jsx).
- .dub-panel-col stays: it sits on .studio-panel (unlayered) and overrides its
- overflow, so a utility would lose. */
-.dub-panel-col { margin-bottom: 0; overflow: hidden; display: flex; flex-direction: column; }
-
-/* ── Idle / drop-zone skeleton ───────────────────────────────── */
-.dub-idle-drop {
- flex: 1;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 10px;
- cursor: pointer;
- border: 2px dashed rgba(255, 255, 255, 0.06);
- border-radius: 8px;
- transition: background 0.3s, border-color 0.3s;
- margin: 2px;
- background: transparent;
-}
-@keyframes dub-pulse {
- 0%, 100% { border-color: rgba(211, 134, 155, 0.3); background: rgba(211, 134, 155, 0.04); }
- 50% { border-color: rgba(211, 134, 155, 0.6); background: rgba(211, 134, 155, 0.08); }
-}
-.dub-idle-drop.is-dragging {
- border-color: #d3869b;
- background: rgba(211, 134, 155, 0.05);
- animation: dub-pulse 1.2s ease-in-out infinite;
-}
-.dub-idle-drop__puck {
- width: 60px; height: 60px; border-radius: 50%;
- display: flex; align-items: center; justify-content: center;
- background: rgba(211, 134, 155, 0.06);
- border: 1px solid rgba(211, 134, 155, 0.1);
- transition: transform 0.3s ease, box-shadow 0.3s ease;
-}
-.dub-idle-drop:hover .dub-idle-drop__puck {
- transform: scale(1.08);
- box-shadow: 0 0 24px rgba(211, 134, 155, 0.15);
-}
-/* .dub-idle-drop__lines/__title/__sub + .dub-ingest-row/__input → utilities
- (IdleSkeleton.jsx). .dub-idle-drop base/__puck stay (pulse keyframes + hover);
- .dub-ingest-row__cta stays (.is-ready modifier interplay). */
-.dub-ingest-row__cta {
- padding: 3px 10px;
- border-radius: 4px;
- font-size: 0.7rem;
- background: rgba(255, 255, 255, 0.03);
- border: 1px solid rgba(255, 255, 255, 0.06);
- color: #665c54;
- cursor: default;
-}
-.dub-ingest-row__cta.is-ready {
- background: rgba(211, 134, 155, 0.15);
- border-color: rgba(211, 134, 155, 0.3);
- color: var(--color-brand);
- cursor: pointer;
-}
-
-/* .dub-idle-upload-label + .dub-hidden-file → utilities (IdleSkeleton.jsx). */
-
-/* .dub-change-row + .dub-speakers-hint → utilities (IdleSkeleton.jsx).
- .dub-change-row__cta stays: it sits on .btn-primary (unlayered) and overrides
- its margin-top, so a utility would lose. */
-.dub-change-row__cta { flex: 1; margin-top: 0; }
-.dub-speakers-input { width: 52px; margin-left: 4px; padding: 4px 6px; border-radius: 6px; border: 1px solid var(--border, #3c3836); background: var(--input-bg, #282828); color: inherit; font-size: 12px; }
-
-/* Compact disabled inputs in the idle skeleton */
-.input-base--xs { font-size: 0.65rem; }
-/* .dub-skel-translate-btn + .dub-skel-transcript-toggle + .dub-inline-icon →
- utilities (IdleSkeleton.jsx). __inner stays: it sits on .override-toggle
- (unlayered) and overrides its margin/padding, so a utility would lose. */
-.dub-skel-transcript-toggle__inner { margin-top: 0; padding: 2px 6px; font-size: 0.65rem; opacity: 0.3; cursor: default; }
-
-.dub-ghost-footer { padding: 4px 8px; flex-shrink: 0; }
-
-/* Settings bar (right-pane) — expanded translation settings.
- Two-row layout: fields on top (CSS grid so nothing truncates),
- primary/secondary actions flush-right underneath. Wraps into a
- container card so it reads as a distinct cluster from the bulk-select
- and segment table that follow it. */
-.dub-settings-bar {
- display: flex;
- flex-direction: column;
- gap: 3px;
- margin-bottom: 4px;
- padding: 4px 8px;
- background: var(--chrome-bg);
- border: 1px solid var(--chrome-border);
- border-radius: var(--chrome-radius-pill);
-}
-.dub-settings-bar__fields {
- display: flex;
- flex-wrap: wrap;
- gap: 4px 6px;
- align-items: end;
-}
-.dub-settings-bar__actions {
- display: flex;
- justify-content: flex-end;
- gap: 6px;
- flex-wrap: wrap;
-}
-.dub-settings-field {
- display: flex;
- flex-direction: column;
- gap: 1px;
- min-width: 0;
-}
-.dub-settings-field .label-row {
- font-size: 0.58rem;
- color: var(--color-fg-muted);
- margin: 0;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
-}
-.dub-settings-field .input-base,
-.dub-settings-field .dub-cast__select,
-.dub-settings-field .dub-engine-select,
-.dub-settings-field .input-base--xs {
- width: 100%;
- font-size: 0.65rem;
- padding: 3px 5px;
-}
-.dub-settings-field__hint {
- font-size: 0.52rem;
- color: var(--color-fg-subtle);
- font-style: italic;
- margin-left: 2px;
-}
-/* Size fields so they share the row proportionally */
-.dub-settings-field--lang { flex: 1 1 100px; min-width: 70px; }
-.dub-settings-field--iso { flex: 0 1 72px; min-width: 52px; }
-.dub-settings-field--dialect { flex: 0 1 110px; min-width: 80px; }
-.dub-settings-field--engine { flex: 1.4 1 130px; min-width: 90px; }
-.dub-settings-field--quality { flex: 0 1 auto; min-width: 80px; }
-.dub-settings-field--quality > *:last-child { width: 100%; }
-.dub-settings-field--style { flex: 1 1 90px; min-width: 64px; }
-@media (max-width: 960px) {
- .dub-settings-bar__fields { flex-direction: column; }
- .dub-settings-field--lang,
- .dub-settings-field--iso,
- .dub-settings-field--dialect,
- .dub-settings-field--engine,
- .dub-settings-field--quality,
- .dub-settings-field--style { flex-basis: 100%; min-width: 0; }
-}
-
-/* Multi-lang mode toggle + picker — inline in actions row */
-.dub-settings-field--multi {
- flex-basis: 100%;
- padding-top: 3px;
- border-top: 1px solid var(--chrome-border);
- margin-top: 1px;
-}
-.dub-multi-toggle {
- display: flex;
- align-items: center;
- gap: 6px;
- font-size: 0.65rem;
- color: var(--chrome-fg-muted);
- cursor: pointer;
- margin-bottom: 2px;
-}
-.dub-multi-toggle input { accent-color: var(--chrome-accent); cursor: pointer; }
-
-/* Collapsed-summary strip that replaces the full grid once language/engine/
- quality are set. Keeps the primary Translate All + Clean Up CTAs inline. */
-.dub-settings-summary {
- display: flex;
- align-items: center;
- gap: var(--space-2);
- padding: 3px var(--space-3);
- margin-bottom: 3px;
- background: var(--chrome-bg);
- border: 1px solid var(--chrome-border);
- border-radius: var(--chrome-radius-pill);
- font-family: var(--font-sans);
- font-size: 0.66rem;
- color: var(--chrome-fg-muted);
-}
-.dub-settings-summary__trigger {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- flex: 1;
- min-width: 0;
- background: none;
- border: none;
- color: var(--color-fg-muted);
- cursor: pointer;
- padding: 2px 0;
- font: inherit;
- text-align: left;
-}
-.dub-settings-summary__trigger strong { color: var(--chrome-fg); font-weight: 600; }
-.dub-settings-summary__style {
- color: var(--chrome-fg-dim);
- font-style: italic;
- margin-left: var(--space-2);
-}
-.dub-settings-close {
- flex: 0 0 auto;
- padding: 0 4px;
- align-self: center;
-}
-.dub-engine-select { font-size: 0.65rem; padding: 3px 5px; min-width: 0; }
-
-/* Small opt-in below the URL ingest row: toggle YouTube caption pulling. */
-.dub-ingest-sub-opt {
- display: flex;
- align-items: center;
- gap: 6px;
- margin-top: 6px;
- padding: 4px 6px;
- font-size: 0.62rem;
- color: var(--color-fg-muted);
- cursor: pointer;
- border-radius: 4px;
- background: rgba(255, 255, 255, 0.02);
-}
-.dub-ingest-sub-opt:hover { color: var(--color-fg); background: rgba(255, 255, 255, 0.05); }
-.dub-ingest-sub-opt input[type="checkbox"] { margin: 0; accent-color: #d3869b; }
-
-/* Inline "install engine" chip next to the Engine label. Tight + brand-tinted
- so it reads as an affordance, not a badge. Disappears after a successful
- install (refreshEngines re-renders). */
-.dub-engine-install-chip {
- margin-left: 6px;
- padding: 1px 6px;
- font-size: 0.55rem;
- line-height: 1.4;
- background: rgba(211, 134, 155, 0.14);
- border: 1px solid rgba(211, 134, 155, 0.35);
- color: #d3869b;
- border-radius: 999px;
- cursor: pointer;
- white-space: nowrap;
- transition: background var(--dur-fast) var(--ease-out);
-}
-.dub-engine-install-chip:hover { background: rgba(211, 134, 155, 0.22); }
-.dub-engine-install-chip:disabled,
-.dub-engine-install-chip--disabled {
- opacity: 0.55;
- cursor: default;
- font-style: italic;
-}
-
-/* Transcript collapsible + content */
-.dub-transcript-toggle-wrap { margin-bottom: 4px; }
-.dub-transcript-toggle__inner {
- margin-top: 0;
- padding: 3px var(--space-3);
- font-family: var(--chrome-font-mono);
- font-size: var(--chrome-label-size);
- color: var(--chrome-fg-muted);
- letter-spacing: var(--chrome-label-track);
- text-transform: uppercase;
- background: var(--chrome-bg);
- border: 1px solid var(--chrome-border);
- border-radius: var(--chrome-radius-pill);
-}
-.dub-transcript-toggle__inner:hover { color: var(--chrome-fg); }
-.dub-transcript-body {
- background: var(--chrome-bg);
- border: 1px solid var(--chrome-border);
- border-top: none;
- border-radius: 0 0 var(--chrome-radius-pill) var(--chrome-radius-pill);
- padding: var(--space-3);
- font-size: var(--text-xs);
- color: var(--chrome-fg-muted);
- line-height: 1.5;
- max-height: 80px;
- overflow-y: auto;
-}
-
-/* Glossary wrapper */
-.dub-glossary-wrap { margin-bottom: 4px; }
-.dub-glossary-chip {
- display: inline-flex;
- align-items: center;
- padding: 3px var(--space-3);
- margin-bottom: 4px;
- font-family: var(--chrome-font-mono);
- font-size: var(--chrome-label-size);
- letter-spacing: var(--chrome-label-track);
- text-transform: uppercase;
- color: var(--chrome-fg-muted);
- background: transparent;
- border: 1px solid var(--chrome-border);
- border-radius: var(--chrome-radius-pill);
- cursor: pointer;
- transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
-}
-.dub-glossary-chip:hover {
- background: var(--chrome-hover-bg);
- border-color: var(--chrome-border-strong);
- color: var(--chrome-fg);
-}
-
-/* Footer layout — flatten the warm studio-panel backdrop so the chrome
- action strip below has somewhere neutral to sit. Keep the content
- padding tight. */
-.dub-footer-panel {
- padding: 4px var(--space-3);
- flex-shrink: 0;
- background: var(--chrome-bg) !important;
- border: 1px solid var(--chrome-border) !important;
- border-radius: 0 !important;
-}
-.dub-footer-panel::before { display: none !important; }
-.dub-footer-btns { display: flex; gap: 3px; }
-.dub-footer-btns--spaced { margin-top: 3px; }
-
-/* Output options row value column */
-.dub-outputs-title-strong {
- font-family: var(--chrome-font-mono);
- font-size: var(--chrome-label-size);
- letter-spacing: var(--chrome-label-track);
- text-transform: uppercase;
- color: var(--chrome-fg-muted);
- font-weight: 600;
-}
-
-/* Bulk-select row controls */
-.dub-bulk-select { font-size: 0.62rem; padding: 2px 4px; }
-.dub-bulk-select--flex { flex: 1; }
-.dub-bulk-select--voice { min-width: 72px; flex: 1 1 100px; }
-.dub-bulk-select--lang { width: auto; min-width: 64px; flex: 0 1 90px; }
-.dub-bulk-row__clear { margin-left: auto; }
-
-/* Footer banner gapping */
-/* .dub-footer-banner__badge-gap → utilities (DubFooter.jsx). */
-
-/* .dub-lazy-fallback → utilities (DubRightColumn.jsx). */
-
-/* ── Landing options — the one pre-upload decision + Advanced disclosure ── */
-/* .dub-landing-opts + __lang/__label → utilities (IdleSkeleton.jsx). __lang
- keeps its class for the descendant select rule below; __adv stays (:hover). */
-.dub-landing-opts__lang select { max-width: 220px; }
-.dub-landing-opts__adv {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- padding: 5px 10px;
- font-size: 0.7rem;
- color: var(--chrome-fg-muted);
- background: transparent;
- border: 1px solid var(--chrome-border);
- border-radius: var(--chrome-radius-pill, 999px);
- cursor: pointer;
- transition: color var(--dur-fast), border-color var(--dur-fast);
-}
-.dub-landing-opts__adv:hover { color: var(--chrome-fg); border-color: var(--chrome-border-strong); }
-/* .dub-landing-adv + __field base → utilities (IdleSkeleton.jsx). __field keeps
- its class for the --grow modifier + descendant input rule below. */
-.dub-landing-adv__field--grow { flex: 1; min-width: 200px; }
-.dub-landing-adv__field--grow input { flex: 1; }
-
-/* Demoted ghost export button (single Export ▾ beside the primary Generate). */
-.dub-skel-gen-btn--secondary {
- display: inline-flex;
- align-items: center;
- gap: 5px;
- background: transparent;
- border: 1px solid var(--chrome-border);
- color: var(--chrome-fg-muted);
- border-radius: 8px;
- flex: 0 0 auto;
-}
-
-/* ── Multi-language preview switcher (tap a language to hear that dub) ── */
-.dub-lang-switch {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 6px;
- padding: 6px 0 8px;
-}
-.dub-lang-pill {
- padding: 5px 14px;
- font-size: 0.72rem;
- font-weight: 600;
- color: var(--chrome-fg-muted);
- background: var(--chrome-hover-bg, rgba(255, 255, 255, 0.05));
- border: 1px solid var(--chrome-border);
- border-radius: var(--chrome-radius-pill, 999px);
- cursor: pointer;
- transition: background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast);
-}
-.dub-lang-pill:hover {
- color: var(--chrome-fg);
- border-color: var(--chrome-border-strong);
-}
-.dub-lang-pill.is-active {
- color: #1b1b1b;
- background: var(--chrome-fg, #ebdbb2);
- border-color: var(--chrome-fg, #ebdbb2);
-}
-
-/* ── Pipeline stepper (top spine) ──────────────────────────────────────
- Upload → Prepare → Transcribe → Edit → Generate → Export. Done steps and
- the connectors behind them take the accent; the active step gets a ring;
- pending steps are dim. */
-.dub-stepper {
- display: flex;
- align-items: center;
- justify-content: center;
- flex-wrap: wrap;
- gap: 2px 0;
- padding: 8px 12px;
- flex: 0 0 auto;
- border-bottom: 1px solid var(--chrome-border);
-}
-.dub-stepper__step {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- font-size: 0.7rem;
- font-weight: 500;
- white-space: nowrap;
- color: var(--chrome-fg-dim, #665c54);
-}
-.dub-stepper__step:not(:first-child)::before {
- content: '';
- width: 24px;
- height: 2px;
- margin: 0 8px;
- border-radius: 1px;
- background: var(--chrome-border-strong, rgba(255, 255, 255, 0.12));
-}
-.dub-stepper__step.is-reached:not(:first-child)::before {
- background: var(--chrome-accent, #d3869b);
-}
-.dub-stepper__icon {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 22px;
- height: 22px;
- flex: 0 0 auto;
- border-radius: 50%;
- border: 1.5px solid var(--chrome-border-strong, rgba(255, 255, 255, 0.15));
- color: var(--chrome-fg-dim, #665c54);
-}
-.dub-stepper__step.is-done .dub-stepper__icon {
- border-color: var(--chrome-accent, #d3869b);
- color: var(--chrome-accent, #d3869b);
- background: color-mix(in srgb, var(--chrome-accent, #d3869b) 16%, transparent);
-}
-.dub-stepper__step.is-active .dub-stepper__icon {
- border-color: var(--chrome-accent, #d3869b);
- color: var(--chrome-accent, #d3869b);
- box-shadow: 0 0 0 3px color-mix(in srgb, var(--chrome-accent, #d3869b) 14%, transparent);
-}
-.dub-stepper__step.is-done .dub-stepper__label,
-.dub-stepper__step.is-active .dub-stepper__label {
- color: var(--chrome-fg);
-}
-.dub-stepper__spin { animation: dub-stepper-spin 1s linear infinite; }
-@keyframes dub-stepper-spin { to { transform: rotate(360deg); } }
-
-/* ── Skeleton segment rows (idle / pre-transcription state) ─────────────
- Shimmer bars, not literal placeholder text — the old "0:00.0–0:00.0 /
- Speaker 1 / Default" wrapped in their narrow cells and read as broken
- data. Bars have a fixed height so nothing wraps or collides. */
-.dub-skel-table { flex: 1; max-height: none; overflow-y: auto; min-height: 0; }
-.dub-skel-row { gap: 6px; }
-.dub-skel-bar {
- height: 11px;
- border-radius: 3px;
- background: linear-gradient(90deg,
- rgba(255, 255, 255, 0.04) 25%,
- rgba(255, 255, 255, 0.09) 37%,
- rgba(255, 255, 255, 0.04) 63%);
- background-size: 400% 100%;
- animation: dub-skel-shimmer 1.4s ease-in-out infinite;
-}
-/* .dub-skel-cell-* and -header-* cells + .dub-skel-hint → utilities
- (IdleSkeleton.jsx). __icon stays: it sits on .segment-del (unlayered) and
- overrides its opacity, so a utility would lose. */
-.dub-skel-cell-acts__icon { opacity: 0.3; }
-
-@keyframes dub-skel-shimmer {
- 0% { background-position: 100% 0; }
- 100% { background-position: 0 0; }
-}
-
-/* Disabled generation controls shown in idle skeleton */
-.dub-skel-gen-btn { margin-top: 0; flex: 1; padding: 4px 8px; font-size: 0.7rem; opacity: 0.4; }
-/* .dub-skel-gen-row + .dub-skel-settings + .dub-skel-field/--sm → utilities
- (IdleSkeleton.jsx). .dub-skel-gen-btn stays (sits on .btn-primary, unlayered). */
-
-/* ── Generating overlay (on top of waveform) ─────────────────── */
-.dub-gen-overlay { display: flex; flex-direction: column; align-items: center; gap: 6px; width: 100%; padding: 10px; backdrop-filter: blur(2px); }
-.dub-gen-overlay__head { display: flex; align-items: center; gap: 6px; }
-.dub-gen-overlay__title { font-weight: 600; font-size: 0.75rem; color: var(--color-fg); font-variant-numeric: tabular-nums; letter-spacing: 0.01em; }
-.dub-gen-overlay__title.is-stopping { color: var(--color-fg-muted); }
-.dub-gen-overlay__bar { width: 80%; max-width: 240px; margin: 1px 0; }
-.dub-gen-overlay__text { font-size: 0.62rem; color: var(--color-fg-muted); }
-.dub-gen-overlay__stats { display: flex; gap: var(--space-4); font-size: 0.65rem; color: var(--color-fg-muted); font-variant-numeric: tabular-nums; }
-
-/* ── Cast / speaker-voices strip below waveform ──────────────── */
-/* .dub-cast base + __row + __kicker/__label base + --muted__chip → utilities
- (IdleSkeleton.jsx). .dub-cast / __kicker / __label keep their classes so the
- .dub-cast--muted (and its descendant) overrides below still apply; __pair
- (:hover) and __select (real cast, not the skeleton) stay. */
-.dub-cast__pair { display: flex; align-items: center; gap: 3px; padding: 1px 5px; border-radius: var(--radius-sm); transition: background 0.2s ease; }
-.dub-cast__pair:hover { background: rgba(255, 255, 255, 0.03); }
-.dub-cast__select { width: 95px; padding: 1px 3px; font-size: 0.60rem; }
-
-/* Muted placeholder cast strip shown in the idle skeleton */
-.dub-cast--muted { background: transparent; border-color: var(--chrome-border); }
-.dub-cast--muted .dub-cast__label,
-.dub-cast--muted .dub-cast__kicker { color: var(--chrome-fg-dim); }
-/* ── Output options row (footer) ─────────────────────────────── */
-.dub-outputs-title { font-weight: 600; color: var(--color-fg); }
-.dub-outputs-default { font-size: 0.6rem; padding: 2px 4px; width: 120px; }
-
-/* ── Footer download buttons ─────────────────────────────────── */
-/* Flat chrome surfaces — color = accent (text+border), not a gradient fill.
- Matches the status-bar chrome so the footer row reads as one strip instead
- of a jelly of pastel pills. Tones tint via color-mix so a single formula
- handles danger/ok/brand/etc consistently. */
-.dub-footer-btn {
- margin-top: 0 !important;
- flex: 1;
- padding: 5px 8px !important;
- font-family: var(--font-sans) !important;
- font-size: 0.72rem !important;
- letter-spacing: 0.02em !important;
- text-transform: none !important;
- background: transparent !important;
- border: 1px solid var(--chrome-border-strong) !important;
- color: var(--chrome-fg) !important;
- border-radius: var(--chrome-radius-pill) !important;
- box-shadow: none !important;
- transition: background var(--dur-fast), border-color var(--dur-fast), color var(--dur-fast) !important;
-}
-.dub-footer-btn:hover:not(:disabled) { background: var(--chrome-hover-bg) !important; }
-.dub-footer-btn:disabled { opacity: 0.45; cursor: not-allowed; }
-.dub-footer-btn--sm { padding: 3px 6px !important; font-size: 0.62rem !important; }
-
-.dub-footer-btn--idle { color: var(--chrome-fg-muted) !important; border-color: var(--chrome-border) !important; }
-.dub-footer-btn--stopping { color: var(--chrome-fg-muted) !important; border-color: var(--chrome-border) !important; }
-.dub-footer-btn--danger {
- color: var(--chrome-severity-err) !important;
- border-color: color-mix(in srgb, var(--chrome-severity-err) 45%, transparent) !important;
- background: color-mix(in srgb, var(--chrome-severity-err) 10%, transparent) !important;
-}
-.dub-footer-btn--danger:hover:not(:disabled) { background: color-mix(in srgb, var(--chrome-severity-err) 18%, transparent) !important; }
-.dub-footer-btn--green {
- color: var(--chrome-severity-ok) !important;
- border-color: color-mix(in srgb, var(--chrome-severity-ok) 45%, transparent) !important;
- background: color-mix(in srgb, var(--chrome-severity-ok) 10%, transparent) !important;
-}
-.dub-footer-btn--green:hover:not(:disabled) { background: color-mix(in srgb, var(--chrome-severity-ok) 18%, transparent) !important; }
-.dub-footer-btn--pink {
- color: var(--chrome-accent) !important;
- border-color: var(--chrome-accent-border) !important;
- background: var(--chrome-accent-bg) !important;
-}
-.dub-footer-btn--pink:hover:not(:disabled) { background: color-mix(in srgb, var(--chrome-accent) 20%, transparent) !important; }
-.dub-footer-btn--blue {
- color: #83a598 !important;
- border-color: color-mix(in srgb, #83a598 45%, transparent) !important;
- background: color-mix(in srgb, #83a598 10%, transparent) !important;
-}
-.dub-footer-btn--lime {
- color: #b8bb26 !important;
- border-color: color-mix(in srgb, #b8bb26 45%, transparent) !important;
- background: color-mix(in srgb, #b8bb26 10%, transparent) !important;
-}
-.dub-footer-btn--amber {
- color: var(--chrome-severity-warn) !important;
- border-color: color-mix(in srgb, var(--chrome-severity-warn) 45%, transparent) !important;
- background: color-mix(in srgb, var(--chrome-severity-warn) 10%, transparent) !important;
-}
-.dub-footer-btn--orange {
- color: #fe8019 !important;
- border-color: color-mix(in srgb, #fe8019 45%, transparent) !important;
- background: color-mix(in srgb, #fe8019 10%, transparent) !important;
-}
-
-/* plan-04 (#131): actionable failure detail beneath the error badge.
- .dub-failure-notice + __hint/__actions → Tailwind utilities (DubFailureNotice.jsx). */
-
-/* ── Reduced motion (10x P4, spec §3): shimmer/pulse die; the stepper
- spinner holds a static frame. ───────────────────────────────────────── */
-@media (prefers-reduced-motion: reduce) {
- .dub-skel-bar,
- .dub-idle-drop.is-dragging,
- .dub-stepper__spin {
- animation: none;
- }
-}
diff --git a/frontend/src/pages/DubTab.jsx b/frontend/src/pages/DubTab.jsx
index bb31f5f9..77ee822b 100644
--- a/frontend/src/pages/DubTab.jsx
+++ b/frontend/src/pages/DubTab.jsx
@@ -14,7 +14,7 @@ import DubHeader from '../components/dub/DubHeader';
import DubLeftColumn from '../components/dub/DubLeftColumn';
import DubRightColumn from '../components/dub/DubRightColumn';
import DubFooter from '../components/dub/DubFooter';
-import './DubTab.css';
+import '../components/dub/dub.css';
export default function DubTab(props) {
const { t, i18n } = useTranslation();
@@ -478,7 +478,7 @@ export default function DubTab(props) {
handleDubQc={handleDubQc}
setExportOpen={setExportOpen}
/>
-