/* ============================================================================
   SnapDub design system - matches design_handoff_snapdub
   Light canvas (#fafafa), white surfaces, Deep Crimson accent (#C81E3A),
   Inter (UI) + Geist Mono (labels/numerals). Componentized via class names.
   ============================================================================ */

@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500;600&family=Inter:wght@400;450;500;600;700;800&display=swap');

:root {
  /* Surfaces */
  --bg: #fafafa;
  --surface: #ffffff;
  --surface-2: #f4f4f5;

  /* Hairlines */
  --line: #ececee;
  --line-strong: #e4e4e7;
  --divider: #f4f4f5;

  /* Text */
  --ink: #18181b;
  --ink-soft: #3f3f46;
  --ink-2: #52525b;
  --ink-3: #71717a;
  --muted: #a1a1aa;

  /* Brand - Deep Crimson */
  --primary: #C81E3A;
  --primary-hover: #a8182f;
  --primary-tint: rgba(200, 30, 58, 0.12);

  /* Status */
  --st-pending: #71717a;
  --st-blue: #2563eb;     --st-blue-bg: #eaeef7;
  --st-purple: #7c3aed;   --st-purple-bg: #f1ebfb;
  --st-orange: #ea580c;   --st-orange-bg: #fbeee4;
  --st-green: #15803d;    --st-green-bg: #edf7ef;  --st-green-line: #cfe9d6;
  --st-red: #b42318;      --st-red-bg: #fbedec;    --st-red-line: #f1d3d1;

  /* Radius */
  --r-pill: 6px;
  --r-sm: 8px;
  --r-md: 10px;
  --r-lg: 14px;
  --r-xl: 18px;

  /* Shadow */
  --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.03);
  --shadow-elevated: 0 24px 60px -20px rgba(24, 24, 27, 0.18);
  --shadow-primary: 0 1px 2px rgba(200, 30, 58, 0.25);

  --font-sans: "Inter", system-ui, -apple-system, sans-serif;
  --font-mono: "Geist Mono", ui-monospace, monospace;

  --sidebar-w: 264px;
  --z-sidebar: 40;
  --z-toast: 1000;
  --z-tour: 1100;
}

/* ---- Reset / base ---------------------------------------------------------- */
*, *::before, *::after { box-sizing: border-box; }
/* overflow-x:clip as a baseline, not a fix for one specific element - no rule here forces the
   page wider than the viewport on purpose, so any accidental sub-pixel overflow (a flex/grid gap
   rounding error, a fixed-width child a hair too wide on a narrow phone, ...) should never be ABLE
   to open a page-level horizontal scrollbar/rubber-band in the first place, on mobile or desktop.
   Reported: a faint horizontal scroll appearing while scrolling through the clip grid on mobile,
   with no single element identified as the cause - this class of bug is exactly what a missing
   baseline like this lets through silently.
   `clip`, NOT `hidden` - confirmed live: overflow-x:hidden on body makes overflow-y's COMPUTED
   value become auto too (a real CSS rule: "if one axis is visible and the other isn't, visible
   becomes auto"), turning body into its own independent scroll container instead of the page
   using the normal viewport/document scroll - broke every position:sticky element on the page
   (topbar/sidebar scrolled fully off-screen instead of staying pinned). `clip` was built
   specifically to avoid that pairing rule - same visual clipping, no scroll-container side effect. */
html { -webkit-text-size-adjust: 100%; overflow-x: clip; }
body {
  margin: 0;
  min-height: 100dvh;
  overflow-x: clip;
  font-family: var(--font-sans);
  font-size: 14px;
  line-height: 1.55;
  letter-spacing: -0.01em;
  color: var(--ink);
  background: var(--bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}
a { color: inherit; text-decoration: none; }
img, svg, video { display: block; max-width: 100%; }
button { font: inherit; cursor: pointer; }
:focus-visible { outline: 2px solid var(--primary); outline-offset: 2px; border-radius: var(--r-sm); }
::selection { background: var(--primary-tint); }

/* Scrollbar (matches handoff) */
::-webkit-scrollbar { width: 10px; height: 10px; }
::-webkit-scrollbar-thumb { background: #d4d4d8; border-radius: 8px; border: 3px solid transparent; background-clip: padding-box; }
::-webkit-scrollbar-thumb:hover { background: var(--muted); background-clip: padding-box; }

/* ---- Typography ------------------------------------------------------------ */
h1, h2, h3, h4 { margin: 0; font-weight: 600; line-height: 1.1; letter-spacing: -0.03em; }
h1 { font-size: 25px; }
h2 { font-size: 38px; letter-spacing: -0.04em; }
h3 { font-size: 16px; }
p { margin: 0 0 16px; color: var(--ink-2); }

.eyebrow {
  font-family: var(--font-mono); font-size: 11.5px; font-weight: 500;
  letter-spacing: 0.05em; text-transform: uppercase; color: var(--primary);
}
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.text-dim { color: var(--ink-2); }
.text-muted { color: var(--muted); }

/* ---- Layout utilities ------------------------------------------------------ */
.container { width: 100%; max-width: 980px; margin-inline: auto; padding-inline: 40px; }
.stack > * + * { margin-top: 16px; }
.cluster { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; }
.cluster select.input { width: auto; flex: 0 0 auto; } /* .input defaults to width:100%, which would eat the whole row */

/* Library search bar (app/views/videos/index.html.erb) - client-side (project_search_controller.js),
   no form/submit involved. */
.search-bar { margin-bottom: 16px; }
.search-field { position: relative; max-width: 420px; }
.search-field__icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--muted); pointer-events: none; }
/* .input.search-field__input (compound, not two separate rules): needs to beat .input's own
   shorthand `padding: 0 12px` - same specificity as a lone `.search-field__input` would have, so
   which one wins came down to source order (and lost). Compounding onto .input's own selector
   sidesteps that instead of relying on rule order never getting reshuffled later. */
