/* ═══════════════════════════════════════════════════════════════
   Persona Shared Components
   Reusable component styles used across all sections.
   Depends on: persona-tokens.css
   ═══════════════════════════════════════════════════════════════ */

body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  overflow-x: hidden;
}

a       { color: inherit; text-decoration: none; }
button  { font-family: inherit; cursor: pointer; border: none; background: transparent; }
img     { display: block; max-width: 100%; }
input, textarea, select { font-family: inherit; }


/* ─── Shared utilities ───────────────────────────────────────────────────────
   Classes used across multiple page groups.
   ─────────────────────────────────────────────────────────────────────────── */

/* Scroll reveal ------------------------------------------------------------ */
.reveal, .rv {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity .5s ease, transform .5s ease;
}
.reveal.visible, .reveal.vis,
.rv.visible,     .rv.vis {
  opacity: 1;
  transform: none;
}
.d1 { transition-delay: .08s; }
.d2 { transition-delay: .16s; }
.d3 { transition-delay: .24s; }
.d4 { transition-delay: .32s; }

/* Gradient text ------------------------------------------------------------ */
.grad {
  background: linear-gradient(90deg, var(--purple), var(--orange-soft));
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}

/* FAQ accordion ------------------------------------------------------------ */
.faq-item { border-bottom: 1px solid var(--border); }
.faq-q {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px 0;
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  cursor: pointer;
  gap: 16px;
  user-select: none;
  width: 100%;
  text-align: start;
  background: transparent;
  border: none;
}
.faq-q svg { width: 16px; height: 16px; flex-shrink: 0; color: var(--text-faint); transition: transform .2s; }
.faq-item.open .faq-q svg { transform: rotate(180deg); }
.faq-a {
  display: none;
  padding: 0 0 18px;
  font-size: 14.5px;
  color: var(--text-muted);
  line-height: 1.75;
}
.faq-item.open .faq-a { display: block; }

/* Language dropdown -------------------------------------------------------- */
/* .web-lang-dd was trimmed out (was: "used by both website and support KB") —
   the marketing header no longer has a <select>, it's an icon button opening a
   full-screen picker (see persona-shared.css). .lang-dd stays: the support KB
   and every other surface (auth, app, admin) still use a native <select>. */
.lang-dd {
  padding: 5px 26px 5px 10px;
  border-radius:var(--radius-sm);
  border: 1px solid var(--border-strong);
  background: var(--bg-card);
  color: var(--text);
  font-size: 12.5px;
  font-weight: 600;
  font-family: var(--font-body);
  cursor: pointer;
  appearance: none;
  -webkit-appearance: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%239088a8' stroke-width='1.5' stroke-linecap='round' fill='none'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 8px center;
}
.lang-dd:focus {
  border-color: rgba(var(--purple-rgb),.4);
}

/* Status dot pulse --------------------------------------------------------- */
@keyframes ps-pulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: .35; }
}

/* Reduced motion -----------------------------------------------------------
   WCAG 2.2.2 (Pause, Stop, Hide): anything that auto-starts, runs >5s and
   plays alongside other content needs a way to turn it off. A user's OS-level
   reduce-motion preference IS that mechanism (technique C39) — loading
   spinners tied to a real in-progress operation are exempt (they stop when
   the operation does, they're not decorative), but a status dot that pulses
   for as long as a video sits in "Generating…" is exactly the case this
   applies to. */
@media (prefers-reduced-motion: reduce) {
  .reveal, .rv {
    opacity: 1;
    transform: none;
    transition: none;
  }
  .status-chip.generating::before { animation: none !important; }
}


/* ═══════════════════════════════════════════════════════════════════════════
   DIALOG SYSTEM  ·  "cd-*" classes
   Single source of truth for every confirmation/alert dialog in the app.
   Used by: persona-app-dialog.html (full preset catalogue + openDialog() JS),
            and any page that calls openDialog() — e.g. persona-create-flow.html,
            persona-videos.html, persona-characters.html, persona-character.html,
            persona-profile-character.html, persona-video-detail.html.
   Change appearance here once — every dialog across the app updates.
   ═══════════════════════════════════════════════════════════════════════════ */

/* Backdrop */
.cd-overlay{position:fixed;inset:0;z-index:400;background:rgba(var(--black-rgb),.48);backdrop-filter:blur(4px);display:none;align-items:center;justify-content:center;padding:20px;}
.cd-overlay.open{display:flex;}

/* Base dialog card */
.cd-dialog{background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-xl);padding:32px 32px 26px;max-width:480px;width:100%;box-shadow:var(--shadow-dialog);animation:cdPop .2s cubic-bezier(.34,1.4,.64,1);position:relative;}
@keyframes cdPop{from{opacity:0;transform:scale(.92) translateY(8px);}to{opacity:1;transform:none;}}

/* Icon circle */
.cd-icon{width:50px;height:50px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin:0 auto 18px;}
.cd-icon svg{width:22px;height:22px;}
.cd-icon.destructive{background:var(--red-dim);color:var(--red);}
.cd-icon.warning{background:var(--orange-dim);color:var(--orange);}
/* --purple-dim, not --purple-bright: every other variant here pairs a DIM
   background with its SATURATED color (--red-dim+--red, --orange-dim+
   --orange, etc.) — this one broke that pattern with two similarly-
   saturated purples. In dark theme --purple and --purple-bright are the
   exact same value (persona-tokens.css), so the icon was fully invisible
   there (0:1), not just low-contrast. */
.cd-icon.neutral{background:var(--purple-dim);color:var(--purple);}
.cd-icon.success{background:var(--teal-dim);color:var(--teal);}
.cd-icon.info{background:var(--yellow-dim);color:var(--yellow);}

/* Text */
.cd-center{text-align:center;}
.cd-title{font-size:16px;font-weight:700;margin-bottom:8px;color:var(--text);}
.cd-body{font-size:13.5px;color:var(--text-muted);line-height:1.6;margin-bottom:24px;}

/* Standard action row */
.cd-actions{display:flex;gap:10px;}
/* Layout hooks only now — colour/border/fill are the shared btn/btn--ghost/btn--primary/
   btn--orange/btn--danger/btn--teal/btn--yellow/btn--text (persona-styles.css, above). */
.cd-cancel,.cd-confirm{flex:1;}
/* Cancel's hover matches its sibling confirm button's colour instead of the shared ghost
   button's default brand-purple hover — a plain purple hover next to a red/orange confirm
   read as if the two buttons belonged to different decisions. Rest state is untouched
   (still the ordinary muted .btn--ghost look); only :hover picks up the pairing. Neutral
   dialogs need no override — brand purple already IS their confirm colour. */
.cd-cancel.destructive:hover, .cd-cancel.destructive.is-hover{ border-color:var(--red); color:var(--red); }
.cd-cancel.warning:hover, .cd-cancel.warning.is-hover{ border-color:var(--orange); color:var(--orange); }
.cd-cancel.success:hover, .cd-cancel.success.is-hover{ border-color:var(--teal); color:var(--teal); }
.cd-cancel.info:hover, .cd-cancel.info.is-hover{ border-color:var(--yellow); color:var(--yellow); }

/* Single primary button (no cancel) */
.cd-solo{display:flex;flex-direction:column;gap:10px;}

/* ── Form elements inside dialogs ── */
.cd-form{text-align:left;margin-bottom:20px;}
.cd-label{font-size:12px;font-weight:600;color:var(--text-muted);margin-bottom:6px;display:block;}
.cd-input{width:100%;padding:10px 14px;border:1px solid var(--border);border-radius:var(--radius-sm);font-size:14px;font-family:inherit;color:var(--text);background:var(--panel-strong);transition:border-color .15s;}
.cd-input:focus{border-color:var(--purple);background:var(--bg-card);}
.cd-textarea{resize:vertical;min-height:80px;}
.cd-hint{font-size:11.5px;color:var(--text-faint);margin-top:5px;}

/* ── Selection chips inside dialogs ── */
.cd-chip-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:20px;text-align:left;}
.cd-chip{padding:10px 12px;border:1px solid var(--border);border-radius:var(--radius-sm);cursor:pointer;transition:border-color .15s,background .15s;background:var(--panel-strong);}
.cd-chip:hover{border-color:var(--purple-bright);}
.cd-chip.selected{border-color:var(--purple);background:var(--purple-solid-fill);color:#fff;}
.cd-chip-label{font-size:13px;font-weight:600;color:var(--text);display:block;margin-bottom:2px;}
.cd-chip-icon{font-size:18px;margin-bottom:4px;display:block;}

/* ── Progress bar ── */
.cd-progress-wrap{background:var(--panel-strong);border-radius:999px;height:6px;overflow:hidden;margin:4px 0 8px;}
.cd-progress-bar{height:100%;border-radius:999px;background:var(--grad-purple-90);transition:width .4s ease;}
.cd-progress-label{display:flex;justify-content:space-between;font-size:12px;color:var(--text-faint);margin-bottom:6px;}

/* ── Plan comparison inside upsell dialog ── */
.cd-plan-row{display:flex;gap:8px;margin-bottom:20px;}
.cd-plan{flex:1;border:1.5px solid var(--border);border-radius:var(--radius-sm);padding:12px;text-align:center;}
.cd-plan.current{opacity:.5;}
.cd-plan.upgrade{border-color:var(--purple);background:var(--purple-bright);}
.cd-plan-name{font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--text-faint);margin-bottom:4px;}
.cd-plan.upgrade .cd-plan-name{color:var(--purple);}
.cd-plan-price{font-size:18px;font-weight:800;color:var(--text);}
.cd-plan-price span{font-size:12px;font-weight:500;color:var(--text-faint);}
.cd-plan-features{font-size:11.5px;color:var(--text-muted);margin-top:6px;line-height:1.5;text-align:left;}

/* ── Success / auto-dismiss ── */
.cd-auto-note{font-size:11.5px;color:var(--text-faint);text-align:center;margin-top:12px;}

/* ── Auth re-entry ── */
.cd-password-wrap{position:relative;margin-bottom:20px;}
.cd-password-wrap .cd-input{padding-right:44px;}
.cd-eye{position:absolute;right:12px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;color:var(--text-faint);padding:4px;}

/* ── Session expired — full-width single btn ── */

/* ── Close button (informational, form, selection, paywall dialogs) ── */
/* Layout hook only — the round icon button itself is the shared btn--icon.round.sm. */
.cd-close{position:absolute;top:12px;inset-inline-end:12px;}


/* ═══════════════════════════════════════════════════════════════════════════
   SHARED UI SYSTEMS — toast, empty state, skeleton, banner
   Single source of truth for these components across every app page.
   JS (showToast, renderEmptyState, showSkeletons, showBanner, etc.) lives
   in persona-app-shared.js. Change appearance here once — every page
   using these components updates.
   ═══════════════════════════════════════════════════════════════════════════ */

