/* ==========================================================================
   ds.css — SaydiVoice design tokens & shared primitives (single source)
   Extracted from the 2026-11-05 studio simplification layer (spec 006, T003).
   Tokens are consumed by every surface; primitives (.ov-*) are reused as-is.
   Do not declare accents, type ramps, radii or elevations outside this file.
   ========================================================================== */

:root{ color-scheme:light dark;

  --accent:#c92a2a; --accent-hover:#b42318; --accent-soft:#fdeceb;
  --brand-coral:#fe655d; --brand-coral-deep:#fc3e57; --brand-coral-soft:#fff1ef;
  --text:#111827; --text-2:#4b5563; --text-3:#5b6472;
  --surface:#ffffff; --canvas:#f7f8fa; --surface-hover:#f3f4f6;
  --border:#e5e7eb; --border-2:#e5e7eb;
  --success:#15803d; --warning:#b45309; --danger:#b91c1c;
  --focus:#2563eb;
  /* ---- Semantic roles split out of the raw brand colours (spec 007, T001).
     A fill colour is NOT a text colour: on a dark surface --accent (#c92a2a)
     measures 3.31:1, so links/icons/labels use --accent-text while --accent
     stays a background. Light aliases them back, so light output is unchanged. */
  --accent-text:var(--accent); --on-accent:#ffffff;
  /* Used by the tip nudge, playbar and player glyphs via var(--muted, …). It was
     never declared, so 8 call sites silently fell back to a literal and could
     not follow the theme (dark measured 3.74:1). */
  --muted:#6b7280;
  /* Hero halo + dot grid behind the Dubbing/Subtitles headers. The literal was
     a bright cyan that only works on white; dark gets a low-alpha tint. */
  --hero-glow:rgba(200,248,252,.78); --hero-dots:rgba(92,174,181,.18);
  --border-strong:#d1d5db;
  --overlay:rgba(20,20,25,.45);
  --danger-soft:#fef2f2; --danger-border:#fecaca;
  --menu-hover:var(--surface-hover); --menu-hover-danger:#fef2f2;
  --glass-bg:rgba(255,255,255,.72); --glass-bg-strong:rgba(255,255,255,.92);
  /* Notice surfaces (inline info/warn chips). Pastel tints are light-only by
     nature: on a dark page they become the brightest thing on screen, so each
     family carries soft/border/ink and the dark block re-tints all three. */
  --info-soft:#eff6ff; --info-border:#bfdbfe; --info-ink:#1e40af;
  --violet-soft:#faf5ff; --violet-border:#e9d5ff; --violet-ink:#6b21a8;
  --warn-soft:#fffbeb; --warn-border:#fde68a; --warn-ink:#92400e;
  /* Soft state tints (chips, toasts, notices). Light values are the exact ones
     the app already used; dark collapses them onto two reds + one amber so the
     page never glows. Consolidated because a dozen near-identical corals is how
     this repo ended up with 500 literal surfaces in the first place. */
  --coral-tint:#fff2f0; --coral-tint-2:#fff7f6;
  --warn-tint:#fff3e6; --success-tint:#ecfdf5;
  --glow-1:color-mix(in srgb, var(--brand-coral) 14%, transparent);
  --glow-2:rgba(155,93,254,.10); --glow-3:rgba(254,211,93,.10);
  --fs-display:32px; --lh-display:40px; --fs-h1:24px; --lh-h1:32px;
  --fs-h2:20px; --lh-h2:28px; --fs-h3:16px; --lh-h3:24px;
  --fs-body:15px; --lh-body:24px; --fs-small:13px; --lh-small:20px;
  --fs-meta:12px; --lh-meta:16px;
  --s-1:4px; --s-2:8px; --s-3:12px; --s-4:16px; --s-5:24px; --s-6:32px; --s-7:48px; --s-8:64px;
  --r-sm:8px; --r-md:12px; --r-lg:16px; --r-full:999px;
  --e-1:0 1px 2px rgba(17,24,39,.06),0 1px 3px rgba(17,24,39,.08);
  --e-2:0 8px 24px rgba(17,24,39,.12);
  --e-3:0 24px 64px rgba(17,24,39,.18);
  --t-fast:120ms ease-out; --t-base:180ms ease-out;
}