.input.search-field__input { padding-left: 36px; padding-right: 34px; }
.search-field__status { position: absolute; right: 34px; top: 50%; transform: translateY(-50%); display: flex; align-items: center; color: var(--muted); }
.search-field__status[hidden] { display: none; } /* [hidden] must beat the display:flex above */
.search-field__status--found { color: #16a34a; }
/* .demo__spin defaults to --primary (this app's red brand color) - blue here instead, so a
   "still searching" spinner doesn't read like an error/urgent state. */
.search-field__status .demo__spin { border-color: color-mix(in srgb, var(--st-blue) 28%, transparent); border-top-color: var(--st-blue); }
.search-field__clear { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); width: 24px; height: 24px; border: none; background: none; border-radius: 50%; display: grid; place-items: center; color: var(--muted); cursor: pointer; }
.search-field__clear[hidden] { display: none; } /* [hidden] must beat the display:grid above */
.search-field__clear:hover { background: var(--surface-2); color: var(--ink); }
.spread { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.grid-cards { display: grid; gap: 16px; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
/* Same trick as turbo-frame.lib-rows-frame below - keeps a lazy-loaded page's turbo-frame wrapper
   from breaking .grid-cards' layout (its cards need to be direct grid items, not nested inside a
   frame box) while still giving lazy_frame_controller.js's inner sentinel div something to size. */
turbo-frame.grid-cards-frame { display: contents; }

/* ---- Brand mark ------------------------------------------------------------ */
.brand-mark {
  width: 28px; height: 28px; border-radius: 7px; flex-shrink: 0;
  background: var(--primary); color: #fff;
  display: grid; place-items: center;
}
.brand-mark svg { width: 16px; height: 16px; }
.brand-word { font-size: 16px; font-weight: 600; letter-spacing: -0.03em; }

/* ---- App shell ------------------------------------------------------------- */
.shell { display: flex; min-height: 100dvh; }
/* Desktop: the burger collapses the persistent sidebar out of the flow (main reflows to full width). */
.shell.nav-collapsed .sidebar { display: none; }

.sidebar {
  position: sticky; top: 0; align-self: flex-start;
  width: var(--sidebar-w); flex-shrink: 0; height: 100dvh; z-index: var(--z-sidebar);
  display: flex; flex-direction: column;
  background: var(--surface); border-right: 1px solid var(--line);
  padding: 20px 16px;
}
.sidebar__brand { display: flex; align-items: center; gap: 10px; padding: 4px 8px 22px; }
.sidebar__section { font-family: var(--font-mono); font-size: 10px; font-weight: 500; letter-spacing: 0.06em; color: var(--muted); text-transform: uppercase; padding: 0 8px 8px; }
.sidebar__nav { display: flex; flex-direction: column; gap: 2px; margin-top: 22px; }
.sidebar__spacer { flex: 1; }

.nav-item {
  display: flex; align-items: center; gap: 11px;
  height: 36px; padding: 0 9px; border-radius: 7px;
  font-size: 13.5px; font-weight: 450; color: var(--ink-2);
  transition: background 140ms ease, color 140ms ease;
}
.nav-item:hover { background: var(--surface-2); color: var(--ink); }
.nav-item svg { width: 17px; height: 17px; flex: none; }
.nav-item.is-active { background: var(--surface-2); color: var(--ink); font-weight: 600; }
.nav-item.is-active svg { color: var(--primary); }
.nav-item__badge { margin-left: auto; font-family: var(--font-mono); font-size: 11px; font-weight: 500; color: var(--muted); }
/* A light "pay attention" treatment for a notable new entry (Earn) - subtler than .btn--danger's
   own red (that's for destructive actions), just enough to stand out from the plain list around
   it. Stacks fine with .is-active (hover/active backgrounds still layer on top). */
.nav-item--highlight { background: var(--st-red-bg); border: 1px solid var(--st-red-line); color: var(--st-red); }
.nav-item--highlight svg { color: var(--st-red); }
.nav-item--highlight:hover { background: var(--st-red-bg); color: var(--st-red); filter: brightness(0.97); }
/* .is-active alone would win with its own plain grey (same rule every other nav item shares) -
   more specific selector here so Earn goes MORE red when active, not grey like the rest. */
.nav-item--highlight.is-active { background: var(--st-red); border-color: var(--st-red); color: #fff; }
.nav-item--highlight.is-active svg { color: #fff; }

/* Billing's sidebar entry - a <details> disclosure grouping Subscription/Payment method/Invoices
   so they stay visually "inside billing" no matter which of their (different) route prefixes is
   current, instead of the parent losing its highlight the moment you leave /billing itself. */
.nav-group__summary { cursor: pointer; list-style: none; }
.nav-group__summary::-webkit-details-marker { display: none; }
.nav-group__summary::marker { content: ""; }
.nav-group__chevron { margin-left: auto; color: var(--muted); transition: transform 140ms ease; }
.nav-group[open] > .nav-group__summary .nav-group__chevron { transform: rotate(90deg); }
.nav-group__items { display: flex; flex-direction: column; gap: 2px; margin: 2px 0 4px 37px; }
.nav-subitem {
  display: flex; align-items: center;
  height: 30px; padding: 0 9px; border-radius: 7px;
  font-size: 13px; font-weight: 450; color: var(--ink-2);
  transition: background 140ms ease, color 140ms ease;
}
.nav-subitem:hover { background: var(--surface-2); color: var(--ink); }
.nav-subitem.is-active { background: var(--surface-2); color: var(--ink); font-weight: 600; }

/* Balance card (sidebar) */
.balance-card { border: 1px solid var(--line); border-radius: var(--r-md); padding: 13px 14px; margin-bottom: 12px; background: var(--bg); }
.balance-card__head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 9px; font-size: 12px; font-weight: 500; color: var(--ink-2); }
.balance-card__value { display: flex; align-items: baseline; gap: 5px; margin-bottom: 9px; }
.balance-card__value b { font-family: var(--font-mono); font-size: 22px; font-weight: 600; letter-spacing: -0.02em; }
.balance-card__value span { font-size: 12px; color: var(--ink-3); }

/* Profile snippet */
.account { display: flex; align-items: center; gap: 10px; padding: 8px; border-radius: var(--r-sm); width: 100%; text-align: left; background: none; border: none; }
.account:hover { background: var(--surface-2); }
.account__meta { min-width: 0; flex: 1; }
.account__name { font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.account__sub { font-size: 11px; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.account svg { width: 15px; height: 15px; color: var(--muted); flex: none; }

.avatar { width: 32px; height: 32px; flex: none; border-radius: var(--r-sm); background: var(--ink); color: #fff; display: grid; place-items: center; font-size: 12px; font-weight: 600; }

/* Main column */
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.topbar {
  position: sticky; top: 0; z-index: 30; height: 57px; flex-shrink: 0;
  display: flex; align-items: center; gap: 16px; padding: 0 26px;
  background: rgba(255,255,255,0.8); backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line);
}
.breadcrumb { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.breadcrumb span.sep { color: #d4d4d8; display: flex; }
.breadcrumb svg { width: 14px; height: 14px; color: #d4d4d8; }
.breadcrumb .crumb-muted { color: var(--muted); }
.breadcrumb .crumb-current { font-weight: 500; }
.content { padding: 28px 26px 40px; flex: 1; }
/* Signed-out pages (no sidebar): keep the content readable and centred. */
.main--plain .content { max-width: 940px; margin: 0 auto; width: 100%; }

.menu-btn { display: inline-flex; }

/* ---- Buttons --------------------------------------------------------------- */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  height: 36px; padding: 0 14px;
  font-family: var(--font-sans); font-size: 13px; font-weight: 600;
  border: 1px solid transparent; border-radius: var(--r-sm);
  transition: background 140ms ease, border-color 140ms ease, filter 140ms ease;
  white-space: nowrap;
}
.btn svg { width: 15px; height: 15px; flex: 0 0 auto; } /* never let flex squish the glyph to a sliver */
.btn--primary { background: var(--primary); color: #fff; box-shadow: var(--shadow-primary); }
.btn--primary:hover { background: var(--primary-hover); }
.btn--dark { background: var(--ink); color: #fff; }
.btn--dark:hover { background: #000; }
.btn--ghost { background: var(--surface); color: var(--ink); border-color: var(--line-strong); font-weight: 500; }
.btn--ghost:hover { background: var(--surface-2); }
.btn--subtle { background: var(--surface-2); color: var(--ink); font-weight: 500; }
.btn--subtle:hover { background: #e9e9eb; }
.btn--danger { background: var(--st-red-bg); color: var(--st-red); border-color: var(--st-red-line); }
.btn--danger:hover { background: var(--primary); color: #fff; border-color: var(--primary); }
/* TikTok brand: black button, white logo, signature cyan/red double-exposure on hover. */
.btn--tiktok { background: #000; color: #fff; border-color: #000; font-weight: 600; }
.btn--tiktok:hover { background: #161823; border-color: #161823; box-shadow: -2px 0 0 #FE2C55, 2px 0 0 #25F4EE; }
.btn--tiktok:focus-visible { outline: 2px solid #25F4EE; outline-offset: 2px; }
/* Instagram brand: signature warm-to-purple gradient, white glyph. */
/* background-clip: padding-box - .btn's own `border: 1px solid transparent` otherwise lets this
   gradient paint straight through the "transparent" border ring (the CSS default clips background
   to the border-box, i.e. under the border), showing the gradient's actual edge colors (yellow/
   orange on one side, purple/blue on the other) as a stray-looking colored border instead of a
   clean edge - a solid-color .btn--* never shows this since its border area is the same flat color
   as its background anyway. */
.btn--instagram { background: linear-gradient(45deg, #FEDA75, #FA7E1E, #D62976, #962FBF, #4F5BD5); background-clip: padding-box; color: #fff; border-color: transparent; font-weight: 600; }
.btn--instagram:hover { filter: brightness(1.06); }
.btn--instagram:focus-visible { outline: 2px solid #D62976; outline-offset: 2px; }
/* YouTube brand: signature red, white glyph. */
/* Destination picker inside the publish dialog (clips/_publish_hub.html.erb) - auto-fit rather
   than a fixed column count so a sixth platform needs no CSS change, and so it collapses to one
   column on a phone without a media query. */
.publish-hub__grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 10px; margin-top: 14px; }
.publish-hub__tile { position: relative; display: flex; align-items: center; justify-content: center; gap: 8px;
                     min-height: 46px; padding: 10px 14px; border: 1px solid transparent; border-radius: var(--r-md);
                     font-weight: 600; font-size: 0.92rem; text-decoration: none; }
.publish-hub__tile svg { flex: 0 0 auto; }
/* A posted platform stays fully clickable - posting the same clip twice is allowed - so this
   marks it without the muting that would read as "disabled". */
.publish-hub__tile.is-posted { box-shadow: inset 0 0 0 2px var(--st-green, #16a34a); }
.publish-hub__check { position: absolute; top: -6px; right: -6px; display: inline-flex; align-items: center;
                      justify-content: center; width: 18px; height: 18px; border-radius: 50%;
                      background: #16a34a; color: #fff; }
.btn--bluesky { background: #0285FF; color: #fff; border-color: #0285FF; font-weight: 600; }
.btn--bluesky:hover { background: #0270db; border-color: #0270db; }
.btn--bluesky:focus-visible { outline: 2px solid #0285FF; outline-offset: 2px; }
.btn--facebook { background: #1877F2; color: #fff; border-color: #1877F2; font-weight: 600; }
.btn--facebook:hover { background: #1465d8; border-color: #1465d8; }
.btn--facebook:focus-visible { outline: 2px solid #1877F2; outline-offset: 2px; }
.btn--youtube { background: #FF0000; color: #fff; border-color: #FF0000; font-weight: 600; }
.btn--youtube:hover { background: #d90000; border-color: #d90000; }
.btn--youtube:focus-visible { outline: 2px solid #FF0000; outline-offset: 2px; }
.btn--block { width: 100%; }
.btn--lg { height: 46px; padding: 0 22px; font-size: 15px; border-radius: var(--r-md); }
.btn--sm { height: 32px; padding: 0 10px; font-size: 13px; }
.btn[disabled], .btn.is-loading { opacity: 0.55; pointer-events: none; }
/* A platform button whose account isn't connected yet - deliberately NOT [disabled] (this stays
   clickable, it navigates to Settings > Social accounts) so opacity/pointer-events:none above
   would send the wrong signal. .btn.btn--not-connected (two classes) beats a single-class
   platform color like .btn--youtube on specificity, regardless of source order, so this always
   overrides the brand color rather than depending on which rule happens to load last. */
.btn.btn--not-connected { background: var(--surface-2); color: var(--muted); border: 1px dashed var(--line-strong); }
.btn.btn--not-connected:hover { background: #e9e9eb; color: var(--ink-2); }

/* ---- Surfaces -------------------------------------------------------------- */
.card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--shadow-card); }
.card__body { padding: 22px; }
.panel { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 26px 24px; box-shadow: var(--shadow-card); }
.connected-account { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding: 12px 14px; border: 1px solid var(--line); border-radius: var(--r-md); }
/* Deliberately NOT inline-flex: the name and handle have to wrap as ordinary inline text. As a
   flex container each became its own unbreakable item, which on a phone tore "@handle" onto a
   second line indented under the label and threw the icon out to the far edge. */
.social-tile { color: inherit; text-decoration: none; }
/* Dimmed but never hidden: on touch there is no hover, so an affordance that only appears on
   hover leaves most of our traffic reading "click the platform tile" over what looks like
   plain text. */
/* display:inline-block undoes the global `img, svg, video { display: block }` reset above -
   left as a block the glyph takes a line of its own and pushes the handle down a third line. */
.social-tile svg { display: inline-block; vertical-align: -2px; margin-left: 5px; color: var(--muted); opacity: 0.5; transition: opacity 0.15s; }
.social-tile:hover svg, .social-tile:focus-visible svg { opacity: 1; }
.social-tile:hover strong, .social-tile:focus-visible strong { text-decoration: underline; }

.stat { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 16px 18px; box-shadow: var(--shadow-card); }
.stat__label { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.05em; text-transform: uppercase; color: var(--muted); }
.stat__value { font-family: var(--font-mono); font-size: 26px; font-weight: 600; letter-spacing: -0.02em; margin-top: 4px; }

/* ---- StatusBadge (reusable) ------------------------------------------------ */
.status { position: relative; min-width: 160px; font-size: 11.5px; }
.status__pill { display: inline-flex; align-items: center; gap: 6px; height: 25px; padding: 0 10px; border-radius: var(--r-pill); font-weight: 600; }
.status__pill svg { width: 13px; height: 13px; }
.status__pill.is-pending { background: var(--surface-2); border: 1px solid var(--line-strong); color: var(--ink-2); font-weight: 500; }
.status__pill.is-completed { background: var(--st-green-bg); border: 1px solid var(--st-green-line); color: var(--st-green); }
.status__pill.is-completed svg { color: #16a34a; }
.status__pill.is-failed { background: var(--st-red-bg); border: 1px solid var(--st-red-line); color: var(--st-red); cursor: default; }
.status__dot { width: 6px; height: 6px; border-radius: 50%; background: var(--st-pending); animation: blink 1.4s ease-in-out infinite; }
.status__info { width: 12px; height: 12px; color: #d99; }

.status__progress .status__row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; }
.status__label { font-size: 11.5px; font-weight: 500; }
.status__pct { font-family: var(--font-mono); font-size: 10.5px; }
.status__bar { height: 5px; border-radius: 3px; overflow: hidden; position: relative; }
.status__bar > i { display: block; height: 100%; border-radius: 3px; }
.status__bar--indet > i { position: absolute; top: 0; width: 42%; animation: indet 1.3s ease-in-out infinite; }

.status--transcribing .status__label { color: var(--st-blue); }
.status--transcribing .status__bar { background: var(--st-blue-bg); }
.status--transcribing .status__bar > i { background: var(--st-blue); }
.status--matching .status__label { color: var(--st-purple); }
.status--matching .status__bar { background: var(--st-purple-bg); }
.status--matching .status__bar > i { background: var(--st-purple); }
.status--lipsync .status__label { color: var(--st-orange); }
.status--lipsync .status__bar { background: var(--st-orange-bg); }
.status--lipsync .status__bar > i { background: var(--st-orange); }
/* DubbingVideo's own progress stages (transcribing reuses the rule above verbatim - same name). */
.status--dubbing .status__label { color: var(--st-purple); }
.status--dubbing .status__bar { background: var(--st-purple-bg); }
.status--dubbing .status__bar > i { background: var(--st-purple); }
.status--rendering .status__label { color: var(--st-orange); }
.status--rendering .status__bar { background: var(--st-orange-bg); }
.status--rendering .status__bar > i { background: var(--st-orange); }
/* pending/queued (DubbingVideo's own detail-page bar - see _detail.html.erb): no real 0-100
   number to show yet, so this only ever renders as the indeterminate sweep, but still needs its
   own track/fill color or that bar is invisible (same gap the states above were missing). */
.status--pending .status__bar, .status--queued .status__bar { background: var(--surface-2); }
.status--pending .status__bar > i, .status--queued .status__bar > i { background: var(--st-pending); }

.status__tip { display: none; position: absolute; top: 30px; left: 0; z-index: 20; width: 230px; background: var(--ink); color: var(--bg); font-size: 11.5px; line-height: 1.5; padding: 9px 11px; border-radius: var(--r-sm); box-shadow: 0 8px 24px rgba(0,0,0,.18); }
.status__pill.is-failed:hover + .status__tip,
.status__pill.is-failed:focus + .status__tip { display: block; }

@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: 0.2; } }
@keyframes indet { 0% { left: -42%; } 100% { left: 100%; } }
@keyframes pulseDot { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }

/* Simple badge (counts / tags) */
.badge { display: inline-flex; align-items: center; gap: 5px; font-family: var(--font-mono); font-size: 11px; font-weight: 500; color: var(--muted); background: var(--bg); border-radius: 5px; padding: 1px 6px; }
.badge--active { color: var(--ink); background: var(--surface-2); }
.badge--warn { color: var(--st-orange); background: var(--st-orange-bg); }
.badge--warn svg { width: 12px; height: 12px; }

/* Progress bar (generic) */
.progress { height: 5px; border-radius: 3px; background: var(--line); overflow: hidden; }
.progress__bar { height: 100%; background: var(--primary); border-radius: 3px; transition: width 400ms ease; }

/* ---- Forms ----------------------------------------------------------------- */
.field { display: block; margin-bottom: 16px; }
.is-disabled { opacity: .45; pointer-events: none; }
.field__label { display: block; font-size: 13px; font-weight: 500; color: var(--ink); margin-bottom: 6px; }
.field__label .req, .check .req { color: var(--primary); }
.input, .textarea, select.input {
  width: 100%; height: 40px; padding: 0 12px;
  font: inherit; font-size: 13.5px; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--r-sm);
  outline: none; transition: border-color 140ms ease, box-shadow 140ms ease;
}
.input::placeholder, .textarea::placeholder { color: var(--muted); }
.input:focus, .textarea:focus, select.input:focus { border-color: var(--primary); box-shadow: 0 0 0 3px var(--primary-tint); }
.textarea { height: auto; min-height: 120px; padding: 11px 12px; resize: vertical; line-height: 1.5; }
.check { display: flex; align-items: flex-start; gap: 10px; padding: 7px 0; font-size: 13.5px; color: var(--ink); cursor: pointer; }
.check input { margin-top: 2px; width: 16px; height: 16px; accent-color: var(--primary); flex: none; }
.check input:disabled { opacity: .5; }
.check span { line-height: 1.45; }
.field__error { color: var(--st-red); font-size: 12.5px; margin-top: 6px; }
.field__hint { color: var(--muted); font-size: 12.5px; margin-top: 6px; }
/* The global reset is `a { color: inherit; text-decoration: none }`, so a link dropped into a
   hint inherits --muted and reads as plain grey text - nobody clicks it. YouTube's Terms of
   Service link and the publish hub's "not connected" list both sat unclickable-looking this way.
   One rule here instead of repeating TikTok's inline color+underline at every call site. */
.field__hint a { color: var(--primary); text-decoration: underline; }
.field__hint a:hover { text-decoration-thickness: 2px; }
.field__hint--warning { display: flex; align-items: flex-start; gap: 6px; color: #7c3a12; background: var(--st-orange-bg); border: 1px solid #f3d9c4; border-radius: var(--r-md); padding: 8px 10px; }
.field__hint--warning svg { width: 13px; height: 13px; color: var(--st-orange); flex-shrink: 0; margin-top: 1px; }
.field__hint--warning-link { color: #7c3a12; font-weight: 600; text-decoration: underline; }
.field__hint-link { color: var(--primary); font-weight: 600; text-decoration: underline; }

/* A <button> that reads as an inline text link (e.g. "contact support" opening Tawk chat via JS,
   where a real <a href> would need somewhere to navigate to) - reset the button chrome, keep the
   surrounding text's own size/line-height. */
.link-button {
  background: none; border: none; padding: 0; margin: 0; font: inherit; cursor: pointer;
  color: var(--primary); text-decoration: underline;
}

/* Inline form row (label-less fields side by side, e.g. the team invite form). `.input`
   defaults to width:100%, which becomes a flex item's basis and crushes its siblings - the
   text field grows to fill the row, the rest get a fixed width instead. */
.invite-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 20px; }
.invite-bar input.input { flex: 1 1 220px; }
.invite-bar select.input { flex: 0 0 170px; width: auto; }

.error-summary { background: var(--st-red-bg); border: 1px solid var(--st-red-line); border-radius: var(--r-sm); padding: 12px 14px; margin-bottom: 16px; color: var(--st-red); font-size: 13px; }
.error-summary ul { margin: 6px 0 0; padding-left: 18px; }

/* ---- Toasts ---------------------------------------------------------------- */
.toast-stack { position: fixed; right: 24px; bottom: 24px; z-index: var(--z-toast); display: flex; flex-direction: column; gap: 10px; width: min(360px, calc(100vw - 32px)); }
.toast { display: flex; gap: 11px; align-items: flex-start; padding: 12px 14px; background: var(--surface); border: 1px solid var(--line-strong); border-left: 3px solid var(--muted); border-radius: var(--r-sm); box-shadow: var(--shadow-elevated); animation: toast-in 240ms cubic-bezier(.2,.8,.2,1); }
.toast.is-leaving { animation: toast-out 180ms ease forwards; }
.toast--success { border-left-color: #16a34a; }
.toast--error { border-left-color: var(--primary); }
.toast--info { border-left-color: var(--st-blue); }
.toast__icon { flex: none; margin-top: 1px; }
.toast--success .toast__icon { color: #16a34a; }
.toast--error .toast__icon { color: var(--primary); }
.toast__title { font-weight: 600; font-size: 13px; }
.toast__body { color: var(--ink-2); font-size: 12.5px; }
.toast__action { display: inline-block; margin-top: 6px; font-size: 12.5px; font-weight: 600; color: var(--primary); text-decoration: none; }
.toast__action:hover { text-decoration: underline; }
/* padding (not just the 16px icon) is the real tap target - reported too small on mobile at the
   old 2px. Negative margin offsets the added padding so the icon's visual position barely moves. */
.toast__close { margin: -8px -6px -8px auto; background: none; border: none; color: var(--muted); padding: 8px; border-radius: 6px; }
.toast__close:hover { color: var(--ink); background: var(--surface-2); }
@keyframes toast-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes toast-out { to { opacity: 0; transform: translateX(16px); } }

/* ---- Empty state ----------------------------------------------------------- */
.empty { text-align: center; padding: 56px 24px; border: 1px dashed var(--line-strong); border-radius: var(--r-lg); background: var(--surface); }
.empty__icon { width: 44px; height: 44px; margin: 0 auto 16px; color: var(--muted); }
.empty h3 { font-size: 16px; }

/* ---- Library table --------------------------------------------------------- */
.page-head { display: flex; align-items: flex-end; justify-content: space-between; margin-bottom: 22px; gap: 16px; }
.page-head h1 { margin-bottom: 5px; }
.page-head p { font-size: 13.5px; color: var(--ink-3); margin: 0; }

.tabs { display: flex; align-items: center; gap: 4px; border-bottom: 1px solid var(--line); margin-bottom: 4px; }
.tab { display: flex; align-items: center; gap: 7px; height: 38px; padding: 0 12px; background: none; border: none; border-bottom: 2px solid transparent; font-size: 13px; font-weight: 450; color: var(--ink-3); margin-bottom: -1px; }
.tab:hover { color: var(--ink); }
.tab.is-active { color: var(--ink); font-weight: 600; border-bottom-color: var(--ink); }

.lib-cols, .lib-row { display: grid; grid-template-columns: 1fr 120px 200px 36px 44px; gap: 16px; align-items: center; }
.lib-row[hidden] { display: none; } /* [hidden] must beat the display:grid above - project_search_controller.js hides non-matching rows this way */
.lib-cols { padding: 14px 16px 10px; font-family: var(--font-mono); font-size: 10.5px; font-weight: 500; letter-spacing: 0.05em; text-transform: uppercase; color: var(--muted); }
/* overflow visible so row dropdown menus aren't clipped; corners rounded on end rows */
.lib-table { display: flex; flex-direction: column; border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--surface); }
/* Infinite-scroll page frames live inside .lib-table (a flex column) - `contents` makes the
   <turbo-frame> itself invisible to layout so its rows stack exactly like direct children. */
turbo-frame.lib-rows-frame { display: contents; }
.lib-row { padding: 13px 16px; border-top: 1px solid var(--divider); transition: background 120ms ease; }
.lib-row:first-child { border-top: none; border-radius: var(--r-lg) var(--r-lg) 0 0; }
.lib-row:last-child { border-radius: 0 0 var(--r-lg) var(--r-lg); }
.lib-row:only-child { border-radius: var(--r-lg); }

/* Dropdown menu */
.menu { position: relative; justify-self: end; }
.menu__panel {
  position: absolute; right: 0; top: 34px; z-index: 50; min-width: 150px;
  background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--r-sm);
  box-shadow: var(--shadow-elevated); padding: 5px; display: flex; flex-direction: column;
}
.menu__panel[hidden] { display: none; } /* [hidden] must beat the display:flex above */
.menu__panel--up { top: auto; bottom: 34px; } /* opens upward - for menus anchored at the bottom of the sidebar */
.menu__item {
  display: flex; align-items: center; width: 100%; text-align: left;
  padding: 8px 10px; border: none; background: none; border-radius: var(--r-pill);
  font-size: 13px; color: var(--ink); cursor: pointer;
}
.menu__item:hover { background: var(--surface-2); }
.menu__item--danger { color: var(--st-red); }
.menu__item--danger:hover { background: var(--st-red-bg); }
.menu form { margin: 0; }

/* Earn page: campaigns as cards (like AutoClip's own Earn tab), not a .lib-table list - each
   campaign carries a description/requirements paragraph and platform chips, too much content per
   row for a table layout. Wider min-width than .grid-cards (240px) - this content needs more
   horizontal room than a clip thumbnail. */
.campaign-grid { display: grid; gap: 16px; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); align-items: start; }
.campaign-card__head { display: flex; align-items: flex-start; gap: 12px; }
.campaign-card__name { font-weight: 600; }
.campaign-card__remove {
  flex-shrink: 0; width: 26px; height: 26px; border: none; background: none; border-radius: 50%;
  display: grid; place-items: center; color: var(--muted); cursor: pointer;
}
.campaign-card__remove:hover { background: var(--st-red-bg); color: var(--st-red); }
.campaign-card__requirements { font-size: 12.5px; margin: 10px 0 0; }

/* Settings > Assets library (app/views/assets/index.html.erb) - a checkered background shows
   transparency in a PNG/WebP the same way an image editor would, instead of looking like a plain
   missing/broken image against a flat surface. */
.asset-card__preview {
  position: relative; aspect-ratio: 1; display: flex; align-items: center; justify-content: center; overflow: hidden;
  background-image: linear-gradient(45deg, var(--surface-2) 25%, transparent 25%), linear-gradient(-45deg, var(--surface-2) 25%, transparent 25%),
                     linear-gradient(45deg, transparent 75%, var(--surface-2) 75%), linear-gradient(-45deg, transparent 75%, var(--surface-2) 75%);
  background-size: 16px 16px; background-position: 0 0, 0 8px, 8px -8px, -8px 0;
}
.asset-card__preview img { max-width: 100%; max-height: 100%; object-fit: contain; }
.asset-card__remove { position: absolute; top: 8px; right: 8px; z-index: 1; }
.asset-card__remove form { margin: 0; }

/* Font library card (assets/_font_card.html.erb) - sample text rendered in the uploaded font
   itself (see font_face_swatch_controller.js), not an image preview like .asset-card__preview. */
.font-card__preview {
  position: relative; aspect-ratio: 1; display: flex; align-items: center; justify-content: center;
  background: var(--surface-2); font-size: 22px; color: var(--ink); padding: 12px; text-align: center;
  word-break: break-word; line-height: 1.3;
}

/* Font picker "smart select" (shared/_font_settings_field.html.erb) - a searchable combobox, not
   a plain <select>: options render as plain text (the page's own UI font), never the font itself,
   so opening the list never loads every uploaded font's file - only the one actually clicked does
   (font_combobox_controller.js). */
.font-combobox { position: relative; max-width: 280px; }
.font-combobox__trigger { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; cursor: pointer; text-align: left; color: var(--ink); }
.font-combobox__trigger svg { color: var(--muted); flex-shrink: 0; }
.font-combobox__panel {
  position: absolute; z-index: 20; top: calc(100% + 4px); left: 0; right: 0; background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r-md); box-shadow: var(--shadow-elevated);
  padding: 8px; display: flex; flex-direction: column; gap: 6px;
}
.font-combobox__panel[hidden] { display: none; } /* [hidden] must beat the display:flex above */
.font-combobox__search { font-size: 13px; }
.font-combobox__list { list-style: none; margin: 0; padding: 0; max-height: 220px; overflow-y: auto; }
.font-combobox__option { display: block; width: 100%; text-align: left; padding: 7px 10px; border: 0; background: none; border-radius: var(--r-sm); cursor: pointer; font-size: 13px; color: var(--ink); }
.font-combobox__option:hover { background: var(--surface-2); }
.font-combobox__option[hidden] { display: none; } /* [hidden] must beat this file's own display:block above */

/* Generic searchable picker (combobox_controller.js). Visually the same object as
   .font-combobox above, kept as its own class set on purpose: that one is welded to the
   font-file loading it exists for, and folding the two together would drag @font-face
   machinery onto every other list that just needs search. */
.combobox { position: relative; max-width: 280px; }
.combobox__trigger { display: flex; align-items: center; justify-content: space-between; gap: 8px; width: 100%; cursor: pointer; text-align: left; color: var(--ink); }
.combobox__trigger svg { color: var(--muted); flex-shrink: 0; }
.combobox__panel {
  position: absolute; z-index: 20; top: calc(100% + 4px); left: 0; min-width: 220px; background: var(--surface);
  border: 1px solid var(--line); border-radius: var(--r-md); box-shadow: var(--shadow-elevated);
  padding: 8px; display: flex; flex-direction: column; gap: 6px;
}
.combobox__panel[hidden] { display: none; } /* [hidden] must beat the display:flex above */
.combobox__search { font-size: 13px; }
.combobox__list { list-style: none; margin: 0; padding: 0; max-height: 220px; overflow-y: auto; }
.combobox__option { display: block; width: 100%; text-align: left; padding: 7px 10px; border: 0; background: none; border-radius: var(--r-sm); cursor: pointer; font-size: 13px; color: var(--ink); }
.combobox__option:hover { background: var(--surface-2); }
.combobox__option[hidden] { display: none; } /* [hidden] must beat this file's own display:block above */
.combobox__sentinel { padding: 7px 10px; font-size: 12px; color: var(--muted); }
.combobox__sentinel[hidden] { display: none; } /* [hidden] must beat the <li> default display */

/* Trigger-only variant that sits inside a .settings-chips pill (the per-channel picker on
   /channels) - no .input box, just the pill's own type plus the chevron. */
.combobox--chip { max-width: none; }
.combobox__chip-trigger {
  display: flex; align-items: center; gap: 5px; padding: 0; border: 0; background: none;
  font-size: 12.5px; font-weight: 600; color: var(--ink); cursor: pointer;
}

/* Earn page's All/Active/Ended filter - plain links (a full page visit each), not a JS-driven
   toggle, so the campaign-search box's own reloads (which forward the active one along, see
   campaign_search_controller.js) and this both agree on one source of truth: the URL. */
.status-filter { display: inline-flex; gap: 2px; background: var(--surface-2); padding: 3px; border-radius: var(--r-pill); flex-shrink: 0; }
.status-filter__opt { padding: 5px 12px; border-radius: var(--r-pill); font-size: 12.5px; font-weight: 500; color: var(--muted); text-decoration: none; white-space: nowrap; }
.status-filter__opt.is-active { background: var(--surface); color: var(--ink); box-shadow: var(--shadow-card); }

/* "More ways to earn" program cards (campaigns/_more_ways_to_earn.html.erb) - a fixed 3-up grid,
   not .grid-cards' own auto-fill (there are always exactly 3 of these; auto-fill would leave them
   narrow with dead space on a wide viewport instead of stretching to fill the row). */
.earn-programs-grid { display: grid; gap: 16px; grid-template-columns: repeat(3, 1fr); }
@media (max-width: 780px) { .earn-programs-grid { grid-template-columns: 1fr; } }

/* "More ways to earn" rate comparison (campaigns/_more_ways_to_earn.html.erb) - a plain label/
   value list, not a real <table> (only ever two columns, and a table's own layout machinery would
   be overkill for that). */
.rate-table { display: flex; flex-direction: column; }
.rate-table__row { display: flex; justify-content: space-between; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--line); font-size: 13px; }
.rate-table__row:last-child { border-bottom: none; }
.rate-table__row span:last-child { font-family: var(--font-mono); color: var(--muted); }
/* The rates disclaimer (shared/_clipping_rates) - same look as .lib-meta but wrapping, since it
   is a full sentence that must never be cut off. */
.rate-table__note { font-size: 11.5px; color: var(--muted); font-family: var(--font-mono);
  line-height: 1.55; margin-top: 10px; }

/* Confirmation modal (native <dialog>) */
.modal {
  border: none; padding: 0; background: var(--surface); color: var(--ink);
  border-radius: var(--r-lg); box-shadow: var(--shadow-elevated);
  max-width: 380px; width: calc(100vw - 40px);
}
.modal::backdrop { background: rgba(24, 24, 27, 0.45); backdrop-filter: blur(2px); }
.modal__card { padding: 22px; display: flex; flex-direction: column; }
.modal__title { font-size: 16px; margin-bottom: 6px; }
.modal__message { color: var(--ink-2); font-size: 13.5px; margin: 0; }
/* Placeholder inside the publish composer's frame, shown from the click until the platform's own
   form arrives (ClipsController#new_publish_* makes live API calls, so that gap is real). Sized
   with a min-height so the dialog opens at roughly the height the form will occupy instead of
   snapping open tiny and then jumping. */
.composer-loading { align-items: center; justify-content: center; gap: 14px; min-height: 210px; }
.composer-loading__text { color: var(--ink-2); font-size: 13.5px; margin: 0; }
.modal__actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 18px; }

/* Dislike-reason picker (shared/_dislike_reason_modal.html.erb) - one option per line, not the
   radios/labels' default inline flow (there was no CSS for these classes at all before, which is
   exactly why every reason ran together on one line). */
.modal__reasons { display: flex; flex-direction: column; gap: 2px; margin-top: 10px; }
.modal__reason {
  display: flex; align-items: center; gap: 9px; padding: 8px 6px; border-radius: var(--r-sm);
  font-size: 13.5px; color: var(--ink); cursor: pointer; user-select: none;
}
.modal__reason:hover { background: var(--surface-2); }
.modal__reason input { width: 15px; height: 15px; accent-color: var(--primary); cursor: pointer; flex: none; }
.modal textarea[name="reason_other"] { margin-top: 10px; width: 100%; resize: vertical; }

/* Publish composer (shared/_publish_composer.html.erb) - wider than the confirm modal (real
   form fields, not just a message), and its content can outgrow a short mobile viewport (video
   preview + several fields) - the card itself scrolls rather than the dialog overflowing the
   screen. */
.modal--wide { max-width: 480px; }
/* Corner close button. Anchored to the <dialog> itself (which is position:fixed while open), not
   to the scrolling card inside it, so it stays in the corner however far the form scrolls. The
   title gets clearance so a long one never runs under it. */
.modal__close {
  position: absolute; top: 10px; right: 10px; z-index: 1; display: grid; place-items: center;
  width: 30px; height: 30px; border: none; border-radius: var(--r-sm); background: transparent;
  color: var(--ink-3); cursor: pointer;
}
.modal__close:hover { background: var(--surface-2); color: var(--ink); }
.modal--wide .modal__title { padding-right: 30px; }
.modal--wide .modal__card { max-height: min(88vh, 720px); overflow-y: auto; }
.modal__preview { display: block; width: 100%; max-height: 38vh; border-radius: var(--r-sm); background: #000; object-fit: contain; margin-bottom: 16px; }
@media (max-width: 520px) {
  .modal--wide { max-width: none; width: calc(100vw - 20px); }
  .modal--wide .modal__card { padding: 18px; max-height: min(90vh, 640px); }
}

.lib-row:hover { background: var(--bg); }
.lib-row a.lib-video { display: flex; align-items: center; gap: 13px; min-width: 0; color: inherit; }
.lib-thumb { position: relative; width: 74px; height: 46px; border-radius: var(--r-pill); border: 1px solid var(--line-strong); flex-shrink: 0; background: repeating-linear-gradient(135deg,#f4f4f5,#f4f4f5 6px,#eaeaec 6px,#eaeaec 12px); display: flex; align-items: flex-end; justify-content: flex-end; padding: 4px; }
.lib-thumb img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
.lib-thumb__dur { position: relative; font-family: var(--font-mono); font-size: 8.5px; color: var(--ink); background: rgba(255,255,255,.87); border-radius: 3px; padding: 1px 4px; }
.lib-thumb__ph { position: absolute; inset: 0; display: grid; place-items: center; color: var(--primary); }
/* Compact "AI" badge shown inline in a generation row's meta line. */
.lib-type { display: inline-flex; align-items: center; gap: 3px; padding: 0 5px; border-radius: var(--r-pill); background: var(--primary-tint); color: var(--primary); font-weight: 700; font-size: 10px; }
.lib-type svg { width: 11px; height: 11px; }
.lib-title { font-size: 13.5px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-bottom: 3px; }
/* Clip cards specifically wrap the full title instead of the shared .lib-title single-line
   truncation above (still used as-is by project/dubbing-video rows, where a fixed single-line
   height matters more than seeing the whole title) - a clip's AI-written title is short-form
   copy meant to be read, not just a library-row label, so cutting it off lost real content. */
.clip-card .lib-title { white-space: normal; overflow: visible; text-overflow: clip; }
.lib-meta { font-size: 11.5px; color: var(--muted); font-family: var(--font-mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.lib-dur { font-family: var(--font-mono); font-size: 12.5px; color: var(--ink-2); }
.lib-langs { display: flex; align-items: center; gap: 7px; font-size: 12.5px; font-weight: 500; color: var(--ink-soft); }
.lib-langs .mono { font-family: var(--font-mono); }
.lib-langs svg { width: 13px; height: 13px; color: #c4c4c8; }
.lib-actions { width: 30px; height: 30px; border: none; background: none; border-radius: 7px; display: grid; place-items: center; color: var(--muted); justify-self: end; }
.lib-actions:hover { background: var(--surface-2); color: var(--ink); }
/* button_to wraps the button in its own <form> - display:contents so THAT (not the form) is the
   grid item .lib-row expects, same trick as turbo-frame.lib-rows-frame above. */
.lib-star__form { display: contents; }
.lib-star { width: 30px; height: 30px; border: none; background: none; border-radius: 7px; display: grid; place-items: center; color: var(--muted); justify-self: end; transition: color 140ms ease, background 140ms ease; }
.lib-star:hover { background: var(--surface-2); color: var(--primary); }
.lib-star.is-pinned { color: var(--primary); }
.lib-star.is-pinned svg { fill: currentColor; }

/* ---- Clip card (inline player) --------------------------------------------- */
.clip-card { overflow: hidden; }
.clip-card__stage { position: relative; aspect-ratio: 9 / 16; background: var(--ink); display: flex; }
/* contain keeps the true 9:16 aspect in the card AND in native fullscreen (no stretch) */
.clip-card__media { width: 100%; height: 100%; object-fit: contain; display: block; background: var(--ink); }
.clip-card__placeholder { width: 100%; height: 100%; display: grid; place-items: center; color: var(--muted); }
/* Re-trim in progress, layered over the still-playable OLD render underneath (see _clip.html.erb's
   own comment on this block - never shown for the clip's very-first render, only a re-trim of an
   already-completed one). backdrop-filter blurs the real thumbnail/video behind it rather than
   hiding it outright, so it still reads as "the same clip, updating" not "gone". */
.clip-card__trimming {
  position: absolute; inset: 0; z-index: 3; display: flex; flex-direction: column; gap: 8px;
  align-items: center; justify-content: center; color: #fff; text-align: center;
  background: rgb(0 0 0 / 45%); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
}
.clip-card__trimming span { font-size: .82rem; font-weight: 600; }
/* Same white-ring-on-dark override .gen-card__overlay needs (see that class's own comment) - the
   spinner's default border colors assume a light background. */
.clip-card__trimming .gen-card__spinner { border-color: rgba(255, 255, 255, .3); border-top-color: #fff; }
/* Blocks clicks reaching the OLD render underneath while a re-render is in flight - the overlay
   above visually covers it, but a native <video controls> element's own built-in play button can
   bypass a plain z-index overlay in some browsers (confirmed live, 2026-08-26) - pointer-events
   on the whole stage is the only reliable way to actually block it, not just visually cover it. */
.clip-card__stage--locked { pointer-events: none; }
/* Centered play affordance over the poster (clip_video_controller.js hides/shows it on play/
   pause) - pointer-events:none so the click lands on the <video> underneath it, which is what
   actually starts playback, not this decoration. */
.clip-card__play {
  position: absolute; inset: 0; z-index: 2; display: flex; align-items: center; justify-content: center;
  pointer-events: none;
}
.clip-card__play span {
  width: 52px; height: 52px; border-radius: 50%; background: rgb(0 0 0 / 45%);
  display: flex; align-items: center; justify-content: center; color: #fff;
}
.clip-card__play svg { margin-left: 3px; } /* optical centering - a play triangle's mass leans left */
.clip-card__play[hidden] { display: none; }
/* Touch/coarse-pointer devices (phones/tablets) already render their OWN native center play
   affordance over a <video controls poster> before playback starts (iOS Safari draws a large
   circular play button there by default) - our overlay duplicated it (reported: two overlapping
   play icons on mobile). Mouse/trackpad devices get no such native affordance, so ours stays the
   only one there. */
@media (hover: none) and (pointer: coarse) {
  .clip-card__play { display: none; }
}
/* Score badge (left) + edit/delete (right) - overlays the top of the video on desktop, matching
   .clip-card - needs `position: relative` for this absolute-positioned bar to anchor to the CARD
   (it's a sibling of .clip-card__stage, not nested inside it) rather than to whatever positioned
   ancestor happens to be further up the page. pointer-events:none + opt back in per-child so
   clicks/taps elsewhere in the top strip still reach the video underneath (matches .clip-card__play's
   own pattern above). .player__score's own `position:absolute` default is overridden back to
   static - positioned by this flex parent instead, not independently. */
.clip-card { position: relative; }
.clip-card__topbar {
  position: absolute; top: 8px; left: 8px; right: 8px; z-index: 3;
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  pointer-events: none;
}
.clip-card__topbar > * { pointer-events: auto; }
.clip-card__topbar .player__score { position: static; }
.clip-card__topbar-actions { display: flex; gap: 8px; margin-left: auto; }
/* Touch devices: iOS Safari draws its own native fullscreen-expand + mute-toggle icons in these
   exact top corners of an inline <video controls> (reported + confirmed via screenshot: real
   overlap with the badge/edit/delete above) - no supported attribute suppresses those, so the
   whole bar moves OUT of the video into its own strip above it instead, in the same near-black
   tone as the video itself (a visual continuation of the clip, not a separate light card header). */
@media (hover: none) and (pointer: coarse) {
  .clip-card__topbar {
    position: static; z-index: auto; background: var(--ink); padding: 8px 10px; pointer-events: auto;
  }
  /* .clip-card__edit/.clip-card__removewrap no longer need a touch-only position:static override
     here (2026-08-25) - they're position:static (normal flex flow) in their own base rule now,
     on every device, not just this one. See that base rule's own comment for the desktop
     misalignment this fixed. */
}
/* When there's a "why" reason, the badge is a <button> (toggles .clip-card__score-popover via
   score_popover_controller.js - click/tap, not a [title] hover tooltip, which never fires on
   touch) instead of a plain <span>. Only reset the button-specific chrome here - NOT font (a
   `font:` shorthand reset once clobbered .player__score's own mono/11px/600 sizing, since this
   selector's specificity beats the plain .player__score rule). */
button.player__score { border: 0; cursor: pointer; }
/* position:fixed, not absolute - top/left are set by score_popover_controller.js from the
   button's real viewport position, so .clip-card's own overflow:hidden (clips the video to its
   rounded corners) never clips this. */
/* Same fixed-over-video architecture as .clip-card__menu just above (and the same theoretical
   exposure to the same video-hardware-compositing issue) - see its comment for why the very high
   z-index + translateZ(0) layer-promotion is here too, even though this specific popover wasn't
   the one reported. */
.clip-card__score-popover {
  position: fixed; z-index: 2147483647; transform: translateZ(0);
  width: max-content; max-width: min(260px, 82vw);
  padding: 8px 10px; font-family: var(--font-sans); font-size: 12px; font-weight: 400;
  line-height: 1.4; color: var(--ink); text-align: left;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--r-md); box-shadow: var(--shadow-elevated);
}
.clip-card__score-popover[hidden] { display: none; }
/* The 4 judged dimensions (hook/flow/value/relevance) as small chips above the free-text reason -
   see clips/_clip.html.erb. A plain wrapping flex row: 4 short label+number pairs never need a
   grid's alignment machinery, and wrapping is the right fallback at the popover's own narrow
   max-width (min(260px, 82vw)) rather than forcing a cramped 4-across row. */
.clip-card__score-breakdown { display: flex; flex-wrap: wrap; gap: 4px 8px; margin-bottom: 6px; }
.clip-card__score-dim {
  font-size: 11px; color: var(--ink-3); background: var(--surface-2);
  border-radius: var(--r-sm); padding: 2px 6px;
}
.clip-card__score-dim b { color: var(--ink); font-weight: 600; }
.clip-card__score-why { margin: 0; }

/* Right-edge action rail (TikTok-style) - like/dislike + share, stacked top to bottom over the
   video's right edge, vertically centered. Gray by default, crimson on hover/active - same
   language as the rest of the site's icon buttons (.lib-actions/.lib-star etc.), just over dark
   video instead of a light card, so it gets its own translucent-dark pill background instead of
   .lib-actions' plain transparent one (a plain gray icon would disappear against footage). */
.clip-card__rail {
  position: absolute; top: 50%; right: 10px; transform: translateY(-50%); z-index: 3;
  display: flex; flex-direction: column; gap: 18px; /* room for .clip-card__railcount below a
  button (god-only) without it overlapping the next one down */
}
.clip-card__railbtn {
  position: relative; width: 34px; height: 34px; padding: 0;
  display: grid; place-items: center; border: 0; border-radius: 50%; cursor: pointer;
  background: rgba(0, 0, 0, .4); color: #fff;
  transition: background 140ms ease, color 140ms ease;
}
/* (hover: hover) - a touch tap satisfies a plain :hover selector too, and with no mouse to move
   away afterward the "hover" state just sticks until something else is tapped (reported: the
   like/dislike/share icon staying lit red permanently after one tap on mobile). Gating on real
   hover capability restores the plain, un-lit resting state there instead. */
@media (hover: hover) {
  .clip-card__railbtn:hover { background: rgba(0, 0, 0, .55); color: var(--primary); }
}
.clip-card__railbtn.is-active { color: var(--primary); }
.clip-card__railbtn.is-active svg { fill: currentColor; }
.clip-card__railcount {
  position: absolute; bottom: -14px; left: 50%; transform: translateX(-50%);
  font-family: var(--font-mono); font-size: 10px; font-weight: 600; color: #fff;
  text-shadow: 0 1px 2px rgba(0, 0, 0, .6);
}

/* Round delete/edit chips, flowing NORMALLY inside .clip-card__topbar-actions's own flex row
   (margin-left:auto there pins the whole pair to the right) - NOT position:absolute. That used
   to be this pair's base rule (a leftover from before .clip-card__topbar-actions existed to lay
   them out at all), which took them out of flow entirely: their OWN independent top:8px offset,
   relative to whichever positioned ancestor they landed on, had nothing to do with the score
   badge's flex-centered position in the SAME row, and .clip-card__topbar-actions itself collapsed
   to a zero-size box (an absolutely-positioned child contributes nothing to a flow parent's own
   size) - confirmed via getBoundingClientRect on a real card: the badge and this pair centered
   ~10px apart vertically, not on the same line despite align-items:center right above them. The
   touch/coarse-pointer media query below already reset this pair to static for exactly this
   reason - this was simply never applied on desktop too. */
.clip-card__removewrap { z-index: 3; margin: 0; }
.clip-card__remove {
  display: grid; place-items: center; width: 28px; height: 28px; padding: 0;
  border: 0; border-radius: 50%; cursor: pointer;
  background: rgba(0, 0, 0, .55); color: #fff;
  transition: background 140ms ease;
}
.clip-card__remove:hover { background: var(--st-red, #dc2626); }
.clip-card__remove svg { width: 15px; height: 15px; flex: none; }
.clip-card__edit { z-index: 3; }
.clip-card__edit:hover { background: var(--primary); }

/* AI-generated caption + hashtags under a clip, with one inline copy icon at the top-right. */
.clip-card__caption { position: relative; padding-right: 24px; }
.clip-card__desc { margin: 10px 0 0; font-size: 13px; line-height: 1.5; color: var(--ink-2); }
.clip-card__copy {
  position: absolute; top: 6px; right: 0; display: inline-flex; padding: 2px; border: 0; background: none;
  color: var(--muted); cursor: pointer; border-radius: var(--r-sm);
}
.clip-card__copy:hover { color: var(--primary); }
.clip-card__copy.is-copied { color: var(--st-green, #16a34a); }
/* Green "Copied!" pill that flashes to the left of the icon so the tap is clearly confirmed. */
.clip-card__copied {
  position: absolute; top: 50%; right: calc(100% + 6px); transform: translateY(-50%);
  padding: 2px 7px; border-radius: 999px; background: var(--st-green, #16a34a); color: #fff;
  font-size: 11px; font-weight: 600; white-space: nowrap; opacity: 0; pointer-events: none;
  transition: opacity .12s ease;
}
.clip-card__copy.is-copied .clip-card__copied { opacity: 1; }
.clip-card__tags { display: flex; flex-wrap: wrap; align-items: center; gap: 5px; margin-top: 8px; }
.clip-card__hashtag { font-size: 11.5px; font-weight: 500; color: var(--primary); background: var(--primary-tint); padding: 2px 7px; border-radius: 999px; }

/* Share / download / delete row under a clip. Share leads (primary), the rest are square icon buttons. */
/* flex-wrap - the row is otherwise all small icon-only buttons (~32px), but the "Remove
   watermark" upsell button carries a text label and doesn't reliably fit alongside them at the
   card's fixed width; without this it silently overflowed and got clipped by .clip-card's own
   overflow:hidden (confirmed visually, 2026-08-25) instead of wrapping to its own line. */
.clip-card__actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
/* Its own row below .clip-card__actions (see clips/_clip.html.erb) - a compact tinted chip, not a
   full .btn, so it reads as a small tip/upsell rather than one more icon button competing with
   share/publish for attention. white-space:nowrap + inline-flex keeps the icon and label on one
   line regardless of the card's own width. */
.clip-card__watermark-upsell {
  display: inline-flex; align-items: center; gap: 5px; width: fit-content;
  margin-top: 8px; padding: 5px 10px; white-space: nowrap;
  font-size: 12px; font-weight: 600; color: var(--primary);
  background: var(--primary-tint); border-radius: var(--r-pill);
  text-decoration: none;
}
.clip-card__watermark-upsell svg { flex: none; }
.clip-card__watermark-upsell:hover { background: var(--primary); color: #fff; }
/* Share is an icon-only button now (so Share + TikTok + Instagram + YouTube + download all fit on
   one line). The popover keeps a readable width even though its trigger is only 36px wide. */
.clip-card__sharewrap { position: relative; flex: 0 0 auto; }
.clip-card__del { margin: 0; }
.btn--icon { width: 36px; padding: 0; flex: 0 0 auto; }
.btn--icon svg { width: 16px; height: 16px; }
/* "Posted" state: keep the platform post button clickable, add a green check badge in its corner. */
.clip-card__post { position: relative; }
.btn--icon.is-posted { position: relative; }
.clip-card__check {
  position: absolute; top: -5px; right: -5px; z-index: 1;
  display: grid; place-items: center; width: 16px; height: 16px;
  border-radius: 50%; background: #16a34a; color: #fff;
  border: 2px solid var(--surface); pointer-events: none;
}
.clip-card__check svg { width: 10px; height: 10px; }

/* ── Clip trim editor (editor_controller.js) ─────────────────────────────────────────────────── */
/* Sidebar (tabs + panel) left, player centered right - the timeline/handles stay BELOW both (see
   clips/edit.html.erb), spanning the full width, since dragging a handle needs all the room it
   can get regardless of how narrow the player itself is. */
/* Full panel width, NOT capped-and-centered: a max-width here centers the whole trio and pulls
   the left column away from the page heading it should line up with. The air around the video is
   controlled by giving the video itself a bigger frame instead (see .editor__stage-inner) - the
   side columns stay pinned to the panel edges where they belong. */
/* Editor shell: three stretch-height columns (details | preview canvas | assets) over a
   full-width timeline, the layout every video editor uses. align-items: stretch, not flex-start,
   so the three read as one continuous shell instead of three cards of different heights with
   page background showing between them. */
.editor__layout { display: flex; gap: 18px; align-items: stretch; flex-wrap: wrap; margin-bottom: 18px; }
.editor__sidebar { flex: 0 1 340px; min-width: 0; }
.editor__sidebar.editor__col {
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 14px;
}
.editor__stage { flex: 1 1 260px; display: flex; justify-content: center; min-width: 0; }
/* Takes over .editor__stage's own former slot/sizing in .editor__layout's flex row, now that the
   asset rail needs to sit INSIDE the same data-controller="asset-stage" wrapper as the stage (see
   clips/edit.html.erb's own comment) rather than as .editor__stage's own flex sibling. */
.editor__stage-and-rail { display: flex; gap: 18px; align-items: stretch; flex: 1 1 560px; min-width: 0; }
/* The middle column is a real PREVIEW CANVAS, not a transparent slot holding a video. That is
   the whole fix for "big white patches": the space around a 9:16 frame is unavoidable on a wide
   screen, so it has to read as the editor's stage - dark, bounded, deliberate - the way it does
   in CapCut/Premiere/Resolve. Left as page background it read as a page that had failed to
   finish loading. It stays flexible on purpose: that surplus is where playback controls and the
   next round of preview tooling go. */
.editor__stage-and-rail .editor__stage {
  flex: 1 1 auto; flex-direction: column; align-items: center; justify-content: center; gap: 14px;
  background: #131316; border: 1px solid #26262b; border-radius: var(--r-lg); padding: 22px 20px;
  min-height: 480px;
}
/* Capped, not full-width - a 9:16 clip doesn't need to fill the same space a much wider sidebar+
   player layout now has to offer; a giant vertical video next to a narrow tools panel read
   lopsided. */
/* .editor__stage-inner (below) is FIXED at the real 9:16 output aspect (reuses .settings-preview__
   frame - same shape/background/rounding the settings page previews against) - the video fills it
   via object-fit/transform (editor#renderCrop), so it's absolutely positioned to the frame, not
   sized/flowed like a plain block image anymore. */
/* cursor: pointer - the video is click-to-play/pause (see clips/edit.html.erb; it carries no
   native controls, and the play button hides itself during playback). */
.editor__video { position: absolute; inset: 0; width: 100%; height: 100%; display: block; object-fit: contain; transition: transform 150ms ease; cursor: pointer; }
/* Sized off the canvas HEIGHT, not a flat pixel width: the frame is 9:16, so driving it from
   width makes the page grow ~1.8x faster than it gets wider. Capping the height instead keeps
   the preview as large as the viewport comfortably allows without the page running away.
   Two classes deep on purpose - .settings-preview__frame (shared with the settings pages) sets
   `width: 100%`, same specificity but later in this file, so a single-class rule here loses and
   the frame stretches to the canvas width instead of staying 9:16. */
.editor__stage .editor__stage-inner {
  width: auto; height: min(58vh, 600px); max-width: 100%; aspect-ratio: 9 / 16; margin: 0;
}
/* Canvas footer - the readout belongs to the preview, so it lives on the dark surface with it
   rather than floating on white between the columns and the timeline. */
.editor__stage-bar {
  display: flex; align-items: center; justify-content: space-between; gap: 16px; width: 100%;
  max-width: 420px; font-family: var(--font-mono); font-size: 11.5px; color: #8b8b93;
}
.editor__stage-bar output { color: #f2f2f5; font-weight: 600; }

/* The editor is three columns: clip details | video | assets. It used to carry a fourth, an
   empty 260px .editor__stage-spacer whose only job was to counterweight the rail so the video
   sat dead-center of the row. That balanced the arithmetic and broke the layout: it opened a
   visibly blank column between the sidebar and the video, which read as the page having failed
   to load something. Centering the video BETWEEN the two side columns (which is what the stage's
   own flex:1 + margin:auto inner box already does) is both what "centered" should mean here and
   what removes the hole - so the spacer is gone rather than resized. */
.editor__col-heading {
  display: block; font-family: var(--font-mono); font-size: 10px; letter-spacing: .07em;
  text-transform: uppercase; color: var(--muted); margin-bottom: 10px;
}
/* Each side column gets its own quiet surface so the trio reads as three deliberate groups
   rather than a bordered panel with two sets of loose controls floating next to it. */
.editor__col--rail {
  flex: 0 1 280px; min-width: 0; background: var(--bg);
  border: 1px solid var(--line); border-radius: var(--r-lg); padding: 14px;
}

/* Asset rail (shared/_asset_rail.html.erb) - a wrapping grid of small clickable previews. The
   flex-basis lives on .editor__asset-picker (the partial's own OUTER element, and the actual flex
   ITEM here - .editor__asset-rail itself is a grandchild once the picker wraps it with its search
   box, so sizing it directly was a no-op: `flex` only does anything on a direct flex child).
   Widened from an earlier 64px (a single cramped column) so multiple thumbnails fit per row and
   the search box has real room to read as a search box, not a token gesture at one. */
.editor__col--rail .editor__asset-picker { width: 100%; }
.editor__asset-rail { display: flex; flex-wrap: wrap; gap: 8px; }
.editor__asset-thumb {
  width: 64px; height: 64px; padding: 4px; border: 1px solid var(--line-strong); border-radius: var(--r-md);
  background: var(--surface); cursor: pointer; display: flex; align-items: center; justify-content: center;
  transition: border-color 140ms ease; flex-shrink: 0;
}
.editor__asset-thumb:hover { border-color: var(--primary); }
.editor__asset-thumb img { max-width: 100%; max-height: 100%; object-fit: contain; }
.asset-stage--full .editor__asset-thumb { opacity: .4; pointer-events: none; }
.editor__asset-thumb[hidden] { display: none; } /* [hidden] must beat the display:flex above */
.editor__asset-rail--empty { max-width: 180px; }
.editor__asset-picker { display: flex; flex-direction: column; gap: 10px; }
.search-field--sm { max-width: 220px; }
.search-field--sm .input.search-field__input { font-size: 12.5px; padding: 7px 10px 7px 30px; }

/* Asset stage handle (asset_stage_controller.js) - left/top position the CENTER (translate(-50%,
   -50%)), same point-anchoring convention .settings-preview__watermark already uses. Corner
   controls sit INSIDE the handle's own box (not poking outside via negative offsets) - the
   stage frame itself is overflow:hidden (see .settings-preview__frame), so anything placed
   outside the handle's bounds would be clipped and unclickable near a frame edge. */
.asset-stage__handle {
  position: absolute; transform: translate(-50%, -50%); cursor: grab; touch-action: none;
}
.asset-stage__handle.is-dragging { cursor: grabbing; }
.asset-stage__handle img { display: block; width: 100%; height: auto; pointer-events: none; }
/* Non-interactive "Preview" overlay (preview_controller.js#renderAssetGhosts) - a bare <img>
   reusing .asset-stage__handle's own position/size math, never a real editable handle (see its
   own comment for why: previewing a DIFFERENT template must never touch the actual submittable
   stage). */
.asset-stage__handle--ghost { display: block; width: 100%; height: auto; cursor: default; pointer-events: none; opacity: .92; }
.asset-stage__remove {
  position: absolute; top: 2px; right: 2px; width: 18px; height: 18px; padding: 0; border: 0; border-radius: 50%;
  display: grid; place-items: center; background: rgba(0, 0, 0, .55); color: #fff; font-size: 13px; line-height: 1;
  cursor: pointer;
}
.asset-stage__remove:hover { background: var(--st-red, #dc2626); }
.asset-stage__resize {
  position: absolute; bottom: 2px; right: 2px; width: 12px; height: 12px; border-radius: 2px;
  background: rgba(255, 255, 255, .85); border: 1px solid rgba(0, 0, 0, .35); cursor: nwse-resize;
}
/* A small asset used to be SHORTER than the two fixed-size corner controls stacked inside it (a
   real case on prod: a 165x32 logo handle holding an 18px remove button above a 12px resize grip),
   so they overlapped and read as if they had swapped places - reported as the controls being
   upside down until the asset was resized bigger. asset_stage_controller.js measures each handle
   and adds this class when either dimension gets tight; the controls then shrink and tuck into the
   corners so both stay whole and grabbable. Driven from JS rather than a container query because
   the binding constraint is usually the handle's HEIGHT, and a height container query would need
   container-type:size - which contains the very intrinsic sizing the image inside relies on. */
.asset-stage__handle--compact .asset-stage__remove {
  top: 1px; right: 1px; width: 13px; height: 13px; font-size: 10px;
}
.asset-stage__handle--compact .asset-stage__resize { bottom: 1px; right: 1px; width: 9px; height: 9px; }
/* A plain sibling of .editor__video, never a descendant - the whole point is staying OUTSIDE its
   transform (see the video tag's own comment in clips/edit.html.erb for why native `controls`
   couldn't stay). Same circular-button look as .player__play--sm. */
.editor__play {
  width: 46px; height: 46px; border-radius: 50%; background: rgba(255,255,255,.9);
  display: grid; place-items: center; position: absolute; left: 50%; top: 50%;
  transform: translate(-50%,-50%); border: none; cursor: pointer; z-index: 2;
}
.editor__play svg { color: var(--ink); margin-left: 3px; }
.editor__play[hidden] { display: none; }

.editor__tabs { display: flex; gap: 4px; margin-bottom: 14px; border-bottom: 1px solid var(--line); }
.editor__tab {
  padding: 8px 4px; margin-bottom: -1px; border: none; background: none; cursor: pointer;
  font-size: 13px; font-weight: 600; color: var(--muted); border-bottom: 2px solid transparent;
}
.editor__tab.is-active { color: var(--ink); border-bottom-color: var(--primary); }
.editor__panel[hidden] { display: none; }

/* Caption transcript - ONE flowing paragraph, not a list of boxed rows (a real clip can have 50+
   cues; a row-per-cue layout falls apart at that scale). Each cue is its own small contenteditable
   span (clicking seeks the player there AND places the cursor - editor#seekToCue) but reads as
   continuous text; the word playing right now highlights itself (editor#highlightActiveCue, on
   the video's own timeupdate) and scrolls into view, same pattern as Descript/Submagic's own
   transcript editors. */
.editor__transcript {
  max-height: 360px; overflow-y: auto; padding: 10px 12px; font-size: 14px; line-height: 1.9;
  border: 1px solid var(--line); border-radius: var(--r-md); background: var(--surface);
}
.editor__cue { border-radius: 3px; padding: 1px 2px; cursor: text; outline: none; }
.editor__cue:hover { background: var(--surface-2); }
.editor__cue:focus { background: var(--surface-2); box-shadow: 0 0 0 1px var(--line-strong); }
.editor__cue.is-active { background: var(--primary-tint); color: var(--primary); font-weight: 600; }
.editor__cues-empty { font-size: 13px; color: var(--muted); }
.editor__timeline {
  position: relative; height: 64px; border-radius: var(--r-sm); overflow: hidden;
  background: var(--ink); user-select: none; touch-action: none;
}
.editor__filmstrip { position: absolute; inset: 0; display: flex; }
.editor__frame { flex: 1 1 0; height: 100%; width: 100%; object-fit: cover; display: block; }
/* The selection div itself is transparent (just a positioning anchor for the two handles) - the
   filmstrip stays fully visible underneath everywhere; a real "dim outside the selection" look
   would need two more layers (chose not to for a first pass - see task #69's own scope notes). */
.editor__selection { position: absolute; top: 0; bottom: 0; box-shadow: 0 0 0 2px var(--primary) inset; }
.editor__handle {
  position: absolute; top: 0; bottom: 0; width: 14px; margin-left: -7px;
  background: var(--primary); cursor: ew-resize; touch-action: none;
  display: flex; align-items: center; justify-content: center;
}
.editor__handle::after { content: ""; width: 3px; height: 18px; border-radius: 2px; background: #fff; opacity: .85; }
.editor__handle--start { left: 0; }
.editor__handle--end { left: 100%; }
/* A save is already in flight (editor_controller.js#onSubmitStart) - startDrag itself is the real
   guard, this just makes it READ as locked instead of silently ignoring further drags. */
.editor__timeline--locked { opacity: .5; }
.editor__timeline--locked .editor__handle { cursor: not-allowed; }

/* Share popover menu. position:fixed (viewport coords, set by share_controller.js#position) so
   it's never clipped by an ancestor's overflow:hidden, regardless of where the trigger sits.
   The trigger itself moved back to the action row below the video (see _clip.html.erb's comment) -
   it used to sit in the right-edge rail, directly on top of the <video>, and the menu never
   visibly appeared at all ("maybe it's under the video" - a real, plausible diagnosis: a
   hardware-decoded <video> can get composited into its own top-level surface that ignores the
   page's normal z-index stacking entirely). That never got fully confirmed fixed, so the trigger
   moved off the video instead of continuing to chase it there - z-index/translateZ(0) below are
   kept as a harmless belt-and-suspenders regardless. */
.clip-card__menu {
  position: fixed; z-index: 2147483647; transform: translateZ(0);
  width: max-content; min-width: 220px; max-width: min(280px, 78vw);
  display: flex; flex-direction: column; padding: 6px;
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: var(--r-md); box-shadow: var(--shadow-elevated);
}
.clip-card__menu[hidden] { display: none; } /* [hidden] must beat the display:flex above */
.clip-card__menuitem {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 8px 10px; border: 0; background: none; border-radius: var(--r-sm);
  font-size: 13px; font-weight: 500; color: var(--ink); text-align: left; cursor: pointer;
}
.clip-card__menuitem:hover { background: var(--surface-2); }
.clip-card__menuitem svg { width: 15px; height: 15px; flex: none; color: var(--muted); }
.clip-card__menuitem.is-copied { color: var(--st-green); }
.clip-card__menuitem.is-copied svg { color: var(--st-green); }
.clip-card__menuhint {
  display: flex; align-items: flex-start; gap: 8px; padding: 8px 10px;
  font-size: 11.5px; line-height: 1.4; color: var(--muted); border-top: 1px solid var(--divider);
  margin-top: 4px;
}
.clip-card__menuhint svg { flex: none; margin-top: 1px; color: var(--muted); }
.clip-card__menuhint[hidden] { display: none; }

/* ---- Contact chat widget (bottom-left) ------------------------------------- */
.chat-widget { position: fixed; left: 24px; bottom: 24px; z-index: 900; }
.chat-widget__btn {
  list-style: none; cursor: pointer; width: 52px; height: 52px; border-radius: 50%;
  background: var(--primary); color: #fff; display: grid; place-items: center;
  box-shadow: 0 8px 24px rgba(200,30,58,.35); transition: transform 140ms ease, background 140ms ease;
}
.chat-widget__btn::-webkit-details-marker { display: none; }
.chat-widget__btn:hover { background: var(--primary-hover); transform: translateY(-2px); }
.chat-widget[open] .chat-widget__btn { background: var(--ink); box-shadow: var(--shadow-elevated); }
.chat-widget__panel {
  position: absolute; left: 0; bottom: 64px; width: min(320px, calc(100vw - 48px));
  background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--r-lg);
  box-shadow: var(--shadow-elevated); padding: 18px;
  animation: toast-in 200ms cubic-bezier(.2,.8,.2,1);
}
.chat-widget__head { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.chat-widget__title { font-weight: 600; font-size: 14px; }
.chat-widget__sub { font-size: 11.5px; color: var(--muted); }
.chat-widget__text { font-size: 13px; color: var(--ink-2); line-height: 1.5; margin: 0 0 14px; }

/* ---- Marketing / landing --------------------------------------------------- */
.marketing { display: flex; flex-direction: column; min-height: 100dvh; }
.marketing-nav { position: sticky; top: 0; z-index: 50; height: 62px; display: flex; align-items: center; gap: 4px; padding: 0 40px; background: rgba(250,250,250,.8); backdrop-filter: blur(10px); border-bottom: 1px solid var(--line); }
.marketing-nav__brand { display: flex; align-items: center; gap: 9px; }
.marketing-nav__brand .brand-mark { width: 26px; height: 26px; }
.marketing-nav__links { display: flex; align-items: center; gap: 4px; margin-left: 36px; }
.marketing-nav__links a { font-size: 13.5px; color: var(--ink-2); padding: 6px 11px; border-radius: 7px; }
.marketing-nav__links a:hover { background: #f0f0f1; color: var(--ink); }
.marketing-nav__spacer { flex: 1; }
.marketing-nav__cta { display: flex; align-items: center; gap: 10px; }
.marketing-nav__cta a.signin { font-size: 13.5px; font-weight: 500; color: var(--ink); padding: 8px 12px; }

.hero { position: relative; padding: 88px 40px 56px; background-image: radial-gradient(var(--line-strong) 1px, transparent 1px); background-size: 22px 22px; background-position: center top; }
.hero__fade { position: absolute; inset: 0; background: linear-gradient(to bottom, transparent, var(--bg) 78%); pointer-events: none; }
.hero__inner { position: relative; max-width: 880px; margin: 0 auto; text-align: center; }
.hero__pill { display: inline-flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 11.5px; font-weight: 500; color: var(--ink-2); background: var(--surface); border: 1px solid var(--line); border-radius: 20px; padding: 6px 13px; margin-bottom: 26px; }
.hero__pill .dot { width: 6px; height: 6px; border-radius: 50%; background: #16a34a; animation: pulseDot 2s ease-in-out infinite; }
.hero h1 { font-size: 62px; line-height: 1.02; font-weight: 700; letter-spacing: -0.045em; margin-bottom: 22px; text-wrap: balance; }
.hero__sub { font-size: 18px; line-height: 1.55; color: var(--ink-2); max-width: 600px; margin: 0 auto 34px; }
.hero__cta { display: flex; align-items: center; justify-content: center; gap: 12px; margin-bottom: 18px; flex-wrap: wrap; }
.hero__micro { font-family: var(--font-mono); font-size: 12px; color: var(--muted); }
/* The "we are not a campaign provider" line: present in the hero on purpose (the page quotes real
   campaign rates further down), but a size below the free-credits line so it reads as scope, not
   as a warning. */
.hero__micro--fine { font-size: 11px; opacity: .78; max-width: 560px; margin-inline: auto; line-height: 1.5; }
.hero__scroll-cue { display: flex; flex-direction: column; align-items: center; gap: 2px; margin-top: 28px; font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
.hero__scroll-chevron { display: inline-flex; transform: rotate(90deg); animation: scroll-cue-bounce 1.6s ease-in-out infinite; }
@keyframes scroll-cue-bounce { 0%, 100% { transform: rotate(90deg) translateX(0); } 50% { transform: rotate(90deg) translateX(4px); } }
@media (prefers-reduced-motion: reduce) {
  .hero__scroll-chevron { animation: none; }
}

/* Before/After player */
.player { position: relative; max-width: 880px; margin: 10px auto 0; }
.player__frame { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-xl); box-shadow: var(--shadow-elevated); overflow: hidden; }
.player__bar { display: flex; align-items: center; gap: 10px; height: 52px; padding: 0 16px; border-bottom: 1px solid var(--divider); }
.player__dots { display: flex; gap: 6px; }
.player__dots span { width: 11px; height: 11px; border-radius: 50%; }
.player__file { font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); }
.player__toggle { margin-left: auto; display: flex; background: var(--surface-2); border-radius: 9px; padding: 3px; gap: 2px; }
.player__toggle button { height: 30px; padding: 0 14px; border: none; border-radius: 7px; font-size: 12.5px; font-weight: 450; color: var(--ink-3); background: transparent; }
.player__toggle button[aria-pressed="true"] { color: var(--ink); font-weight: 600; background: var(--surface); box-shadow: 0 1px 2px rgba(0,0,0,.1); }
.player__stage { position: relative; height: 430px; background: var(--ink); display: flex; align-items: center; justify-content: center; gap: 34px; overflow: hidden; }
.player__layer { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 34px; transition: opacity .45s ease, transform .45s ease; }
.player__layer[hidden] { display: flex; opacity: 0; transform: scale(.96); pointer-events: none; }
.player__16x9 { position: relative; width: 620px; height: 349px; border-radius: 10px; background: repeating-linear-gradient(135deg,#2a2a2e,#2a2a2e 9px,#242427 9px,#242427 18px); display: flex; align-items: center; justify-content: center; }
.player__9x16 { position: relative; width: 218px; height: 388px; border-radius: var(--r-lg); background: repeating-linear-gradient(135deg,#2a2a2e,#2a2a2e 9px,#242427 9px,#242427 18px); border: 1px solid #3f3f46; flex-shrink: 0; }
/* Optional Kling loop behind the play button/caption/tag overlays - falls back to the plain
   striped placeholder above when marketing_video_path finds nothing yet. */
.player__bg-video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
.player__play { width: 54px; height: 54px; border-radius: 50%; background: rgba(255,255,255,.9); display: grid; place-items: center; }
.player__play svg { color: var(--ink); margin-left: 3px; }
.player__play--sm { width: 46px; height: 46px; position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); }
.player__cap { position: absolute; bottom: 18px; left: 50%; transform: translateX(-50%); font-size: 15px; font-weight: 600; color: #fff; background: rgba(0,0,0,.5); padding: 6px 14px; border-radius: var(--r-sm); white-space: nowrap; }
.player__cap--es { bottom: 16px; left: 12px; right: 12px; transform: none; text-align: center; white-space: normal; font-size: 13.5px; font-weight: 700; background: none; text-shadow: 0 2px 4px rgba(0,0,0,.5); padding: 0; line-height: 1.3; }
.player__tag { position: absolute; top: 10px; left: 10px; font-family: var(--font-mono); font-size: 9.5px; color: #fff; background: var(--primary); border-radius: 5px; padding: 3px 6px; }
.player__src-tag { position: absolute; top: 12px; left: 12px; font-family: var(--font-mono); font-size: 10.5px; color: var(--muted); background: rgba(0,0,0,.4); border-radius: 5px; padding: 3px 7px; }
.player__score { position: absolute; top: 10px; right: 10px; display: flex; align-items: center; gap: 5px; background: rgba(0,0,0,.5); border-radius: 6px; padding: 3px 7px; font-family: var(--font-mono); font-size: 11px; font-weight: 600; color: #fff; }
.player__score svg { width: 12px; height: 12px; color: var(--st-orange); }
.clip-card__score-tier { font-weight: 500; opacity: .75; padding-left: 5px; border-left: 1px solid rgba(255,255,255,.25); }
.player__chips { display: flex; flex-direction: column; gap: 11px; align-items: flex-end; }
.player__chip { display: flex; align-items: center; gap: 8px; background: #fff; border-radius: 9px; padding: 8px 12px; box-shadow: 0 6px 18px rgba(0,0,0,.25); font-size: 12.5px; font-weight: 500; }
.player__chip svg { width: 15px; height: 15px; }

/* Earn demo (shared/_earn_demo.html.erb) - a standalone "before/after" block: the real
   marketing sample clip (snapdub-ai-video-clipper-demo, same file _player.html.erb and
   _settings_preview.html.erb already use) shown as-uploaded (16:9, same HEIGHT as the clip frame
   next to it - only the aspect ratio differs) on the left, then the SAME file re-shown in a
   .player__9x16 frame on the right - .player__bg-video's own object-fit:cover does the "cropped
   into a vertical clip" framing for free, no separate export needed. The SnapDub bug sits on top
   of the clip itself (a real watermark position, not a floating logo), and "Branded content" +
   a small arrow mark the hand-off between the two frames. */
.earn-demo { display: flex; justify-content: center; }
.earn-demo__row { display: flex; align-items: center; justify-content: center; gap: 32px; flex-wrap: wrap; }
.earn-demo__original {
  position: relative; height: 388px; aspect-ratio: 16 / 9; width: auto; border-radius: 10px; overflow: hidden; flex-shrink: 0;
  background: repeating-linear-gradient(135deg,#2a2a2e,#2a2a2e 9px,#242427 9px,#242427 18px); border: 1px solid #3f3f46;
}
.earn-demo__original video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.earn-demo__label {
  position: absolute; left: 10px; bottom: 10px; background: rgba(0, 0, 0, .6); color: #fff; font-size: 11px;
  font-weight: 600; padding: 4px 9px; border-radius: 6px; letter-spacing: .01em;
}
.earn-demo__mid { display: flex; flex-direction: column; align-items: center; gap: 8px; flex-shrink: 0; }
.earn-demo__disclosure { font-size: 11px; font-weight: 600; color: var(--ink-3); white-space: nowrap; }
.earn-demo__arrow { flex-shrink: 0; width: 30px; height: auto; color: var(--primary); }
.earn-demo__clip { margin: 0; }
.earn-demo__brand { position: absolute; top: 12px; left: 12px; height: 16px; width: auto; filter: drop-shadow(0 1px 3px rgba(0,0,0,.5)); }
@media (max-width: 640px) {
  .earn-demo__row { gap: 16px; }
  .earn-demo__original { height: 267px; }
  .earn-demo__mid { flex-direction: row-reverse; }
  .earn-demo__arrow { transform: rotate(90deg); }
}

/* Same "standalone card" treatment as .cta-band - reads as its own block, not glued to the
   copy/CTA above it. */
.earn-demo-block { max-width: 1180px; margin: 32px auto 0; padding: 36px 40px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-xl); box-shadow: var(--shadow-card); }

/* Pricing */
.section-head { max-width: 980px; margin: 0 auto 42px; text-align: center; }
.section-head .eyebrow { display: block; margin-bottom: 12px; }
.section-head h2 { margin-bottom: 12px; }
.section-head p { font-size: 16px; color: var(--ink-2); margin: 0; }
.pricing-grid { max-width: 980px; margin: 0 auto; display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; align-items: start; }

.pricing-card { position: relative; display: flex; flex-direction: column; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 26px 24px; box-shadow: var(--shadow-card); }
.pricing-card.is-highlighted { border-color: var(--primary); box-shadow: 0 12px 32px rgba(200,30,58,.12); }
.pricing-card.is-current { border-color: var(--ink-3); }
.pricing-card__badge { position: absolute; top: -11px; left: 24px; font-family: var(--font-mono); font-size: 10px; font-weight: 600; letter-spacing: 0.05em; text-transform: uppercase; color: #fff; background: var(--primary); border-radius: var(--r-pill); padding: 4px 9px; }
.pricing-card__badge--current { background: var(--ink-3); }
.pricing-card__name { font-size: 14px; font-weight: 600; margin-bottom: 4px; }
.pricing-card__blurb { font-size: 12.5px; color: var(--ink-3); line-height: 1.45; margin-bottom: 18px; min-height: 36px; }
.pricing-card__price { display: flex; align-items: baseline; gap: 4px; margin-bottom: 20px; }
.pricing-card__price b { font-family: var(--font-mono); font-size: 34px; font-weight: 600; letter-spacing: -0.03em; }
.pricing-card__price span { font-size: 13px; color: var(--muted); }
.pricing-card__cta { display: inline-flex; align-items: center; justify-content: center; width: 100%; height: 40px; border-radius: 9px; font-size: 13.5px; font-weight: 600; margin-bottom: 22px; border: 1px solid var(--line-strong); background: var(--surface); color: var(--ink); }
.pricing-card__cta[hidden] { display: none; } /* [hidden] must beat the display:inline-flex above */
.pricing-card.is-highlighted .pricing-card__cta { background: var(--primary); color: #fff; border-color: var(--primary); }
.pricing-card__cta:hover { filter: brightness(0.97); }
.pricing-card__features { display: flex; flex-direction: column; gap: 11px; }
.pricing-card__feature { display: flex; align-items: flex-start; gap: 9px; font-size: 13px; color: var(--ink-soft); line-height: 1.4; }
.pricing-card__feature svg { width: 15px; height: 15px; color: var(--primary); flex-shrink: 0; margin-top: 1px; }
.pricing-card__subtext { color: var(--muted); }

.notice { max-width: 980px; margin: 24px auto 0; display: flex; align-items: center; gap: 10px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 14px 18px; font-size: 13px; color: var(--ink-soft); line-height: 1.45; }
.notice svg { width: 18px; height: 18px; color: #16a34a; flex-shrink: 0; }

.dupe-banner { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-md); padding: 10px 14px; margin-top: 10px; font-size: 13px; color: var(--ink-soft); }
.dupe-banner[hidden] { display: none; } /* [hidden] must beat the display:flex above */
.dupe-banner__actions { display: flex; gap: 8px; flex-shrink: 0; }

/* Non-blocking "confirm your email" reminder */
.confirm-banner { display: flex; align-items: center; gap: 12px; margin: 0 0 20px; padding: 11px 14px; background: var(--st-orange-bg); border: 1px solid #f3d9c4; border-radius: var(--r-md); font-size: 13px; line-height: 1.45; color: #7c3a12; }
.confirm-banner__icon { display: inline-flex; }
.confirm-banner__icon svg { width: 16px; height: 16px; color: var(--st-orange); flex-shrink: 0; }
.confirm-banner__text { margin: 0; flex: 1; }
.confirm-banner__action { flex-shrink: 0; background: transparent; border: 1px solid #e6bd97; color: #7c3a12; padding: 5px 12px; border-radius: var(--r-pill); font-size: 12px; font-weight: 600; cursor: pointer; }
.confirm-banner__action:hover { background: #fff; border-color: var(--st-orange); color: var(--st-orange); }

/* Upload "keep this tab open" note - flex only when shown, so the `hidden` attribute still wins. */
.upload-note { color: var(--st-orange); margin: 0 0 10px; }
.upload-note:not([hidden]) { display: flex; align-items: center; gap: 6px; }
.upload-note svg { flex-shrink: 0; }

.cta-band { max-width: 980px; margin: 50px auto; text-align: center; padding: 56px 40px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-xl); box-shadow: var(--shadow-card); }
.cta-band h2 { margin: 0 0 10px; }
.cta-band p { max-width: 520px; margin: 0 auto 24px; color: var(--ink-2); }

/* Landing: how-it-works steps + feature grid + FAQ */
.steps { max-width: 980px; margin: 0 auto; display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; }
.step { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 22px; box-shadow: var(--shadow-card); }
.step__num { font-family: var(--font-mono); font-size: 12px; font-weight: 600; color: var(--primary); }
.step h3 { font-size: 14.5px; margin: 8px 0 6px; }
.step p { font-size: 12.5px; color: var(--ink-2); line-height: 1.5; margin: 0; }

/* The workflow row on the landing page describes a JOB, not just our pipeline: six steps, only two
   of which are ours. --flow drops to 3 columns (six cards in four columns leaves a broken row) and
   __who names the actor, so "SnapDub is two of them" is visible without reading the copy. */
.steps--flow { grid-template-columns: repeat(3, 1fr); max-width: 1040px; }
.step__who {
  display: inline-block; font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .07em;
  text-transform: uppercase; color: var(--muted); margin-top: 6px;
}
.step--ours { border-color: var(--primary); }
.step--ours .step__who { color: var(--primary); }
@media (max-width: 900px) { .steps--flow { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .steps--flow { grid-template-columns: 1fr; } }

/* "Build once, clip many", drawn rather than claimed: the template's own settings on the left, one
   arrow, and six IDENTICAL output frames on the right - identical being the entire point, so they
   must not be individually art-directed. The frames fade in one after another when the section
   scrolls into view (the .reveal class on the wrapper flips is-visible), which reads as a batch
   being produced rather than a static diagram. */
.tpl {
  max-width: 1040px; margin: 0 auto 34px; display: grid; gap: 22px; align-items: center;
  grid-template-columns: minmax(0, 1fr) auto minmax(0, 1.1fr);
}
.tpl__label {
  display: block; font-family: var(--font-mono); font-size: 11px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--muted); margin-bottom: 8px;
}
.tpl__card {
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg);
  padding: 16px 18px; box-shadow: var(--shadow-card); display: flex; flex-direction: column; gap: 11px;
}
.tpl__row { display: flex; align-items: center; gap: 10px; font-size: 12.5px; color: var(--ink-2); }
.tpl__dot { width: 9px; height: 9px; border-radius: 3px; flex: none; background: var(--primary); }
.tpl__dot--caption { background: #C7F036; }          /* the real default caption colour */
.tpl__dot--logo { background: var(--primary); }
.tpl__dot--layout { background: var(--ink-2); }
.tpl__dot--length { background: var(--line-strong); }
.tpl__arrow { display: flex; flex-direction: column; align-items: center; gap: 6px; color: var(--muted); }
.tpl__arrow svg { width: 40px; height: 16px; }
.tpl__arrow span { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .05em; text-align: center; max-width: 88px; line-height: 1.3; }
.tpl__batch { display: grid; grid-template-columns: repeat(6, 1fr); gap: 8px; }
.tpl__clip {
  position: relative; aspect-ratio: 9 / 16; border-radius: 7px; background: var(--ink);
  border: 1px solid var(--line-strong); overflow: hidden;
  opacity: 0; transform: translateY(8px);
  transition: opacity 420ms ease, transform 420ms ease;
  transition-delay: calc(220ms + var(--i) * 90ms);
}
.reveal.is-visible .tpl__clip, .tpl.is-visible .tpl__clip { opacity: 1; transform: none; }
.tpl__clip-logo {
  position: absolute; top: 6px; right: 6px; width: 12px; height: 12px; border-radius: 3px;
  background: var(--primary);
}
.tpl__clip-caption {
  position: absolute; left: 8%; right: 8%; bottom: 20%; height: 6px; border-radius: 3px;
  background: #C7F036;
}
@media (max-width: 900px) {
  .tpl { grid-template-columns: 1fr; }
  .tpl__arrow { flex-direction: row; gap: 10px; }
  .tpl__arrow svg { transform: rotate(90deg); }
  .tpl__arrow span { max-width: none; }
}
@media (prefers-reduced-motion: reduce) {
  .tpl__clip { opacity: 1; transform: none; transition: none; }
}

.feature-grid { max-width: 980px; margin: 0 auto; display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
.feature { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 22px; box-shadow: var(--shadow-card); }
.feature__icon { display: inline-grid; place-items: center; width: 38px; height: 38px; border-radius: 10px; background: var(--primary-tint); color: var(--primary); margin-bottom: 14px; }
.feature h3 { font-size: 15px; margin: 0 0 6px; }
.feature p { font-size: 13px; color: var(--ink-2); line-height: 1.5; margin: 0; }

/* Flagship (moment detection / captions / dubbing) vs compact (everything else) split - gives
   the differentiating features visual weight instead of 8 identical cards. */
.feature-grid--flagship { margin-bottom: 18px; }
.feature-grid--compact { grid-template-columns: repeat(4, 1fr); }
.feature-grid--compact .feature { padding: 18px; }
.feature-grid--compact .feature__icon { width: 32px; height: 32px; margin-bottom: 11px; }
.feature-grid--compact .feature h3 { font-size: 13.5px; }
.feature-grid--compact .feature p { font-size: 12px; }
/* overflow:hidden clips .feature__art's negative-margin bleed to the card's rounded top corners.
   .feature__art itself keeps the card's normal padding on every side (no padding override on the
   card), so its own margin is what makes it bleed to the edges - nothing else has to change if
   this ever goes back to holding an <img> instead of inline SVG. */
.feature--flagship { overflow: hidden; }
/* Same dark diagonal-stripe placeholder as the hero player's .player__16x9/.player__9x16 "video"
   panels - the flagship icons are hand-built inline SVG (see landing.html.erb), drawn on this
   background so they read as part of the same visual system instead of a bolted-on illustration. */
.feature__art {
  display: flex; align-items: center; justify-content: center;
  width: calc(100% + 44px); margin: -22px -22px 18px; aspect-ratio: 16 / 10;
  background: repeating-linear-gradient(135deg, #2a2a2e, #2a2a2e 9px, #242427 9px, #242427 18px);
}
.feature__art svg { width: 62%; height: auto; }
.feature--flagship .feature__icon { margin-bottom: 12px; }
/* 5 items in a 4-column grid leaves one orphan alone at the left in row 2 - center it under the
   middle two tracks instead, at the one breakpoint where 4 columns are actually active. */
@media (min-width: 861px) { .feature-grid--compact .feature:last-child { grid-column: 2 / 4; } }
@media (max-width: 860px) { .feature-grid--compact { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .feature-grid--compact { grid-template-columns: 1fr; } }

/* "How we find moments" - vision-AI section (landing.html.erb #vision). Real sample footage
   (same file the #how demo reuses) with a pure-CSS "detection box" + a stack of signal chips
   that fade in one at a time, same staggered-loop technique as .demo__caption's word reveal:
   ONE keyframe shared by every .vision__tag, each given a different animation-delay so they
   never all read the same phase of the loop at once - it's 4 independent 8s loops, offset 2s
   apart, not one choreographed 8s sequence, but at any instant that reads as "one at a time". */
.vision { max-width: 980px; margin: 0 auto; display: flex; align-items: center; gap: 48px; }
.vision__frame {
  position: relative; width: 240px; aspect-ratio: 9 / 16; flex-shrink: 0; overflow: hidden;
  border-radius: var(--r-lg); border: 1px solid #3f3f46; box-shadow: var(--shadow-elevated);
  background: repeating-linear-gradient(135deg, #2a2a2e, #2a2a2e 9px, #242427 9px, #242427 18px);
}
.vision__video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.vision__scrim { position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,.65), transparent 55%); }
/* Positioned against the actual sample clip's framing (face roughly top-9%/44%-tall) - not a
   generic centered guess, so it visibly tracks the face instead of floating in empty space. */
.vision__box {
  position: absolute; top: 9%; left: 20%; right: 18%; height: 44%;
  border: 2px solid var(--primary); border-radius: 14px;
  box-shadow: 0 0 0 4px color-mix(in srgb, var(--primary) 18%, transparent);
  animation: vision-box 2.4s ease-in-out infinite;
}
.vision__tags { position: absolute; left: 10px; right: 10px; bottom: 12px; display: flex; flex-direction: column; gap: 6px; }
.vision__tag {
  align-self: flex-start; opacity: 0; background: rgba(0, 0, 0, .6); color: #fff;
  font-size: 11.5px; font-weight: 600; padding: 5px 9px; border-radius: 20px;
  animation: vision-tag 8s ease infinite; animation-delay: calc(var(--i) * 2s);
}
.vision__signals { flex: 1; display: flex; flex-direction: column; gap: 22px; }
.vision__signal { display: flex; gap: 14px; align-items: flex-start; }
.vision__signal .feature__icon { flex-shrink: 0; }
.vision__signal h3 { font-size: 15px; margin: 0 0 4px; }
.vision__signal p { font-size: 13px; color: var(--ink-2); line-height: 1.5; margin: 0; }

/* Two-column "points beside a real component", same geometry as .vision above but holding a
   static render of the app's OWN campaign card instead of a video. Exists because four sections
   in a row (#opportunity, #campaign-ready, #also, #sources) each rendered as an identical
   three-card .feature-grid strip - the page read as one long grey stretch with no hierarchy, and
   the one genuinely campaign-specific thing on it (the parsed listing) was described in prose
   while never being shown. */
.split { max-width: 980px; margin: 0 auto; display: flex; align-items: center; gap: 48px; }
.split__visual { width: 340px; flex-shrink: 0; }
.split__caption { font-family: var(--font-mono); font-size: 11px; color: var(--muted); text-align: center; margin: 10px 0 0; }
/* .lib-meta is nowrap+ellipsis by design (short metadata lines inside the app's own cards), which
   truncates the rate/cap line in this wider static render - let it wrap here only. */
.split__visual .lib-meta { white-space: normal; overflow: visible; }
.split__points { flex: 1; display: flex; flex-direction: column; gap: 22px; }
.split__point { display: flex; gap: 14px; align-items: flex-start; }
.split__point .feature__icon { flex-shrink: 0; margin-bottom: 0; }
.split__point h3 { font-size: 15px; margin: 0 0 4px; }
.split__point p { font-size: 13px; color: var(--ink-2); line-height: 1.5; margin: 0; }
@media (max-width: 860px) {
  .split { flex-direction: column; align-items: stretch; gap: 30px; }
  .split__visual { width: 100%; max-width: 380px; margin: 0 auto; }
}

/* Quiet tier: a .feature-grid with the card chrome removed, for sections that SUPPORT the
   argument rather than carry it (#also, #sources). Same markup, visibly lower weight - so the
   page has a hierarchy instead of five equally-loud card strips. */
.feature-grid--quiet .feature { background: none; border: none; box-shadow: none; padding: 0; }
/* Chromeless items need more vertical separation than carded ones: on desktop the columns keep
   them apart, but once the grid stacks (<=860px, and one column <=560px) an 18px gap runs them
   together with nothing to mark where one ends. Only affects stacked rows - a single desktop row
   never uses row-gap. */
.feature-grid--quiet { row-gap: 32px; }
@keyframes vision-box { 0%, 100% { opacity: .55; transform: scale(1); } 50% { opacity: 1; transform: scale(1.05); } }
@keyframes vision-tag { 0% { opacity: 0; transform: translateY(6px); } 4% { opacity: 1; transform: none; } 20% { opacity: 1; } 26%, 100% { opacity: 0; } }
@media (max-width: 760px) { .vision { flex-direction: column; text-align: center; } .vision__signal { text-align: left; } }

/* ---- Automation band (#automation on the landing page) --------------------
   The one full-bleed tinted section on an otherwise white page: this is the
   differentiator the rest of the copy builds toward, so the brand crimson runs
   at strength here instead of as a small accent. Everything inside is tuned for
   that darker ground - hairlines, chips and the eyebrow all get their own
   on-band variants rather than reusing the light-surface ones. */
.auto-band {
  padding: 74px 40px 78px;
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(200, 30, 58, .10), transparent 60%),
    linear-gradient(180deg, #fdf6f7 0%, #fbeff1 100%);
  border-top: 1px solid rgba(200, 30, 58, .12);
  border-bottom: 1px solid rgba(200, 30, 58, .12);
}
.eyebrow--on-band { color: var(--primary); }

/* Narrower than .auto-points below it on purpose: the arrow stretches to fill whatever is
   left over, and at 1080px it spanned a stretch of empty band that read as a gap rather
   than a connection. */
.auto-flow { max-width: 900px; margin: 0 auto; display: flex; align-items: center; gap: 22px; }

/* The template card - the "one thing you set". Accent-bordered so it reads as the
   source the arrow flows OUT of, not just another card. */
.auto-template {
  flex: 0 0 246px; background: var(--surface); border: 1px solid rgba(200, 30, 58, .28);
  border-radius: var(--r-lg); padding: 16px 18px 14px;
  box-shadow: 0 10px 30px rgba(200, 30, 58, .10);
}
.auto-template__head { display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 14px; margin-bottom: 12px; }
.auto-template__head svg { color: var(--primary); flex: 0 0 auto; }
.auto-template__badge {
  margin-left: auto; font-family: var(--font-mono); font-size: 10px; letter-spacing: .04em;
  text-transform: uppercase; color: var(--primary); background: var(--primary-tint);
  border-radius: var(--r-pill); padding: 2px 7px;
}
.auto-template__rows { margin: 0; display: flex; flex-direction: column; gap: 0; }
.auto-template__rows > div { display: flex; justify-content: space-between; gap: 12px; padding: 7px 0; border-top: 1px solid var(--divider); }
.auto-template__rows > div:first-child { border-top: 0; }
.auto-template__rows dt { font-size: 12px; color: var(--ink-3); margin: 0; }
.auto-template__rows dd { font-size: 12px; color: var(--ink); font-weight: 500; margin: 0; text-align: right; }

.auto-arrow { flex: 1; display: flex; align-items: center; gap: 10px; min-width: 0; }
.auto-arrow__line { flex: 1; height: 1px; background: linear-gradient(90deg, rgba(200,30,58,.15), rgba(200,30,58,.45)); }
.auto-arrow__line:last-child { background: linear-gradient(90deg, rgba(200,30,58,.45), rgba(200,30,58,.15)); }
.auto-arrow__label {
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .04em; text-transform: uppercase;
  color: var(--primary); white-space: nowrap;
}

.auto-clips { flex: 0 0 auto; display: flex; gap: 12px; }
/* 9:16, the real output shape. Each frame carries the SAME three overlays - that
   repetition IS the argument the section is making. */
.auto-clip {
  position: relative; width: 116px; aspect-ratio: 9 / 16; border-radius: var(--r-md);
  overflow: hidden; background: #18181b; border: 1px solid rgba(255,255,255,.12);
  box-shadow: 0 12px 28px rgba(24, 24, 27, .18);
  transform: translateY(calc(var(--i, 0) * -6px));
}
.auto-clip__video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.auto-clip__mark { position: absolute; top: 7px; right: 7px; width: 16px; height: 16px; opacity: .95; }
.auto-clip__mark img { width: 100%; height: 100%; display: block; }
.auto-clip__title {
  position: absolute; top: 8px; left: 8px; right: 28px; font-size: 8.5px; font-weight: 700;
  line-height: 1.25; color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,.7);
}
.auto-clip__caption {
  position: absolute; bottom: 12px; left: 8px; right: 8px; text-align: center;
  font-size: 8.5px; font-weight: 800; text-transform: uppercase; letter-spacing: .02em;
  color: #fff; background: rgba(200, 30, 58, .92); border-radius: 3px; padding: 3px 4px;
}

.auto-points {
  max-width: 1080px; margin: 44px auto 0; display: grid; gap: 26px;
  grid-template-columns: repeat(3, 1fr);
}
.auto-point { display: flex; gap: 13px; align-items: flex-start; }
.auto-point__icon {
  flex: 0 0 auto; display: inline-grid; place-items: center; width: 34px; height: 34px;
  border-radius: 10px; background: var(--surface); color: var(--primary);
  border: 1px solid rgba(200, 30, 58, .22);
}
.auto-point h3 { font-size: 14.5px; margin: 0 0 4px; }
.auto-point p { font-size: 13px; color: var(--ink-2); line-height: 1.5; margin: 0; }

/* Below ~1000px the arrow has nothing useful to span, so the flow stacks and the
   clip row scrolls sideways in its OWN container rather than the page body. */
@media (max-width: 1000px) {
  .auto-flow { flex-direction: column; gap: 20px; }
  .auto-template { flex: 0 0 auto; width: 100%; max-width: 320px; }
  .auto-arrow { width: 100%; max-width: 320px; }
  .auto-clips { max-width: 100%; overflow-x: auto; padding-bottom: 6px; }
  .auto-clip { transform: none; }
}
@media (max-width: 760px) {
  .auto-band { padding: 54px 20px 58px; }
  .auto-points { grid-template-columns: 1fr; gap: 20px; margin-top: 34px; }
}
@media (prefers-reduced-motion: reduce) { .auto-clip { transform: none; } }

/* ---- SEO landing pages (SeoController) -----------------------------------
   These pages are built from the landing page's own vocabulary - .section-head,
   .feature-grid, .cta-band, .faq, .auto-band - so they stay visually part of the
   site rather than becoming a separate template. Only the few things the landing
   page solved with inline styles get real classes here. */
.seo-section { padding: 62px 40px 20px; }
.seo-section--tight { padding-top: 34px; }
.seo-hero { padding: 74px 40px 10px; }
.seo-hero .section-head { margin-bottom: 26px; }
.seo-hero h1 { font-size: clamp(29px, 4vw, 45px); line-height: 1.1; letter-spacing: -.028em; margin: 8px 0 14px; text-wrap: balance; }
.seo-hero .section-head > p { font-size: 16.5px; max-width: 60ch; margin-inline: auto; }
.seo-fineprint { font-size: 12.5px; color: var(--muted); margin: 14px 0 0; }

/* A readable measure for the body copy these pages carry more of than the landing does. */
/* One-line contextual pointers from the landing page into the SEO pages. Deliberately quiet
   and inside a real sentence - a block of keyword links would read as SEO furniture on what is
   primarily a conversion page. */
.landing-crosslinks { max-width: 700px; margin: 26px auto 0; text-align: center; font-size: 13.5px; color: var(--ink-3); }
.landing-crosslinks a { color: var(--primary); }

.seo-prose { max-width: 720px; margin: 0 auto; }
.seo-prose p, .seo-prose li { font-size: 15px; color: var(--ink-2); line-height: 1.65; }
.seo-prose p { margin: 0 0 14px; }
.seo-prose ul { padding-left: 20px; margin: 0 0 16px; }
.seo-prose li { margin-bottom: 7px; }
.seo-prose li strong, .seo-prose p strong { color: var(--ink); font-weight: 620; }
.seo-prose h3 { font-size: 17px; letter-spacing: -.012em; margin: 30px 0 10px; }

/* Numbered workflow steps. The numbering is real information here (these are ordered
   procedures), not decoration - which is why it's an <ol> with visible counters. */
.seo-steps { counter-reset: seo-step; list-style: none; max-width: 720px; margin: 0 auto; padding: 0; }
.seo-steps > li { counter-increment: seo-step; position: relative; padding-left: 46px; margin-bottom: 20px; }
.seo-steps > li::before {
  content: counter(seo-step); position: absolute; left: 0; top: -1px; width: 30px; height: 30px;
  border-radius: 50%; background: var(--primary); color: #fff; font-family: var(--font-mono);
  font-size: 13px; font-weight: 600; display: grid; place-items: center;
}
.seo-steps b { display: block; font-size: 15px; color: var(--ink); margin-bottom: 3px; }
.seo-steps p { font-size: 14px; color: var(--ink-2); margin: 0; line-height: 1.55; }

/* The campaign disclaimer. Deliberately quiet rather than a warning box - it's a standing
   legal clarification on every campaign page, not an alert about something going wrong. */
.seo-disclaimer { max-width: 720px; margin: 26px auto 0; padding: 14px 18px; background: var(--surface-2); border: 1px solid var(--line); border-radius: var(--r-md); font-size: 12.5px; color: var(--ink-3); line-height: 1.55; }

@media (max-width: 760px) {
  .seo-section { padding: 42px 20px 14px; }
  .seo-hero { padding: 44px 20px 6px; }
  .seo-steps > li { padding-left: 40px; }
}

.faq { max-width: 760px; margin: 0 auto; display: flex; flex-direction: column; gap: 12px; }
.faq__item { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-md); padding: 18px 20px; }
.faq__item h3 { font-size: 14px; margin: 0; }
.faq__item p { font-size: 13px; color: var(--ink-2); line-height: 1.55; margin: 12px 0 0; }
/* <details>/<summary> accordion (was plain always-open divs) - native disclosure, no JS. */
.faq__item summary { display: flex; align-items: center; justify-content: space-between; gap: 12px; cursor: pointer; list-style: none; }
.faq__item summary::-webkit-details-marker { display: none; }
.faq__item summary::after { content: ""; flex: none; width: 9px; height: 9px; border-right: 1.5px solid var(--ink-3); border-bottom: 1.5px solid var(--ink-3); transform: rotate(45deg); transition: transform 200ms ease; margin-top: -3px; }
.faq__item[open] summary::after { transform: rotate(-135deg); margin-top: 3px; }

/* Comparison table (#compare, landing.html.erb) - a real table (not a hand-rolled grid), so
   semantic markup/accessibility stay free on desktop. No competitor names, by design - generic
   "typical tools". Below 640px it reflows into stacked cards (see the media query below) - a
   plain overflow-x:auto horizontal scroll was the original mobile plan but looked bad in
   practice (reported) - a fixed 3-column table just gets clipped/scrolled, not readable. */
.compare-wrap { max-width: 980px; margin: 0 auto; overflow-x: auto; border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--surface); box-shadow: var(--shadow-card); }
.compare { width: 100%; border-collapse: collapse; font-size: 13.5px; }
/* Footnote under a .compare table (sourcing, caveats). Matches .compare-wrap's own 980px so it
   lines up with the table it annotates instead of running wider than it. Previously this reused
   .lib-meta, which is an app-UI class for SINGLE-LINE truncated metadata (nowrap + ellipsis) -
   it needed three inline overrides just to undo its own rules, and still rendered a two-sentence
   caveat in 11.5px monospace at full section width. */
.table-note { max-width: 980px; margin: 12px auto 0; font-size: 12.5px; line-height: 1.55; color: var(--ink-3); }
.compare th, .compare td { text-align: left; padding: 15px 20px; white-space: nowrap; }
.compare thead th { font-weight: 600; color: var(--ink); border-bottom: 1px solid var(--line); }
.compare thead th:first-child { min-width: 150px; }
.compare thead th.compare__winner-head { color: var(--primary); }
.compare tbody tr + tr td { border-top: 1px solid var(--line); }
.compare tbody td:first-child { font-weight: 600; color: var(--ink); }
.compare tbody td:not(:first-child) { color: var(--ink-2); white-space: normal; }
/* Icon sits in a fixed-size circle so it lines up in one clean column regardless of how much the
   text next to it wraps - previously the bare icon was inline with variable-length text, so
   check/x marks drifted out of alignment row to row. */
.compare__cell { display: flex; align-items: flex-start; gap: 10px; }
.compare__badge { flex: 0 0 auto; width: 20px; height: 20px; border-radius: 50%; display: grid; place-items: center; margin-top: 1px; }
.compare__badge--yes { background: var(--st-green-bg); color: var(--st-green); }
.compare__badge--no { background: var(--surface-2); color: var(--muted); }

/* Below 640px: native horizontal scroll (the original design) turned out to look bad in practice
   (reported) - a fixed-width table just gets clipped/scrolled rather than reading as a real
   comparison. Reflow each row into its own stacked mini-card instead: the feature name becomes a
   heading, and each answer gets its own labeled line below it - the standard responsive-table
   pattern (td::before + data-label, set on the view's SnapDub/competitor cells) rather than a
   second, duplicated mobile markup. */
@media (max-width: 640px) {
  .compare-wrap { overflow-x: visible; }
  .compare thead { display: none; }
  .compare, .compare tbody, .compare tr { display: block; width: 100%; }
  .compare tbody tr { padding: 14px 16px; }
  .compare tbody tr + tr { border-top: 1px solid var(--line); }
  .compare td { display: block; padding: 0; white-space: normal; }
  .compare tbody td:first-child { font-size: 14.5px; margin-bottom: 10px; }
  .compare td:not(:first-child) { margin-top: 10px; }
  .compare td:not(:first-child)::before {
    content: attr(data-label); display: block; margin-bottom: 4px;
    font-family: var(--font-mono); font-size: 10.5px; font-weight: 600; letter-spacing: 0.05em;
    text-transform: uppercase; color: var(--muted);
  }
}

@media (max-width: 860px) { .feature-grid, .steps { grid-template-columns: 1fr 1fr; } }
@media (max-width: 560px) { .feature-grid, .steps { grid-template-columns: 1fr; } }

.marketing-footer { margin-top: 50px; border-top: 1px solid var(--line); padding: 30px 40px; }
/* Was a single centred row of links; became labelled columns once the SEO pages roughly
   tripled the link count (see layouts/marketing.html.erb). Brand block stays top-left, the
   columns sit to its right and wrap underneath it on narrow screens. */
.marketing-footer .container { display: flex; align-items: flex-start; gap: 40px; max-width: 1360px; flex-wrap: wrap; }
.marketing-footer .brand-mark { width: 22px; height: 22px; }
.marketing-footer__brand { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }
.marketing-footer .copy { font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); margin-left: 8px; }
.marketing-footer__cols { margin-left: auto; display: flex; flex-wrap: wrap; gap: 26px 44px; }
.marketing-footer__col { display: flex; flex-direction: column; gap: 7px; font-size: 12.5px; min-width: 130px; }
.marketing-footer__heading { font-family: var(--font-mono); font-size: 10px; letter-spacing: .07em; text-transform: uppercase; color: var(--muted); margin-bottom: 2px; }
.marketing-footer__col a { color: var(--ink-3); }
.marketing-footer__col a:hover { color: var(--ink); }

@media (max-width: 900px) {
  .marketing-footer .container { gap: 26px; }
  .marketing-footer__cols { margin-left: 0; gap: 22px 32px; }
  .marketing-footer__col { min-width: 120px; }
}

/* ---- Auth ------------------------------------------------------------------ */
.auth-wrap { min-height: 100dvh; display: grid; place-items: center; padding: 24px; }
.auth-card { width: min(420px, 100%); }
.auth-head { text-align: center; margin-bottom: 24px; }
.auth-head .brand-mark { margin: 0 auto 14px; }
/* h1 as well as h2: these pages had no <h1> at all, which Bing reports as a missing main
   heading. The visual size is unchanged - only the tag is. */
.auth-head h1, .auth-head h2 { font-size: 24px; margin: 0; }
.auth-head p { margin: 6px 0 0; }
.auth-foot { text-align: center; margin-top: 16px; color: var(--ink-3); font-size: 13px; }
.auth-foot a { color: var(--primary); font-weight: 500; }
.auth-row { display: flex; align-items: center; justify-content: space-between; margin: -4px 0 16px; font-size: 13px; }
.auth-row .checkbox { display: flex; align-items: center; gap: 8px; color: var(--ink-2); cursor: pointer; user-select: none; }
.auth-row .checkbox input { width: 15px; height: 15px; accent-color: var(--primary); cursor: pointer; }
.auth-link { color: var(--primary); font-weight: 500; }

/* Per-option credit cost chip on the upload form */
.check__cost { display: inline-block; margin-left: 6px; font-family: var(--font-mono); font-size: 11px; font-weight: 500; color: var(--primary); background: var(--primary-tint); border-radius: var(--r-pill); padding: 1px 7px; }
.check__cost--free { color: var(--st-green); background: var(--st-green-bg); }

/* Billing monthly/annual toggle */
.interval-toggle { display: inline-flex; background: var(--surface-2); border-radius: 9px; padding: 3px; gap: 2px; }
.interval-toggle button { height: 32px; padding: 0 14px; border: none; border-radius: 7px; font-size: 12.5px; font-weight: 450; color: var(--ink-3); background: transparent; cursor: pointer; }
.interval-toggle button[aria-pressed="true"] { color: var(--ink); font-weight: 600; background: var(--surface); box-shadow: 0 1px 2px rgba(0,0,0,.1); }
.pricing-card__save { font-size: 11.5px; font-weight: 600; color: var(--primary); margin: -14px 0 16px; }

/* Out-of-credits modal */
.credits-modal { position: fixed; inset: 0; z-index: 1100; display: grid; place-items: center; padding: 20px; background: rgba(24,24,27,0.45); animation: cm-fade .18s ease-out; }
.credits-modal[hidden] { display: none; }
.credits-modal__card { position: relative; width: min(420px, 100%); background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-xl); box-shadow: var(--shadow-elevated); padding: 28px 26px; text-align: center; }
.credits-modal__close { position: absolute; top: 12px; right: 12px; display: inline-grid; place-items: center; width: 28px; height: 28px; border-radius: 8px; color: var(--ink-3); }
.credits-modal__close:hover { background: var(--surface-2); color: var(--ink); }
.credits-modal__icon { display: inline-grid; place-items: center; width: 46px; height: 46px; border-radius: 12px; background: var(--primary-tint); color: var(--primary); margin-bottom: 14px; }
.credits-modal__card h3 { font-size: 18px; margin: 0 0 8px; }
.credits-modal__card p { font-size: 14px; color: var(--ink-2); line-height: 1.55; margin: 0 0 20px; }
.credits-modal__actions { display: flex; flex-direction: column; align-items: center; gap: 10px; }
/* The top-up is deliberately not a second button: at this moment there is one right answer, and
   two equal-weight buttons turned the wall into a decision instead of a fix. */
.credits-modal__secondary { font-size: 13px; color: var(--muted); text-decoration: underline; }
.credits-modal__secondary:hover { color: var(--ink); }

/* --- credit wall, the persuasive half ------------------------------------------------------
   This screen catches someone mid-work, so it is framed as "here is how far you got and what
   unlocks the rest", not "you are deficient": a meter showing real balance against the real job
   cost, then the price as the largest thing on the card. Every number is measured, never dressed
   up - see shared/_credits_modal.html.erb. */
.credits-modal__card { animation: cm-rise .28s cubic-bezier(.2,.8,.2,1) both; }
@keyframes cm-rise { from { opacity: 0; transform: translateY(12px) scale(.97); } }

.cm-meter { height: 8px; border-radius: 999px; background: var(--surface); overflow: hidden; margin: 14px 0 7px; }
/* Fills from empty on open, so the eye lands on the gap rather than reading a static bar. */
.cm-meter__fill { height: 100%; min-width: 8px; border-radius: 999px; background: var(--primary); animation: cm-fill .7s .12s cubic-bezier(.2,.8,.2,1) both; }
@keyframes cm-fill { from { width: 0 !important; } }

.cm-meter__labels { display: flex; justify-content: space-between; font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); }
.cm-meter__labels b { color: var(--ink); font-weight: 500; }

/* The offer sits in its own tinted block so the price is not just another line of prose. */
.cm-offer { margin: 18px 0 16px; padding: 16px 14px; border-radius: var(--r-lg); background: var(--primary-tint); animation: cm-rise .28s .18s cubic-bezier(.2,.8,.2,1) both; }
.cm-price { font-size: 34px; font-weight: 700; line-height: 1; color: var(--primary); letter-spacing: -.02em; }
.cm-price span { font-size: 14px; font-weight: 500; color: var(--ink-2); letter-spacing: 0; }
.cm-offer__what { font-size: 13px; color: var(--ink-2); margin: 7px 0 0; }
.credits-modal__card h3 + .cm-offer { margin-top: 16px; }
.cm-offer__after { font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); margin: 9px 0 0; }
.cm-offer__after b { color: var(--ink); font-weight: 500; }

@media (prefers-reduced-motion: reduce) {
  .credits-modal__card, .cm-offer, .cm-meter__fill { animation: none; }
}
@keyframes cm-fade { from { opacity: 0; } to { opacity: 1; } }

/* Post-first-video 5-star review prompt (see review_prompt_controller.js) */
.review-modal { position: fixed; inset: 0; z-index: 1100; display: grid; place-items: center; padding: 20px; background: rgba(24,24,27,0.45); animation: cm-fade .18s ease-out; }
.review-modal[hidden] { display: none; }
.review-modal__card { position: relative; width: min(400px, 100%); background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-xl); box-shadow: var(--shadow-elevated); padding: 28px 26px; text-align: center; }
.review-modal__close { position: absolute; top: 12px; right: 12px; display: grid; place-items: center; width: 28px; height: 28px; border: none; border-radius: 8px; background: transparent; color: var(--ink-3); cursor: pointer; }
.review-modal__close:hover { background: var(--surface-2); color: var(--ink); }
.review-modal__card h3 { font-size: 18px; margin: 0 0 8px; }
.review-modal__card p { font-size: 14px; color: var(--ink-2); line-height: 1.5; margin: 0 0 18px; }
.review-modal__stars { display: flex; justify-content: center; gap: 4px; margin-bottom: 16px; }
.review-modal__stars[hidden] { display: none; }
.review-modal__star { border: none; background: transparent; color: var(--muted); padding: 2px; cursor: pointer; border-radius: 6px; transition: color 120ms ease, transform 120ms ease; }
.review-modal__star:hover { color: var(--primary); transform: scale(1.08); }
.review-modal__star.is-active { color: var(--primary); }
.review-modal__star.is-active svg { fill: currentColor; }
.review-modal__comment { width: 100%; box-sizing: border-box; resize: vertical; min-height: 60px; padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--r-sm); font: inherit; font-size: 13.5px; margin-bottom: 16px; }
.review-modal__comment[hidden] { display: none; }
.review-modal__error { font-size: 12.5px; color: var(--st-red); margin: -8px 0 12px; }
.review-modal__actions { display: flex; flex-direction: column; align-items: center; gap: 10px; }
.review-modal__actions[hidden] { display: none; }
.review-modal__dismiss { font-size: 13px; color: var(--ink-3); background: none; border: none; cursor: pointer; }
.review-modal__thanks { display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 14.5px; font-weight: 550; color: var(--primary); padding: 8px 0; }

/* Cookie consent banner */
.cookie-consent {
  position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%);
  z-index: 1000; width: min(680px, calc(100vw - 32px));
  display: flex; align-items: center; gap: 18px; flex-wrap: wrap;
  background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--r-lg);
  box-shadow: var(--shadow-elevated); padding: 14px 18px;
  animation: cookie-rise 0.28s ease-out;
}
.cookie-consent__text { flex: 1; min-width: 220px; font-size: 13px; color: var(--ink-2); line-height: 1.5; }
.cookie-consent__text a { color: var(--primary); font-weight: 500; }
.cookie-consent__actions { display: flex; gap: 10px; margin-left: auto; }
.cookie-consent__actions .btn { height: 34px; padding: 0 16px; font-size: 13px; }
.cookie-consent.is-leaving { opacity: 0; transform: translateX(-50%) translateY(8px); transition: opacity .2s, transform .2s; }
@keyframes cookie-rise { from { opacity: 0; transform: translateX(-50%) translateY(12px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }
@media (max-width: 520px) { .cookie-consent__actions { width: 100%; } .cookie-consent__actions .btn { flex: 1; } }

/* ---- Prose / legal --------------------------------------------------------- */
.prose { max-width: 720px; margin-inline: auto; padding: 48px 0; }
.prose h1 { font-size: 32px; }
.prose h2 { font-size: 20px; margin: 28px 0 12px; }
.prose p, .prose li { color: var(--ink-2); }
.prose ul { padding-left: 22px; }

/* ---- Settings -------------------------------------------------------------- */
.settings-grid { display: grid; gap: 24px; grid-template-columns: 1fr; max-width: 720px; }

/* Wraps .settings-grid + .settings-preview side by side on wide viewports (configure/templates
   pages only - .settings-grid itself stays untouched since videos/new.html.erb also uses it
   alone, no preview there). Preview drops BELOW the form on narrow/mobile viewports - plain DOM
   order, no CSS reordering trickery needed since the partial is rendered after the grid. */
.settings-page { display: flex; flex-direction: column; gap: 28px; align-items: flex-start; }
.settings-page .settings-grid { flex: 1 1 480px; min-width: 0; }
@media (min-width: 1180px) {
  .settings-page { flex-direction: row; }
  /* top must clear .topbar (sticky, 57px tall, z-index:30) - anything less and the topbar's own
     higher z-index paints OVER the top of the stuck preview once scrolled (confirmed live: the
     title/scrim overlays, which sit near the top of the frame, were silently hidden behind it -
     not a video-compositing issue like the earlier share-menu one, just an offset too small).
     24px below that so it starts level with where the form itself begins, not flush under the bar. */
  .settings-page .settings-preview { flex: 0 0 300px; position: sticky; top: 81px; }
}

.settings-preview { width: 100%; max-width: 300px; margin: 0 auto; }
.settings-preview__frame {
  position: relative; width: 100%; aspect-ratio: 9 / 16; border-radius: var(--r-lg);
  background: #000; overflow: hidden; border: 1px solid var(--line-strong);
  box-shadow: var(--shadow-elevated);
  /* Makes cqw available to the overlays inside: the real render sizes its text as a fraction of
     the OUTPUT WIDTH (see worker/render.py's _ass_file), so the preview has to do the same or it
     only tells the truth at one particular frame size. */
  container-type: inline-size;
}
.settings-preview__layer { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
.settings-preview__layer[hidden] { display: none; } /* [hidden] must beat the display:flex above */
.settings-preview__vid { width: 100%; height: 100%; }
.settings-preview__vid--cover { object-fit: cover; }
.settings-preview__vid--contain { object-fit: contain; }
.settings-preview__vid--blurbg { object-fit: cover; filter: blur(18px) brightness(.7); transform: scale(1.15); }

/* Text watermark position mirrors render.py's real render as closely as a static preview can -
   the actual watermark now roams the WHOLE frame between renders (see _WATERMARK_DEFAULT_SPOTS),
   not one fixed spot, so this shows its topmost band (~16% down from the top - deliberately below
   the title's own ~8% so the two never collide, see _write_ass's own comment) as the
   representative default. NOT bottom-right - that's only where the BRAND-KIT LOGO goes (a
   separate mechanism, a real ffmpeg overlay filter, not the ASS subtitle layer the text watermark
   burns through) - confirmed wrong here once already; the two are easy to conflate but render
   completely differently. */
.settings-preview__watermark {
  position: absolute; top: 16%; left: 8%; right: 8%; z-index: 3; text-align: center;
  font-family: var(--font-sans); font-weight: 600; font-size: 11px; color: rgba(255,255,255,.85);
  text-shadow: 0 1px 3px rgba(0,0,0,.7);
}
.settings-preview__watermark[hidden] { display: none; }
.settings-preview__logo {
  position: absolute; right: 10px; bottom: 10px; max-width: 15%; max-height: 15%;
  border-radius: 4px; z-index: 3; filter: drop-shadow(0 1px 3px rgba(0,0,0,.6));
}
.settings-preview__logo[hidden] { display: none; }
/* Scrim, not just text-shadow - a real render burns the title over whatever the source frame
   happens to show there, and plain white text + shadow alone reads fine against a dark shot but
   disappears against a bright one (confirmed live: unreadable over this demo's brighter frames).
   A gradient behind it guarantees contrast regardless of what's playing underneath - also now
   covers the watermark's own topmost ~16% band, not just the title's ~11%, since it's still
   within this same top area. */
.settings-preview__title-scrim {
  position: absolute; inset: 0 0 auto 0; height: 30%; z-index: 2;
  background: linear-gradient(rgba(0,0,0,.55), transparent);
}
.settings-preview__title-scrim[hidden] { display: none; }
.settings-preview__title {
  /* top: 11%, matching render.py's wm_margin (bumped from 8% 2026-08-23 - TikTok's own
     top-of-screen controls were still covering an 8%-placed title) - keep these two in sync, or
     the preview lies about where the title actually lands in the real render. */
  position: absolute; top: 11%; left: 8%; right: 8%; z-index: 3; text-align: center;
  font-family: var(--font-sans); font-weight: 800; font-size: 15px; line-height: 1.25; color: #fff;
  text-shadow: 0 2px 6px rgba(0,0,0,.7);
}
.settings-preview__title[hidden] { display: none; }
/* Every number here mirrors worker/render.py's Caption style - keep them in step, or the preview
   lies about what the render will do (reported: the caption sat at a different size and height
   than the burned-in one):
     font-size 6.25cqw  = font_size  (out_width / 16)
     bottom 20%         = margin_v   (out_height * 0.20)
     left/right 9%      = margin_x   (out_width * 0.09)
   Percentages, not px, because `bottom`/`left` resolve against the frame itself - the same
   fraction at any preview size. */
.settings-preview__caption {
  position: absolute; left: 9%; right: 9%; bottom: 20%; z-index: 3; text-align: center;
  font-family: var(--font-sans); font-weight: 700; font-size: 6.25cqw; line-height: 1.3;
}
.settings-preview__caption[hidden] { display: none; }
.settings-preview__caption span { padding: 2px 4px; border-radius: 4px; box-decoration-break: clone; -webkit-box-decoration-break: clone; }

.settings-preview__hint { margin: 10px 0 0; text-align: center; font-size: 11.5px; color: var(--muted); }

/* Drag-to-reposition handles (draggable_overlay_controller.js) - touch-action:none stops the
   browser from also trying to scroll the page on a touch drag; outline (not border) so it never
   nudges the element's own box/centering math. -webkit-user-drag/user-select: the logo handle is
   an <img> (natively draggable in every browser) and the others are plain text - either one
   starting its own native drag/text-selection instead of our pointer-based one fights the JS
   controller for the same gesture, so both are switched off explicitly rather than relying on
   preventDefault() on a different event type to also cover them. */
.settings-preview__drag {
  cursor: grab; touch-action: none; -webkit-user-drag: none; user-drag: none;
  -webkit-user-select: none; user-select: none;
}
.settings-preview__drag.is-dragging { cursor: grabbing; outline: 2px dashed rgba(255,255,255,.85); outline-offset: 3px; }
/* Mandatory (non-god) watermark - position isn't user-controllable, see _settings_preview.html.erb. */
.settings-preview__drag--locked { cursor: default; }

/* ---- Upload dropzone ------------------------------------------------------- */
.upload-drop { position: relative; display: flex; flex-direction: column; align-items: center; gap: 8px; padding: 48px 24px; text-align: center; cursor: pointer; border: 1px dashed var(--line-strong); border-radius: var(--r-lg); background: var(--bg); color: var(--ink-3); transition: border-color 140ms ease, background 140ms ease; }
.upload-drop:hover { border-color: var(--primary); background: var(--surface); }
.upload-drop svg { color: var(--primary); }
.upload-drop__hint { color: var(--ink); font-weight: 500; }
.upload-input { position: absolute; inset: 0; opacity: 0; cursor: pointer; }

/* ---- Responsive ------------------------------------------------------------ */
@media (max-width: 900px) {
  .shell { display: block; }
  .sidebar { position: fixed; inset: 0 auto 0 0; transform: translateX(-100%); transition: transform 220ms ease; box-shadow: var(--shadow-elevated); }
  /* Mobile: a desktop-collapse must never hide the drawer; the drawer is driven by nav-open only. */
  .shell.nav-collapsed .sidebar { display: flex; }
  .shell.nav-open .sidebar { transform: none; }
  .menu-btn { display: inline-flex; }
  .scrim { position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 35; }
  .hero h1 { font-size: 40px; }
  .container { padding-inline: 20px; }
  .pricing-grid { grid-template-columns: 1fr; }
  /* Projects list on mobile: flex (not grid) so a variable number of cells never "explodes" the
     row. Title takes the space, duration is dropped, status + the 3-dot menu stay put. */
  .lib-cols { display: none; }
  .lib-row { display: flex; align-items: center; gap: 10px; }
  .lib-row a.lib-video { flex: 1; min-width: 0; }
  .lib-dur { display: none; }
  .lib-row .status, .lib-row .menu { flex: 0 0 auto; }
}

/* ---- Phone (≤640px): the everyday app + landing ---------------------------- */
@media (max-width: 640px) {
  /* App shell: reclaim horizontal space, stack the page header */
  .content { padding: 20px 16px 32px; }
  .topbar { padding: 0 16px; gap: 12px; }
  .page-head { flex-direction: column; align-items: stretch; gap: 14px; }
  .panel { padding: 20px 16px; }

  /* Touch targets ≥44px; 16px inputs stop iOS zoom-on-focus */
  .btn { min-height: 44px; }
  .btn--sm { min-height: 40px; }
  .btn--icon { width: 44px; }
  .input, select.input { min-height: 44px; font-size: 16px; }
  .textarea { font-size: 16px; }

  .search-field { max-width: none; }

  /* Clip editor: the three columns stack instead of squeezing into one row. */
  .editor__stage-and-rail { flex-wrap: wrap; }
  .editor__col--rail { flex-basis: 100%; }
  .editor__sidebar { flex-basis: 100%; }
  .editor__asset-rail { justify-content: center; }

  /* Marketing nav: drop the section links, keep brand + primary CTA */
  .marketing-nav { padding: 0 16px; gap: 8px; }
  .marketing-nav__links, .marketing-nav__cta a.signin { display: none; }

  /* Landing: trim the 40px inline gutters (inline wins, so override with !important) */
  #how, #features, #pricing, #faq { padding-left: 16px !important; padding-right: 16px !important; }
  .hero { padding: 60px 16px 40px; }
  .hero h1 { font-size: 32px; }
  .hero__sub { font-size: 16px; }
  .cta-band { padding: 40px 20px; margin: 40px 16px; }
  .section-head { margin-bottom: 30px; }

  /* Hero demo player: one 9:16 mock, no side-by-side overflow */
  .player { margin-top: 32px; }
  .player__bar { height: 46px; padding: 0 12px; gap: 8px; }
  .player__file { display: none; }
  .player__toggle button { padding: 0 10px; font-size: 11.5px; }
  .player__stage { height: 360px; gap: 0; }
  .player__chips, .player__spacer { display: none; }
  .player__16x9 { width: min(86vw, 440px); height: auto; aspect-ratio: 16 / 9; }
  .player__9x16 { width: 150px; height: 267px; }
  .player__cap { font-size: 12px; white-space: normal; max-width: 88%; text-align: center; }
  .earn-demo__disclosure { font-size: 9.5px; padding: 3px 6px; right: 8px; top: 8px; }
  .player__play { width: 44px; height: 44px; }
  .player__play--sm { width: 38px; height: 38px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}

/* Scroll-reveal (scroll_reveal_controller.js) - items start faded/offset and animate in once as
   they enter the viewport. --d lets a list stagger children (e.g. style="--d:2"), same pattern
   _demo.html.erb already uses for its own per-item stagger (--ci/--i custom properties). */
.reveal { opacity: 0; transform: translateY(16px); transition: opacity 550ms cubic-bezier(.2,.8,.2,1), transform 550ms cubic-bezier(.2,.8,.2,1); transition-delay: calc(var(--d, 0) * 70ms); }
.reveal.is-visible { opacity: 1; transform: none; }
/* transform on a <tr> is a long-standing cross-browser gotcha (some engines briefly lay it out
   outside normal table flow, which showed up as a horizontal scrollbar flash in .compare-wrap
   during page load) - fade only for table rows, no translateY. */
.compare tbody tr.reveal { transform: none; }
@media (prefers-reduced-motion: reduce) {
  .reveal { opacity: 1; transform: none; transition: none; }
}

/* ── Landing hero demo: a long podcast scanned & sliced into vertical clips ──── */
.demo { max-width: 620px; margin: 44px auto 0; }

.demo__source { background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--r-lg); padding: 14px; box-shadow: var(--shadow-elevated); }
.demo__srchead { display: flex; justify-content: space-between; align-items: center; gap: 12px; font-family: var(--font-mono); font-size: 11px; color: var(--ink-2); }
.demo__file { display: inline-flex; align-items: center; gap: 7px; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.demo__rec { flex: none; width: 8px; height: 8px; border-radius: 50%; background: #ef4444; animation: demo-blink 1.4s ease-in-out infinite; }
.demo__status { flex: none; display: inline-flex; align-items: center; gap: 6px; color: var(--primary); font-weight: 600; }
.demo__spin { width: 11px; height: 11px; border-radius: 50%; border: 2px solid color-mix(in srgb, var(--primary) 28%, transparent); border-top-color: var(--primary); animation: demo-spin .8s linear infinite; }

.demo__track { position: relative; height: 80px; margin: 12px 0; border-radius: var(--r-md); background: var(--surface-2); overflow: hidden; }
.demo__wave { position: absolute; inset: 0; display: flex; align-items: center; gap: 3px; padding: 0 10px; }
.demo__wave i { flex: 1; height: 22%; border-radius: 2px; background: color-mix(in srgb, var(--primary) 50%, var(--surface)); animation: demo-eq 1.1s ease-in-out infinite alternate; animation-delay: calc(var(--b) * -0.06s); }
.demo__marker { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--primary); opacity: .3; }
.demo__scan { position: absolute; top: 0; bottom: 0; width: 88px; margin-left: -44px; background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--primary) 32%, transparent), transparent); animation: demo-scan 8s cubic-bezier(.65, 0, .35, 1) infinite; }

.demo__srcfoot { display: flex; justify-content: space-between; gap: 12px; font-size: 11.5px; color: var(--ink-3); }
.demo__srcfoot span { display: inline-flex; align-items: center; gap: 5px; min-width: 0; }
.demo__srcfoot svg { flex: none; }
.demo__found { color: var(--primary); font-weight: 600; }

.demo__clips { display: flex; gap: 14px; justify-content: center; margin-top: 18px; }
.demo__clip { position: relative; flex: 1 1 0; max-width: 138px; aspect-ratio: 9 / 16; border-radius: 16px; overflow: hidden; padding: 12px 10px; color: #fff; display: flex; flex-direction: column; justify-content: flex-end; background: linear-gradient(160deg, #273449, #0f172a); border: 1px solid rgba(255, 255, 255, .12); animation: demo-active 8s ease-in-out infinite; animation-delay: calc(var(--ci) * -2.667s); }
/* Optional per-card Kling loop (marketing_video_path) behind the ratio/score/caption/hashtag
   overlay - the .demo__clip gradient above stays as the fallback when no file exists yet. The
   scrim keeps the white overlay text readable regardless of what's in the video. */
.demo__bg-video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.demo__scrim { position: absolute; inset: 0; background: linear-gradient(to top, rgba(0,0,0,.75), rgba(0,0,0,.15) 60%); }
.demo__ratio { position: absolute; top: 9px; left: 9px; font-family: var(--font-mono); font-size: 8px; letter-spacing: .04em; background: var(--primary); color: #fff; border-radius: 5px; padding: 2px 5px; }
.demo__score { position: absolute; top: 9px; right: 9px; display: inline-flex; align-items: center; gap: 3px; background: rgba(0, 0, 0, .5); border-radius: 6px; padding: 2px 6px; font-size: 11px; font-weight: 700; }
.demo__score svg { color: #fb923c; }
.demo__caption { margin: 0; font-weight: 700; font-size: 12.5px; line-height: 1.32; }
.demo__caption span { opacity: 0; animation: demo-word 8s infinite; animation-delay: calc((var(--base) + var(--i)) * 0.26s); }
.demo__hash { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; }
.demo__hash span { font-size: 9.5px; font-weight: 600; color: color-mix(in srgb, var(--primary) 45%, #fff); }

@keyframes demo-scan { 0% { left: -5%; } 100% { left: 105%; } }
@keyframes demo-eq { 0% { height: 18%; } 100% { height: 82%; } }
@keyframes demo-blink { 0%, 100% { opacity: 1; } 50% { opacity: .25; } }
@keyframes demo-spin { to { transform: rotate(360deg); } }
@keyframes demo-word { 0% { opacity: 0; transform: translateY(3px); } 3% { opacity: 1; transform: none; } 86% { opacity: 1; } 94%, 100% { opacity: 0; } }
@keyframes demo-active {
  0% { border-color: rgba(255, 255, 255, .12); box-shadow: 0 10px 26px rgba(2, 6, 23, .22); transform: translateY(0); }
  5%, 28% { border-color: color-mix(in srgb, var(--primary) 70%, transparent); box-shadow: 0 0 0 2px var(--primary), 0 14px 34px color-mix(in srgb, var(--primary) 34%, transparent); transform: translateY(-4px); }
  40%, 100% { border-color: rgba(255, 255, 255, .12); box-shadow: 0 10px 26px rgba(2, 6, 23, .22); transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
  .demo__caption span { opacity: 1 !important; }
  .demo__wave i { height: 50% !important; }
  .demo__clip { transform: none !important; }
}

/* ── Settings form: priority blocks + range slider ─────────────────────────── */
.form-block { border: 1px solid var(--line); border-radius: var(--r-md); padding: 16px 16px 6px; margin: 0 0 18px; }
.form-block__title {
  font-family: var(--font-mono); font-size: 11px; font-weight: 500; letter-spacing: 0.06em;
  text-transform: uppercase; color: var(--ink-3); padding: 0 6px; margin-bottom: 4px;
}

/* Collapsible secondary block (translation & dub): closed by default, out of the clipper flow. */
.form-block--fold { padding: 0; }
.form-block--fold > *:not(summary) { padding: 0 16px; }
.form-block--fold[open] { padding-bottom: 6px; }
.form-block__summary {
  display: flex; align-items: center; gap: 8px; cursor: pointer; user-select: none;
  list-style: none; padding: 14px 16px; margin: 0;
}
.form-block__summary::-webkit-details-marker { display: none; }
.form-block__summary::after {
  content: "\25B8"; margin-left: auto; color: var(--ink-3); transition: transform .15s ease;
}
.form-block--fold[open] .form-block__summary::after { transform: rotate(90deg); }
.form-block__summary-hint {
  font-family: var(--font-sans); font-size: 11px; font-weight: 400; letter-spacing: 0;
  text-transform: none; color: var(--muted);
}

.range {
  -webkit-appearance: none; appearance: none; width: 100%; height: 6px; margin: 10px 0 2px;
  background: var(--surface-2); border-radius: 999px; outline: none; cursor: pointer;
}
.range::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none; width: 20px; height: 20px; border-radius: 50%;
  background: var(--primary); border: 3px solid var(--surface); box-shadow: var(--shadow-primary); cursor: pointer;
}
.range::-moz-range-thumb {
  width: 20px; height: 20px; border-radius: 50%; background: var(--primary);
  border: 3px solid var(--surface); box-shadow: var(--shadow-primary); cursor: pointer;
}
.range:focus-visible { box-shadow: 0 0 0 3px var(--primary-tint); }

/* Segmented radio control (clip strategy: best moments vs sequential parts). */
.segmented { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 6px; }
.segmented__opt {
  display: flex; align-items: flex-start; gap: 8px; padding: 10px 12px; cursor: pointer;
  border: 1px solid var(--line-strong); border-radius: var(--r-md); background: var(--surface);
  transition: border-color 120ms ease, background 120ms ease;
}
.segmented__opt:hover { border-color: var(--muted); }
.segmented__opt input { margin-top: 2px; width: 16px; height: 16px; accent-color: var(--primary); flex: none; }
.segmented__opt span { display: flex; flex-direction: column; line-height: 1.3; }
.segmented__opt small { color: var(--ink-3); font-size: 11.5px; margin-top: 2px; }
.segmented__opt:has(input:checked) { border-color: var(--primary); background: var(--primary-tint); }
.segmented--3 { grid-template-columns: repeat(3, 1fr); }
@media (max-width: 560px) { .segmented, .segmented--3 { grid-template-columns: 1fr; } }

/* AI-video frame pickers: a click-to-upload tile that fills with the chosen image. */
.frame-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 6px; }
@media (max-width: 560px) { .frame-grid { grid-template-columns: 1fr; } }
.frame-drop { position: relative; }
.frame-drop .req { color: var(--danger, #dc2626); }
.frame-drop input[type="file"] {
  position: absolute; inset: 26px 0 0 0; width: 100%; height: calc(100% - 26px);
  opacity: 0; cursor: pointer;
}
.frame-drop__preview {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 6px;
  margin-top: 6px; aspect-ratio: 9 / 16; max-height: 220px; border-radius: var(--r-md);
  border: 1px dashed var(--line-strong); background: var(--surface) center/cover no-repeat;
  color: var(--ink-3); font-size: 12px;
}
.frame-drop__preview.has-image { border-style: solid; color: transparent; }
.frame-drop__preview.has-image svg, .frame-drop__preview.has-image span { display: none; }

/* AI-video generation gallery: cards that stream in and swap to a clip when the render lands. */
.gen-library { margin-top: 40px; }
.gen-library__title { font-size: 18px; font-weight: 650; margin: 0 0 16px; }
.gen-item { min-width: 0; }
.gen-card__overlay {
  position: absolute; inset: 0; z-index: 2;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px;
  padding: 16px; text-align: center;
  background: rgba(0, 0, 0, .55); color: #fff; font-size: 12.5px; line-height: 1.4;
}
.gen-card--failed .gen-card__overlay { background: rgba(80, 10, 10, .62); }
.gen-card__overlay svg { color: #fff; }
.gen-card__spinner {
  display: inline-block; width: 26px; height: 26px; border-radius: 50%;
  border: 2.5px solid var(--line); border-top-color: var(--primary);
  animation: gen-spin .8s linear infinite;
}
/* Reused by any .btn's own loading state (see .btn-wrap below) - same ring, smaller. */
.btn-wrap { position: relative; display: inline-block; }
/* Room for .btn-wrap__spinner (sits at left:16px, 15px wide) so the button's own label never
   overlaps it - the button has no fixed width, so this has to come from padding, not layout. */
.btn-wrap .btn.is-loading { padding-left: 40px; }
.btn-wrap__spinner {
  /* margin-top (not a translateY transform) for vertical centering - the spin animation below
     already owns `transform` (rotate()), and a keyframe only defining `to` interpolates FROM the
     element's static transform, so a translateY(-50%) here would fight the rotation every frame
     (confirmed: rendered as the ring visibly jumping up/down instead of spinning in place). */
  position: absolute; top: 50%; left: 16px; margin-top: -7.5px;
  width: 15px; height: 15px; border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, .35); border-top-color: #fff;
  animation: gen-spin .7s linear infinite;
}
.btn-wrap__spinner[hidden] { display: none; }
/* Inside the dark .gen-card__overlay the light .panel colors above go invisible - keep it white
   there instead. */
.gen-card__overlay .gen-card__spinner {
  border-color: rgba(255, 255, 255, .3); border-top-color: #fff;
}
@keyframes gen-spin { to { transform: rotate(360deg); } }

/* Configured-settings chips on the project header (what this job is set to do). */
.settings-chips { display: flex; flex-wrap: wrap; gap: 8px; margin: 18px 0 0; }
.settings-chips__item {
  display: flex; align-items: baseline; gap: 6px; padding: 5px 10px;
  background: var(--surface-2); border: 1px solid var(--line); border-radius: 999px;
}
.settings-chips__item dt {
  font-family: var(--font-mono); font-size: 10.5px; font-weight: 500; letter-spacing: 0.04em;
  text-transform: uppercase; color: var(--ink-3);
}
.settings-chips__item dd { margin: 0; font-size: 12.5px; font-weight: 600; color: var(--ink); }

/* A <select> living INSIDE a .settings-chips pill (the per-channel template picker on
   /channels). Two things go wrong with a plain .input there: it's 40px tall, so its pill towers
   over the plain-text chips on the same flex line, and .settings-chips__item's own
   `align-items: baseline` then parks every neighbour's text at the TOP of that stretched pill.
   Stripping the control back to the pill's own type fixes both at once - nothing left to
   stretch. Scoped to this class, so the templates/configure pages (no control in a chip) keep
   their baseline alignment untouched. */
.settings-chips--controls { align-items: center; }
.settings-chips--controls .settings-chips__item { align-items: center; }

/* Templates list rows. */
.tmpl-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 16px 18px; border-bottom: 1px solid var(--line); }
.tmpl-row:last-child { border-bottom: 0; }
.tmpl-row__name { font-weight: 600; }
/* Let the name/chips block shrink so the wrapping chips never push the delete X off-screen (mobile). */
.tmpl-row > div:first-child { min-width: 0; flex: 1; }

/* ---- Product tours (tour_controller.js) -----------------------------------
   Both nodes are appended to <body> by JS only while a tour is running and removed
   when it ends - never toggled via [hidden], so there's no [hidden]-vs-display
   override to fight (see the controller for why). */
.tour-backdrop {
  position: fixed; inset: 0; z-index: var(--z-tour);
  background: rgba(24, 24, 27, 0.4); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
  opacity: 0; transition: opacity 200ms ease;
}
.tour-backdrop.is-visible { opacity: 1; }

/* Added to the ACTUAL target element (not a clone) for the duration of its step - lifting it
   above the backdrop in stacking order is what keeps it sharp while everything else blurs (the
   backdrop's blur only samples what's already painted behind it). !important is deliberate and
   narrow: this class is applied to an arbitrary, unpredictable target element and must reliably
   win regardless of whatever that element's own styles already set - same reasoning as the
   share-menu z-index override elsewhere in this file. */
.tour-spotlight-target {
  position: relative !important; z-index: calc(var(--z-tour) + 1) !important;
  border-radius: var(--r-md);
  box-shadow: 0 0 0 4px var(--primary-tint), 0 0 0 1px var(--primary), var(--shadow-elevated) !important;
  transition: box-shadow 160ms ease;
}

.tour-card {
  position: fixed; z-index: calc(var(--z-tour) + 2);
  width: min(320px, calc(100vw - 32px));
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg);
  box-shadow: var(--shadow-elevated); padding: 18px 20px;
  opacity: 0; transform: translateY(6px); transition: opacity 180ms ease, transform 180ms ease;
}
.tour-card.is-visible { opacity: 1; transform: translateY(0); }
.tour-card__eyebrow {
  font-size: 11px; font-weight: 600; letter-spacing: 0.06em; text-transform: uppercase;
  color: var(--primary); margin: 0 0 6px;
}
.tour-card__title { font-size: 15px; font-weight: 700; margin: 0 0 6px; color: var(--ink); }
.tour-card__body { font-size: 13px; line-height: 1.55; color: var(--ink-2); margin: 0; }
.tour-card__actions { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 16px; }
.tour-card__skip { font-size: 12.5px; color: var(--muted); background: none; border: none; cursor: pointer; padding: 6px 2px; }
.tour-card__skip:hover { color: var(--ink-2); }
.tour-card__nav { display: flex; gap: 8px; }

@media (prefers-reduced-motion: reduce) {
  .tour-backdrop, .tour-card { transition: none; }
}
.tmpl-row form { flex: 0 0 auto; margin: 0; }

/* ---- Posting plan (calendar) ------------------------------------------------ */
.calendar-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
/* Scrolls inside itself: seven day columns don't fit a phone, and the PAGE must never scroll
   sideways to accommodate them. */
.calendar { overflow-x: auto; border: 1px solid var(--line); border-radius: var(--r-md); }
.calendar__row { display: grid; grid-template-columns: 62px repeat(7, minmax(96px, 1fr)); border-top: 1px solid var(--line); }
.calendar__row:first-child { border-top: 0; }
.calendar__row--head { background: var(--surface-2); font-size: 12px; }
.calendar__day-head { padding: 8px 6px; text-align: center; border-left: 1px solid var(--line); }
.calendar__day-head span { display: block; color: var(--muted); font-size: 11px; text-transform: uppercase; letter-spacing: .04em; }
.calendar__day-head.is-today strong { color: var(--primary); }
.calendar__time { padding: 8px 6px; font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); text-align: right; }
.calendar__cell { min-height: 54px; padding: 5px; border-left: 1px solid var(--line); display: flex; flex-direction: column; gap: 4px; }
.cal-post { position: relative; display: flex; align-items: center; gap: 5px; padding: 4px 18px 4px 6px;
            border-radius: 6px; font-size: 11.5px; line-height: 1.25; }
/* Two signals on one chip. PLATFORM owns the background - which network a post is going to is
   what you scan a week for - and all five brand colours are dark enough to carry white text.
   STATUS therefore cannot also be a background, so it becomes a marker: posted fades and gains a
   tick, failed keeps full strength and gains a red ring. Both stay legible on any brand. */
.cal-post__platform { font-family: var(--font-mono); font-size: 9.5px; font-weight: 700; opacity: .85; }
.cal-post.is-tiktok    { background: #010101; color: #fff; }
.cal-post.is-instagram { background: linear-gradient(120deg, #FA7E1E, #D62976 55%, #962FBF); color: #fff; }
.cal-post.is-youtube   { background: #FF0000; color: #fff; }
.cal-post.is-facebook  { background: #1877F2; color: #fff; }
.cal-post.is-bluesky   { background: #0285FF; color: #fff; }
/* Posted: still clearly that network, visibly done. */
.cal-post.is-posted { opacity: .58; }
.cal-post.is-posted .cal-post__state::after { content: "✓"; }
/* Failed: full strength, because it is the one that needs acting on. The ring sits OUTSIDE the
   chip, against the cell's white background - an inset ring vanished on the blue brands, which is
   the opposite of what a failure marker is for. */
.cal-post.is-failed { outline: 2px solid var(--st-red); outline-offset: 2px; }
.cal-post.is-failed .cal-post__state::after { content: "!"; }
.cal-post__state { font-weight: 700; font-size: 10px; }
.cal-post__title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Three states only - see the calendar partial's own comment on keeping the vocabulary small. */
.cal-post__remove { position: absolute; top: 2px; right: 2px; background: none; border: 0; padding: 2px;
                    color: inherit; opacity: 0; cursor: pointer; line-height: 0; }
.cal-post:hover .cal-post__remove, .cal-post__remove:focus-visible { opacity: .75; }
/* One row per day+platform-set, with its times as chips - five Sunday slots going to the same
   three networks used to repeat the whole platform list five times over. */
.slot-group { padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--r-md); margin-bottom: 8px; }
/* The day name and the platform chips are different heights, so aligning on `center` left the
   word sitting slightly above the pills. Baseline puts the text on one line and the chips'
   own padding does the rest. */
.slot-group__head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
.slot-group__head .settings-chips { margin: 0; align-items: baseline; }
.slot-group__times { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.slot-time { display: inline-flex; align-items: center; gap: 4px; padding: 3px 6px 3px 9px;
             border: 1px solid var(--line-strong); border-radius: var(--r-pill);
             font-family: var(--font-mono); font-size: 12px; }
.slot-time__remove { display: inline-flex; color: var(--muted); line-height: 0; }
.slot-time__remove:hover { color: var(--st-red); }
.slot-form { display: grid; gap: 12px; }
/* The week as a list instead of a grid on phones - see the calendar partial's own comment. Both
   are rendered; exactly one is ever visible. */
.calendar-agenda { display: none; list-style: none; margin: 0; padding: 0; }
.calendar-agenda__day { margin-top: 14px; }
.calendar-agenda__day h4 { margin: 0 0 6px; font-size: 13px; }
.calendar-agenda .cal-post { margin-bottom: 4px; padding: 7px 22px 7px 8px; font-size: 12.5px; }
.calendar-agenda .cal-post__remove { opacity: .6; }
@media (max-width: 760px) {
  .calendar { display: none; }
  .calendar-agenda { display: block; }
}
/* View switcher (week / month / list), the segmented control a calendar app is expected to have. */
.view-switch { display: inline-flex; border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; }
.view-switch__btn { padding: 5px 11px; font-size: 12.5px; font-weight: 500; color: var(--ink-2);
                    text-decoration: none; border-left: 1px solid var(--line); }
.view-switch__btn:first-child { border-left: 0; }
.view-switch__btn.is-active { background: var(--primary); color: #fff; }
/* Month grid: seven equal columns, no time gutter - the rows are weeks, not times. */
.calendar--month .calendar__row--month { grid-template-columns: repeat(7, minmax(0, 1fr)); }
.calendar__cell--month { min-height: 84px; align-items: stretch; }
.calendar__cell--month.is-outside { background: var(--surface-2); }
.calendar__cell--month.is-outside .calendar__daynum { color: var(--muted); }
.calendar__cell--month.is-today .calendar__daynum { color: var(--primary); font-weight: 700; }
.calendar__daynum { font-size: 11.5px; color: var(--ink-2); }
/* One row per post: the platform code alone said a day was busy without saying what was going
   out, which is the question a month view is actually asked. Same colour rules as the week chips
   - brand for the background, status as a marker. */
.cal-dot { display: flex; align-items: baseline; gap: 4px; width: 100%; margin-bottom: 2px;
           padding: 2px 5px; border-radius: 4px; font-size: 10.5px; line-height: 1.3; color: #fff; }
.cal-dot__code { font-family: var(--font-mono); font-size: 9px; font-weight: 700; opacity: .85; flex: none; }
.cal-dot__title { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.cal-dot.is-tiktok    { background: #010101; }
.cal-dot.is-instagram { background: linear-gradient(120deg, #FA7E1E, #D62976 55%, #962FBF); }
.cal-dot.is-youtube   { background: #FF0000; }
.cal-dot.is-facebook  { background: #1877F2; }
.cal-dot.is-bluesky   { background: #0285FF; }
.cal-dot.is-posted { opacity: .58; }
.cal-dot.is-failed { outline: 2px solid var(--st-red); outline-offset: 1px; }
.cal-more { font-size: 10.5px; color: var(--muted); text-decoration: underline; }
/* List view asks for the agenda explicitly, so it shows at any width. */
.calendar-agenda.is-shown { display: block; }
@media (max-width: 760px) { .calendar--month { display: none; } }
/* A panel that reads as one row: label on the left, its single action on the right. Wraps rather
   than squeezing the button on a phone. */
.panel--inline { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; }
/* "You are here" in the week grid. Snapped to a slot row rather than drawn as a floating line -
   the rows are posting times, not a continuous hour axis, so a line between them would point at
   nothing. */
.calendar__cell.is-now { box-shadow: inset 2px 0 0 var(--primary); background: var(--primary-tint); }
/* A posting time with nothing in it. Without this an empty slot looks identical to a cell that
   was never a slot, and a clip that landed in next week's occurrence reads as "my slot vanished". */
.cal-free { font-size: 10.5px; color: var(--muted); border: 1px dashed var(--line-strong); border-radius: 5px;
            padding: 2px 6px; align-self: flex-start; }
/* Calendar filters. Each control flexes from a 150px basis so two share a wide row and stack
   cleanly on a phone - .cluster sizes selects to their content, which puts them shoulder to
   shoulder and lets them collide once the screen is narrower than their labels. */
.calendar-filters { display: flex; flex-wrap: wrap; gap: 10px; margin: 14px 0; }
.calendar-filters select.input { flex: 1 1 150px; width: auto; min-width: 0; }
/* Why a post did not go out, visible rather than hidden in a title attribute - a red chip with no
   readable reason just tells you to go looking. */
.cal-post__reason { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap;
                    margin: 3px 0 6px; font-size: 11px; color: var(--st-red); }
.cal-post__reason form { display: inline; }
.cal-post__reason .link-button { font-size: 11px; color: var(--primary); text-decoration: underline; }

/* ---- Watched channel rows ---------------------------------------------------- */
/* The platform reads as a brand mark rather than a "Source: YouTube" chip - it is the one thing
   you scan a list of channels for, and it now costs a glyph instead of a whole row. */
.channel-row__name { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 15px; }
.channel-row__platform { display: inline-flex; align-items: center; justify-content: center;
                         width: 26px; height: 26px; border-radius: 7px; color: #fff; flex: none; }
.channel-row__platform.is-youtube { background: #FF0000; }
.channel-row__platform.is-twitch  { background: #9146FF; }
/* A channel that quietly stopped is what this page exists to make impossible to miss, so the
   reason is a state, not a footnote in muted grey. */
.channel-row__notice { display: flex; align-items: flex-start; gap: 6px; margin: 10px 0 0;
                       padding: 7px 10px; border-radius: var(--r-md); white-space: normal;
                       background: var(--st-orange-bg); color: #7c3a12; font-size: 12.5px; line-height: 1.4; }
.channel-row__notice svg { flex: none; margin-top: 1px; color: var(--st-orange); }