/* === SHARED UI SYSTEMS — auto-injected === */
/* ════════════════════════════════════════════════════
   TOAST / NOTIFICATION SYSTEM
   Usage: showToast(variant, message, opts?)
   Variants: 'success' | 'error' | 'warning' | 'info'
   Opts: { action, onAction, duration, dismissible }
════════════════════════════════════════════════════ */
.toast-stack{
  position:fixed;bottom:28px;left:50%;transform:translateX(-50%);
  display:flex;flex-direction:column-reverse;gap:8px;
  z-index:9000;pointer-events:none;
  align-items:center;min-width:280px;max-width:420px;width:max-content;
}
.toast{
  display:flex;align-items:center;gap:10px;
  padding:11px 16px;border-radius:999px;
  font-size:13px;font-weight:600;line-height:1.3;
  box-shadow:0 4px 16px -4px rgba(var(--black-rgb),.18),0 1px 4px rgba(var(--black-rgb),.08);
  pointer-events:auto;cursor:default;
  animation:toastIn .25s cubic-bezier(.34,1.4,.64,1);
  transition:opacity .25s,transform .25s;
  border:1px solid rgba(var(--white-rgb),.1);
  max-width:100%;
}
.toast.hiding{opacity:0;transform:translateY(8px);}
@keyframes toastIn{from{opacity:0;transform:translateY(14px);}to{opacity:1;transform:none;}}
.toast svg{width:15px;height:15px;flex-shrink:0;}
.toast-msg{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:280px;}
.toast-action{
  font-size:12px;font-weight:700;white-space:nowrap;
  background:rgba(var(--white-rgb),.2);border:none;color:inherit;
  padding:3px 9px;border-radius:999px;cursor:pointer;
  margin-inline-start:4px;transition:background .15s;
}
.toast-action:hover{background:rgba(var(--white-rgb),.3);}
.toast-close{
  width:18px;height:18px;border-radius:50%;
  background:rgba(var(--white-rgb),.15);border:none;
  color:inherit;cursor:pointer;flex-shrink:0;
  display:flex;align-items:center;justify-content:center;
  padding:0;transition:background .15s;
}
.toast-close:hover{background:rgba(var(--white-rgb),.25);}
.toast-close svg{width:10px;height:10px;}
/* Variants */
.toast.success{background:var(--toast-success);color:#fff;}
.toast.error{background:var(--toast-error);color:#fff;}
.toast.destructive{background:var(--toast-error);color:#fff;}
.toast.warning{background:var(--toast-warning);color:#fff;}   /* white, like every other toast — see --warning-solid */
.toast.info{background:var(--toast-info);color:#fff;}

/* ════════════════════════════════════════════════════
   EMPTY STATE SYSTEM
   Usage: renderEmptyState('preset-name', containerEl)
   Or use HTML: <div class="empty-state" data-empty="preset-name">
════════════════════════════════════════════════════ */
.empty-state{
  display:flex;flex-direction:column;align-items:center;
  text-align:center;gap:10px;
  padding:52px 24px;width:100%;
}
.empty-icon{
  width:52px;height:52px;border-radius:16px;
  display:flex;align-items:center;justify-content:center;
  margin-bottom:6px;flex-shrink:0;
}
.empty-icon svg{width:24px;height:24px;flex-shrink:0;display:block;}
.empty-icon.purple{background:rgba(var(--purple-rgb),.08);color:var(--purple);}
.empty-icon.teal{background:rgba(var(--teal-rgb),.08);color:var(--teal);}
.empty-icon.orange{background:rgba(var(--orange-rgb),.08);color:var(--orange);}
.empty-icon.red{background:rgba(var(--red-rgb),.08);color:var(--red);}
.empty-icon.faint{background:var(--panel-strong);border:1px solid var(--border-soft);color:var(--text-faint);}
.empty-title{font-size:14px;font-weight:700;color:var(--text);margin:0;}
.empty-body{font-size:13px;color:var(--text-muted);line-height:1.55;max-width:300px;margin:0;}
.empty-cta{
  display:inline-flex;align-items:center;gap:7px;
  padding:10px 20px;border-radius:999px;
  font-size:13px;font-weight:700;font-family:inherit;
  border:none;cursor:pointer;transition:opacity .15s;
  text-decoration:none;margin-top:4px;
}
.empty-cta:hover{opacity:.88;}
.empty-cta.purple{background:var(--purple);color:var(--cta-text);}
.empty-cta.teal{background:var(--teal);color:var(--cta-text);}
.empty-cta.ghost{background:var(--panel-strong);border:1px solid var(--border);color:var(--text-muted);}
.empty-cta svg{width:13px;height:13px;}
.empty-note{font-size:12px;color:var(--text-faint);margin-top:2px;}
/* Compact variant for smaller containers */
.empty-state.compact{padding:32px 20px;gap:7px;}
.empty-state.compact .empty-icon{width:40px;height:40px;border-radius:12px;}
.empty-state.compact .empty-icon svg{width:18px;height:18px;}
.empty-state.compact .empty-title{font-size:13px;}
.empty-state.compact .empty-body{font-size:12px;}

/* ════════════════════════════════════════════════════
   SKELETON SCREEN SYSTEM
   Usage: showSkeletons('video-card', containerEl, 4)
          hideSkeletons(containerEl)
   Or: add class .sk to any element for shimmer
════════════════════════════════════════════════════ */

/* Base shimmer — apply to any element */
@keyframes sk-shimmer{
  0%{background-position:-600px 0;}
  100%{background-position:600px 0;}
}
.sk{
  background:linear-gradient(
    90deg,
    var(--panel-strong) 25%,
    rgba(var(--white-rgb),.65) 50%,
    var(--panel-strong) 75%
  );
  background-size:1200px 100%;
  animation:sk-shimmer 1.5s infinite linear;
  border-radius:6px;
  color:transparent;
  user-select:none;
}
/* Skeleton card wrapper */
.sk-card{
  background:var(--panel);
  border:1px solid var(--border-soft);
  border-radius:var(--radius-lg);
  overflow:hidden;
  flex-shrink:0;
}
/* Thumbnail placeholder */
.sk-thumb{
  width:100%;
  aspect-ratio:9/16;
  position:relative;
}
.sk-thumb.landscape{aspect-ratio:16/9;}
.sk-thumb.square{aspect-ratio:1/1;}
.sk-thumb.wide{aspect-ratio:4/3;}
.sk-thumb > .sk{width:100%;height:100%;border-radius:0;}
/* Info area below thumb */
.sk-body{padding:12px 14px;display:flex;flex-direction:column;gap:8px;}
/* Generic line placeholders */
.sk-line{height:11px;}
.sk-line.h-14{height:14px;}
.sk-line.h-28{height:28px;}
.sk-line.w-80{width:80%;}
.sk-line.w-60{width:60%;}
.sk-line.w-40{width:40%;}
.sk-line.w-30{width:30%;}
/* Avatar / circle */
.sk-avatar{border-radius:50%;flex-shrink:0;}
.sk-avatar.sm{width:32px;height:32px;}
.sk-avatar.md{width:40px;height:40px;}
.sk-avatar.lg{width:52px;height:52px;}
/* Row layout (list view) */
.sk-row{
  display:flex;align-items:center;gap:12px;
  padding:13px 16px;
  border-bottom:1px solid var(--border-soft);
}
.sk-row:last-child{border-bottom:none;}
.sk-row-thumb{
  width:48px;height:60px;border-radius:var(--radius-sm);flex-shrink:0;
}
.sk-col{flex:1;display:flex;flex-direction:column;gap:7px;}
/* Page header */
.sk-page-header{
  padding:28px 32px 0;
  display:flex;flex-direction:column;gap:10px;
  margin-bottom:24px;
}
/* Pill */
.sk-pill{height:24px;border-radius:999px;}
/* Detail page layout */
.sk-detail-preview{
  border-radius:var(--radius-md);
  aspect-ratio:9/16;
  max-height:520px;
  width:100%;
}
.sk-detail-preview.landscape{aspect-ratio:16/9;max-height:none;}
.sk-detail-preview.square{aspect-ratio:1/1;}

/* ════════════════════════════════════════════════════
   BANNER / ALERT BAR SYSTEM
   Usage: showBanner(preset, opts?)   → page-level fixed banner
          dismissBanner(id)           → dismiss by id
   Or inline HTML: <div class="banner success"> ... </div>
   Variants: success | warning | error | info | neutral
════════════════════════════════════════════════════ */

/* Base banner */
.banner{
  display:flex;align-items:center;gap:12px;
  padding:13px 18px;
  border-radius:var(--radius-md);
  font-size:13.5px;font-weight:500;
  border:1px solid transparent;
  width:100%;
}
.banner svg{width:16px;height:16px;flex-shrink:0;}
.banner-body{flex:1;line-height:1.5;}
.banner-body strong{font-weight:700;}
.banner-action{
  font-size:13px;font-weight:700;white-space:nowrap;
  cursor:pointer;background:none;border:none;
  color:inherit;opacity:.85;padding:0;font-family:inherit;
  text-decoration:none;
}
.banner-action:hover{opacity:1;}
.banner-close{
  width:26px;height:26px;border-radius:50%;
  border:none;background:rgba(var(--black-rgb),.06);
  cursor:pointer;display:flex;align-items:center;justify-content:center;
  flex-shrink:0;color:inherit;opacity:.6;transition:opacity .15s,background .15s;
  padding:0;
}
.banner-close:hover{opacity:1;background:rgba(var(--black-rgb),.12);}
.banner-close svg{width:11px;height:11px;}

/* Color variants */
/* Variant semantics live in the tint + icon; body text stays --text so it clears
   AA in BOTH themes (coloured text on a coloured tint fails at small sizes in one
   theme or the other — see the component-library audit). */
.banner.success{
  background:rgba(var(--teal-rgb),.08);
  border-color:rgba(var(--teal-rgb),.2);
  color:var(--text);
}
.banner.warning{
  background:rgba(var(--yellow-rgb),.08);
  border-color:rgba(var(--yellow-rgb),.2);
  color:var(--text);
}
.banner.error{
  background:rgba(var(--red-rgb),.08);
  border-color:rgba(var(--red-rgb),.2);
  color:var(--text);
}
.banner.info{
  background:rgba(var(--purple-rgb),.07);
  border-color:rgba(var(--purple-rgb),.18);
  color:var(--text);
}
.banner.neutral{
  background:var(--panel-strong);
  border-color:var(--border);
  color:var(--text);
}

/* Icon carries the variant colour, in both themes (adaptive brand tokens) */
.banner.success svg{color:var(--teal);}
.banner.warning svg{color:var(--yellow);}
.banner.error svg{color:var(--red);}
.banner.info svg{color:var(--purple);}

/* Page-level fixed banner (stacks below topbar) */
.banner-page{
  position:fixed;top:0;left:0;right:0;
  z-index:100;
  border-radius:0;
  border-left:none;border-right:none;border-top:none;
  padding:12px 24px;
  animation:bannerSlideDown .2s ease;
}
@keyframes bannerSlideDown{from{transform:translateY(-100%);opacity:0;}to{transform:none;opacity:1;}}
.banner-page.hiding{animation:bannerSlideUp .2s ease forwards;}
@keyframes bannerSlideUp{to{transform:translateY(-100%);opacity:0;}}

/* ─────────────────────────────────────────────
   Shared video card / masonry grid
   Used by: persona-videos.html, persona-dashboard.html
   ───────────────────────────────────────────── */

/* Grid container — absolute-positioned masonry via JS */
.video-grid{ position:relative; width:100%; }

/* Card */
.video-card{ position:absolute; border-radius:var(--radius-md); background:var(--panel); border:1px solid var(--border-strong); overflow:visible; transition:box-shadow .2s, border-color .2s; box-shadow:var(--card-shadow); }
.video-card:hover{ box-shadow:0 8px 28px -8px rgba(var(--black-rgb),0.18), 0 0 0 1px var(--purple-bright); border-color:var(--purple-bright); }

/* Thumbnail — overflow:hidden here so card border-radius still clips thumb, menu can escape */
.video-thumb{ position:relative; overflow:hidden; border-radius:var(--radius-md) var(--radius-md) 0 0; background:linear-gradient(165deg, var(--g1), var(--g2)); }
[data-format="9:16"] .video-thumb{ aspect-ratio:9/16; }
[data-format="1:1"]  .video-thumb{ aspect-ratio:1/1;  }
[data-format="16:9"] .video-thumb{ aspect-ratio:16/9; }
[data-format="4:5"]  .video-thumb{ aspect-ratio:4/5;  }

/* Status chip (overlaid on thumbnail) */
.status-chip{ position:absolute; top:10px; inset-inline-start:10px; font-size:10.5px; font-weight:700; padding:4px 9px; border-radius:999px; display:flex; align-items:center; gap:5px; backdrop-filter:blur(6px); border:1px solid rgba(var(--white-rgb),0.14); }
.status-chip.generating{ background:rgba(var(--purple-rgb),0.22); color:#d9ccff; }
.status-chip.generating::before{ content:''; width:6px; height:6px; border-radius:50%; background:var(--purple-bright); animation:pulse 1.4s infinite; }
.status-chip.ready    { background:rgba(var(--teal-rgb),0.2);   color:#7ff3da; }
.status-chip.draft    { background:rgba(var(--white-rgb),0.16); color:#fff; }
.status-chip.failed   { background:rgba(var(--red-rgb),0.28);   color:#ffb3ab; }

/* Format badge (top-right of thumbnail) */
.format-tag{ position:absolute; top:10px; inset-inline-end:10px; font-size:9.5px; font-weight:700; padding:3px 7px; border-radius:7px; background:rgba(var(--bg-rgb),0.55); border:1px solid rgba(var(--white-rgb),0.14); color:#fff; font-family:var(--font-mono); }

/* Card info / actions */
.video-info h3{ font-size:12.5px; font-weight:600; margin-bottom:3px; line-height:1.3; }
.video-info .meta{ font-size:10.5px; color:var(--text-faint); font-family:var(--font-mono); }

/* Video action buttons (Watch / Download / Share / Delete row) */

/* ── Thumbnail play overlay (ready cards) ── */
.video-thumb-play{
  position:absolute; inset:0;
  display:flex; align-items:center; justify-content:center;
  background:rgba(var(--black-rgb),0); transition:background .2s;
  cursor:pointer;
}
.video-thumb-play svg{
  width:44px; height:44px;
  background:rgba(var(--black-rgb),0.55); color:#fff;
  border-radius:50%; padding:10px;
  opacity:0.35; transform:scale(.88);
  transition:opacity .2s, transform .2s;
  flex-shrink:0;
}
/* On devices with hover (desktop): hide until hover */
@media (hover: hover){
  .video-thumb-play svg{ opacity:0; }
  .video-card:hover .video-thumb-play{ background:rgba(var(--black-rgb),0.12); }
  .video-card:hover .video-thumb-play svg{ opacity:1; transform:scale(1); }
}
/* On touch devices: always visible at resting opacity, full on active */
@media (hover: none){
  .video-thumb-play svg{ opacity:0.55; transform:scale(1); }
  .video-thumb-play:active svg{ opacity:1; }
}

/* ── Video player overlay (shared: videos + dashboard) ── */
/* ── Video quick-view player modal ── */
.player-overlay{
  position:fixed; inset:0; z-index:200;
  background:rgba(var(--black-rgb),0.72);
  display:none; align-items:center; justify-content:center;
  padding:24px 16px;
  backdrop-filter:blur(6px); -webkit-backdrop-filter:blur(6px);
}
.player-overlay.open{ display:flex; }

/* Entry animation */
.player-shell{
  display:flex; flex-direction:column; align-items:center; gap:0;
  max-height:calc(100dvh - 48px);
  opacity:0; transform:scale(.92) translateY(12px);
  transition:opacity .22s ease, transform .22s ease;
}
.player-overlay.open .player-shell{
  opacity:1; transform:scale(1) translateY(0);
}

/* Video frame — respects aspect ratio */
.player-frame{
  position:relative; border-radius:16px 16px 0 0;
  overflow:hidden; background:#0d0d14;
  /* 9:16 default — tallest allowed is 72vh */
  width:min(260px, 42vh); aspect-ratio:9/16;
  flex-shrink:0;
}
.player-frame.ratio-11{
  aspect-ratio:1/1;
  width:min(380px, 72vw);
}
.player-frame.ratio-169{
  aspect-ratio:16/9;
  width:min(560px, 88vw);
}

.player-bg{
  position:absolute; inset:0;
  background-size:cover; background-position:center top;
  filter:brightness(.8);
}

/* Gradient overlay so controls always readable */
.player-frame::after{
  content:''; position:absolute; inset:0;
  background:linear-gradient(to bottom, transparent 50%, rgba(var(--black-rgb),0.5) 100%);
  pointer-events:none;
}

/* Play/pause button */
.player-play{
  position:absolute; inset:0; display:flex;
  align-items:center; justify-content:center;
  cursor:pointer; z-index:2;
}
.player-play svg{
  width:56px; height:56px; color:#fff;
  filter:drop-shadow(0 2px 16px rgba(var(--black-rgb),0.5));
  transition:transform .15s, opacity .15s;
}
.player-play:hover svg{ transform:scale(1.08); opacity:.9; }

/* Progress bar pinned to bottom of frame */
.player-progress{
  position:absolute; bottom:0; inset-inline:0;
  height:4px; background:rgba(var(--white-rgb),0.22); z-index:3;
}
.player-progress-fill{
  height:100%; background:var(--bg-card); width:0%;
  transition:none;
}
.player-progress-fill.animating{
  transition:width linear;
}

/* Info panel below frame — seamless dark card feel */
.player-info{
  width:100%; background:rgba(18,14,38,0.9);
  border-radius:0 0 16px 16px;
  padding:12px 16px 16px;
  display:flex; align-items:center; gap:12px;
}
.player-info-text{ flex:1; min-width:0; }
.player-title{
  font-size:13.5px; font-weight:700; color:#fff;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.player-meta{
  font-size:11.5px; color:rgba(var(--white-rgb),0.5);
  margin-top:2px; font-family:var(--font-mono);
}
.player-time{
  font-size:11px; font-weight:600; color:rgba(var(--white-rgb),0.55);
  font-family:var(--font-mono); flex-shrink:0; min-width:36px; text-align:end;
}

/* Close button — inside top-right corner of frame */
.player-close{
  position:absolute; top:10px; inset-inline-end:10px;
  width:32px; height:32px; border-radius:50%;
  background:rgba(var(--black-rgb),0.45); backdrop-filter:blur(4px);
  border:1px solid rgba(var(--white-rgb),0.18); color:#fff;
  display:flex; align-items:center; justify-content:center;
  cursor:pointer; z-index:10; transition:background .15s;
}
.player-close:hover{ background:rgba(var(--black-rgb),0.65); }
.player-close svg{ width:13px; height:13px; }

/* ── Inline card video player ── */
.card-player{
  position:absolute; inset:0;
  background:rgba(var(--black-rgb),0.45);
  display:flex; flex-direction:column; align-items:center;
  justify-content:center;
  padding-bottom:20px;
  gap:0; z-index:10;
  opacity:0; pointer-events:none;
  transition:opacity .2s;
}
.card-player.active{ opacity:1; pointer-events:auto; }

.card-player-btn{
  width:52px; height:52px; border-radius:50%;
  background:rgba(var(--black-rgb),0.55); backdrop-filter:blur(8px);
  border:2px solid rgba(var(--white-rgb),0.5); color:#fff;
  display:flex; align-items:center; justify-content:center;
  cursor:pointer; transition:background .15s, transform .15s; flex-shrink:0;
}
.card-player-btn:hover{ background:rgba(var(--black-rgb),0.72); transform:scale(1.06); }
.card-player-btn svg{ width:20px; height:20px; }

/* Progress bar pinned to bottom of thumbnail */
.card-player-bar{
  position:absolute; bottom:0; inset-inline:0;
  height:4px; background:rgba(var(--white-rgb),0.25);
  overflow:hidden;
}
.card-player-fill{
  height:100%; width:0%; background:var(--bg-card); transition:none;
}
.card-player-fill.playing{ transition:width 8s linear; width:100%; }

/* Play overlay hidden while player active — must override hover state */
.video-card.playing .video-thumb-play{ opacity:0 !important; pointer-events:none !important; }
.video-card.playing:hover .video-thumb-play svg{ opacity:0 !important; }

/* ── Shared meatball menu (dashboard, videos, characters) ── */
.meatball-wrap{ position:relative; flex-shrink:0; }
.meatball-btn{ width:44px; height:44px; border-radius:50%; display:flex; align-items:center; justify-content:center; background:transparent; border:none; color:var(--text-faint); font-size:16px; font-weight:700; letter-spacing:1px; line-height:1; cursor:pointer; transition:background .15s, color .15s; margin:-8px; }
.meatball-btn.compact{ width:32px; height:32px; font-size:17px; margin:0; }
.meatball-btn:hover{ background:var(--panel-strong); color:var(--text); }
.meatball-menu{ position:absolute; inset-inline-end:0; top:calc(100% + 4px); min-width:160px; background:var(--bg); border:1px solid var(--border); border-radius:var(--radius-md); box-shadow:0 8px 24px -8px rgba(var(--black-rgb),0.2); z-index:40; display:none; overflow:hidden; }
.meatball-menu.open{ display:block; }
.meatball-item{ display:flex; align-items:center; gap:9px; padding:9px 13px; font-size:13px; font-weight:500; color:var(--text); cursor:pointer; text-decoration:none; width:100%; background:none; border:none; font-family:var(--font-body); text-align:start; }
.meatball-item:hover{ background:var(--panel-strong); }
.meatball-item.danger{ color:var(--red); }
.meatball-item svg{ width:14px; height:14px; color:var(--text-faint); flex-shrink:0; }
.meatball-item.danger svg{ color:var(--red); }
.meatball-divider{ height:1px; background:var(--border-soft); margin:4px 0; }

/* ── Language switcher dropdown (registry-driven via renderLangSwitchers) ── */
.lang-dd{ padding:6px 28px 6px 12px; border-radius:8px; border:1px solid var(--border-strong); background:var(--panel-strong); color:var(--text); font-size:12.5px; font-weight:600; font-family:inherit; cursor:pointer; appearance:none; -webkit-appearance:none; background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%239088a8' stroke-width='1.5' stroke-linecap='round' fill='none'/%3E%3C/svg%3E"); background-repeat:no-repeat; background-position:right 10px center; }
html[dir="rtl"] .lang-dd{ padding:6px 12px 6px 28px; background-position:left 10px center; }
.lang-dd:focus{ border-color:var(--purple); }

/* ══ Button system — SINGLE SOURCE. Compose: class="btn btn--primary btn--sm" ══
   • light/dark: colors come from tokens, so both themes are automatic
   • translations: padding-based width + white-space:nowrap → grows with longer
     text (e.g. Hebrew) instead of clipping; line-height:1 + min-height keeps it
     centred at any label length
   • RTL: symmetric padding + gap + inline-flex mirror automatically; icons use em
   • responsive: no fixed widths; .btn--block goes full-width for mobile CTAs
   • a11y: :focus-visible ring, disabled state, ≥40px tap target                */
.btn{
  display:inline-flex; align-items:center; justify-content:center; gap:8px;
  font-family:inherit; font-size:13.5px; font-weight:700; line-height:1;
  padding:11px 20px; min-height:40px;
  border-radius:999px; border:1px solid transparent;
  cursor:pointer; white-space:nowrap; text-decoration:none; text-align:center;
  transition:transform .15s, background .15s, color .15s, border-color .15s, opacity .15s;
  -webkit-tap-highlight-color:transparent;
}
.btn:focus-visible, .btn.is-focus{ outline:2px solid var(--purple); outline-offset:2px; }
.btn:disabled, .btn[aria-disabled="true"]{ opacity:.5; cursor:default; pointer-events:none; }

/* PRESSED. This did not exist: nothing in the product visually acknowledged a press, which on
   touch reads as lag — the user taps, nothing happens, they tap again. The press must beat the
   hover lift (translateY(-1px)), so it is declared after it. */
.btn:active, .btn.is-pressed{ transform:translateY(1px); box-shadow:none; }
/* GHOST stays fill-less in every state — pressed feedback is the translateY above alone,
   not a background (see .btn--ghost below for why hover doesn't fill either). */
.btn--text:active, .btn--text.is-pressed{ background:var(--border); }

/* LOADING. A button that has been clicked and is waiting. Without it, pages invent their own —
   or worse, leave the button live and let the user double-submit.
   The label STAYS VISIBLE and the spinner sits beside it. The first version hid the label
   (color:transparent) and the portal's contrast audit failed it on the spot: invisible text is
   unreadable text, whatever the intent. It also made the button's width jump. Keeping the label
   fixes both. aria-busy is the source of truth, so screen readers hear it too. */
.btn[aria-busy="true"], .btn.is-loading{ pointer-events:none; opacity:.85; }
.btn[aria-busy="true"]::after, .btn.is-loading::after{
  content:""; width:13px; height:13px; flex-shrink:0;
  border:2px solid currentColor; border-top-color:transparent; border-radius:50%;
  animation:btn-spin .6s linear infinite;
}
@keyframes btn-spin{ to{ transform:rotate(360deg); } }
.btn > svg{ width:1.15em; height:1.15em; flex-shrink:0; }
/* PRIMARY is purple. It used to be --grad-brand (purple → ORANGE), which is exactly why
   orange meant nothing: it was already in every primary button. Orange is now reserved. */
.btn--primary{ background:var(--grad-purple); color:#fff; box-shadow:0 8px 20px -8px rgba(var(--purple-rgb),.45); }
.btn--orange{ background:var(--grad-orange); color:#fff; box-shadow:0 8px 20px -8px rgba(var(--orange-rgb),.45); }
.btn--primary:hover, .btn--orange:hover, .btn--primary.is-hover, .btn--orange.is-hover{ transform:translateY(-1px); }
/* GHOST. Always transparent — no fill at rest, on hover, or pressed. Border matches the
   button's own text colour (--text-muted), not a neutral overlay — --border read as
   barely-there (~1.2:1 against white, under WCAG's 3:1 minimum for UI component
   boundaries); an outline in the same colour as its own label reads clearly at any size
   without introducing a second, unrelated hue. Hover swaps both to the brand purple
   instead of filling in — the affordance is a colour shift, not a background. */
.btn--ghost{ background:transparent; color:var(--text-muted); border-color:var(--text-muted); }
.btn--ghost:hover, .btn--ghost.is-hover{ background:transparent; border-color:var(--purple); color:var(--purple); }
/* DANGER on GHOST — for destructive actions that must read as dangerous before any
   interaction, not just on hover. .btn--text.danger (below) only reddens on :hover, which
   doesn't exist on touch — a delete control needs to look different at rest, not be
   discovered by accident. Higher specificity than .btn--ghost:hover, so it wins the colour
   (stays red, doesn't shift to purple); a light red tint on hover gives the same "about to
   act" feedback .btn--text.danger already uses elsewhere. */
.btn--ghost.danger{ border-color:var(--red); color:var(--red-text); }
.btn--ghost.danger:hover, .btn--ghost.danger.is-hover{ background:rgba(var(--red-rgb),.08); border-color:var(--red); color:var(--red-text); }
.btn--solid{ background:var(--text); color:var(--bg); }
.btn--danger{ background:var(--danger-solid); color:var(--danger-solid-text); }
/* TEAL / YELLOW. Success and info confirm actions (dialog system) — added so those two
   dialog variants could move onto the shared button system instead of a bespoke recipe. */
.btn--teal{ background:var(--teal); color:#fff; }
.btn--yellow{ background:var(--yellow); color:#fff; }
/* Text button — no fill, no border. The quiet inline action ("Back", "Delete character",
   "Show more"). Added because NINETEEN page-local classes had independently invented it:
   there was no shared variant to reach for, so everyone built their own. */
.btn--text{ background:transparent; color:var(--text-muted); border-color:transparent; box-shadow:none; }
.btn--text:hover, .btn--text.is-hover{ background:var(--panel-strong); color:var(--text); }
.btn--text:disabled, .btn--text[aria-disabled="true"]{ background:transparent; }
/* Destructive text button. A delete action must READ as destructive — migrating one to the
   plain muted .btn--text would have quietly made "Delete character" look like "Back".
   Uses the same `danger` modifier convention as
.btn--text.danger:hover, .btn--text.danger.is-hover{ background:rgba(var(--red-rgb),.08); color:var(--red-text); }

/* Icon-only button. Square by default (--radius-md); .round for play/media controls;
   .lg for the oversized ones. Give it an aria-label — it has no text to announce. */
.btn--icon{ padding:0; width:38px; height:38px; min-height:0; border-radius:var(--radius-md);
            background:transparent; color:var(--text-muted); border-color:transparent; }
.btn--icon:hover, .btn--icon.is-hover{ background:var(--panel-strong); color:var(--text); }
.btn--icon > svg{ width:18px; height:18px; }
.btn--icon.round{ border-radius:50%; }
/* Resting fill. The default icon button is transparent (it lives on a plain surface and only
   fills on hover). Some sit ON something coloured — an edit affordance on a colour swatch, a
   close button on a modal image — and need a fill to stay visible at rest. */
.btn--icon.filled{ background:var(--panel-strong); }
.btn--icon.filled:hover{ background:var(--border); }
/* Selected state for an icon toggle (e.g. list/grid view switcher) — solid brand fill,
   distinct from .filled's neutral panel tint used for "stay visible at rest" cases. */
.btn--icon.active{ background:var(--purple-solid-fill); color:#fff; }
/* Four sizes, deliberately chosen. The product had SIX (30/32/36/38/54/76px) doing four jobs —
   nobody picked those, they were four people guessing on four different days. */
.btn--icon.sm{ width:32px; height:32px; }            /* inline / dense */
.btn--icon.sm > svg{ width:15px; height:15px; }
.btn--icon.lg{ width:54px; height:54px; }            /* media controls */
.btn--icon.lg > svg{ width:22px; height:22px; }
.btn--icon.xl{ width:76px; height:76px; }            /* hero play */
.btn--icon.xl > svg{ width:30px; height:30px; }
/* Icon button sitting ON media (a play control over a thumbnail/video). FROSTED WHITE, not
   dark: that is what the product's play buttons already were (rgba(255,255,255,.18)), and a
   dark scrim would have quietly restyled the homepage hero. Backdrop-blur gives it legibility
   over any frame without needing a heavy fill. */
.btn--icon.on-media{ background:rgba(var(--white-rgb),.18); color:#fff; backdrop-filter:blur(8px); border:1px solid rgba(var(--white-rgb),.25); }
.btn--icon.on-media:hover{ background:rgba(var(--white-rgb),.30); color:#fff; }
/* GHOST, on media — same problem as .btn--icon.on-media but for a labelled button (a
   secondary action inside a dark overlay, e.g. "Change settings" beside a failed-generation
   retry). The ordinary .btn--ghost border/text colour is --text-muted, tuned for a light
   surface — invisible on a dark scrim. Frosted white instead, same family as the icon one. */
.btn--ghost.on-media{ background:rgba(var(--white-rgb),.08); border-color:rgba(var(--white-rgb),.3); color:#fff; }
.btn--ghost.on-media:hover, .btn--ghost.on-media.is-hover{ background:rgba(var(--white-rgb),.16); border-color:rgba(var(--white-rgb),.5); color:#fff; }
/* PLAIN. No hover fill — the icon (or icon+label, on .btn--text) shifts straight to the brand
   purple instead. For a row of utility icons sitting on plain background (a header globe next to
   a text link, a footer switcher) where a filled hover box reads as heavier than the content
   around it. Works on .btn--icon and .btn--text; not meant for .btn--ghost/.btn--solid, which are
   filled/bordered by design. */
.btn--icon.plain:hover, .btn--icon.plain.is-hover,
.btn--text.plain:hover, .btn--text.plain.is-hover{ background:transparent; color:var(--purple); }

.btn--sm{ font-size:12.5px; padding:8px 14px; min-height:34px; }
.btn--lg{ font-size:15px; padding:14px 26px; min-height:48px; }
.btn--block{ display:flex; width:100%; }

/* ══ Chip system — SINGLE SOURCE. Compose: class="chip chip--success" ══
   status pills / tags / badges. Same discipline as .btn:
   • light/dark: rgba(var(--*-rgb)) tints + token text adapt automatically
   • translations: padding + white-space:nowrap → grows with label, no clip
   • RTL: symmetric padding + gap + inline-flex mirror; icons use em
   • a11y: contrast verified by the WCAG gate                                    */
.chip{
  display:inline-flex; align-items:center; gap:5px;
  font-size:11.5px; font-weight:700; line-height:1;
  padding:4px 10px; border-radius:999px;
  white-space:nowrap; vertical-align:middle;
}
.chip > svg{ width:1.1em; height:1.1em; flex-shrink:0; }
.chip--success{ background:rgba(var(--teal-rgb),.14);   color:var(--teal-text); }
.chip--info{    background:rgba(var(--purple-rgb),.12);  color:var(--purple-text); }
.chip--warn{    background:rgba(var(--orange-rgb),.16);  color:var(--orange-text); }
.chip--danger{  background:rgba(var(--red-rgb),.12);     color:var(--red-text); }
.chip--neutral{ background:var(--panel-strong);          color:var(--text-muted); }

/* Card info row: title+meta left, meatball right */
.video-info{ display:flex; align-items:flex-start; justify-content:space-between; gap:6px; padding:11px 10px 11px 13px; border-radius:0 0 var(--radius-md) var(--radius-md); }
.video-info-body{ flex:1; min-width:0; }
.video-info-body h3{ font-size:12.5px; font-weight:600; margin-bottom:3px; line-height:1.3; }
.video-info-body .meta{ font-size:10.5px; color:var(--text-faint); font-family:var(--font-mono); }

/* ── Mobile horizontal card (1-column layout, <480px grid width) ── */
.video-card.card-row{ display:flex; flex-direction:row; align-items:stretch; }

.video-card.card-row .video-thumb{
  width:96px; min-width:96px; height:96px;
  /* Logical corners. The physical shorthand kept the rounding on the LEFT in Hebrew,
     where the thumbnail is on the RIGHT — so it read as rounded on all four sides. */
  border-start-start-radius:var(--radius-md); border-end-start-radius:var(--radius-md);
  border-start-end-radius:0; border-end-end-radius:0;
  flex-shrink:0;
  /* Override aspect-ratio — fixed square crop */
  aspect-ratio:unset !important;
}

.video-card.card-row .video-thumb .status-chip{ display:none; } /* shown in info col instead */
.video-card.card-row .video-thumb-play svg{ width:32px; height:32px; padding:7px; }
.video-card.card-row .card-player-btn{ width:36px; height:36px; }
.video-card.card-row .card-player-btn svg{ width:14px; height:14px; }

.video-card.card-row .video-info{
  flex:1; min-width:0;
  padding:10px 8px 10px 12px;
  border-start-end-radius:var(--radius-md); border-end-end-radius:var(--radius-md);
  border-start-start-radius:0; border-end-start-radius:0;
  align-items:center;
}

.video-card.card-row .video-info-body{ display:flex; flex-direction:column; gap:3px; min-width:0; }
.video-card.card-row .video-info-body h3{ font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin:0; }
.video-card.card-row .video-info-body .meta{ font-size:11px; }

/* Status chip inline in info col for card-row */
.video-card.card-row .card-row-status{ display:inline-flex; }
.card-row-status{ display:none; } /* hidden by default, shown only in card-row */

/* ── Mobile bottom sheet (meatball on touch) ── */
#bottom-sheet-backdrop{
  display:none; position:fixed; inset:0; z-index:300;
  background:rgba(var(--black-rgb),0.45); opacity:0;
  transition:opacity .25s;
}
#bottom-sheet-backdrop.open{ display:block; opacity:1; }

#bottom-sheet{
  position:fixed; bottom:0; inset-inline:0; z-index:301;
  background:var(--panel);
  border-radius:var(--radius-lg) var(--radius-lg) 0 0;
  padding:0 0 max(env(safe-area-inset-bottom), 16px);
  transform:translateY(100%);
  transition:transform .3s cubic-bezier(.32,.72,0,1);
  box-shadow:0 -8px 40px -8px rgba(var(--black-rgb),0.25);
}
#bottom-sheet.open{ transform:translateY(0); }

.bs-handle{
  width:36px; height:4px; border-radius:2px;
  background:var(--border); margin:12px auto 8px;
}

.bs-item{
  display:flex; align-items:center; gap:14px;
  padding:0 20px; height:52px;
  font-size:15px; font-weight:500; color:var(--text);
  cursor:pointer; border:none; background:none;
  width:100%; text-align:start; font-family:var(--font-body);
  text-decoration:none;
}
.bs-item:active{ background:var(--panel-strong); }
.bs-item svg{ width:18px; height:18px; color:var(--text-faint); flex-shrink:0; }
.bs-item.danger{ color:var(--red); }
.bs-item.danger svg{ color:var(--red); }
.bs-item.disabled{ opacity:.45; pointer-events:none; }
.bs-divider{ height:1px; background:var(--border-soft); margin:4px 0; }

/* Long-press visual feedback */
.video-card.press-hold{
  transform:scale(0.96);
  transition:transform .3s ease;
}
/* In select mode on touch, tapping a card toggles it (whole card is tap target) */
@media(hover: none){
  body.select-mode .video-card{ cursor:pointer; }
}


/* ── Share modal (shared across videos, dashboard, video-detail) ── */
/* ── Share dialog content (rendered inside the central dialog shell) ── */
.share-platform-row{ display:flex; gap:14px; margin-bottom:18px; overflow-x:auto; padding-bottom:2px; justify-content:center; }
.share-plat-btn{ display:flex;flex-direction:column;align-items:center;gap:7px; flex-shrink:0; background:none;border:none;cursor:pointer; }
.share-plat-icon{ width:52px;height:52px; border-radius:16px; display:flex;align-items:center;justify-content:center; }
.share-plat-icon svg{ width:22px;height:22px; color:#fff; }
.share-plat-label{ font-size:11px; font-weight:500; color:var(--text-muted); }
.share-copy-row{ display:flex; gap:8px; align-items:center; background:var(--panel); border:1px solid var(--border); border-radius:10px; padding:10px 12px; }
.share-copy-url{ flex:1; font-size:12px; color:var(--text-faint); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.share-copy-btn{ font-size:12px; font-weight:700; color:var(--purple-bright); border:none; background:none; flex-shrink:0; cursor:pointer; }

/* ── Demo dark mode toggle ── */
.demo-theme-btn{
  font-size:11px; font-weight:700; font-family:var(--font-body);
  padding:4px 10px; border-radius:999px; border:1px solid var(--border-strong);
  background:transparent; color:var(--text-muted); cursor:pointer;
  transition:background .15s, color .15s, border-color .15s;
  white-space:nowrap;
}
.demo-theme-btn:hover{ border-color:var(--purple); color:var(--purple); }
.demo-theme-btn.active{ background:var(--purple-solid-fill); color:#fff; border-color:var(--purple); }


/* ── Video card components (added post-June-30) ────────── */
.card-format-tag{ font-size:9px; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--text-faint); font-family:var(--font-mono); }
.cp-play-icon{ width:44px; height:44px; color:#fff; filter:drop-shadow(0 2px 8px rgba(var(--black-rgb),.4)); }
.cp-pause-icon{ width:44px; height:44px; color:#fff; filter:drop-shadow(0 2px 8px rgba(var(--black-rgb),.4)); }
/* `left:auto` used to sit after inset-inline-end and quietly cancel the RTL flip: in Hebrew
   the checkbox stayed on the right while .status-chip (inset-inline-start) flipped to the
   right too, so the two landed in the same corner and overlapped. Logical properties only —
   let them mirror. */
.card-check-wrap{ position:absolute; top:8px; inset-inline-end:8px; z-index:3; }
.card-check{ width:18px; height:18px; border-radius:50%; border:2px solid rgba(var(--white-rgb),.7); background:transparent; cursor:pointer; transition:all .15s; appearance:none; -webkit-appearance:none; }
.card-check:checked{ background:var(--purple); border-color:var(--purple); }

/* ── Utility ───────────────────────────────────────────── */
.w-50{ width:50% !important; }

/* ── Create flow: new character ring ───────────────────── */
.create-new-item{ border:1.5px dashed var(--border); background:transparent; }
.create-new-item:hover{ border-color:var(--purple-bright); }
/* .create-new-ring — ONE border, not two.

   .avatar-ring paints the brand conic gradient: that ring MEANS "this is a character".
   .create-new-ring then added a dashed circle inside it, which MEANS "add a new one".
   Wearing both said two contradictory things at once, and read as a double border.

   The "create new" card on persona-characters.html is a plain dashed circle with no
   gradient — that is the established affordance. Match it: drop the gradient here and
   keep the dashed ring. */
/* Specificity, not source order. .avatar-ring is defined in the create-flow page's own
   <style> block, which loads AFTER this file, so a single-class selector here would lose to
   it. Repeating the class gives (0,2,0) and wins wherever it sits — without introducing
   .avatar-ring as a root in the shared layer, which it is not (the portal caught that).
   That a page shadows a shared component at all is the debt css-drift-check exists to cap. */
.create-new-ring.create-new-ring{ background:none; }
.create-new-ring .avatar-ring-inner{ background:rgba(var(--purple-rgb),.08); border:1.5px dashed rgba(var(--purple-rgb),.3); display:flex; align-items:center; justify-content:center; }
.create-new-ring .avatar-ring-inner svg{ width:18px; height:18px; color:var(--purple); }

/* ── Character template grid ───────────────────────────── */
.tpl-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(160px,1fr)); gap:12px; }
.tpl-card{ background:var(--panel); border:1.5px solid var(--border-strong); border-radius:var(--radius-md);
  overflow:hidden; cursor:pointer; transition:border-color .15s, box-shadow .15s;
  display:flex; flex-direction:column; }
.tpl-card:hover{ border-color:var(--purple-bright); box-shadow:0 4px 16px -6px rgba(var(--purple-rgb),.2); }
/* .tpl-card is reused for two different jobs: a clickable template pick (gets
   role="button"/tabindex from the keyboard shim, persona-characters.html) and
   a plain decorative preview tile (persona-portal.html, no interaction at all).
   Only the interactive instances get the 1px interactive-component width. */
.tpl-card[role="button"]{ border-width:1px; }
.tpl-img{ width:100%; aspect-ratio:3/4; object-fit:cover; object-position:top; display:block;
  background-color:var(--bg-deep); background-size:cover; background-position:center top; background-repeat:no-repeat; }
.tpl-info{ padding:10px 12px; display:flex; flex-direction:column; flex:1; }
.tpl-info > .tpl-use-btn{ margin-top:auto; }
.tpl-name{ font-size:13px; font-weight:700; color:var(--text); margin-bottom:4px; }
.tpl-tags{ display:flex; gap:4px; flex-wrap:wrap; margin-bottom:8px; }
.tpl-tag,
.tpl-tags > span{ font-size:10px; font-weight:600; padding:2px 7px; border-radius:999px;
  background:rgba(var(--purple-rgb),.08); color:var(--purple); white-space:nowrap; }
/* color: var(--cta-text), NOT #fff — in dark, --purple lightens (see persona-tokens.css) and
   white on it is only 2.71:1. Same trap as the empty-state CTAs: a hue that must be LIGHT for
   text cannot also be a fill that carries white. --cta-text is near-black in dark → 7.02:1. */
.tpl-use-btn{ width:100%; padding:7px; border-radius:var(--radius-sm); background:var(--purple); color:var(--cta-text); font-size:12px; font-weight:700; border:none; cursor:pointer; font-family:inherit; transition:opacity .15s; }
.tpl-use-btn:hover{ opacity:.88; }

/* ── Account page ──────────────────────────────────────── */
.profile-row-info{ flex:1; min-width:0; }
.sec-row-text{ flex:1; }

/* ── Step options column (create flow) ─────────────────── */
/* No overflow-y:auto here (an earlier version had one, for its own internal
   scrollbar) — removed once it turned out to not be doing what it looked
   like it was doing. On create-flow's 2-column desktop layout this column
   shares a CSS Grid row with the sticky preview column; without an explicit
   height of its own, it naturally sizes to fit its OWN content, and the row
   grows to match whichever column needs more room — there was never a
   height ceiling here forcing an inner scrollbar for genuine content
   overflow. What actually caused one: every combo-dropdown field's CLOSED
   panel (position:absolute, invisible but still present in layout) used to
   default to a max-height of up to 280px, extending that far past its
   trigger even while closed — which this column's own overflow:auto
   dutifully turned into scrollable overflow nobody would ever need to
   reach. Fixed at the source (.combo-panel now defaults to max-height:0
   while closed), so this column no longer needs to clip anything — the
   whole page (.wiz-main) scrolls naturally instead, same as every other
   step-flow page in the app.
   padding-bottom stays as plain breathing room below the last field/button,
   not clearance against a clip boundary that no longer exists. */
.step-options-col{ flex:1; display:flex; flex-direction:column; gap:18px; padding-bottom:20px; }

/* ── Notification empty state ──────────────────────────── */
.notif-empty{ padding:24px 16px; text-align:center; color:var(--text-faint); font-size:13px; }

/* ── Admin bar item ────────────────────────────────────── */
.bar-item{ display:flex; align-items:center; gap:8px; padding:4px 0; font-size:13px; color:var(--text-muted); }

/* ── AI assist label ───────────────────────────────────── */
.ai-assist-label{ font-size:11px; font-weight:600; color:var(--purple); display:flex; align-items:center; gap:4px; }

/* ── Support article ───────────────────────────────────── */
.prose h2,.prose h3{ font-family:var(--font-display); font-weight:700; margin:24px 0 12px; color:var(--text); }
.prose p,.prose li{ font-size:15px; color:var(--text-muted); line-height:1.75; margin-bottom:12px; }
.prose ul,.prose ol{ padding-inline-start:20px; }
/* ═══════════════════════════════════════════════════════════════════════════
   LINK  ·  ".link"  — an inline text link. SINGLE SOURCE.

   There was no link component. `a { color:inherit; text-decoration:none }` is a RESET, and the
   only real styling lived in `.prose a` — defined three separate times (persona-styles.css,
   persona-marketing.css, persona-support.css) with three sets of values, and carrying not one
   interactive state between them. No hover, no focus, no visited, no active, no disabled.
   A keyboard user tabbing through a paragraph of legal copy could not see where they were.

   `.prose a` now IS this component, so long-form copy gets the states for free.
   ═══════════════════════════════════════════════════════════════════════════ */
.link, .prose a{
  color:var(--purple); text-decoration:underline; text-underline-offset:2px;
  text-decoration-thickness:1px; border-radius:3px;
  transition:color .15s, text-decoration-thickness .15s;
}
.link:hover, .prose a:hover, .link.is-hover{ color:var(--purple-bright); text-decoration-thickness:2px; }
.link:visited, .prose a:visited{ color:var(--purple-text); }   /* browsers restrict what :visited may style — colour is allowed. --purple-text, not --purple-dim: dim is a decorative purple and fails AA as text. */
.link:focus-visible, .prose a:focus-visible, .link.is-focus{ outline:2px solid var(--purple); outline-offset:2px; text-decoration:none; }
.link:active, .prose a:active, .link.is-pressed{ color:var(--purple-text); }   /* not --purple-dim — the contrast audit failed it */
.link[aria-disabled="true"], .link.is-disabled{
  color:var(--text-faint); text-decoration:none; pointer-events:none; cursor:default;
}
/* quiet variant — a link that must not shout in dense UI (breadcrumbs, meta rows) */
.link--quiet{ color:var(--text-muted); text-decoration:none; }
.link--quiet:hover, .link--quiet.is-hover{ color:var(--text); text-decoration:underline; }

/* ═══════════════════════════════════════════════════════════════════════════
   CHECKBOX + RADIO  ·  ".check" / ".radio"  — SINGLE SOURCE.

   Neither existed. The product had .auth-checkbox (signup consent), .card-check (selecting a
   video) and .voice-radio (picking a voice) — three bespoke implementations, none with a focus
   ring. A keyboard user could not tell which one they were on.
   Built on the NATIVE input so keyboard, screen-reader and form semantics come for free.
   ═══════════════════════════════════════════════════════════════════════════ */
.check, .radio{ display:inline-flex; align-items:center; gap:9px; cursor:pointer; font-size:13.5px; color:var(--text); }
.check input, .radio input{
  appearance:none; margin:0; flex-shrink:0; width:18px; height:18px;
  border:1px solid var(--border-strong); background:var(--panel);
  cursor:pointer; transition:background .15s, border-color .15s;
}
.check input{ border-radius:5px; }
.radio input{ border-radius:50%; }
.check input:hover, .radio input:hover, .check.is-hover input, .radio.is-hover input{ border-color:var(--purple-bright); }
.check input:checked, .radio input:checked{ background:var(--purple); border-color:var(--purple); }
/* the tick — drawn, not an image, so it inherits the fill's text colour */
.check input:checked{
  background-image:linear-gradient(var(--cta-text),var(--cta-text)), linear-gradient(var(--cta-text),var(--cta-text));
  background-size:2px 7px, 2px 10px; background-position:5px 8px, 10px 4px;
  background-repeat:no-repeat; transform:none;
}
.radio input:checked{ box-shadow:inset 0 0 0 3.5px var(--panel); }
.check input:focus-visible, .radio input:focus-visible,
.check.is-focus input, .radio.is-focus input{ outline:2px solid var(--purple); outline-offset:2px; }
.check input:disabled, .radio input:disabled, .check.is-disabled input, .radio.is-disabled input{
  opacity:.5; cursor:not-allowed; background:var(--panel-strong);
}
.check.is-disabled, .radio.is-disabled{ color:var(--text-faint); cursor:not-allowed; }
.stepc{ font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--text-faint); margin-bottom:8px; }

/* ── Contact page ──────────────────────────────────────── */
.contact-info{ display:flex; flex-direction:column; gap:16px; }

/* ── Char extra meta ───────────────────────────────────── */
.char-extra-meta{ font-size:12px; color:var(--text-faint); }
.char-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(180px,1fr)); gap:14px; }
.char-avatar{ width:100%; aspect-ratio:3/4; object-fit:cover; object-position:top; border-radius:var(--radius-sm); background:var(--bg-tint); }
.pmeta{ font-size:11.5px; color:var(--text-faint); font-family:var(--font-mono); }

/* ── Video page specific ──────────────────────────────── */
.final-progress-bar{ height:4px; border-radius:999px; background:rgba(var(--purple-rgb),.15); overflow:hidden; position:relative; }
.regen-label{ font-size:11px; font-weight:600; color:var(--purple); display:flex; align-items:center; gap:4px; cursor:pointer; }
.lib-filter-chip{ padding:4px 12px; border-radius:999px; border:1px solid var(--border-strong); font-size:12px; font-weight:600; color:var(--text-muted); cursor:pointer; background:transparent; font-family:inherit; transition:all .15s; }
.lib-filter-chip.active,.lib-filter-chip:hover{ border-color:var(--purple); color:var(--purple); background:var(--selected-fill); }
.preview-fullscreen-btn{ position:absolute; top:10px; inset-inline-end:10px; width:32px; height:32px; border-radius:var(--radius-sm); background:rgba(var(--black-rgb),.5); border:none; cursor:pointer; display:flex; align-items:center; justify-content:center; color:#fff; }
.empty-secondary{ font-size:13px; color:var(--text-faint); margin-top:4px; }

/* ── Footer newsletter text ─────────────────────────────── */
.footer-nl-text{ flex:1; }
.footer-nl-title{ font-size:14px; font-weight:700; color:var(--text); margin-bottom:4px; }
.footer-nl-sub{ font-size:12px; color:var(--text-muted); }

/* ── Homepage feature row text ───────────────────────────── */
.feat-text{ flex:1; }

/* ── Videos: row title link ──────────────────────────────── */
.vrow-title-link{ font-size:14px; font-weight:600; color:var(--text); text-decoration:none; display:block; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.vrow-title-link:hover{ color:var(--purple); }

/* ── Character randomizer modal ──────────────────────────── */





/* ── Character: settings warning dialog → now uses the central dialog system (cd-*) ─── */


/* Custom-styled buttons must reset native appearance so translucent surface backgrounds (dark mode) composite over the page, not the UA button face. */
button{ -webkit-appearance:none; appearance:none; }

/* ── Keyboard focus indicator ──────────────────────────────────────────────
   A clear, consistent focus ring for keyboard users. :focus-visible targets
   keyboard focus only (not mouse clicks), so mouse/rendered states are
   unchanged; it applies to native controls and to the tabindex/role="button"
   elements made focusable by the keyboard-a11y shim. */
:focus-visible { outline: 2px solid var(--purple-bright, var(--purple)); outline-offset: 2px; border-radius: 3px; }

/* Visually-hidden but screen-reader-available (skip-links, structural headings). */
.sr-only { position: absolute !important; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }

/* First focusable element on every page — jumps a keyboard user past the
   header/nav straight to <main id="main-content">, off-screen until focused.
   Promoted from the design portal's page-local .cl-skip (same rule), now
   shared by every page. inset-inline-start, not left: with `left:-999px` the
   off-screen link sits 999px past the RIGHT edge in RTL and gives the whole
   page a horizontal scrollbar. */
.skip-link { position: absolute; inset-inline-start: -999px; top: 0; background: var(--purple-solid-fill); color: #fff; padding: 8px 14px; border-radius: var(--radius-sm); z-index: 1000; }
.skip-link:focus { inset-inline-start: 12px; top: 12px; }

/* ═══════════════════════════════════════════════════════════════════════════
   CONTEXT MENU / DROPDOWN SYSTEM  ·  ".ctx-*" classes
   Single source of truth for right-click / meatball dropdown menus.
   Promoted here from persona-app-ui-systems.html so every page (and the
   component-library catalogue) composes the SAME menu. Compose:
     <div class="ctx-menu open"> <button class="ctx-item">…</button> </div>
   Modifiers:
@keyframes ctxIn{from{opacity:0;transform:scale(.95) translateY(-4px);}to{opacity:1;transform:none;}}

/* ═══════════════════════════════════════════════════════════════════════════
   TOOLTIP SYSTEM  ·  "[data-tooltip]" attribute
   Single source of truth for hover tooltips. Promoted here from
   persona-app-ui-systems.html. Usage:
     <button data-tooltip="Copy link">…</button>
   Position modifier: data-tooltip-pos="bottom" | "right" (default: top)
   Note: tooltip surface (#111827) is an intentional near-black in BOTH themes;
   a follow-up could tokenise it (e.g. --tooltip-bg) — left verbatim to keep this
   a pure, low-risk promotion.
   ═══════════════════════════════════════════════════════════════════════════ */
[data-tooltip]{position:relative;}
[data-tooltip]::after{
  content:attr(data-tooltip);
  position:absolute;bottom:calc(100% + 6px);left:50%;transform:translateX(-50%);
  background:#111827;color:#fff;
  font-size:12px;font-weight:500;line-height:1.3;
  padding:5px 10px;border-radius:6px;
  white-space:nowrap;max-width:200px;white-space:normal;text-align:center;
  pointer-events:none;z-index:800;
  opacity:0;transition:opacity .15s;
}
[data-tooltip]::before{
  content:'';position:absolute;bottom:calc(100% + 2px);left:50%;transform:translateX(-50%);
  border:4px solid transparent;border-top-color:#111827;
  pointer-events:none;z-index:800;
  opacity:0;transition:opacity .15s;
}
[data-tooltip]:hover::after,[data-tooltip]:hover::before{opacity:1;}
/* Tooltip positions */
[data-tooltip-pos="bottom"]::after{bottom:auto;top:calc(100% + 6px);}
[data-tooltip-pos="bottom"]::before{bottom:auto;top:calc(100% + 2px);border-top-color:transparent;border-bottom-color:#111827;}
[data-tooltip-pos="right"]::after{bottom:auto;left:calc(100% + 8px);top:50%;transform:translateY(-50%);}
[data-tooltip-pos="right"]::before{bottom:auto;left:calc(100% + 2px);top:50%;transform:translateY(-50%);border-top-color:transparent;border-right-color:#111827;}

/* ═══════════════════════════════════════════════════════════════════════════
   FORM FIELD / VALIDATION SYSTEM  ·  ".field-*" classes
   Single source of truth for labelled inputs + validation states. Promoted here
   from persona-app-ui-systems.html. Compose:
     <div class="field">
       <label class="field-label">Email <span class="required">*</span></label>
       <input class="field-input" />           (add .valid / .invalid for state)
       <div class="field-hint">helper text</div>
       <div class="field-error">…</div> / <div class="field-success">…</div>
     </div>
   • light/dark: token borders/bg/text adapt automatically
   • translation-safe: no fixed widths; labels/hints wrap
   • RTL: margin-inline-start / text-align:end mirror automatically
   • a11y: 3px focus ring via box-shadow, colour + text on validation states
   ═══════════════════════════════════════════════════════════════════════════ */
.field{display:flex;flex-direction:column;gap:5px;width:100%;}
.field-label{font-size:12.5px;font-weight:600;color:var(--text-muted);}
.field-label .required{color:var(--red);margin-inline-start:2px;}
.field-input{
  width:100%;padding:10px 14px;
  border:1px solid var(--border-strong);border-radius:var(--radius-sm);
  font-size:14px;font-family:inherit;color:var(--text);
  background:var(--panel-strong);
}
/* Hover, disabled and read-only did not exist. A disabled input that looks identical to an
   enabled one is a small cruelty: the user types into it and nothing happens. */
.field-input:hover:not(:focus):not(:disabled), .field-input.is-hover{ border-color:var(--purple-bright); }
.field-input:disabled, .field-input.is-disabled{
  opacity:.55; cursor:not-allowed; background:var(--panel-strong); color:var(--text-faint);
}
.field-input[readonly], .field-input.is-readonly{
  background:var(--panel-strong); color:var(--text-muted); cursor:default; border-style:dashed;
}
.field-input:focus, .field-input.is-focus{border-color:var(--purple);border-width:1.5px;background:var(--bg-card);outline:none;}
.field-input--pill{ border-radius:999px; padding-block:10px; padding-inline:36px 14px; font-size:12.5px; background:var(--panel); }
.field-input.valid{border-color:var(--teal);background:var(--bg-card);}
.field-input.invalid{border-color:var(--red);background:var(--bg-card);}
.field-hint{font-size:12px;color:var(--text-muted);}
.field-error{font-size:12px;color:var(--red);display:flex;align-items:center;gap:4px;}
.field-error svg{width:12px;height:12px;flex-shrink:0;}
.field-success{font-size:12px;color:var(--teal);display:flex;align-items:center;gap:4px;}
.field-success svg{width:12px;height:12px;flex-shrink:0;}
.field-count{font-size:11.5px;color:var(--text-faint);text-align:end;}
.field-count.near-limit{color:var(--yellow);}
.field-count.at-limit{color:var(--red);}
.form-submit{
  width:100%;padding:12px;border-radius:999px;
  font-size:14px;font-weight:700;font-family:inherit;
  background:var(--purple);color:#fff;border:none;cursor:pointer;
  transition:opacity .15s;margin-top:8px;
}
.form-submit:hover{opacity:.88;}
.form-submit:disabled{opacity:.4;cursor:not-allowed;}

/* ═══════════════════════════════════════════════════════════════════════════
   SWITCH  ·  ".switch"  — the on/off toggle. SINGLE SOURCE.

     <label class="switch">
       <input type="checkbox">
       <span class="switch-track"></span>
       <span class="switch-thumb"></span>
     </label>

   Promoted out of persona-pricing.html, where it was the product's only real
   switch and carried two bugs a shared+audited component makes impossible:

     · It did not mirror in RTL. `left:3px` + `translateX(22px)` are not
       direction-aware, so in Hebrew the thumb still rested left and slid right.
       Here the resting side is `inset-inline-start` and the travel flips with dir.
     · It rendered identically in both themes (its "light mode" override sat at
       top level, after the base rule, and clobbered it — the same self-clobbering
       pattern found in persona-marketing.css). Colours are tokens now, so it
       adapts.
   ═══════════════════════════════════════════════════════════════════════════ */
.switch{ position:relative; width:48px; height:26px; flex-shrink:0; display:inline-block; }
.switch input{ opacity:0; width:0; height:0; position:absolute; }
.switch-track{
  position:absolute; inset:0; border-radius:999px;
  background:var(--panel-strong); border:1px solid var(--border);
  cursor:pointer; transition:background .2s, border-color .2s;
}
.switch-thumb{
  position:absolute; top:3px; inset-inline-start:3px;
  width:18px; height:18px; border-radius:50%;
  background:var(--text-muted);
  transition:transform .2s, background .2s;
  pointer-events:none;
}
.switch input:checked ~ .switch-track{
  background:rgba(var(--purple-rgb),.28); border-color:var(--purple);
}
/* travel is direction-aware: +22px in LTR, -22px in RTL (the thumb rests on the
   inline-start edge in both, so it must move toward inline-end in both) */
.switch input:checked ~ .switch-thumb{ transform:translateX(22px); background:var(--purple-bright); }
html[dir="rtl"] .switch input:checked ~ .switch-thumb{ transform:translateX(-22px); }
.switch input:focus-visible ~ .switch-track{ outline:2px solid var(--purple); outline-offset:2px; }

/* Label beside a switch (e.g. "Monthly / Annual") */
.switch-label{ font-size:14px; font-weight:600; color:var(--text-muted); }
.switch-label.active{ color:var(--text); }

/* ── Segmented control · ".mini-toggle" — a row of buttons, one .active.
   Promoted verbatim from persona-character.html / persona-profile-character.html
   (byte-identical on both). Distinct from .switch: it picks one of N, not on/off. ── */
.mini-toggle{ display:flex; background:var(--panel-strong); border:1px solid var(--border); border-radius:999px; padding:3px; margin-bottom:18px; }
.mini-toggle button{ flex:1; display:flex; align-items:center; justify-content:center; gap:6px; font-size:12.5px; font-weight:600; padding:9px 10px; border-radius:999px; border:none; background:transparent; color:var(--text-muted); }
.mini-toggle button.active{ background:var(--panel); color:var(--text); box-shadow:var(--card-shadow); }
.mini-toggle button svg{ width:13px;height:13px; }

/* ═══════════════════════════════════════════════════════════════════════════
   COMBO DROPDOWN  ·  ".combo-dd"  — a searchable single/multi-select dropdown
   that collapses a chip grid into one field. Promoted out of the create-flow's
   step-2 clothing-style field after a second field asked for the same pattern;
   the driving JS (comboOpen/comboSelect/etc.) stays in persona-create-flow.html
   since nothing outside that page uses it yet — this is the CSS half of the
   contract. Markup shape each instance must follow:

     <div class="combo-dd" id="X-dd">
       <input class="dd-trigger" id="X-trigger" .../>      <!-- IS the search box -->
       <svg class="dd-chev">…</svg>
       <div class="combo-panel" id="X-grid" role="listbox">
         <div class="combo-list">                           <!-- scrolls, filtered -->
           <button class="opt-chip" data-i18n="…" role="option">Label</button>
           …
           <p class="combo-empty" id="X-empty" style="display:none">…</p>
         </div>
         <div class="combo-custom-row">                     <!-- optional, pinned -->
           <button class="opt-chip" data-custom="1" role="option">Custom…</button>
           <input class="text-input custom-input" id="X-custom-input" .../>
         </div>
       </div>
     </div>

   .opt-chip is the existing pill component (persona-app.css) — reused here so
   every field this replaces keeps reading/writing the exact same
   `.opt-chip.selected` + `data-i18n` state its settings/restore functions
   already relied on; only ITS APPEARANCE is overridden below, scoped to
   .combo-panel, so the pill shape is untouched everywhere .opt-chip still
   renders flat (persona-character.html, persona-profile-character.html).
   Add `data-multi` to .combo-dd for a checkbox multi-select (Accessories):
   selecting doesn't close the panel, and multiple .opt-chip can carry
   .selected at once. ═══════════════════════════════════════════════════════ */
.combo-dd{ position:relative; }
/* The trigger IS the search field — no separate search row wasting vertical
   space. Its own typed text drives comboFilter(); closing without picking
   anything reverts whatever was mid-search back to the real selection. */
.dd-trigger{
  position:relative; z-index:21; width:100%;
  background:var(--panel); border:1px solid var(--border-strong); border-radius:12px;
  padding:11px 34px 11px 15px; font-size:13.5px; font-weight:500; color:var(--text);
  cursor:pointer; font-family:inherit; text-align:start;
  transition:border-radius .15s;
}
html[dir="rtl"] .dd-trigger{ padding:11px 15px 11px 34px; }
.dd-trigger::placeholder{ color:var(--text-faint); font-weight:400; opacity:1; }
.dd-trigger:hover{ border-color:var(--purple-bright); }
/* No outline:none override here on purpose — the shared FOCUS FLOOR's
   input:focus-visible ring is the keyboard-focus signal, and it has to be
   instant (native, untransitioned) rather than depend on the .open border
   colour transition below finishing first: a keyboard-focus check that
   samples immediately after Tab (no wait, to catch a genuinely absent
   signal) caught this racing the transition — 5–6 of 11 fields on
   persona-character.html failed, a different subset each run. */
/* Open state: the trigger's bottom edge disappears into the panel below it —
   same border colour, no gap, matching radii — so the two read as one
   continuous shape instead of a control floating over a separate card. */
.combo-dd.open .dd-trigger{
  border-color:var(--purple); border-width:1.5px; border-bottom-color:transparent;
  border-radius:12px 12px 0 0; outline:none;
}
/* pointer-events:auto (not the usual decorative-icon none) — the chevron is
   its own click target, toggling the dropdown independently of the trigger
   text field beneath it (see the delegated click handler in
   persona-app-shared.js). cursor:pointer signals that on top of the
   trigger's own, since this sits visually inside the trigger's box. */
.dd-chev{
  position:absolute; top:50%; transform:translateY(-50%); inset-inline-end:14px; z-index:22;
  width:15px; height:15px; color:var(--text-faint); pointer-events:auto; cursor:pointer;
  transition:transform .18s;
}
.combo-dd.open .dd-chev{ transform:translateY(-50%) rotate(180deg); }

/* The panel itself does not scroll — only .combo-list does — so
   .combo-custom-row stays visible below it regardless of scroll position
   or search filter, instead of being just the last item that happens to fit.
   position:absolute, anchored to .combo-dd (position:relative) — NOT fixed.
   fixed used to be load-bearing here (a JS-positioned overlay is the only
   way to escape a scrolling ancestor's clipping), but that JS positioning
   is exactly what broke on a real trackpad: macOS's two-finger scroll can
   move the page via the compositor thread faster than our main-thread JS
   could re-read the trigger's position and update the fixed panel's
   coordinates, so the two visually separated for a beat — "two different
   elements". Absolute positioning removes the JS from the loop entirely:
   the panel is laid out relative to its own trigger, so the SAME native
   scroll/compositor pass that moves the trigger moves the panel with it,
   with no synchronization possible to fall behind on.
   The two problems fixed positioning used to dodge are handled differently
   now: clipping by an independently-scrolling ancestor (.step-options-col)
   is solved by capping max-height to the tightest of every clipping
   ancestor's own visible bound (see comboPositionPanel's comboClipBottom
   walk) instead of escaping it; stacking (an open panel needing to render
   over a LATER field's trigger) is handled by this z-index alone, since
   nothing between .combo-dd and the page root creates its own stacking
   context to trap it in.
   Always opens downward now (no more open-up variant) — see
   comboPositionPanel for why. */
/* max-height:0 while closed — NOT the 280px a plain "default panel size"
   might suggest. A closed panel is still opacity:0/pointer-events:none, not
   display:none, so it stays in normal layout — and being position:absolute
   doesn't exempt it from contributing to a scrolling ancestor's scrollable
   overflow region if it visually extends past that ancestor's box. At 280px
   tall, closed panels sat past the bottom of whatever they were in far more
   often than open ones ever would, quietly forcing an internal scrollbar on
   containers (like create-flow's options column) that had no real content
   overflow at all — confirmed by measurement: hiding every closed panel on
   one such column dropped its scrollHeight from 556px to exactly its
   clientHeight, 467px. comboOpen always sets a real inline max-height via
   comboPositionPanel before a panel ever becomes visible, so 0 here costs
   nothing on open — there's no flash of an unstyled height to fall back to. */
.combo-panel{
  position:absolute; z-index:200; top:100%; left:0; width:100%;
  display:flex; flex-direction:column; max-height:0;
  background:var(--panel); border:1.5px solid var(--purple-bright); border-top:none;
  border-radius:0 0 12px 12px; overflow:hidden;
  opacity:0; pointer-events:none;
  transition:opacity .16s ease;
}
:root[data-theme="dark"] .combo-panel{ background:var(--bg); }
.combo-dd.open .combo-panel{ opacity:1; pointer-events:auto; }
.combo-list{ display:flex; flex-direction:column; gap:1px; overflow-y:auto; min-height:0; padding:6px; }
.combo-custom-row{ flex-shrink:0; border-top:1px solid var(--border-soft); padding:6px; }
.combo-custom-row input{ margin-top:6px; }
.combo-panel .opt-chip{
  display:flex; align-items:center; justify-content:space-between; gap:8px; width:100%;
  text-align:start; background:none; border:none; border-radius:9px;
  padding:9px 10px; font-size:13.5px; font-weight:400; color:var(--text); box-shadow:none;
  transition:background .12s; cursor:pointer; flex-shrink:0;
}
.combo-panel .opt-chip:hover, .combo-panel .opt-chip.hl{ background:var(--panel-strong); }
/* .opt-chip.selected (persona-app.css) fills a purple tint for the pill form —
   not wanted on a list row, so it's explicitly cleared here rather than left
   to whichever of the two same-specificity rules happens to load last. */
.combo-panel .opt-chip.selected{ background:none; border-color:transparent; color:var(--purple-bright); font-weight:600; }
.combo-panel .opt-chip.selected::after{ content:'✓'; flex-shrink:0; }
.combo-empty{ padding:20px 10px; text-align:center; font-size:12.5px; color:var(--text-faint); margin:0; }
/* Multi-select (Accessories): a checkbox stands in for the plain checkmark, and
   it fills rather than just tinting text — several can be checked at once, so
   the single purple-text treatment above wouldn't read as "on" per row. */
.combo-dd[data-multi] .combo-panel .opt-chip.selected{ color:var(--text); font-weight:400; }
.combo-dd[data-multi] .combo-panel .opt-chip.selected::after{ content:none; }
/* Multi-select rows have no rendered ::after (content:none above), leaving
   only the ::before checkbox and the label as flex items — the base rule's
   justify-content:space-between then pushes those two to opposite ends
   instead of keeping the label right next to its checkbox. */
.combo-dd[data-multi] .combo-panel .opt-chip{ justify-content:flex-start; }
.combo-dd[data-multi] .combo-panel .opt-chip::before{
  content:''; width:16px; height:16px; flex-shrink:0; border-radius:5px;
  border:1.5px solid var(--border-strong); background:var(--panel);
}
.combo-dd[data-multi] .combo-panel .opt-chip.selected::before{
  background:var(--purple-bright); border-color:var(--purple-bright);
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
  background-size:11px; background-repeat:no-repeat; background-position:center;
}
/* A preset option's optional "add detail" field (Clothing style, Location,
   Accessories) — lives right after its .opt-chip in markup and opens only
   while that option is selected, same idea as .combo-custom-row for Custom…
   but per-option instead of pinned. Multi-select can show more than one at
   once (each checked accessory keeps its own). Reuses .text-input for the
   actual field styling; only layout/visibility lives here. */
/* .text-input — was 4 near-identical copies pasted into page-local <style>
   blocks (persona-account/character/create-flow/profile-character.html),
   one genuinely different (account.html: smaller radius/padding/font — no
   evidence it was intentional, just drift). Single definition now; change
   it once. */
.text-input{ width:100%; background:var(--panel); border:1px solid var(--border-strong); border-radius:12px; padding:11px 15px; color:var(--text); font-size:13.5px; }
.text-input:focus, .text-input.is-focus{ border-color:var(--purple); border-width:1.5px; outline:none; }
.dd-inline-detail{ display:none; padding:0 10px 8px; }
.combo-panel .opt-chip.selected + .dd-inline-detail{ display:block; }
.dd-inline-detail .text-input{ padding:8px 10px; font-size:13px; font-weight:400; }


/* ═══════════════════════════════════════════════════════════════════════════
   SLIDER  ·  ".pslider"  — a real <input type="range">, custom-styled, in two
   flavours that share one markup shape and one driving function,
   pRangeValueChanged(sliderId, valueId), in persona-app-shared.js (it also
   calls pfillTrack() to redraw the fill). The current value always shows in a
   label row above the track, never on the thumb or in a row below it:

     Continuous (Age range):
       <div class="slider-label-row">
         <span class="flabel">Age range</span>
         <span class="slider-value" id="X-value">34</span>
       </div>
       <div class="slider-track-wrap">
         <input type="range" class="pslider" id="X-slider" min="21" max="99" step="1"
                value="34" oninput="pRangeValueChanged('X-slider','X-value')">
       </div>

     Discrete, stepped (Speaking pace) — data-labels is a comma-separated list of
     LOCALE KEYS (not literal text), read by pRangeValueChanged so the displayed
     word stays translated; small tick marks replace the old under-track step
     labels, one per stop:
       <div class="slider-label-row">
         <span class="flabel">Speaking pace</span>
         <span class="slider-value" id="X-value">Normal</span>
       </div>
       <div class="slider-track-wrap slider-track-wrap--ticked">
         <div class="slider-ticks" aria-hidden="true">
           <span class="slider-tick"></span><span class="slider-tick"></span><span class="slider-tick"></span>
         </div>
         <input type="range" class="pslider" id="X-slider" min="0" max="2" step="1"
                data-labels="pc_slow,pc_normal,pc_fast" oninput="pRangeValueChanged('X-slider','X-value')">
       </div>

   The filled portion of the track (purple up to the thumb) is set as an inline
   background gradient from JS, not CSS alone — a range input's fill position
   isn't expressible in CSS without JS reading the live value regardless, so
   the gradient is computed there once rather than duplicated as a second
   mechanism. ═══════════════════════════════════════════════════════════════ */
/* Wizard form field (character-creation + create-flow pages): a label above a
   single control. Single-sourced here after persona-character.html,
   persona-profile-character.html and persona-create-flow.html all ended up
   with the exact same rule copied into their own page <style> — caught by
   css-drift-check.js ("define it once, in the shared layer"). */
.field-block > .flabel{ display:block; font-size:13px; font-weight:600; margin-bottom:4px; }
.slider-label-row{ display:flex; align-items:baseline; justify-content:space-between; margin-bottom:10px; }
/* `.field-block > .flabel` above is a DIRECT-child selector that stops
   matching once the label moves in here under an extra wrapper row, silently
   falling back to unstyled text. Restated explicitly rather than loosening
   that selector sitewide for every other field-block. */
.slider-label-row .flabel{ font-size:13px; font-weight:600; margin-bottom:0; }
.slider-value{ font-size:13px; font-weight:700; color:var(--purple-bright); font-variant-numeric:tabular-nums; }
.slider-track-wrap{ position:relative; padding:10px 0; }
.pslider{
  -webkit-appearance:none; appearance:none;
  width:100%; height:6px; border-radius:999px;
  background:var(--border-soft);
  outline:none; cursor:pointer; margin:0; display:block;
}
.pslider::-webkit-slider-runnable-track{ height:6px; border-radius:999px; background:transparent; }
/* Solid fill (not the old panel-colored hollow ring) — with the value now
   shown in the label row above, the thumb carries no other identity, and a
   hollow dot blends into the page (--panel and --bg are the same white in
   light theme, and nearly the same near-black in dark theme). The ring reads
   as a cutout to the page underneath — var(--bg), not var(--panel) — so it's
   light in light theme and dark in dark theme, same as the page it sits on.
   var(--panel) looked right in light theme purely by coincidence (there it
   happens to be opaque white, same as --bg), but in dark theme it's a near-
   transparent white TINT over the near-black page, not a solid dark color —
   it very nearly disappeared there instead of reading as a dark ring. */
.pslider::-webkit-slider-thumb{
  -webkit-appearance:none; appearance:none;
  width:22px; height:22px; border-radius:50%;
  background:var(--purple-bright); border:3px solid var(--bg);
  box-shadow:0 2px 8px rgba(0,0,0,.18);
  cursor:grab; margin-top:-8px;
  transition:transform .12s;
}
.pslider:active::-webkit-slider-thumb{ transform:scale(1.12); cursor:grabbing; }
.pslider::-moz-range-track{ height:6px; border-radius:999px; background:transparent; }
.pslider::-moz-range-progress{ height:6px; border-radius:999px; background:var(--purple-bright); }
.pslider::-moz-range-thumb{
  width:16px; height:16px; border-radius:50%;
  background:var(--purple-bright); border:3px solid var(--bg);
  box-shadow:0 2px 8px rgba(0,0,0,.18); cursor:grab;
}
.pslider:focus-visible{ outline:2px solid var(--purple); outline-offset:4px; border-radius:999px; }

/* Discrete slider — small tick marks drawn directly on the track instead of
   step labels below it. Two-tone (white fill + dark ring) so a tick stays
   visible whether it lands on the colored fill or the unfilled track, in
   both themes: a flat single color can't do that (dark theme's
   --purple-bright and --text-faint are both light lavender tones, nearly the
   same lightness, so a plain --text-faint tick vanished on the fill). Inset
   4px from each edge so the end ticks don't sit on top of — and visually
   flatten — the track's own rounded end caps. The thumb needs its own
   stacking context above the ticks (a plain, non-positioned <input> paints
   BELOW positioned siblings regardless of DOM order), or a tick under the
   current value draws a line straight through it. */
.slider-ticks{ position:absolute; top:50%; left:4px; right:4px; height:12px; transform:translateY(-50%); pointer-events:none; z-index:1; }
.slider-tick{ position:absolute; top:0; width:2px; height:12px; border-radius:1px; background:#fff; box-shadow:0 0 0 1px rgba(0,0,0,.45); }
.slider-tick:first-child{ left:0; }
.slider-tick:last-child{ right:0; }
.slider-tick:not(:first-child):not(:last-child){ left:50%; transform:translateX(-50%); }
.slider-track-wrap--ticked .pslider{ position:relative; z-index:2; }


/* ── FOCUS RINGS ─────────────────────────────────────────────────────────────
   Only .btn had one. A keyboard user tabbing through a menu, a card, a chip or a segmented
   control had no idea where they were. This is an accessibility failure that no contrast gate
   can catch, because there was nothing rendered to grade. */

/* Clickable cards also acknowledge a press */
.video-card:active, .video-card.is-pressed,
.tpl-card:active, .tpl-card.is-pressed{ transform:translateY(1px); }


/* ═══════════════════════════════════════════════════════════════════════════
   FOCUS FLOOR — every form control gets a visible ring, whether or not it uses a
   shared component.

   tools/focus-visible-check.js tabbed through the product and found 21 controls that
   showed NOTHING when focused: the newsletter input on seven pages, the whole contact
   form (text, select, textarea), the admin fields, the forgot-password email box, the
   support search. Several actively stripped the browser's own ring (`outline:none`) and
   put nothing back — the worst outcome, because the browser had it right by default.

   A floor is the correct fix, not twenty-one page patches: any control anyone adds from
   now on inherits a ring, and the gate stops the next one from shipping without it.
   Components with their own richer focus treatment (.field-input, .check, .radio, .btn,
   .link) already declare it and are not affected — a ring on top of a ring is still a ring.
   ═══════════════════════════════════════════════════════════════════════════ */
input:focus-visible,
select:focus-visible,
textarea:focus-visible{
  outline:2px solid var(--purple);
  outline-offset:1px;
}

/* ============================================================
   .brand-mark — the logo tile. THE SHARED DEFINITION.

   It had no home in the shared layer, so every surface sheet invented its own
   (app 34px, admin 30px, auth 32px) and then TWELVE pages redefined it again in
   their own <style> blocks. Six of those were exact duplicates of the sheet they
   were already loading. Four were genuine drift: persona-create-flow — the heart
   of the product — rendered the logo at 30px while the dashboard rendered it at
   34px. Nobody could see it, because nobody has two pages open side by side.

   The base lives here now. Surface sheets may still tune the size, but that is
   then ONE governed line per surface, not an accident spread across twelve pages.
   page-css-check stops a page growing its own CSS; this stops a page needing to.
   ============================================================ */
.brand-mark{
  width:32px; height:32px;
  border-radius:9px;
  background:var(--grad-brand);
  display:flex; align-items:center; justify-content:center;
  flex-shrink:0;
}
.brand-mark svg{ width:17px; height:17px; }

/* ============================================================
   svg.i-flip — a directional icon (an arrow) must point the other way in RTL.

   This mechanism already existed, in persona-app.css AND persona-admin.css — two copies,
   and neither reachable from the marketing pages. So the arrow on the features page's CTA
   went on pointing right in Hebrew, into the text it was supposed to lead away from.
   A mechanism every surface needs belongs in the shared layer.
   ============================================================ */
svg.i-flip{ transition:transform .15s; }
html[dir="rtl"] svg.i-flip{ transform:scaleX(-1); }