/* ---------- Dark theme (spec 007, T006) -----------------------------------
   ONE override block: every surface keeps consuming the same semantic tokens,
   so light stays identical and dark needs no second stylesheet. Contrast is
   measured, not eyeballed (WCAG 2.1 on #15161a — specs/007-dark-theme/
   research.md §4): text 16.28:1 · text-2 8.36:1 · text-3 5.84:1 ·
   accent-text 7.92:1 · focus 7.58:1 · border-strong 3.74:1.
   Rules for future edits (also enforced by tests/test_design_tokens.mjs):
     • --accent is a FILL only; text uses --accent-text.
     • --border is a decorative hairline (1.31:1) — never a control boundary.
       Form controls use --border-strong.
     • --black is the "inverse chip" token, not the colour black: on dark it is
       a slightly raised neutral (white labels keep 12:1) instead of a hole. */
:root[data-theme="dark"]{
  color-scheme:dark;
  --accent:#ff5a4e; --accent-hover:#ff7166; --accent-soft:#2a1517;
  --accent-text:#ff8a80; --on-accent:#15161a;
  --muted:#a9b1bd;
  --hero-glow:rgba(34,211,238,.09); --hero-dots:rgba(34,211,238,.10);
  --brand-coral-soft:#3a1b1d;
  --text:#f2f3f5; --text-2:#a9b1bd; --text-3:#8b93a1;
  --surface:#15161a; --canvas:#0f1013; --surface-hover:#1d1f24;
  --border:#2a2d34; --border-2:#2a2d34; --border-strong:#6b7280;
  --success:#4ade80; --warning:#fbbf24; --danger:#f87171;
  --focus:#7aa7ff;
  --overlay:rgba(0,0,0,.62);
  --danger-soft:#2a1517; --danger-border:#4a2020;
  --menu-hover:#1d1f24; --menu-hover-danger:#2a1517;
  --glass-bg:rgba(21,22,26,.72); --glass-bg-strong:rgba(21,22,26,.92);
  --info-soft:#10203a; --info-border:#1e3a5f; --info-ink:#9cc4ff;
  --violet-soft:#211930; --violet-border:#39254f; --violet-ink:#c9a9ff;
  --warn-soft:#2a2113; --warn-border:#4a3a1a; --warn-ink:#fbbf24;
  --coral-tint:#2a1517; --coral-tint-2:#241416;
  --warn-tint:#2a2113; --success-tint:#10281a;
  --glow-1:color-mix(in srgb, var(--brand-coral) 9%, transparent);
  --glow-2:rgba(155,93,254,.07); --glow-3:rgba(254,211,93,.05);
  /* A shadow is invisible on a dark field, so each elevation also carries a
     hairline highlight; without it popovers read as "pasted" on the page. */
  --e-1:0 1px 2px rgba(0,0,0,.55),0 0 0 1px rgba(255,255,255,.04);
  --e-2:0 8px 24px rgba(0,0,0,.62),0 0 0 1px rgba(255,255,255,.06);
  --e-3:0 24px 64px rgba(0,0,0,.70),0 0 0 1px rgba(255,255,255,.08);
}

:focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }

/* Primitives */
/* Brand lockup — mark + text, for the places that need a text wordmark at the
   size of their own type (Sizes come from the font-size of whatever wraps it,
   1em mark). NOT the site header's brand: since 2026-11 the header uses the
   wordmark image `a.brand-name.site-brand > img.brand-logo`
   (/static/saydivoice-logo.svg, 118×20) on the home page and on every other
   page — the mark-as-the-"S" in that file is the logo, not a clipping bug.
   Do not swap the header back to this component without asking the owner. */
.ov-lockup{ display:inline-flex; align-items:center; gap:.45em; font-weight:800;
  letter-spacing:-.02em; line-height:1; color:var(--text); text-decoration:none; white-space:nowrap; }
.ov-lockup__mark{ flex:none; width:1.1em; height:1.1em; display:block; }
.ov-lockup__text{ color:var(--text); }

.ov-btn{ display:inline-flex; align-items:center; justify-content:center; gap:8px; min-height:40px;
  padding:0 var(--s-4); border:1px solid transparent; border-radius:var(--r-sm); font:inherit;
  font-size:14px; font-weight:650; cursor:pointer; text-decoration:none; white-space:nowrap;
  transition:background var(--t-fast), border-color var(--t-fast), color var(--t-fast); }

.ov-btn--primary{ background:var(--accent); color:var(--on-accent); }

.ov-btn--primary:hover{ background:var(--accent-hover); }

.ov-btn--secondary{ background:var(--surface); color:var(--text); border-color:var(--border); }

.ov-btn--secondary:hover{ background:var(--surface-hover); }

.ov-btn--ghost{ background:transparent; color:var(--text-2); }

.ov-btn--ghost:hover{ background:var(--surface-hover); color:var(--text); }

.ov-btn--danger{ background:var(--surface); color:var(--danger); border-color:var(--danger-border); }

.ov-btn:disabled{ background:var(--surface-hover); color:var(--text-3); border-color:var(--border); cursor:not-allowed; opacity:1; }

.ov-card{ background:var(--surface); border:1px solid var(--border); border-radius:var(--r-md); padding:var(--s-4); box-shadow:none; }

.ov-field{ width:100%; min-height:40px; padding:var(--s-2) var(--s-3); border:1px solid var(--border); border-radius:var(--r-sm);
  background:var(--surface); font:inherit; font-size:14px; color:var(--text); }

.ov-field:focus{ border-color:var(--focus); outline:2px solid color-mix(in srgb,var(--focus) 22%,transparent); outline-offset:0; }

.ov-dialog{ background:var(--surface); border:1px solid var(--border); border-radius:var(--r-lg); box-shadow:var(--e-3); }

.ov-empty{ border:1px dashed var(--border); border-radius:var(--r-md); padding:var(--s-5); text-align:center; background:var(--surface); }

.ov-advanced{ border:1px solid var(--border); border-radius:var(--r-sm); margin:14px 0; background:var(--surface); }

.ov-advanced summary{ display:flex; align-items:center; gap:8px; cursor:pointer; padding:12px; font-size:13px; font-weight:650; list-style:none; }

.ov-advanced summary::-webkit-details-marker{ display:none; }

.ov-advanced summary svg{ margin-left:auto; color:var(--text-3); transition:transform var(--t-fast); }

.ov-advanced[open] summary svg{ transform:rotate(180deg); }

.ov-advanced-hint{ font-size:12px; color:var(--text-3); font-weight:400; }

.ov-advanced .st-section{ padding:0 12px 12px; border-bottom:none; }

.ov-btn[hidden]{ display:none; }

.ov-btn--sm{ min-height:32px; padding:0 12px; font-size:13px; }

.ov-btn--md{ min-height:40px; padding:0 16px; font-size:14px; }

.ov-btn--lg{ min-height:48px; padding:0 20px; font-size:15px; }

.ov-btn--primary:disabled,.ov-btn--primary[aria-disabled="true"]{ background:var(--surface-hover); color:var(--text-3); border-color:var(--border); }

.ov-segmented{ display:inline-flex; gap:2px; padding:3px; border:1px solid var(--border); border-radius:var(--r-sm); background:var(--surface-hover); }

.ov-segmented [role="radio"]{ display:inline-flex; align-items:center; justify-content:center; gap:6px; min-height:32px;
  padding:0 12px; border:1px solid transparent; border-radius:var(--r-sm); background:transparent;
  color:var(--text-2); font:inherit; font-size:13px; font-weight:600; cursor:pointer; }

.ov-segmented [role="radio"][aria-checked="true"]{ background:var(--surface); border-color:var(--border); color:var(--text); }

.ov-tabs{ display:flex; gap:4px; border-bottom:1px solid var(--border); }

.ov-tabs [role="tab"]{ min-height:40px; padding:0 12px; border:0; border-bottom:2px solid transparent;
  background:transparent; color:var(--text-2); font:inherit; font-size:14px; font-weight:600; cursor:pointer; }

.ov-tabs [role="tab"][aria-selected="true"]{ color:var(--accent); border-bottom-color:var(--accent); }

.ov-empty__title{ font-size:16px; font-weight:650; margin-bottom:4px; }

.ov-empty__text{ color:var(--text-2); font-size:14px; margin-bottom:12px; }

.ov-dialog__head{ display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:12px; }

.ov-dialog__title{ font-size:20px; font-weight:650; }

/* ---------- Motion contract (T010) ---------------------------------------
   Two durations, one easing curve. Everything that moves respects the OS
   reduced-motion setting; reveal-on-scroll content is shown, never hidden. */
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{
    animation-duration:.01ms !important; animation-iteration-count:1 !important;
    transition-duration:.01ms !important; scroll-behavior:auto !important;
  }
  .v-reveal{ opacity:1 !important; transform:none !important; filter:none !important; }
}

/* Chromatic coral CTA — marketing pages (home, pricing, donate, header). */
:is(.open-studio-btn[class],.hero-cta[class],.generate-voice-btn[class],.clone-btn[class],.try-before-buy-cta[class],.d-primary[class]){
  position:relative !important; isolation:isolate; overflow:hidden !important;
  border:0 !important; border-radius:999px !important;
  color:#fff !important; background:#ff665e !important;
  font-weight:500 !important; line-height:1; white-space:nowrap;
  box-shadow:none !important;
}
:is(.open-studio-btn[class],.hero-cta[class],.generate-voice-btn[class],.clone-btn[class],.try-before-buy-cta[class],.d-primary[class])::before{
  content:""; position:absolute; inset:0; z-index:-2; border-radius:inherit; pointer-events:none;
  background-color:#ff665e; background-repeat:no-repeat;
  background-image:url(pill-lime.svg),url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='142'%20height='85'%20viewBox='-41%20-3%20142%2085'%20preserveAspectRatio='none'%20fill='none'%3e%3cdefs%3e%3cfilter%20id='pm-tex'%20x='-41'%20y='-3'%20width='142'%20height='85'%20filterUnits='userSpaceOnUse'%20color-interpolation-filters='sRGB'%3e%3cfeTurbulence%20type='fractalNoise'%20baseFrequency='0.999'%20numOctaves='3'%20seed='4033'/%3e%3cfeDisplacementMap%20in='SourceGraphic'%20scale='8'%20xChannelSelector='R'%20yChannelSelector='G'/%3e%3c/filter%3e%3cfilter%20id='pm-b'%20x='-66'%20y='-66'%20width='183.8755'%20height='162.9506'%20filterUnits='userSpaceOnUse'%20color-interpolation-filters='sRGB'%3e%3cfeGaussianBlur%20stdDeviation='8'/%3e%3c/filter%3e%3c/defs%3e%3cg%20filter='url(%23pm-tex)'%3e%3cg%20transform='matrix(0.953%200.3028%20-0.9233%200.384%2016.5773%2025.1191)'%3e%3cg%20filter='url(%23pm-b)'%3e%3cpath%20d='M%2051.8754%204.3500%20C%2051.8754%2010.8253%2037.6978%2030.9505%2024.2854%2030.9505%20C%2010.8729%2030.9505%200%2025.7013%200%2019.2260%20C%200%2012.7507%2011.8601%2011.0072%2025.2726%2011.0072%20C%2027.5346%20-2.4873%2051.8754%20-2.1251%2051.8754%204.3500%20Z'%20fill='%23FA41C9'%20fill-opacity='0.6'/%3e%3c/g%3e%3c/g%3e%3c/g%3e%3c/svg%3e"),url(pill-gold.svg),url("data:image/svg+xml,%3csvg%20xmlns='http://www.w3.org/2000/svg'%20width='267'%20height='122'%20viewBox='-119%20-39%20267%20122'%20preserveAspectRatio='none'%20fill='none'%3e%3cdefs%3e%3cfilter%20id='pr-tex'%20x='-119'%20y='-39'%20width='267'%20height='122'%20filterUnits='userSpaceOnUse'%20color-interpolation-filters='sRGB'%3e%3cfeTurbulence%20type='fractalNoise'%20baseFrequency='0.999'%20numOctaves='3'%20seed='4218'/%3e%3cfeDisplacementMap%20in='SourceGraphic'%20scale='8'%20xChannelSelector='R'%20yChannelSelector='G'/%3e%3c/filter%3e%3cfilter%20id='pr-b'%20x='-66'%20y='-66'%20width='281'%20height='179'%20filterUnits='userSpaceOnUse'%20color-interpolation-filters='sRGB'%3e%3cfeGaussianBlur%20stdDeviation='8'/%3e%3c/filter%3e%3c/defs%3e%3cg%20filter='url(%23pr-tex)'%3e%3cg%20transform='matrix(1%200%200%201%20-34%203)'%3e%3cg%20filter='url(%23pr-b)'%3e%3cpath%20d='M%20149%2029.9682%20C%20104.0814%2038.5563%20125.8917%2047.0000%2090.8689%2047.0000%20C%2055.8461%2047.0000%20-51.8578%2026.7157%2030.0540%2021.6638%20C%2050.8711%2026.2243%20110.3438%2016.3076%20116.2910%202.9153%20C%20121.3219%20-8.4132%20149%2015.9675%20149%2029.9682%20Z'%20fill='%23EA483F'%20fill-opacity='1'/%3e%3c/g%3e%3c/g%3e%3c/g%3e%3c/svg%3e");
  background-position:188.8889% 85.4167%,186.3636% 6.6667%,-42.623% 38.2979%,80.9524% 47.561%;
  background-size:130% 220%,118.3333% 212.5%,150.8333% 217.5%,222.5% 305%;
  background-blend-mode:normal,lighten,normal,normal;
}
:is(.open-studio-btn[class],.hero-cta[class],.generate-voice-btn[class],.clone-btn[class],.try-before-buy-cta[class],.d-primary[class])::after{
  content:""; position:absolute; inset:0; z-index:-1; border-radius:inherit; pointer-events:none;
  background:transparent; transition:background .12s;
}
:is(.open-studio-btn[class],.hero-cta[class],.generate-voice-btn[class],.clone-btn[class],.try-before-buy-cta[class],.d-primary[class]):hover::after{ background:rgba(0,0,0,.08); }
:is(.open-studio-btn[class],.hero-cta[class],.generate-voice-btn[class],.clone-btn[class],.try-before-buy-cta[class],.d-primary[class]):disabled::after,:is(.open-studio-btn[class],.hero-cta[class],.generate-voice-btn[class],.clone-btn[class],.try-before-buy-cta[class],.d-primary[class])[aria-disabled="true"]::after{ background:rgba(0,0,0,.5); z-index:1; }

/* The header "Studio" link used a font-size:0 + ::after label trick below
   1240px. The chromatic layer also owns ::after, so restore the real label and
   drop the overlay there — otherwise the link collapses into a coral dot. */
@media (max-width:1240px){
  .open-studio-btn[class]{
    font-size:14px !important;
    height:32px !important;
    padding:0 14px !important;
    width:auto !important;
  }
  .open-studio-btn[class]::after{
    content:none !important;
    position:static !important;
    inset:auto !important;
    background:none !important;
  }
}
@media (max-width:430px){
  .open-studio-btn[class]{
    font-size:12px !important;
    padding:0 6px !important;
    height:30px !important;
  }
}

/* Keep the chromatic surface on a disabled CTA. Ant's `:disabled` default
   (#eaebed / #7d7d7d) turned the home demo button into a washed grey pill. */
:is(.open-studio-btn[class],.hero-cta[class],.generate-voice-btn[class],.clone-btn[class],.try-before-buy-cta[class],.d-primary[class]):disabled,
:is(.open-studio-btn[class],.hero-cta[class],.generate-voice-btn[class],.clone-btn[class],.try-before-buy-cta[class],.d-primary[class])[aria-disabled="true"]{
  background:#ff665e !important;
  color:#fff !important;
  opacity:.72 !important;
  box-shadow:none !important;
}
:is(.open-studio-btn[class],.hero-cta[class],.generate-voice-btn[class],.clone-btn[class],.try-before-buy-cta[class],.d-primary[class]):disabled::after,
:is(.open-studio-btn[class],.hero-cta[class],.generate-voice-btn[class],.clone-btn[class],.try-before-buy-cta[class],.d-primary[class])[aria-disabled="true"]::after{
  background:rgba(0,0,0,.16) !important;
  z-index:-1 !important;
}

/* ---------- Home demo CTA "Generate Voice" — muted mauve -------------------
   Owner's reference (2026-11-06): a dusty lavender-gray pill, not the chromatic
   coral. Ink/icon #292529, pill radius 20–24px, one very soft shadow so it
   lifts off the card, 160ms hover/active, active scale .98.

   The first pass used #AAA3A5 (HSL 343 4% 65%). Measured against the reference
   that hex is right — the pill there samples #B3ADAF — but it also sits on a
   dark plum backdrop, and on the home card's white surface 4% saturation reads
   as plain gray ("xám quá", owner 2026-11-06). So put the mauve back into the
   chroma AND lift the value, because the owner then read the correction as
   "xám đậm" too: #C3B0BA is HSL 328 14% 73% — a light dusty mauve, still muted
   but never neutral gray. Do NOT push into pastel purple (hue <300 / sat >20%),
   and do not darken it again: darker values read as gray on white.
   hover #CAB9C2 (L 76%), active #BBA5B1 (L 69%). Shadow tint follows the pill
   (rgba(74,54,68,…)) so the lift does not read as a gray drop.

   Scope note: this replaces the coral treatment for the two demo cards on the
   home page only; .hero-cta / .clone-btn / .open-studio-btn / .d-primary and
   the studio's own buttons keep the chromatic layer above. The selectors double
   `[class]` (and keep the generated `[data-v-61be32a1]` scope) because the built
   homepage/index.css pins this button with (0,4,0) + !important — that pairing
   is what out-ranks it. */
.card-tts .tts-input-area .generate-voice-btn[data-v-61be32a1][class],
[data-card="card-tts"] .generate-voice-btn[class][class],
.card-voice-design .generate-voice-btn[class][class],
.generate-voice-btn[class][class]{
  background:#c3b0ba !important;
  background-image:none !important;
  color:#292529 !important;
  opacity:1 !important;
  border:0 !important;
  border-radius:22px !important;
  transform:none !important;
  box-shadow:0 1px 2px rgba(74,54,68,.10), 0 6px 16px rgba(74,54,68,.07) !important;
  transition:background-color 160ms ease, box-shadow 160ms ease, transform 160ms ease !important;
}
/* The chromatic layer owns ::before (coral mass + pill blobs) and ::after (dark
   hover wash); both have to go or the mauve pill reads dirty. */
.card-tts .tts-input-area .generate-voice-btn[data-v-61be32a1][class]::before,
[data-card="card-tts"] .generate-voice-btn[class][class]::before,
.card-voice-design .generate-voice-btn[class][class]::before,
.generate-voice-btn[class][class]::before,
.card-tts .tts-input-area .generate-voice-btn[data-v-61be32a1][class]::after,
[data-card="card-tts"] .generate-voice-btn[class][class]::after,
.card-voice-design .generate-voice-btn[class][class]::after,
.generate-voice-btn[class][class]::after{
  background:none !important;
  z-index:-1 !important;
}
/* Ink follows the label: the markup pins fill="#fff" on the sparkle icon. */
.generate-voice-btn[class][class] svg{ fill:currentColor !important; }
.card-tts .tts-input-area .generate-voice-btn[class][class] .btn-loading-spinner{
  border-color:rgba(43,33,41,.24) !important;
  border-top-color:#292529 !important;
}
.card-tts .tts-input-area .generate-voice-btn[data-v-61be32a1][class]:hover:not(:disabled):not(.loading),
[data-card="card-tts"] .generate-voice-btn[class][class]:hover:not(:disabled):not(.loading),
.card-voice-design .generate-voice-btn[class][class]:hover:not(:disabled):not(.loading),
.generate-voice-btn[class][class]:hover:not(:disabled):not(.loading){
  background:#cab9c2 !important;
  opacity:1 !important;
  transform:none !important;
  box-shadow:0 1px 2px rgba(74,54,68,.12), 0 8px 20px rgba(74,54,68,.10) !important;
}
.card-tts .tts-input-area .generate-voice-btn[data-v-61be32a1][class]:active:not(:disabled):not(.loading),
[data-card="card-tts"] .generate-voice-btn[class][class]:active:not(:disabled):not(.loading),
.card-voice-design .generate-voice-btn[class][class]:active:not(:disabled):not(.loading),
.generate-voice-btn[class][class]:active:not(:disabled):not(.loading){
  background:#bba5b1 !important;
  transform:scale(.98) !important;
  box-shadow:0 1px 2px rgba(74,54,68,.10), 0 3px 10px rgba(74,54,68,.07) !important;
}
/* Disabled / rendering: same mauve, dimmed — never an Ant grey pill. */
.card-tts .tts-input-area .generate-voice-btn[data-v-61be32a1][class]:disabled,
[data-card="card-tts"] .generate-voice-btn[class][class]:disabled,
.card-voice-design .generate-voice-btn[class][class]:disabled,
.generate-voice-btn[class][class]:disabled,
.card-tts .tts-input-area .generate-voice-btn[data-v-61be32a1][class].loading,
[data-card="card-tts"] .generate-voice-btn[class][class].loading,
.card-voice-design .generate-voice-btn[class][class].loading,
.generate-voice-btn[class][class].loading{
  background:#c3b0ba !important;
  color:#292529 !important;
  opacity:.62 !important;
  transform:none !important;
  cursor:not-allowed !important;
  box-shadow:0 1px 2px rgba(74,54,68,.07) !important;
}

/* ---------- pb-bar — audio progress bar (assets/lib/pb-progress.js) --------
   A slim rail, a coral fill and a draggable thumb. It is the playbar's seek
   control and the demo players' position display (home page cards, landing
   panels). The waveform this replaced decoded the whole clip with an
   AudioContext just to draw a shape that carried no more information than the
   fill below it — and it was still unreadable and undraggable on touch.

   Markup is built by the lib, never authored by hand:
     .pb-bar > [.pb-bar-time] .pb-bar-rail[role=slider] > .pb-bar-track > .pb-bar-fill
                                                        , .pb-bar-thumb
                                                          [.pb-bar-time]
   State hooks: [data-playing="true"] on .pb-bar drives the playback motion;
   .is-scrubbing on the rail turns motion OFF, because while dragging the bar
   must follow the pointer exactly. Coral/white only — no second accent. */
.pb-bar{
  --pb-rail-h:4px;
  --pb-thumb:12px;
  --pb-rail:color-mix(in srgb, var(--brand-coral) 26%, var(--surface));
  display:flex; align-items:center; gap:10px; flex:1 1 auto; min-width:0;
}
.pb-bar-rail{
  position:relative; flex:1 1 auto; min-width:0; height:28px;
  display:flex; align-items:center; border-radius:var(--r-full);
  cursor:pointer; touch-action:none; -webkit-tap-highlight-color:transparent;
}
.pb-bar-track{
  position:relative; width:100%; height:var(--pb-rail-h);
  border-radius:var(--r-full); background:var(--pb-rail); overflow:hidden;
}
.pb-bar-fill{
  position:absolute; inset:0; border-radius:inherit;
  background:linear-gradient(to right, var(--brand-coral), var(--brand-coral-deep));
  transform:scaleX(0); transform-origin:left center;
  /* Eased only while STOPPED, so a keyboard seek glides instead of jumping.
     During playback the lib renders every animation frame and this transition
     is switched off: easing on top of per-frame values would lag the bar
     behind the audio and re-introduce exactly the stutter it is meant to fix. */
  transition:transform .16s ease-out;
}
/* RTL (ar, he, fa, ur): the clip fills from the right. */
[dir="rtl"] .pb-bar-fill{ transform-origin:right center; }
.pb-bar-thumb{
  position:absolute; top:50%; width:var(--pb-thumb); height:var(--pb-thumb);
  /* Logical start + the --pb-pos the lib writes, so RTL needs no JS branch. */
  inset-inline-start:var(--pb-pos, 0%);
  margin-block-start:calc(var(--pb-thumb) / -2);
  margin-inline-start:calc(var(--pb-thumb) / -2);
  border-radius:var(--r-full); background:var(--surface);
  border:2px solid var(--brand-coral);
  box-shadow:0 1px 3px rgba(254,101,93,.35);
  transition:inset-inline-start .16s ease-out, transform .18s ease-out, box-shadow .18s ease-out;
}
.pb-bar-time{
  font-size:11px; color:var(--text-3); font-variant-numeric:tabular-nums; flex:none;
}
.pb-bar-rail:hover .pb-bar-thumb,
.pb-bar-rail:focus-visible .pb-bar-thumb{ transform:scale(1.14); }
/* The ring reads as the focus state; the 28px-tall hit area would make a
   default outline around empty space. Selector carries `.pb-bar` because
   styles.css styles `[tabindex]:focus-visible` globally at the same
   specificity — without it the blue UA-ish ring wins.
   Deep coral, not coral: 3.5:1 on white, so it satisfies 1.4.11 (3:1). */
.pb-bar .pb-bar-rail:focus-visible{ outline:none; }
/* Ring with a hairline gap, so the 4px track keeps its own edge instead of
   reading as a bordered bar. */
.pb-bar .pb-bar-rail:focus-visible .pb-bar-track{
  box-shadow:0 0 0 1px var(--surface), 0 0 0 3px var(--brand-coral-deep);
}
.pb-bar-rail[aria-disabled="true"]{ cursor:default; }
.pb-bar-rail.is-scrubbing{ cursor:grabbing; }
.pb-bar-rail.is-scrubbing .pb-bar-fill,
.pb-bar-rail.is-scrubbing .pb-bar-thumb{ transition-duration:0s; animation:none; }

/* Playback: position follows the audio clock frame by frame (no transition on
   the moving parts), and a slow halo on the thumb makes a playing clip read as
   live without animating the bar itself. */
.pb-bar[data-playing="true"] .pb-bar-fill{ transition:none; }
.pb-bar[data-playing="true"] .pb-bar-thumb{
  transition:transform .18s ease-out, box-shadow .18s ease-out;
  animation:pb-bar-pulse 2.4s ease-out infinite;
}
@keyframes pb-bar-pulse{
  0%{ box-shadow:0 1px 3px rgba(254,101,93,.35), 0 0 0 0 color-mix(in srgb, var(--brand-coral) 45%, transparent); }
  70%,100%{ box-shadow:0 1px 3px rgba(254,101,93,.35), 0 0 0 7px transparent; }
}
@media (prefers-reduced-motion: reduce){
  .pb-bar-fill, .pb-bar-thumb{ transition:none; }
  .pb-bar[data-playing="true"] .pb-bar-thumb{ animation:none; }
}
/* Coarse pointers get a bigger thumb and hit area; the track stays slim. */
@media (pointer:coarse){
  .pb-bar{ --pb-thumb:14px; }
  .pb-bar-rail{ height:32px; }
}

/* Chromatic CTAs paint a WHITE label on coral (#ff665e) — a documented 2.90:1
   brand exception, kept for light. On the dark theme the same coral accepts dark
   ink at 6.2:1, so the label flips and the button finally clears AA: the brand
   keeps its colour, only the ink changes. The selector list mirrors every rule
   that paints coral + white (audited 2026-11-07), because a half-flipped set
   would leave some buttons unreadable on dark.
   NOT flipped: .site-cta / .mob-cta / .ovw-btn in the marketing header, which
   are transparent-background links inside the coloured .open-studio-btn pill. */
:root[data-theme="dark"] :is(
  .open-studio-btn,.hero-cta,.generate-voice-btn,.clone-btn,.try-before-buy-cta,.d-primary,
  .ov-btn--primary,.generate-btn,.mv-btn.mv-use,.vc-btn-primary,.tr-btn-primary,.vd-btn-primary,
  .modal-btn:not(.ghost),.first-audio-go,.hp-btn-primary,.tip-cta,.hview-cta,.vf-use,.vb-fav-star,
  .rec-btn,.clone-save-btn,.vc-play-btn,.vd-design-cta,.vd-chip.is-on,.tr-step-num,.tr-chip.on,
  .tr-voice-opt.on .tr-voice-ico,.tr-lang-tab.on,.tr-dl-video,.tr-stepx.on .tr-stepx-n,
  .tr-sync-opt.on,.tr-next-btn-primary,.srt-step-n,.srt-switch button.on,.tr-pv-switch-btn.on,
  .ov-soft-cta,.dl-notice-btn:first-child,.share-link button,.tr-pick-cta,.tr-pick-cta-label
){ color:var(--on-accent) !important; }
:root[data-theme="dark"] :is(
  .open-studio-btn,.hero-cta,.generate-voice-btn,.clone-btn,.try-before-buy-cta,.d-primary,
  .ov-btn--primary,.generate-btn,.mv-btn.mv-use,.vc-btn-primary,.tr-btn-primary,.vd-btn-primary,
  .modal-btn:not(.ghost),.first-audio-go,.hp-btn-primary,.tip-cta,.hview-cta,.vf-use,.vb-fav-star,
  .rec-btn,.clone-save-btn,.vc-play-btn,.vd-design-cta,.vd-chip.is-on,.tr-step-num,.tr-chip.on,
  .tr-voice-opt.on .tr-voice-ico,.tr-lang-tab.on,.tr-dl-video,.tr-stepx.on .tr-stepx-n,
  .tr-sync-opt.on,.tr-next-btn-primary,.srt-step-n,.srt-switch button.on,.tr-pv-switch-btn.on,
  .ov-soft-cta,.dl-notice-btn:first-child,.share-link button,.tr-pick-cta,.tr-pick-cta-label
) svg{ color:var(--on-accent) !important; }

/* ---------- Print (spec 007, FR-016) --------------------------------------
   Paper is light. Whatever the screen is set to, printing resets the whole
   token layer rather than chasing individual rules. */
@media print{
  :root, :root[data-theme="dark"]{
    color-scheme:light;
    --surface:#ffffff; --canvas:#ffffff; --surface-hover:#ffffff; --bg:#ffffff;
    --text:#111827; --text-2:#4b5563; --text-3:#5b6472;
    --border:#e5e7eb; --border-2:#e5e7eb; --border-strong:#d1d5db;
    --accent:#c92a2a; --accent-text:#c92a2a; --on-accent:#ffffff;
    --hover:#ffffff; --callout:#ffffff; --black:#000000;
    --glass-bg:#ffffff; --glass-bg-strong:#ffffff;
    --e-1:none; --e-2:none; --e-3:none;
  }
  body{ background:var(--surface) !important; }
}
