/* ============================================================================
   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;
  /* Kept out of the status vocabulary on purpose. Blue/green/red above mean scheduled/posted/
     failed and a fourth status colour would make the calendar harder to read at a glance; this
     one never appears on a post, only on a posting TIME that is reserved for one folder. */
  --st-amber: #92610a;    --st-amber-bg: #fdf6e3;  --st-amber-line: #ecd9a4;

  /* 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;
}
/* The brand, the New Project button, the balance card and the account row must never be squeezed:
   they are fixed furniture, and flex's default is to shrink every child rather than overflow. That
   is what bent the sidebar the moment the Library group gained a fourth subitem - the button at
   the top and the card at the bottom lost height instead of the nav gaining a scrollbar. */
.sidebar > :not(.sidebar__nav):not(.sidebar__spacer) { flex-shrink: 0; }
/* And nothing inside the nav shrinks either: a squeezed row reads as broken, where a scrollbar at
   least reads as a list that is too long. The accordion above means it rarely comes to that. */
.sidebar__nav > *, .nav-group__items > * { flex-shrink: 0; }
.sidebar__brand { display: flex; align-items: center; gap: 10px; padding: 2px 8px 14px; }
.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; }
/* min-height: 0 is what actually lets this scroll: a flex item's default min-height is auto, so
   without it the nav refuses to be shorter than its content and pushes the overflow onto its
   siblings instead of scrolling itself. */
/* margin-top 10, not 14, purely for slack: at a common laptop height the column came out ONE pixel
   taller than its box, which is sub-pixel rounding rather than real overflow - but a scrollbar for
   one pixel reads as a broken sidebar. The few pixels here are what keep rounding from showing. */
.sidebar__nav { display: flex; flex-direction: column; gap: 2px; margin-top: 10px;
                min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
.sidebar__spacer { flex: 1; }

.nav-item {
  display: flex; align-items: center; gap: 11px;
  /* 33, not 36: with four library subitems the column runs past a 13-inch screen, and three
     pixels a row across twelve rows is more than the whole overflow. Still a comfortable target -
     the recommended minimum for a pointer target is 24. */
  height: 33px; 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: 1px; margin: 1px 0 3px 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) */
/* Kept deliberately short: this sits at the foot of the sidebar, below a nav that grows every time
   the product does, and every pixel it takes is a pixel the nav has to scroll for. The balance is
   the only thing here that has to be readable at a glance - the label and the bar are supporting
   detail and are sized as such. */
.balance-card { border: 1px solid var(--line); border-radius: var(--r-md); padding: 9px 11px; margin-bottom: 8px; background: var(--bg); }
.balance-card__plan { color: var(--muted); font-weight: 400; }
.balance-card__head { display: flex; align-items: center; justify-content: space-between; gap: 6px; margin-bottom: 4px; font-size: 11px; font-weight: 500; color: var(--ink-2); }
.balance-card__head > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.balance-card__value { display: flex; align-items: baseline; gap: 5px; margin-bottom: 6px; }
.balance-card__value b { font-family: var(--font-mono); font-size: 18px; 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; }
/* display: block is what makes the ellipsis work at all - these were inline spans, and
   text-overflow needs a block box with a width to clip against, so a long address simply ran out
   of the sidebar instead of being cut. */
.account__name { display: block; font-size: 13px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.account__sub { display: block; 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);
}
/* min-width:0 + the ellipsis on .crumb-current below are what let the crumb GIVE WAY. The topbar
   is a single non-wrapping row, and a flex item never shrinks below its own min-content, so
   without these the row's minimum is "whole breadcrumb + every action button" - a page carrying
   two topbar actions then pushes the last one off the right edge of a phone instead of anything
   getting narrower (found at 412px on the folder page: body.scrollWidth 448 vs 412). */
.breadcrumb { display: flex; align-items: center; gap: 8px; font-size: 13px; min-width: 0; }
.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; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* The bottom padding reserves the support widget's corner. Tawk is a fixed 88x88 iframe pinned to
   the viewport's bottom-right (see shared/_tawk_chat.html.erb), so ANY control a page puts in its
   own bottom-right corner disappears under it the moment you scroll to the end of that page - the
   clip editor's Save button did, which is the worst control in the app to hide. Reserving the
   strip here fixes it once for every page rather than nudging each page's last row out of the
   corner; it is only ever visible at the very bottom of a scroll. */
.content { padding: 28px 26px 104px; 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;
}
/* [hidden] must beat the display:inline-flex above - a class selector outranks the browser's own
   [hidden] rule, so any button toggled with .hidden = true stays on screen. Found as "Cancel edit
   shows on a new template too" (templates/index.html.erb, template_form_controller.js), and the
   seventh time this file has needed such a line - hence fixing it for every .btn rather than for
   that one button. */
.btn[hidden] { display: none; }
.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). One row per
   platform rather than a grid of brand-filled buttons: five saturated blocks fought each other for
   attention and left a ragged last row, and rows stay tidy however many platforms there are. */
.publish-hub__list { display: flex; flex-direction: column; gap: 6px; margin-top: 14px; }
.publish-hub__row { display: flex; align-items: center; gap: 10px; padding: 9px 10px;
                    border: 1px solid var(--line); border-radius: var(--r-md);
                    color: var(--ink); text-decoration: none; background: var(--surface); }
.publish-hub__row:hover { background: var(--surface-2); }
/* The brand colour lives ONLY here - enough to identify the platform at a glance, not enough to
   turn the dialog into five competing blocks. --brand is set per row from the platform table. */
.publish-hub__chip { flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
                     width: 30px; height: 30px; border-radius: 8px;
                     background: var(--brand, var(--ink)); color: #fff; }
.publish-hub__name { flex: 1 1 auto; font-weight: 600; font-size: 0.92rem; }
.publish-hub__meta { flex: 0 0 auto; display: inline-flex; align-items: center; gap: 4px;
                     font-size: 0.78rem; color: var(--ink-2); }
.publish-hub__meta--posted { color: #16a34a; font-weight: 600; }
.publish-hub__go { flex: 0 0 auto; color: var(--ink-2); opacity: .5; }
/* A platform with nothing connected is still listed (its row goes to settings instead), just
   quieter - the point is that the list is never missing an entry, not that it is all equally
   clickable. */
.publish-hub__row:not(.is-linked) .publish-hub__chip { background: var(--surface-2); color: var(--ink-2); }
.publish-hub__row:not(.is-linked) .publish-hub__name { font-weight: 500; color: var(--ink-2); }
.btn--pinterest { background: #E60023; color: #fff; border-color: #E60023; font-weight: 600; }
.btn--pinterest:hover { background: #c1001d; border-color: #c1001d; }
.btn--pinterest:focus-visible { outline: 2px solid #E60023; outline-offset: 2px; }
.btn--linkedin { background: #0A66C2; color: #fff; border-color: #0A66C2; font-weight: 600; }
.btn--linkedin:hover { background: #08519b; border-color: #08519b; }
.btn--linkedin:focus-visible { outline: 2px solid #0A66C2; outline-offset: 2px; }
.btn--telegram { background: #229ED9; color: #fff; border-color: #229ED9; font-weight: 600; }
.btn--telegram:hover { background: #1c87b9; border-color: #1c87b9; }
.btn--telegram:focus-visible { outline: 2px solid #229ED9; outline-offset: 2px; }
.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); }
/* align-items: flex-start, not center: a row whose left column runs to two or three lines (every
   platform with a hint under it) put its button halfway down, so the buttons never lined up with
   each other or with the platform names beside them. Starting them all at the top is what makes
   the column read as a column.

   margin-left: auto on the action instead of justify-content: space-between, because these rows
   wrap: once the button drops onto a line of its own there is nothing left for space-between to
   push it against, and it landed hard against the LEFT edge - the opposite side from where it sat
   a row above. auto keeps it on the same side either way. */
.connected-account { display: flex; align-items: flex-start; gap: 10px 16px; flex-wrap: wrap; padding: 12px 14px; border: 1px solid var(--line); border-radius: var(--r-md); }
/* Takes the slack so the action only wraps when the row genuinely cannot fit it, and min-width: 0
   lets a long hint shrink rather than forcing the wrap itself. */
.connected-account > :first-child { flex: 1 1 16rem; min-width: 0; }
/* A column so a branch with something under its button (the "Coming soon" hint) stays attached to
   it, right-aligned with it, instead of becoming a third flex item on the row. */
.connected-account__action { margin-left: auto; flex-shrink: 0; display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
/* Capped on the TEXT, not on the column: capping the column broke the two rows whose action is a
   handle field beside a button (Bluesky, Telegram) - 190px of input plus a button does not fit in
   15rem, so the button wrapped under the field and the row grew a second storey. The only thing
   that ever needed a ceiling was the explanatory line under "Coming soon". */
.connected-account__action .field__hint { margin: 0; text-align: right; max-width: 15rem; }
/* 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; }
/* The running clip count under the progress bar while the worker is still uploading clips -
   quieter than the label above it, because the bar is what the eye should follow. */
.status__ready { font-size: 11px; margin-top: 5px; opacity: .72; }
.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 price of one generation, on the generate page. Deliberately larger than a hint: it is the
   number somebody has to see before pressing the button, not a footnote under it. */
.gen-cost { margin: 0; font-size: 15px; font-weight: 600; color: var(--primary); }
/* The guard state on the generate page, shown instead of the form to accounts that cannot use it
   yet. Built to sell rather than to refuse: it says what the feature is, what it costs at every
   quality, and where the plan is - a greyed-out form would have said none of those. */
.gen-guard { text-align: center; padding: 10px 6px 4px; }
.gen-guard__icon { display: inline-grid; place-items: center; width: 46px; height: 46px; margin-bottom: 12px;
                   border-radius: 50%; background: var(--primary-tint); color: var(--primary); }
.gen-guard h2 { font-size: 19px; margin: 0 0 8px; }
.gen-guard > p { max-width: 52ch; margin: 0 auto 18px; color: var(--ink-2); font-size: 14px; line-height: 1.6; }
.gen-guard__prices { max-width: 380px; margin: 0 auto 12px; border: 1px solid var(--line); border-radius: var(--r-md); }
.gen-guard__row { display: flex; justify-content: space-between; gap: 12px; padding: 9px 14px; font-size: 13.5px;
                  border-bottom: 1px solid var(--line); }
.gen-guard__row:last-child { border-bottom: none; }
.gen-guard__price { font-family: var(--font-mono); color: var(--muted); }
.gen-guard__actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-top: 18px; }


/* 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,
.asset-card__preview video { 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 {
.clip-card__views { display: inline-flex; align-items: center; gap: 4px; margin-left: 10px;
                    font-family: var(--font-mono); font-size: 12px; color: var(--muted); }
.clip-card__views svg { width: 13px; height: 13px; }
  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: 14px; align-items: stretch; flex-wrap: wrap; margin-bottom: 12px; }
.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. */
/* wrap + a full-width track lane: the stage and the rail sit side by side as before, and the
   overlay timeline drops onto its own line beneath them both instead of becoming a third column
   squeezed against the edge. */
.editor__stage-and-rail { display: flex; flex-wrap: wrap; 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. */
/* 46vh, not 58: the editor grew a track lane under the timeline, and at 58vh the Save button and
   the lane both sat below the fold on a laptop - the page measured 977px against a 717px window.
   A 9:16 frame costs height fast, so the preview is where that has to come from. */
/* Grows into whatever height the stage column actually has, keeping 9:16 exactly. A fixed
   min(42vh, 500px) left the frame sitting in a large black surround - the column stretches to
   match the settings sidebar beside it, and the preview ignored every pixel of that (measured on
   a 1440x684 window: a 412x491 canvas holding a 162x287 frame, 186px of it empty). That same
   height is the FLOOR here, so nothing gets smaller than it used to be anywhere; the frame only
   claims space that was already black.
   max-width keeps a narrow column from pushing it past the padding - the height follows the
   width there, so the ratio holds either way (checked at a 320px-wide stage). */
.editor__stage .editor__stage-inner {
  width: auto;
  height: auto;
  flex: 1 1 auto;
  min-height: min(42vh, 500px);
  max-height: min(84vh, 900px);
  max-width: 100%;
  min-width: 0;
  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,
/* A video asset previews as its own first frame - same box, same fit, so the rail reads as one
   row of pictures rather than as two kinds of thing. */
.editor__asset-thumb video { max-width: 100%; max-height: 100%; object-fit: contain; }
.asset-stage--full .editor__asset-thumb { opacity: .4; pointer-events: none; }
/* What the stage says about the whole set rather than about one overlay - a refused add, or too
   many sharing a moment (shared/_asset_inspector). */
.asset-stage__notice { margin: 8px 0 0; padding: 7px 9px; border-radius: var(--r-sm);
  font-size: 12px; line-height: 1.45; color: var(--st-red); background: var(--st-red-bg);
  border: 1px solid var(--st-red-line); }
.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; }
/* Select, then edit - the model every editor uses (see shared/_asset_inspector). An unselected
   overlay is still draggable; what selection buys is the ROOM its controls need, which four sets
   of always-on corner buttons did not leave. The outline is the only thing saying which of them
   the inspector below is talking about, so it has to survive a dark frame and a bright asset
   alike: a solid ring plus a dark halo, rather than a single-colour border that vanishes into
   whichever of the two it happens to land on. */
.asset-stage__handle.is-selected {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
  box-shadow: 0 0 0 4px rgba(0, 0, 0, .35);
}
/* Off the clip at this instant. The render simply does not draw an overlay outside its own window
   (ffmpeg's `enable=between(...)`, see render._overlay_window), so neither does the stage - it was
   showing all four at once whatever the playhead said, which made the frame you compose against a
   frame the render never produces. visibility, not display: the handle keeps its box, so its size
   observer and the lane's own layout do not thrash every time the playhead crosses a boundary. */
.asset-stage__handle--offstage { visibility: hidden; pointer-events: none; }

/* The corner controls belong to the selected overlay only. Kept in the DOM either way so the
   handle's own size math (and .asset-stage__handle--compact's measuring) never changes underfoot
   the moment something is selected. */
.asset-stage__handle .asset-stage__remove,
.asset-stage__handle .asset-stage__resize { opacity: 0; pointer-events: none; transition: opacity .12s; }
.asset-stage__handle.is-selected .asset-stage__remove,
.asset-stage__handle.is-selected .asset-stage__resize { opacity: 1; pointer-events: auto; }
.asset-stage__handle img,
.asset-stage__handle video { 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); }
/* One height for all three tabs, and it scrolls inside itself.
   Two problems, one cause: Details is taller than Captions and Crop, so switching tabs resized the
   whole column - and since the preview grows into that column (see .editor__stage-inner), the
   video jumped size every time somebody changed tabs. Left to itself the Details tab also just
   kept growing as settings were added to it, which is how it stopped fitting on a screen at all.
   A fixed window ends both: the tabs cannot disagree about how tall they are, and a new setting
   costs scroll rather than layout. */
.editor__panel {
  height: min(56vh, 500px);
  overflow-y: auto;
  overscroll-behavior: contain;
  padding-right: 4px;
}
.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;
}
/* The clip's own still, tiled across the bar, for when the real frames cannot be read (see
   clips/edit.html.erb). Dimmed and desaturated on purpose: it is a backdrop, not information, and
   at full strength a repeating face reads as a bug rather than as a placeholder. */
.editor__timeline--poster { background-repeat: repeat-x; background-size: auto 100%;
  background-position: center; }
.editor__timeline--poster::before { content: ""; position: absolute; inset: 0;
  background: rgba(10, 10, 12, .62); backdrop-filter: saturate(.5); }
.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; }
/* flex-shrink:0 and a top-aligned dot: a pill whose label wraps on a narrow screen otherwise
   squashes the dot to an oval and leaves it floating against the middle of two lines. */
.hero__pill { align-items: flex-start; }
.hero__pill .dot { flex: 0 0 auto; width: 6px; height: 6px; margin-top: 5px; 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; }
/* The hero's paste field. One row on a desktop, stacked on a phone - and the input takes the
   remaining width rather than a fixed one, because a YouTube URL is long and a field that visibly
   cannot hold it undercuts the whole "just paste it" invitation. */
.hero__paste { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; max-width: 620px; margin: 22px auto 0; }
.hero__paste-input { flex: 1 1 320px; min-width: 0; height: 48px; font-size: 15px; }
.hero__paste .btn { flex: 0 0 auto; }
/* flex-basis follows the MAIN axis, so the 320px basis above becomes a 320px HEIGHT the moment
   this turns the row into a column - which is exactly what it did: a full-screen-tall empty input
   on a phone. The basis has to be reset here, not just the direction. */
@media (max-width: 560px) {
  .hero__paste { flex-direction: column; }
  .hero__paste-input { flex: 0 0 auto; width: 100%; }
  .hero__paste .btn { width: 100%; justify-content: center; }
}
/* Sits under the paste field, so it needs its own air: without it the line reads as part of
   the input rather than as a separate reassurance under it. */
.hero__micro { font-family: var(--font-mono); font-size: 12px; color: var(--muted); margin-top: 14px; }
/* 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; }

/* ── Autopilot loop cards ─────────────────────────────────────────────────────
   Not .feature-grid--quiet: under a framed, animated panel a row of borderless text reads as an
   afterthought. Each card carries a small piece of its own subject at the top - a ping, five
   filling slots, the six real platform logos - so the row is scanned rather than read. */
.loop-grid { max-width: 980px; margin: 0 auto; display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.loop-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); padding: 0 0 20px; overflow: hidden; box-shadow: var(--shadow-card); }
.loop-card h3 { font-size: 15px; margin: 16px 20px 6px; }
.loop-card p { font-size: 13px; color: var(--ink-2); line-height: 1.55; margin: 0 20px; }
.loop-card__art { position: relative; height: 84px; display: grid; place-items: center; background: var(--surface-2); border-bottom: 1px solid var(--line); }

.loop-card__chip { display: inline-flex; align-items: center; gap: 6px; padding: 5px 11px; border-radius: 999px; background: var(--surface); border: 1px solid var(--line); font-family: var(--font-mono); font-size: 11px; color: var(--ink-2); }
.loop-card__ping { position: absolute; width: 46px; height: 46px; border-radius: 50%; border: 1px solid color-mix(in srgb, var(--primary) 45%, transparent); animation: loop-ping 3.2s ease-out infinite; }

.loop-card__art--slots { display: flex; align-items: flex-end; justify-content: center; gap: 7px; padding-bottom: 22px; }
.loop-card__bar { width: 18px; height: 34px; border-radius: 5px; background: var(--line); animation: loop-bar 4s ease-in-out infinite; animation-delay: calc(var(--b) * .3s); }

.loop-card__art--places { display: flex; align-items: center; justify-content: center; gap: 9px; }
.loop-card__logo { display: inline-grid; place-items: center; width: 32px; height: 32px; border-radius: 9px; background: var(--surface); border: 1px solid var(--line); color: var(--ink-2); animation: loop-logo 5s ease-in-out infinite; animation-delay: calc(var(--p) * .35s); }

@keyframes loop-ping { 0% { transform: scale(.6); opacity: 0; } 25% { opacity: 1; } 100% { transform: scale(1.5); opacity: 0; } }
@keyframes loop-bar { 0%, 10% { background: var(--line); height: 34px; } 25%, 75% { background: color-mix(in srgb, var(--primary) 55%, transparent); height: 46px; } 90%, 100% { background: var(--line); height: 34px; } }
@keyframes loop-logo { 0%, 12% { color: var(--ink-2); border-color: var(--line); } 22%, 46% { color: var(--primary); border-color: color-mix(in srgb, var(--primary) 50%, transparent); } 56%, 100% { color: var(--ink-2); border-color: var(--line); } }


/* The three inputs, each drawn as itself. Same .loop-card shell as the autopilot row above, so
   these are art variants rather than a second card system. */
.loop-card__filecard { display: inline-flex; flex-direction: column; gap: 7px; width: 148px; padding: 11px 12px; border-radius: 10px; background: var(--surface); border: 1px solid var(--line); }
.loop-card__filecard > svg { color: var(--primary); }
.loop-card__filename { font-family: var(--font-mono); font-size: 10.5px; color: var(--ink-2); }
.loop-card__progress { display: block; height: 4px; border-radius: 3px; background: var(--line); overflow: hidden; }
.loop-card__progress i { display: block; height: 100%; width: 0; border-radius: 3px; background: var(--primary); animation: loop-upload 4.5s ease-in-out infinite; }

.loop-card__url { display: inline-flex; align-items: center; gap: 8px; padding: 9px 13px; border-radius: 999px; background: var(--surface); border: 1px solid var(--line); font-family: var(--font-mono); font-size: 11px; color: var(--ink-2); }
.loop-card__typing { display: inline-block; overflow: hidden; white-space: nowrap; animation: loop-type 5s steps(20) infinite; }
.loop-card__caret { width: 1.5px; height: 13px; background: var(--primary); animation: blink 1s step-end infinite; }

.loop-card__art--channel { display: flex; align-items: center; justify-content: center; gap: 10px; }
.loop-card__avatar { display: inline-grid; place-items: center; width: 36px; height: 36px; border-radius: 11px; background: var(--surface); border: 1px solid var(--line); color: var(--ink-2); }
.loop-card__sync { width: 34px; height: 2px; border-radius: 2px; background: var(--line); position: relative; overflow: hidden; }
.loop-card__sync::after { content: ""; position: absolute; inset: 0; width: 40%; border-radius: 2px; background: var(--primary); animation: loop-sync 2.4s ease-in-out infinite; }

@keyframes loop-upload { 0%, 6% { width: 0; } 55%, 82% { width: 100%; } 92%, 100% { width: 0; } }
@keyframes loop-type { 0%, 8% { width: 0; } 45%, 80% { width: 128px; } 92%, 100% { width: 0; } }
@keyframes loop-sync { 0% { left: -40%; } 100% { left: 100%; } }

@media (max-width: 860px) { .loop-grid { grid-template-columns: 1fr; } }
@media (prefers-reduced-motion: reduce) {
  .loop-card__ping, .loop-card__bar, .loop-card__logo,
  .loop-card__progress i, .loop-card__typing, .loop-card__caret, .loop-card__sync::after { animation: none; }
  .loop-card__progress i { width: 100%; }
  .loop-card__typing { width: auto; }
  .loop-card__ping { opacity: .5; transform: scale(1); }
}

/* ── Crop preview tool ────────────────────────────────────────────────────────
   Two canvases side by side, because the page's whole argument is about what is LOST - showing
   only the surviving 9:16 demonstrates nothing. Stacks on a phone, where side-by-side would make
   both too small to read. */
.croptool { max-width: 720px; margin: 0 auto; text-align: center; }
.croptool__empty { display: grid; place-items: center; gap: 12px; padding: 40px 20px; border: 1px dashed var(--line); border-radius: var(--r-lg); background: var(--surface-2); color: var(--muted); }
.croptool__empty p { margin: 0; font-size: 13.5px; }
.croptool__privacy { font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
.croptool__busy { font-family: var(--font-mono); font-size: 12px; color: var(--muted); padding: 30px 0; }
.croptool__stage { display: grid; grid-template-columns: 1fr auto; gap: 22px; align-items: end; justify-content: center; }
.croptool__side { margin: 0; display: grid; gap: 9px; justify-items: center; }
.croptool__side canvas { max-width: 100%; height: auto; border-radius: var(--r-md); border: 1px solid var(--line); background: #17171a; }
.croptool__side--out canvas { max-width: 190px; }
.croptool__side figcaption { font-size: 12px; line-height: 1.45; color: var(--muted); max-width: 30ch; }
/* The app switcher under the preview. Chips rather than a select: there are three of them, they
   are the subject of the section, and a closed dropdown hides the fact that you can compare at
   all. */
.croptool__apps { display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 8px; margin-top: 18px; }
.croptool__apps-label { font-size: 12.5px; color: var(--muted); margin-right: 2px; }
.croptool__app { padding: 6px 13px; font-size: 13px; font-weight: 550; color: var(--ink-2);
  background: var(--surface); border: 1px solid var(--line); border-radius: 999px; cursor: pointer; }
.croptool__app:hover { color: var(--ink); }
.croptool__app.is-active { color: #fff; background: var(--primary); border-color: var(--primary); }
.croptool__apps[hidden] { display: none !important; }
.croptool__hint { max-width: 52ch; margin: 20px auto 0; display: grid; justify-items: center; gap: 12px;
                  font-size: 13px; line-height: 1.6; color: var(--ink-2); }
.croptool__hint p { margin: 0; }
/* Toggled with the hidden ATTRIBUTE, and a class that sets display beats it - so each needs its
   own override or two states show at once. Repeat mistake in this codebase; the rule is that a
   hidden-toggled element whose class sets display gets its [hidden] rule in the same breath. */
.croptool__stage[hidden], .croptool__empty[hidden], .croptool__hint[hidden] { display: none !important; }
@media (max-width: 620px) {
  .croptool__stage { grid-template-columns: 1fr; gap: 26px; }
  .croptool__side--out canvas { max-width: 160px; }
}

/* ── The free tools (file checker, subtitle tool, frame picker) ───────────────
   Three pages, one visual idea: a dashed dropzone that becomes a result in place. They share the
   dropzone, the privacy line and the "do another" button, and differ only in what the result is,
   which is what keeps three tools from becoming three designs.

   All three toggle their panes with the hidden ATTRIBUTE while their classes set display, so each
   one needs its own [hidden] override - the repeat mistake this stylesheet has a note about over
   by .croptool. */
.filecheck, .subtool, .framer { max-width: 720px; margin: 0 auto; }
.filecheck__empty, .framer__empty { display: grid; place-items: center; gap: 12px; padding: 40px 20px;
  border: 1px dashed var(--line); border-radius: var(--r-lg); background: var(--surface-2); color: var(--muted); }
.filecheck__empty p, .framer__empty p { margin: 0; font-size: 13.5px; }
.filecheck__privacy, .framer__privacy, .subtool__privacy {
  font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
.filecheck__busy, .framer__busy { font-family: var(--font-mono); font-size: 12px; color: var(--muted);
  padding: 30px 0; text-align: center; }
/* The whole card lifts while a file is over it, rather than only the dashed border: at a glance
   from above a cursor, a 1px border change is invisible. */
.filecheck.is-dragging, .framer.is-dragging { outline: 2px solid var(--primary); outline-offset: 6px; border-radius: var(--r-lg); }

.filecheck__result { display: grid; gap: 14px; justify-items: start; padding: 22px 24px;
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--shadow-card); }
.filecheck__name { margin: 0; font-family: var(--font-mono); font-size: 12px; color: var(--muted);
  word-break: break-all; }
.filecheck__rows { width: 100%; display: grid; gap: 1px; background: var(--line);
  border: 1px solid var(--line); border-radius: var(--r-md); overflow: hidden; }
.filecheck__row { display: grid; grid-template-columns: 1fr 1.4fr; gap: 1px; background: var(--line); }
.filecheck__row > span { background: var(--surface); padding: 9px 12px; font-size: 13.5px; color: var(--ink-2); }
.filecheck__row > span:first-child { color: var(--muted); }
.filecheck__shape { margin: 0; font-size: 14px; line-height: 1.6; color: var(--ink-2); }
.filecheck__verdicts { list-style: none; margin: 0; padding: 0; width: 100%; display: grid; gap: 8px; }
.filecheck__verdict { display: grid; grid-template-columns: 9.5rem 1fr; gap: 10px; align-items: baseline;
  font-size: 13.5px; line-height: 1.55; color: var(--ink-2); padding-left: 12px; border-left: 2px solid var(--line); }
.filecheck__verdict.is-ok { border-left-color: var(--primary); }
.filecheck__verdict.is-warn { border-left-color: #d97706; }
.filecheck__verdict strong { color: var(--ink); font-weight: 620; }
.filecheck__result[hidden], .filecheck__empty[hidden] { display: none !important; }

.subtool__panes { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.subtool__pane { display: grid; gap: 8px; }
.subtool__label { font-size: 13px; color: var(--ink-2); }
.subtool__label em { font-style: normal; font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
.subtool__pane textarea { width: 100%; padding: 12px 13px; font-family: var(--font-mono); font-size: 12px;
  line-height: 1.55; color: var(--ink); background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r-md); resize: vertical; }
.subtool__actions { display: flex; gap: 8px; }
.subtool__controls { display: flex; flex-wrap: wrap; gap: 16px; margin-top: 16px; padding-top: 16px;
  border-top: 1px solid var(--line); font-size: 13px; color: var(--ink-2); }
.subtool__controls label { display: inline-flex; align-items: center; gap: 8px; }
.subtool__controls select, .subtool__controls input { padding: 6px 9px; font-size: 13px; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-sm); }
.subtool__controls input[type="number"] { width: 5.5rem; }
.subtool__status { margin: 14px 0 4px; font-size: 13px; color: var(--primary); }

.framer__stage { display: grid; gap: 16px; justify-items: center; }
.framer__previews { display: grid; grid-template-columns: auto auto; gap: 20px; align-items: end; }
.framer__full, .framer__grid { margin: 0; display: grid; gap: 8px; justify-items: center; }
.framer__previews canvas { max-width: 100%; height: auto; border-radius: var(--r-md);
  border: 1px solid var(--line); background: #17171a; }
.framer__full canvas { max-width: 380px; }
.framer__grid canvas { width: 120px; height: 120px; }
.framer__previews figcaption { font-size: 12px; color: var(--muted); }
.framer__scrub { display: flex; align-items: center; gap: 10px; width: 100%; max-width: 520px; }
.framer__scrub input[type="range"] { flex: 1; }
.framer__time { font-family: var(--font-mono); font-size: 12px; color: var(--muted); min-width: 4.5rem; }
.framer__actions { display: flex; gap: 10px; align-items: center; }
.framer__stage[hidden], .framer__empty[hidden] { display: none !important; }

/* The reverse-arithmetic line on the pay calculator: a target typed into the middle of a sentence
   rather than given its own row, because it is an aside to the four sliders above it. */
.calc__inline { display: inline-flex; align-items: center; gap: 6px; margin-left: 6px; }
.calc__inline input { width: 6rem; padding: 4px 8px; font-size: 13px; color: var(--ink);
  background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-sm); }

@media (max-width: 720px) {
  .subtool__panes { grid-template-columns: 1fr; }
  .framer__previews { grid-template-columns: 1fr; justify-items: center; gap: 14px; }
  .filecheck__verdict { grid-template-columns: 1fr; gap: 2px; }
  .filecheck__result { padding: 18px 16px; }
}

/* ── Growth chart ─────────────────────────────────────────────────────────────
   Three bars at the cadences somebody actually measured. Heights are set per bar from --h and only
   animate the first time the chart is seen - a chart that keeps replaying reads as an ornament,
   and this one is meant to be read as evidence. */
.growth { max-width: 560px; margin: 0 auto 30px; }
.growth__bars { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; align-items: end; height: 210px; }
.growth__bar { display: grid; grid-template-rows: auto 1fr auto; align-items: end; justify-items: center; gap: 8px; height: 100%; }
.growth__bar i { display: block; width: 100%; max-width: 76px; height: 0; border-radius: 8px 8px 0 0; background: var(--line); align-self: end; transition: height .9s cubic-bezier(.2,.7,.3,1); }
.growth.is-grown .growth__bar i { height: var(--h); }
.growth__bar--peak i { background: var(--primary); }
.growth__val { font-family: var(--font-mono); font-size: 13px; font-weight: 600; color: var(--ink-2); align-self: end; }
.growth__bar--peak .growth__val { color: var(--primary); }
.growth__lab { font-size: 11.5px; line-height: 1.35; color: var(--muted); text-align: center; }
.growth__source { max-width: 60ch; margin: 16px auto 0; font-size: 12.5px; line-height: 1.55; color: var(--muted); text-align: center; }
@media (max-width: 520px) { .growth__bars { height: 170px; gap: 10px; } .growth__lab { font-size: 10.5px; } }

/* ── Clip calculator ──────────────────────────────────────────────────────────
   Two sliders and a number. Deliberately plain: the thing being demonstrated is that our pricing
   is arithmetic anybody can check, so the control should look like a calculator rather than a
   sales widget. */
.calc { max-width: 620px; margin: 0 auto; padding: 22px 24px; background: var(--surface); border: 1px solid var(--line); border-radius: var(--r-lg); box-shadow: var(--shadow-card); }
.calc__row { display: grid; grid-template-columns: 1fr auto; gap: 6px 14px; align-items: center; margin-bottom: 18px; }
.calc__label { grid-column: 1 / -1; font-size: 13.5px; color: var(--ink-2); }
.calc__range { width: 100%; accent-color: var(--primary); }
.calc__value { font-family: var(--font-mono); font-size: 13px; color: var(--ink); white-space: nowrap; }
.calc__out { margin-top: 4px; padding-top: 18px; border-top: 1px solid var(--line); text-align: center; }
.calc__credits { font-size: 34px; font-weight: 700; letter-spacing: -.02em; color: var(--ink); }
.calc__credits small { font-size: 14px; font-weight: 500; color: var(--muted); }
.calc__verdict { margin: 8px 0 0; font-size: 13.5px; color: var(--primary); }
.calc__verdict--muted { color: var(--muted); }
.calc__firstrun { margin: 14px 0 0; padding-top: 12px; border-top: 1px dashed var(--line); font-size: 12.5px; color: var(--primary); }
@media (max-width: 560px) { .calc { padding: 18px 16px; } .calc__credits { font-size: 28px; } }

/* Cost comparison table (/clipping-cost-calculator). Wider than the other calculators because a
   row here carries three columns of text, not one big number. Rows are drawn by
   cost_comparison_controller.js and re-sorted on every drag, so nothing here may depend on order -
   only .costrow--ours marks a row, and it marks it by whose it is, never by where it sits. */
.calc--wide { max-width: 760px; }
.calc__shape { margin: -6px 0 14px; font-size: 12.5px; color: var(--muted); font-family: var(--font-mono); }
.costtable { display: flex; flex-direction: column; margin-top: 4px; border-top: 1px solid var(--line); }
/* The name column is sized to the NAMES (6.5rem holds the longest of them), not given a share of
   the row. As a 1fr it was 231px wide for the word "Klap", which left the plan description
   floating in the middle of the row looking centred. Fixed rather than max-content because each
   row is its own grid: max-content would size every row to its own name and the descriptions would
   start at a different place on each line. */
.costrow { display: grid; grid-template-columns: 6.5rem 1fr auto; gap: 10px 14px; align-items: baseline;
           padding: 11px 12px; border-bottom: 1px solid var(--line); font-size: 13.5px; }
.costrow--ours { background: var(--primary-tint); border-radius: var(--r-sm); }
.costrow__name { font-weight: 600; color: var(--ink); }
.costrow--ours .costrow__name { color: var(--primary); }
.costrow__plan { color: var(--muted); font-size: 12.5px; }
.costrow__price { font-family: var(--font-mono); text-align: right; color: var(--ink); white-space: nowrap; }
.costrow__price--muted { color: var(--muted); font-size: 12px; }
@media (max-width: 560px) {
  /* Three columns become two LINES: the tool and its price on the first, which plan that is
     underneath. Placed explicitly rather than left to auto-flow - the markup order is name, plan,
     price, so flowing it puts the price alone on a third line with nothing beside it. */
  .costrow { grid-template-columns: 1fr auto; gap: 2px 12px; padding: 10px 8px; }
  .costrow__name { grid-area: 1 / 1; }
  .costrow__price { grid-area: 1 / 2; }
  .costrow__plan { grid-area: 2 / 1 / 3 / -1; }
}

/* ── Caption style demo ───────────────────────────────────────────────────────
   Four takes stacked in one box, cross-fading on a shared 10s loop (2.5s each). Stacked rather
   than side-by-side on purpose: the argument is that the SAME line looks different, which only
   reads if it occupies the same place. */
.capdemo { max-width: 560px; margin: 0 auto; }
.capdemo__stage { position: relative; height: 132px; border-radius: var(--r-lg); background: #17171a; overflow: hidden; display: grid; place-items: center; }
.capdemo__take { position: absolute; inset: 0; display: grid; place-items: center; opacity: 0; animation: capdemo-take 10s ease-in-out infinite; animation-delay: calc(var(--t) * 2.5s); }
.capdemo__line { margin: 0; font-size: 21px; font-weight: 800; letter-spacing: -.01em; text-align: center; }
.capdemo__name { position: absolute; bottom: 10px; right: 12px; font-family: var(--font-mono); font-size: 10px; color: rgba(255,255,255,.45); }
.capdemo__take--0 .capdemo__line { color: #fff; }
.capdemo__take--0 em { font-style: normal; color: #c6f24e; }
.capdemo__take--1 .capdemo__line { color: #fff; }
.capdemo__take--1 em { font-style: normal; background: #ffd400; color: #17171a; padding: 0 6px; border-radius: 4px; }
.capdemo__take--2 .capdemo__line { color: #fff; text-shadow: 0 2px 0 rgba(0,0,0,.6); }
.capdemo__take--2 em { font-style: normal; }
.capdemo__take--3 .capdemo__line { color: #fff; }
.capdemo__take--3 em { font-style: normal; color: #ff3d8b; }
.capdemo__foot { display: flex; align-items: center; justify-content: center; gap: 6px; margin: 12px 0 0; font-family: var(--font-mono); font-size: 11.5px; color: var(--muted); }
@keyframes capdemo-take { 0% { opacity: 0; } 3%, 22% { opacity: 1; } 25%, 100% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .capdemo__take { animation: none; opacity: 0; }
  .capdemo__take--0 { opacity: 1; }
}

/* ── Autopilot loop ───────────────────────────────────────────────────────────
   A week of scheduled posts filling itself, built from the same three marketing clips the page has
   already shown being produced - so this reads as the output of the earlier section rather than as
   unrelated decoration. One shared 14s timeline; each day's delay comes from its own --d index,
   which is what fills the week left-to-right without a line of JavaScript. */
.pilot { max-width: 880px; margin: 0 auto; }
.pilot__frame { border: 1px solid var(--line); border-radius: var(--r-lg); background: var(--surface); box-shadow: var(--shadow-card); padding: 16px 18px 14px; }
.pilot__head { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; padding-bottom: 12px; border-bottom: 1px solid var(--line); }
.pilot__src, .pilot__event { display: inline-flex; align-items: center; gap: 7px; font-family: var(--font-mono); font-size: 11.5px; color: var(--ink-2); }
.pilot__event { color: var(--muted); }
.pilot__rec { width: 7px; height: 7px; border-radius: 50%; background: var(--primary); animation: pilot-rec 14s ease-in-out infinite; }
.pilot__spin { width: 11px; height: 11px; border-radius: 50%; border: 1.5px solid var(--line); border-top-color: var(--primary); animation: gen-spin .9s linear infinite; }
.pilot__week { display: grid; grid-template-columns: repeat(7, 1fr); gap: 8px; padding: 14px 0 12px; }
.pilot__day { display: flex; flex-direction: column; align-items: center; gap: 5px; }
.pilot__dayname, .pilot__time { font-family: var(--font-mono); font-size: 10px; letter-spacing: .03em; color: var(--muted); }
.pilot__slot { position: relative; width: 100%; aspect-ratio: 9 / 14; border-radius: 9px; overflow: hidden; background: var(--surface-2); border: 1px dashed var(--line); opacity: .35; transform: scale(.94); animation: pilot-slot 14s cubic-bezier(.2,.7,.3,1) infinite; animation-delay: calc(var(--d) * .5s); }
.pilot__video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; }
.pilot__badge { position: absolute; left: 5px; bottom: 5px; display: inline-grid; place-items: center; width: 19px; height: 19px; border-radius: 6px; background: rgba(0,0,0,.62); color: #fff; }
.pilot__foot { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 11px; border-top: 1px solid var(--line); font-family: var(--font-mono); font-size: 11px; color: var(--muted); }
.pilot__foot span { display: inline-flex; align-items: center; gap: 6px; }
.pilot__posted { color: var(--primary); }
@keyframes pilot-slot {
  0%, 4% { opacity: .35; transform: scale(.94); border-style: dashed; }
  14%, 88% { opacity: 1; transform: scale(1); border-style: solid; border-color: transparent; }
  96%, 100% { opacity: .35; transform: scale(.94); border-style: dashed; }
}
@keyframes pilot-rec { 0%, 90%, 100% { opacity: 1; } 45% { opacity: .25; } }
@media (max-width: 720px) {
  .pilot__week { grid-template-columns: repeat(4, 1fr); }
  .pilot__day:nth-child(n+5) { display: none; }
}
/* Decorative: the section states every fact in text beside it. */
@media (prefers-reduced-motion: reduce) {
  .pilot__slot, .pilot__rec { animation: none; opacity: 1; transform: none; border-style: solid; border-color: transparent; }
  .pilot__spin { 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); }
.confirm-banner__form { display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
/* Monospace and letter-spaced so six digits read as six digits while they are typed - the same
   treatment the code gets in the email, so the two look like the same thing. */
.confirm-banner__code { width: 92px; padding: 5px 8px; border: 1px solid #e6bd97; border-radius: var(--r-sm);
                        background: #fff; color: #7c3a12; font-family: var(--font-mono); font-size: 13px;
                        letter-spacing: .12em; text-align: center; }
.confirm-banner__code:focus { outline: 2px solid var(--st-orange); outline-offset: 1px; }
.confirm-banner__action--quiet { border-color: transparent; text-decoration: underline; }
.confirm-banner__resend { flex-shrink: 0; }
@media (max-width: 620px) {
  /* The banner is a row of four things on a desktop and cannot stay one on a phone, which is the
     device this whole feature exists for. Text across the top, then the code field and its two
     buttons wrapping underneath. */
  .confirm-banner { flex-wrap: wrap; }
  .confirm-banner__text { flex: 1 1 100%; order: 1; }
  .confirm-banner__icon { order: 0; }
  .confirm-banner__form { order: 2; flex: 1 1 auto; }
  .confirm-banner__resend { order: 3; }
  .confirm-banner__action--quiet { padding-left: 0; }
}

/* 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; }
/* Two cards in the default 3-up grid leave an empty third column, which reads as a missing
   item rather than a deliberate pair. Sibling of --compact above: the point of both is that
   the column count matches what the section actually holds. */
.feature-grid--pair { grid-template-columns: repeat(2, 1fr); max-width: 700px; }
.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; }
/* A real generated clip sitting in the card that describes it, in the 9:16 shape it was made in -
   the /ai-video-generator page argues that generating is the easy part and the captions and title
   are the product, and a still frame cannot make that argument. Narrower than the card on purpose:
   at full card width a 9:16 frame is ~550px tall and three of them turn the section into a wall. */
.feature__reel {
  position: relative; width: 100%; max-width: 200px; aspect-ratio: 9 / 16;
  margin: 0 auto 16px; 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);
}
.feature__reel video { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }
/* Icon on the same row as the heading, not stacked above it. Above a card that opens with a tall
   9:16 reel the standalone 38px tile reads as an orphan between two things that belong together -
   the video and the title of what it is. Inline it is a bullet for the heading, which is what it
   was always doing. */
.feature__head { display: flex; align-items: center; gap: 9px; margin-bottom: 6px; }
.feature__head h3 { margin: 0; }
.feature__icon--inline { width: 26px; height: 26px; border-radius: 8px; margin-bottom: 0; flex-shrink: 0; }
/* Cards built around a centred 9:16 reel. The reel is narrower than the card on purpose, so the
   text under it has to be centred too - left-aligned copy under a centred picture reads as a
   mistake rather than as a choice. Only this grid: everywhere else .feature is a text card whose
   left edge is the alignment. */
.feature-grid--reels .feature { text-align: center; }
.feature-grid--reels .feature__head { justify-content: center; }
/* 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.
   :nth-child(5):last-child, not a bare :last-child - the bare version fired on a grid of exactly
   FOUR too, dragging the last card out of column 4 and stretching it across row 2 for no reason.
   The orphan only exists when the count is 5. */
@media (min-width: 861px) { .feature-grid--compact .feature:nth-child(5):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. */
/* .section is the landing page's own band class, reused by every marketing and /compare page
   (14 views). It had NO rule at all - so every one of those pages ran its sections flush against
   each other, with the only breathing room coming from .section-head's own 42px bottom margin
   BELOW its heading and nothing above it. The symptom is a heading that appears glued to the
   cards of the section before it. Given the same rhythm as its sibling .seo-section below, which
   is what the FAQ partial has always used and what these two were plainly meant to share. */
.section { padding: 46px 40px 22px; }
/* Declared but never styled, same as .section was. Left as a no-op hook rather than inventing an
   alternating band the pages were not designed against - adding a background here would change
   the look of 14 pages, which is a design decision, not a spacing fix. */
.section--alt { }
.seo-section { padding: 46px 40px 22px; }
.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); }

/* A section-head sitting above a card grid needs its 42px of air; sitting above its own body
   copy it reads as a heading that has come loose from the text it introduces. Measured on
   /clipping-campaigns, where "What SnapDub is, and what it is not" floats 42px clear of its first
   sentence. Scoped with :has so only the prose case tightens - every grid section keeps the
   rhythm it was designed with. */
.section-head:has(+ .seo-prose) { margin-bottom: 22px; }
.seo-prose { max-width: 720px; margin: 0 auto; }
/* The three-column comparison used by the explainer pages. A real table is the shape this content
   wants - and the shape an assistant can lift - but a <table> at phone width either scrolls
   sideways or squashes to nothing, so it is a grid that becomes stacked labelled blocks.

   The phone labels come from each cell's own data-label, NOT from content: "..." typed in here.
   It started life on /why-clips-get-claimed with the headings hard-coded in this stylesheet, and
   the second page to use it would have silently inherited "Who starts it:" above the wrong column.
   A cell with no data-label simply gets no label, which is what the first column of every row
   wants - it is the row's own title. */
.factgrid { display: grid; gap: 1px; background: var(--line); border: 1px solid var(--line);
  border-radius: var(--r-md); overflow: hidden; margin: 0 0 16px; }
.factgrid__row { display: grid; grid-template-columns: 1.1fr 1fr 1.4fr; gap: 1px;
  background: var(--line); }
.factgrid__row > span { background: var(--surface); padding: 12px 14px; font-size: 14px;
  line-height: 1.5; color: var(--ink-2); }
.factgrid__row--head > span { background: var(--surface-2); font-weight: 600; font-size: 12.5px;
  text-transform: uppercase; letter-spacing: .04em; color: var(--muted); }
/* Two columns instead of three, for a table whose rows are a thing and a consequence. */
.factgrid--pairs .factgrid__row { grid-template-columns: 1fr 1.6fr; }
.claims-note { font-size: 13px !important; color: var(--muted) !important; }
@media (max-width: 720px) {
  .factgrid__row { grid-template-columns: 1fr; }
  .factgrid--pairs .factgrid__row { grid-template-columns: 1fr; }
  .factgrid__row--head { display: none; }
  .factgrid__row > span[data-label]::before { content: attr(data-label) ": "; color: var(--muted); }
}
.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; }
/* The last paragraph's own bottom margin is dead space at the foot of a section - the section's
   padding is what should separate it from the next one, not a stray 14px on top of that. */
.seo-prose > :last-child { margin-bottom: 0; }

/* The global reset is `a { color: inherit; text-decoration: none }`, which leaves a citation inside
   prose looking exactly like the sentence around it. These pages quote other people's numbers and
   send the reader to check them, and a source nobody can see is not a source. Scoped to the
   marketing prose blocks - the app's own chrome styles its links per component. */
.seo-prose a, .seo-source a { color: var(--primary); text-decoration: underline; text-underline-offset: 2px; }
.seo-prose h3 { font-size: 17px; letter-spacing: -.012em; margin: 30px 0 10px; }

/* The before/after crop pairs on /vertical-crop and the landing's reframing block. One file holds
   both halves (see shared/_crop_compare), so the element is a plain 9:8 video and the only job
   here is to keep it inside the prose column and give it an edge. */
.crop-compare { margin: 0 0 18px; max-width: 520px; }
.seo-prose .crop-compare { margin-left: auto; margin-right: auto; }
.crop-compare__vid {
  display: block; width: 100%; aspect-ratio: 9 / 8; object-fit: cover;
  border-radius: 14px; border: 1px solid var(--line); background: #000;
}
.crop-compare__cap { margin-top: 9px; font-size: 13px; line-height: 1.5; color: var(--ink-3); }
.crop-compare__cap strong { display: block; color: var(--ink-2); font-weight: 620; margin-bottom: 2px; }
/* Two pairs side by side where there is room, stacked on a phone - which is most of this traffic. */
.crop-compare-pair { display: grid; gap: 22px; max-width: 720px; margin: 6px auto 26px; }
@media (min-width: 760px) { .crop-compare-pair { grid-template-columns: 1fr 1fr; } }
.crop-compare-pair .crop-compare { max-width: none; margin-bottom: 0; }
/* Three across needs more room than the prose column, or each pair is too small to read the thing
   it is there to show. Two of three on a mid-size screen, one below that. */
.crop-compare-pair--three { max-width: 980px; }
@media (min-width: 1000px) { .crop-compare-pair--three { grid-template-columns: repeat(3, 1fr); } }
/* One on its own is a figure, not half a grid. */
.crop-compare-pair--one { max-width: 520px; }
@media (min-width: 760px) { .crop-compare-pair--one { grid-template-columns: 1fr; } }

/* The alternatives tables. A real table because the content is tabular and because assistants
   quote tables; the wrapper scrolls rather than the page, since four columns of prose do not fit
   a phone and a body that scrolls sideways is a bug everywhere else on the site. */
/* The "coming from somewhere else" line under the landing's comparison table. Quiet on purpose:
   it is a signpost for the minority who arrived with a specific tool in mind, not a second CTA. */
.compare__leaving { max-width: 62ch; margin: 22px auto 0; text-align: center; font-size: 13.5px; color: var(--muted); line-height: 1.6; }
.compare__leaving a { color: var(--primary); text-decoration: underline; text-underline-offset: 2px; }

.alt-table__scroll { max-width: 900px; margin: 0 auto; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.alt-table { border-collapse: collapse; width: 100%; min-width: 660px; font-size: 13.5px; }
.alt-table th, .alt-table td { text-align: left; vertical-align: top; padding: 12px 14px; border-bottom: 1px solid var(--line); }
.alt-table thead th { font-size: 11.5px; letter-spacing: .06em; text-transform: uppercase; color: var(--muted); font-weight: 600; }
.alt-table tbody th { font-weight: 650; color: var(--ink); white-space: nowrap; }
.alt-table td { color: var(--ink-2); line-height: 1.5; }
.alt-table__price { font-family: var(--font-mono); white-space: nowrap; }
.alt-table tr.ours { background: var(--primary-tint); }
.alt-table tr.ours th { color: var(--primary); }
.alt-table a { color: var(--primary); text-decoration: underline; text-underline-offset: 2px; }
@media (max-width: 720px) {
  /* Each tool becomes a card. Scrolling the table sideways technically worked, but the last
     column - the one that says which tool suits you, the entire point of the page - sat off the
     edge with nothing to suggest it was there. */
  .alt-table, .alt-table tbody, .alt-table tr, .alt-table td, .alt-table tbody th { display: block; width: auto; }
  .alt-table { min-width: 0; }
  .alt-table thead { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
  .alt-table tr { border: 1px solid var(--line); border-radius: var(--r-md); padding: 4px 0; margin-bottom: 12px; }
  .alt-table tbody th { border: none; padding-bottom: 4px; font-size: 15px; }
  .alt-table td { border: none; padding-top: 4px; padding-bottom: 8px; }
  .alt-table td::before { content: attr(data-label); display: block; font-size: 10.5px;
    letter-spacing: .06em; text-transform: uppercase; color: var(--muted); margin-bottom: 3px; }
  .alt-table tr.ours { border-color: var(--primary); }
}

/* 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) {
  .section { padding: 42px 20px 14px; }
  .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; }
/* flex:0 0 auto stops it shrinking, so on a phone the logo + wordmark + copyright line runs
   past the footer's own edge. It is one line of three separate things - let it wrap. */
.marketing-footer__brand { display: flex; align-items: center; gap: 8px; flex: 0 1 auto; flex-wrap: wrap; min-width: 0; }
.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; }
/* Shown only when a link was carried in from the landing hero - reassurance, not decoration, so
   it is tinted like a confirmation rather than styled as another paragraph of copy. */
.auth-head__carried { display: inline-flex; align-items: center; gap: 6px; margin: 10px 0 0; padding: 6px 11px; border-radius: 999px; background: var(--primary-tint); color: var(--primary); font-size: 12px; }
.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); }
/* The same pill, as a way out: it is the only part of a locked row a finger can use, since the
   row's own tooltip needs a hover this traffic mostly does not have (shared/_broll_check). */
.check__cost--link { text-decoration: none; }
.check__cost--link:hover { filter: brightness(.95); }
/* A setting the plan does not carry: shown, greyed, unticked, with the why on hover. */
.check--locked { position: relative; color: var(--muted); cursor: default; }
.check--locked::after { content: attr(data-tip); position: absolute; left: 0; bottom: calc(100% - 4px);
  z-index: 5; width: max-content; max-width: 290px; padding: 7px 9px; border-radius: var(--r-sm);
  background: var(--ink); color: #fff; font-size: 11.5px; line-height: 1.4; font-weight: 400;
  opacity: 0; visibility: hidden; transition: opacity .12s ease; pointer-events: none; }
.check--locked:hover::after, .check--locked:focus-within::after { opacity: 1; visibility: visible; }

/* 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 4px; }

/* Launch pricing. The banner sits over the whole grid; the badge repeats it per card on the RIGHT,
   so it never collides with Most popular / Current plan, which own the left corner. */
.promo-banner { display: flex; align-items: center; justify-content: center; gap: 10px; flex-wrap: wrap;
  max-width: 660px; margin: 0 auto 18px; padding: 11px 16px; text-align: center;
  font-size: 13px; line-height: 1.45; color: var(--ink-soft);
  background: color-mix(in srgb, var(--primary) 7%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--primary) 26%, var(--line)); border-radius: var(--r-lg); }
.promo-banner__tag { 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; white-space: nowrap; }
.pricing-card__badge--launch { left: auto; right: 24px; background: var(--ink); }
/* The monthly price, struck through beside the annual one. */
.pricing-card__was { align-self: center; font-family: var(--font-mono); font-size: 17px; color: var(--muted);
  text-decoration-thickness: 1.5px; margin-right: 2px; }
.pricing-card__was[hidden] { display: none; } /* [hidden] must beat the display the flex parent gives it */
.pricing-card__note { font-size: 11.5px; color: var(--muted); margin: 0 0 16px; }
.pricing-card__note[hidden] { display: none; }

/* 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; }
/* The minutes sit on their own line under the credit count - it is the half of that
   sentence a person can actually act on, so it must not read as a trailing clause. */
.cm-offer__minutes { display: block; margin-top: 3px; font-weight: 600; color: var(--ink-1); }
.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; }
/* Its sibling rules all had this and this one did not, so "Thanks for the feedback!" was on screen
   from the moment the prompt appeared - before anybody had given any. Found 2026-09-17 by opening
   the modal in a browser; the markup carries `hidden` and `display: flex` simply beat it. Same
   trap as .review-modal__comment[hidden] two rules up. */
.review-modal__thanks[hidden] { display: none; }

/* 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; }
/* The automatic emoji's starting spot. Sized as a fraction of the frame like the caption block is,
   so dragging an edge changes the same number the renderer reads. Default 12% sits it comfortably
   above the caption band without covering a face. */
.settings-preview__emoji {
  /* top 83%: the same value the renderer defaults to (worker/pipeline.py#_EMOJI_DEFAULT_Y) -
     directly under the caption block, which ends 20% up from the bottom, close enough to read as
     part of it and high enough to stay out of the very bottom edge. */
  position: absolute; left: 44%; top: 83%; width: 12%; z-index: 4; text-align: center;
  font-size: 9cqw; line-height: 1; transform: translateY(-50%);
}
.settings-preview__emoji[hidden] { display: none; }
/* The resize grip, bottom-right, like the clip editor's asset handles. It exists because the
   emoji is one glyph wide: an edge-drag zone on something that small leaves nothing in the middle
   to move by, so the two gestures were fighting. Sized in px rather than cqw - a grab target has
   to stay grabbable whatever size the preview is drawn at. */
.settings-preview__grip {
  position: absolute; right: -7px; bottom: -7px; width: 14px; height: 14px; border-radius: 50%;
  background: var(--primary); border: 2px solid #fff; box-shadow: 0 1px 3px rgba(0,0,0,.4);
  cursor: nwse-resize; touch-action: 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 96px; } /* same widget reserve as above, phone spacing */
  .topbar { padding: 0 16px; gap: 12px; }
  /* A library row on a phone: the title gets a line to itself. Sharing one with the status badge
     left it about two characters wide - the badge is a fixed 160px and the title's own column is
     flex-basis 0, so everything the badge took came straight out of the title, which then wrapped
     one WORD (sometimes one letter) per line. Reported; confirmed at 412px, where a.lib-video was
     96px wide with a 56px thumbnail inside it. */
  .lib-row { flex-wrap: wrap; row-gap: 10px; }
  .lib-row a.lib-video { flex: 1 1 100%; }
  .lib-star { margin-left: auto; }
  /* "Workspace >" is the first thing to go on a phone: it never names anything you can navigate
     to, the page's own <h1> repeats the current crumb right below, and dropping it buys back the
     width a second topbar action needs. */
  .breadcrumb .crumb-muted, .breadcrumb .sep { display: none; }
  .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; }
  /* The spacer goes; the chips do NOT. They are the only thing explaining what happened to the
     video, and hiding them on the viewport that carries most of our traffic left the "after" tab
     showing a clip and nothing else. Stacked under it instead, laid out in a row. */
  .player__spacer { display: none; }
  .player__layer { flex-direction: column; gap: 14px; }
  .player__chips { flex-direction: row; flex-wrap: wrap; justify-content: center; gap: 6px; align-items: center; order: 2; }
  .player__chip { padding: 5px 9px; font-size: 11px; border-radius: 7px; box-shadow: 0 3px 10px rgba(0,0,0,.2); }
  .player__chip svg { width: 12px; height: 12px; }
  .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; }
}
/* Faster on a phone, because a thumb flick moves the page at around 2000px/s and a 550ms fade
   simply cannot keep up: measured on the landing, every revealing block finished its animation
   after its top had already left the screen, the worst by more than a full screen. Halving the
   duration and the stagger, together with the earlier trigger in scroll_reveal_controller.js,
   is what puts the fade back where it can be seen. Not a reduced-motion concern - that is handled
   above, and this is still a real animation, just one paced for the device it runs on. */
@media (max-width: 768px) {
  .reveal { transition-duration: 380ms; transition-delay: calc(var(--d, 0) * 35ms); }
}

/* ── 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; }
/* A time kept for one folder. The border carries as much of the signal as the fill does, so it
   still reads as different on a screen where the tint washes out. */
.slot-time--reserved { background: var(--st-amber-bg); border-color: var(--st-amber-line);
                       color: var(--st-amber); }
.slot-time--reserved .slot-time__remove { color: var(--st-amber); opacity: .65; }
.slot-time__remove { display: inline-flex; color: var(--muted); line-height: 0; }
.slot-time__remove:hover { color: var(--st-red); }
/* The picked file, shown before it is uploaded. Capped the same way the imported cover is: it is
   confirmation, not the subject of the page. */
.upload-preview { display: block; width: 100%; max-width: 320px; aspect-ratio: 16 / 9;
                  object-fit: cover; border-radius: var(--r-md); background: #000;
                  margin-bottom: 10px; }
.upload-preview[hidden] { display: none; }

/* The shape under Stripe's Payment Element while its iframe boots - see
   shared/_payment_element.html.erb. Deliberately the same geometry the Element settles into, so
   the swap is a fade rather than a jump. */
/* min-height so the card is its final size from the first paint - the Element settles around
   this, and without it the card grows as Stripe's iframe fills in. */
/* The consent line under the review box. */
/* The comment box carries margin-bottom: 16px, which used to be the whole gap above the buttons.
   Putting this row in between ate it, so the spacing is restored here rather than by changing the
   actions rule - the buttons look right in every other state and should keep doing so. */
.review-modal__consent { display: flex; align-items: center; gap: 8px; margin: -6px 0 16px;
                         font-size: 13px; color: var(--ink-2); cursor: pointer; }
/* review_prompt_controller hides this with .hidden = true after submitting, and `display: flex`
   above beats the browser's own [hidden] rule without this line - the repeat mistake recorded in
   css-hidden-vs-display-override. */
.review-modal__consent[hidden] { display: none !important; }

.pay-mount { position: relative; min-height: 232px; }
.pay-skeleton { display: grid; gap: 10px; position: absolute; inset: 0;
                background: var(--surface); }
/* Stimulus hides this with .hidden = true, and `display: grid` above would win over the browser's
   own [hidden] rule without this - the same trap noted in css-hidden-vs-display-override. */
.pay-skeleton[hidden] { display: none; }
.pay-skeleton__tabs { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.pay-skeleton__pair { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
.pay-skeleton__block { display: block; height: 46px; border-radius: var(--r-md); }
.pay-skeleton__block--field { height: 42px; }
.pay-skeleton__line { display: block; height: 10px; width: 90px; border-radius: 4px;
                      margin-bottom: 6px; }
.pay-skeleton__block, .pay-skeleton__line {
  background: linear-gradient(90deg, var(--surface-2) 25%, var(--line) 37%, var(--surface-2) 63%);
  background-size: 400% 100%;
  animation: pay-skeleton-sweep 1.4s ease-in-out infinite;
}
@keyframes pay-skeleton-sweep { 0% { background-position: 100% 50% } 100% { background-position: 0 50% } }
/* Somebody who asked for less motion gets the shape without the sweep - it is a placeholder, and
   the information is the shape, not the animation. */
@media (prefers-reduced-motion: reduce) {
  .pay-skeleton__block, .pay-skeleton__line { animation: none; }
}

.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; }

/* ---- Affiliate offer on the Earn page --------------------------------------
   Deliberately not a .card: everything else on that page is a third-party payout program we do
   not operate, and this is the one we pay ourselves. Same surface and radius as the cards so it
   still belongs to the page, but a left accent rail and a tinted ground say "different in kind"
   before the label is read. */
.earn-affiliate {
  display: flex; align-items: center; justify-content: space-between; gap: 20px; flex-wrap: wrap;
  margin: 4px 0 20px; padding: 22px 24px;
  background: linear-gradient(180deg, rgba(200, 30, 58, .045), rgba(200, 30, 58, .015));
  border: 1px solid var(--line); border-left: 3px solid var(--primary);
  border-radius: var(--r-lg);
}
/* Takes the slack so the action only drops to its own line when the row genuinely cannot fit it,
   and min-width: 0 lets the paragraphs wrap instead of forcing that. */
.earn-affiliate__body { flex: 1 1 22rem; min-width: 0; }
.earn-affiliate__label {
  display: inline-block; font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .08em;
  text-transform: uppercase; color: var(--primary); margin-bottom: 6px;
}
.earn-affiliate h2 { font-size: 1.05rem; margin: 0 0 6px; }
.earn-affiliate p { margin: 0 0 8px; font-size: .92rem; color: var(--ink-2); max-width: 46rem; }
.earn-affiliate__note { font-size: .86rem; color: var(--muted); margin-bottom: 0; }
/* margin-left: auto rather than justify-content, so the button stays on the same side once the
   row wraps on a narrow screen instead of jumping to the opposite edge. */
.earn-affiliate__action {
  margin-left: auto; flex-shrink: 0; display: flex; flex-direction: column; align-items: flex-end;
  gap: 6px;
}
.earn-affiliate__meta { font-size: .78rem; color: var(--muted); }
/* "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); }

/* ---- Caption style picker -------------------------------------------------
   A swatch grid instead of a <select>. With four presets a dropdown was fine;
   with twenty-five the whole point is seeing the looks next to each other, and
   a native select shows one name at a time and no look at all.

   Each tile's SAMPLE carries inline CSS derived from the same ASS values the
   renderer burns (CaptionStyle#preview), so nothing about a preset's appearance
   is described here - this file only lays the tiles out. Adding a preset to
   worker/caption_styles.yml needs no change in this stylesheet. */
.capstyle {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(104px, 1fr));
  gap: 8px;
  max-height: 320px;
  overflow-y: auto;
  padding: 2px;
}

.capstyle__tile { display: block; cursor: pointer; }

/* Visually hidden rather than display:none - a hidden radio is unreachable by
   keyboard and invisible to a screen reader, and this is the only control for
   the setting. */
.capstyle__input {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* Stands in for the footage: captions are never read on white, so a swatch on
   the page's own background would flatter every light preset and hide the
   outline that makes them legible in the real render. */
.capstyle__stage {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 54px;
  border-radius: 8px;
  border: 2px solid var(--line-strong);
  background:
    linear-gradient(135deg, #2f3238 0%, #1b1d21 55%, #34383f 100%);
  overflow: hidden;
  transition: border-color .12s ease, box-shadow .12s ease;
}

.capstyle__sample {
  display: inline-block;
  font-size: 20px;
  line-height: 1.15;
  padding: 2px 7px;
  border-radius: 3px;
  white-space: nowrap;
}

.capstyle__name {
  display: block;
  margin-top: 5px;
  font-size: 11.5px;
  line-height: 1.25;
  color: var(--ink-3);
  text-align: center;
}

.capstyle__input:checked + .capstyle__stage {
  border-color: var(--primary);
  box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 22%, transparent);
}

.capstyle__input:checked ~ .capstyle__name { color: var(--ink); font-weight: 500; }

.capstyle__input:focus-visible + .capstyle__stage {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

.capstyle__tile:hover .capstyle__stage { border-color: var(--muted); }

/* ---- Locked preview -------------------------------------------------------
   A worked EXAMPLE of a paid feature, behind glass. Free accounts used to hit
   auto-import and the content plan and find a paywall notice followed by an
   empty page, which shows what you do not have without showing what it does.

   The stage is inert in the markup, so nothing inside is focusable or
   clickable; pointer-events here is the belt to that braces. */
.locked { position: relative; margin-bottom: 24px; }

/* Readable on purpose. The first pass blurred and faded it so hard, and centred the card over
   the middle of it, that the example was barely visible - which defeats the point: the picture
   IS the pitch, the card is only the price tag. */
.locked__stage {
  pointer-events: none;
  user-select: none;
  opacity: .8;
  filter: blur(.6px);
}

/* Fades INTO the page rather than covering the example evenly: the top stays
   legible enough to read as a real week of posts, which is the whole pitch,
   while the bottom recedes behind the card. */
/* Bottom-aligned, so the top of the example stays clear and the card sits under it like a
   caption rather than on top of it like a blindfold. */
.locked__veil {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: flex-end;
  justify-content: center;
  padding: 16px;
  background: linear-gradient(
    to bottom,
    transparent 0%,
    color-mix(in srgb, var(--bg, #ffffff) 30%, transparent) 38%,
    color-mix(in srgb, var(--bg, #ffffff) 82%, transparent) 100%
  );
  border-radius: 12px;
}

.locked__card {
  max-width: 420px;
  text-align: center;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: 12px;
  padding: 20px 22px;
  box-shadow: 0 4px 10px rgba(0, 0, 0, .05), 0 18px 40px -22px rgba(0, 0, 0, .35);
}

.locked__card h3 { margin: 10px 0 6px; font-size: 17px; }

.locked__card p {
  margin: 0 0 16px;
  font-size: 13.5px;
  line-height: 1.5;
  color: var(--ink-3);
}

/* The chips sit between the paragraph and the button and had no space of their own, so on a
   narrow screen the button landed flush against the last chip. */
.locked__chips {
  justify-content: center;
  margin: 0 0 16px;
}

/* Says "example" out loud. Fake channels and fake scheduled posts are only
   honest while they are unmistakably fake. */
.locked__tag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 11px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-3);
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 3px 9px;
}

@media (prefers-reduced-motion: no-preference) {
  .locked__card { transition: box-shadow .2s ease; }
}

/* Below 760px the real week grid hides itself in favour of an agenda list (see .calendar-agenda).
   The EXAMPLE has no agenda to fall back to, so its stage collapsed to nothing while the card,
   pinned to inset:0, kept its own height and floated up over the page header - it read as a
   button stuck to the top of the screen. Reported from a phone-width window.

   Two halves to the fix: the example's grid stays visible (it scrolls sideways inside its own box,
   which .calendar already allows), and the card stops being an overlay and simply follows the
   example down the page. Nothing can escape a box it is no longer positioned inside. */
@media (max-width: 760px) {
  .calendar--example { display: block; }

  .locked__veil {
    position: static;
    background: none;
    padding: 12px 0 0;
  }

  .locked__card { max-width: none; }
}

/* The picker inside the CLIP EDITOR, where the sidebar is narrow and shared with the transcript.
   At the settings page's sizing the grid wrapped into a tall block that pushed the caption
   transcript - the thing you actually came to this tab to edit - almost off the panel. Four to a
   row, one row deep, scrolls for the rest. */
.editor__panel .capstyle {
  grid-template-columns: repeat(4, 1fr);
  gap: 7px;
  max-height: 188px;
}

/* Roughly a row and a half rather than two and a half: enough to see that it scrolls, small
   enough that the Details tab still ends above the fold. */
.editor__panel .capstyle--compact { max-height: 112px; }
.editor__panel .capstyle__stage { height: 48px; }
.editor__panel .capstyle__sample { font-size: 18px; }
.editor__panel .capstyle__name { font-size: 11px; margin-top: 4px; }

/* ---- Emoji picker -----------------------------------------------------------
   Sits above the asset rail in the editor's narrow side column. Nothing here
   loads an image: the browser draws the glyph, and the renderer resolves the
   same character to a Noto PNG inside its own image, so an emoji costs no
   upload and no storage. */
.emoji-picker { display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px; }

.emoji-picker__label {
  display: block;
  font-size: 10px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 4px;
}

.emoji-picker__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(26px, 1fr));
  gap: 3px;
}

.emoji-picker__item {
  appearance: none;
  border: 1px solid transparent;
  background: none;
  border-radius: 6px;
  padding: 2px;
  font-size: 17px;
  line-height: 1.35;
  cursor: pointer;
}

.emoji-picker__item:hover { background: var(--surface-2); border-color: var(--line); }
.emoji-picker__item:focus-visible { outline: 2px solid var(--primary); outline-offset: 1px; }

/* The glyph on the stage. An <img> scales with the handle's width for free; text
   does not, so the handle becomes a size container and the glyph is sized in cqw
   - dragging the corner then resizes an emoji exactly like an uploaded image. */
.asset-stage__handle--emoji {
  container-type: inline-size;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Normally an <img> of Noto's own artwork, so it fills the handle exactly the way the burned
   overlay fills its placement. The text properties are for the fallback <span>, which only appears
   when Google has no picture for this character (see #emojiFace). */
.asset-stage__emoji {
  display: block;
  width: 100%;
  height: auto;
  text-align: center;
  font-size: 92cqw;
  line-height: 1;
  user-select: none;
  pointer-events: none;
}

/* Sections inside the emoji tab - seven groups at once made the list long enough
   to push the stage off the screen in the editor's narrow column. */
.emoji-picker__tabs { display: flex; gap: 2px; margin-bottom: 8px; }

.emoji-picker__tab {
  flex: 1;
  appearance: none;
  border: 0;
  background: none;
  padding: 4px 2px;
  font-size: 10.5px;
  color: var(--ink-3);
  border-bottom: 2px solid transparent;
  cursor: pointer;
}

.emoji-picker__tab.is-active { color: var(--ink); border-bottom-color: var(--primary); font-weight: 500; }
.emoji-picker__tab:focus-visible { outline: 2px solid var(--primary); outline-offset: -2px; }

/* ---- Overlay track lane -----------------------------------------------------
   When each overlay is on screen, on the CLIP's own time axis. Deliberately its
   own strip rather than a row under the trim filmstrip: that one runs on the
   SOURCE's timeline (padded buffer included) while a placement's window is
   measured from the start of the clip, so stacking them would imply an
   alignment that does not exist. */
/* Sits directly under the trim filmstrip and shares its scale - the rows are
   positioned to the SELECTION (see #alignToSelection), because the strip covers
   the padded source while an overlay window is measured from the start of the
   clip. */
.overlay-track { margin-top: 6px; }

.overlay-track__ruler {
  display: flex;
  justify-content: space-between;
  font-size: 10.5px;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
  margin-bottom: 4px;
}

.overlay-track__rows { display: flex; flex-direction: column; gap: 4px; }

.overlay-track__row {
  position: relative;
  height: 30px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: 6px;
}

/* Absolute inside its row: left and width ARE the window, so the bar is a
   direct picture of when the overlay is on screen. */
.overlay-track__bar {
  position: absolute;
  top: 2px;
  bottom: 2px;
  min-width: 26px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--primary) 16%, var(--surface));
  border: 1px solid var(--primary);
  border-radius: 5px;
  cursor: grab;
  user-select: none;
  /* The bar owns its own horizontal gesture: without this a drag on a phone becomes a page scroll
     and the pointer stream is cancelled mid-window-drag. */
  touch-action: none;
}

.overlay-track__bar:active { cursor: grabbing; }
/* Which bar the inspector is talking about. Same ring as the handle on the frame, because they
   are two views of one selected thing and reading as two different states would be a lie. */
.overlay-track__bar.is-selected {
  outline: 2px solid var(--primary);
  outline-offset: 1px;
  background: color-mix(in srgb, var(--primary) 30%, var(--surface));
}

.overlay-track__face { font-size: 15px; line-height: 1; pointer-events: none; }
.overlay-track__face img,
/* A video overlay's own first frame, at the same size an image thumbnail gets - the bar was
   empty for these until 2026-09-21 (asset_stage_controller#buildBar). */
.overlay-track__face video { height: 18px; width: auto; max-width: 32px; object-fit: cover;
  display: block; border-radius: 3px; }

/* Wide enough to grab on a trackpad without swallowing a short bar's middle. */
.overlay-track__grip {
  position: absolute;
  top: -1px;
  bottom: -1px;
  width: 9px;
  cursor: ew-resize;
  background: var(--primary);
  opacity: .55;
  touch-action: none;
}
/* 9px is a mouse target, not a finger one, and widening the visible grip instead would swallow
   the middle of a short bar - the part you grab to slide the whole window. So the LOOK stays 9px
   and only the hit area grows outward, the same trick a native slider thumb uses. Kept off the
   bar's middle: it extends past the bar's own edge rather than inward. */
.overlay-track__grip::before {
  content: "";
  position: absolute;
  top: -6px;
  bottom: -6px;
  left: -6px;
  right: -6px;
}
.overlay-track__grip--start::before { left: -11px; right: -2px; }
.overlay-track__grip--end::before { right: -11px; left: -2px; }

.overlay-track__grip:hover { opacity: 1; }
.overlay-track__grip--start { left: -1px; border-radius: 5px 0 0 5px; }
.overlay-track__grip--end { right: -1px; border-radius: 0 5px 5px 0; }

.overlay-track__empty {
  margin: 0;
  padding: 10px 0;
  font-size: 12px;
  color: var(--ink-3);
  text-align: center;
}

/* ── The playhead, and the wrapper that lets one line cross both strips ─────────────────────
   The trim strip and the overlay lane are laid out on the same axis (see clips/edit.html.erb's
   own comment), so the line is positioned once, against this wrapper, and reads correctly against
   both of them. */
.editor__tracks { position: relative; touch-action: pan-y; }
.editor__playhead {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  width: 2px;
  margin-left: -1px;
  background: #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .45);
  /* The line is a readout, never a target: presses belong to the strips underneath it, which is
     what makes a scrub that starts ON the playhead behave like every other scrub. */
  pointer-events: none;
  z-index: 3;
}
/* A head to grab with the eye - the line alone disappears against a busy filmstrip. */
.editor__playhead::before {
  content: "";
  position: absolute;
  top: -3px;
  left: 50%;
  width: 11px;
  height: 11px;
  margin-left: -5.5px;
  border-radius: 2px;
  background: #fff;
  box-shadow: 0 0 0 1px rgba(0, 0, 0, .45);
}

/* ── The inspector: the selected overlay's own controls ─────────────────────────────────────
   One row on a desktop, under the lane it belongs to. See shared/_asset_inspector.html.erb. */
.asset-inspector {
  display: flex;
  align-items: center;
  gap: 14px;
  flex-wrap: wrap;
  margin-top: 10px;
  padding: 10px 12px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  border-radius: var(--r-lg);
}
/* .asset-inspector sets `display`, so the plain [hidden] attribute needs saying again or the panel
   stays on screen with nothing selected. */
.asset-inspector[hidden] { display: none; }
.asset-inspector__id { display: flex; align-items: center; gap: 8px; min-width: 0; }
.asset-inspector__face {
  display: grid;
  place-items: center;
  width: 30px;
  height: 30px;
  border-radius: 6px;
  background: var(--surface);
  border: 1px solid var(--line);
  overflow: hidden;
  font-size: 17px;
  line-height: 1;
  flex: none;
}
.asset-inspector__face img,
.asset-inspector__face video { max-width: 100%; max-height: 100%; display: block; }
.asset-inspector__label { font-size: 12px; color: var(--ink-3); }
.asset-inspector__group { display: flex; align-items: center; gap: 10px; }
.asset-inspector__field { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--ink-3); }
.asset-inspector__field--grow { flex: 1 1 180px; min-width: 150px; }
.asset-inspector__num { width: 72px; height: 32px; padding: 0 8px; font-size: 13px; }
/* .range's own track is --surface-2, which is exactly this panel's background - on it the slider
   was a thumb floating in nothing (seen in the browser, not reasoned about). The track needs to
   differ from whatever it sits on, so it is restated here rather than the panel being lightened:
   the panel reads as a toolbar precisely because it is not the same white as the page. */
.asset-inspector__range { flex: 1 1 auto; min-width: 90px; background: var(--surface); border: 1px solid var(--line-strong); }
.asset-inspector__out { font-size: 12px; min-width: 38px; text-align: right; }
.asset-inspector__remove { margin-left: auto; }
/* .asset-inspector__field sets display:flex, so the plain [hidden] attribute needs saying again -
   the volume row is hidden for everything that cannot make a sound. */
.asset-inspector__field[hidden], .asset-inspector__group[hidden] { display: none !important; }

/* A finger is not a mouse: the lane's rows and grips grow for touch, independently of screen
   width (a tablet is wide AND touched). The bar's LOOK grows here, unlike the grip's, because a
   taller row has the space for it - what a wider grip would cost is the bar's grabbable middle,
   which is why that one only grows its hit area (see .overlay-track__grip::before). */
@media (pointer: coarse) {
  .overlay-track__row { height: 40px; }
  .overlay-track__bar { min-width: 40px; }
  .overlay-track__grip { width: 12px; }
}

@media (max-width: 640px) {
  /* CapCut's phone shape: what you selected gets a bar at the bottom of the screen, not a row
     buried somewhere in the scroll. Sticky rather than fixed, so it stays inside the form and
     Save is never stranded underneath it. */
  .asset-inspector {
    position: sticky;
    bottom: 10px;
    z-index: 6;
    background: var(--surface);
    box-shadow: 0 6px 24px rgba(0, 0, 0, .18);
    row-gap: 10px;
  }
  /* Restores the phone rules for inputs (44px targets, and 16px so iOS doesn't zoom the page on
     focus) that .asset-inspector__num's own desktop sizing would otherwise win against, sitting
     later in this file than the shared phone block. */
  .asset-inspector__num { min-height: 44px; font-size: 16px; width: 84px; }
  /* Three rows on a phone: what it is (with Remove), when it shows, how big. Remove is pulled up
     onto the FIRST row rather than left at the end, where the page's own support bubble sits over
     the sticky bar's bottom-right corner - a destructive control underneath something else is the
     one place that overlap actually costs something. */
  .asset-inspector__id { order: -1; flex: 1 1 auto; }
  .asset-inspector__remove { order: 0; min-height: 40px; }
  .asset-inspector__group { order: 1; flex-basis: 100%; }
  .asset-inspector__field--grow { order: 2; flex-basis: 100%; }
}

/* ---- Re-render panel actions ------------------------------------------------
   A flex item may shrink below the width of its own content, and a button whose
   label is a whole sentence ("Re-render 8 clips - 240 credits") then wraps one
   letter per line instead of moving down to a row of its own. Reported on a
   phone.

   Two halves: the label never breaks mid-word, and below the width where the
   picker and the button stop fitting side by side each takes its own line. */
.template-rerender__actions .btn { white-space: nowrap; }

/* Below the width where the explanation and the controls stop fitting side by
   side, each half takes the full row - otherwise the text column keeps shrinking
   until it reads two words at a time, which is the same "a flex item may shrink
   below its content" problem as the button, one level up. */
@media (max-width: 620px) {
  .template-rerender > *,
  .template-rerender__actions > * { flex: 1 1 100%; min-width: 0; }
}


/* ---------------------------------------------------------------------------------------------
   SEASONAL: HALLOWEEN (shared/_halloween, marketing layout only, Seasonal::WINDOWS decides when)

   Everything here is fixed-position, pointer-events:none and aria-hidden, so it cannot move the
   page, cannot take a click, and cannot reach a screen reader. Sizes are deliberately modest: the
   job is "somebody decorated the office", not "you have wandered into a haunted house", and this
   is still the page somebody decides whether to trust us with a card on.
   --------------------------------------------------------------------------------------------- */
.hw { position: fixed; inset: 0; z-index: 3; pointer-events: none; overflow: hidden; }
.hw * { pointer-events: none; }

.hw__web { position: absolute; top: 0; width: 118px; height: 118px; color: var(--ink-3); opacity: .28; }
.hw__web--left { left: 0; }
.hw__web--right { right: 0; }

/* The thread hangs from the very top, so the spider reads as coming from off-screen. */
.hw__spider { position: absolute; top: 0; right: 17%; display: flex; flex-direction: column; align-items: center;
  color: #1b1b1f; animation: hw-drop 2.6s cubic-bezier(.22,1.2,.36,1) 1 both, hw-bob 4.5s ease-in-out 2.6s infinite; }
.hw__thread { display: block; width: 1px; height: 96px; background: linear-gradient(to bottom, transparent, var(--ink-3)); }
.hw__spider-body { display: block; width: 34px; height: 28px; margin-top: -1px; }
.hw__legs { animation: hw-legs 1.8s ease-in-out infinite; transform-origin: 22px 22px; }

@keyframes hw-drop { from { transform: translateY(-130px); } to { transform: translateY(0); } }
@keyframes hw-bob  { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(13px); } }
/* The legs flex rather than swing - a rotation here makes the whole spider look like it is falling. */
@keyframes hw-legs { 0%, 100% { transform: scaleX(1) scaleY(1); } 50% { transform: scaleX(1.06) scaleY(.94); } }

.hw__pumpkins { position: absolute; left: 14px; bottom: 0; display: flex; align-items: flex-end; gap: 6px; }
.hw__pumpkin { display: block; width: calc(64px * var(--hw-scale)); height: calc(56px * var(--hw-scale));
  filter: drop-shadow(0 0 14px rgba(251, 139, 46, .38)); }
.hw__pumpkin--b { margin-bottom: -2px; }
/* Two candles, two paces, so they never blink in unison - the tell that it is one animation. */
.hw__candle { animation: hw-flicker 3.1s ease-in-out infinite; }
.hw__pumpkin--b .hw__candle { animation-duration: 2.3s; animation-delay: .45s; }

@keyframes hw-flicker { 0%, 100% { opacity: 1; } 42% { opacity: .82; } 58% { opacity: .93; } 71% { opacity: .78; } }

/* Phones: 60% of the traffic, and a 34px spider swinging over a 390px-wide hero is not charm, it
   is an obstruction. The webs stay (they sit in the corners and cost nothing), the spider goes. */
@media (max-width: 700px) {
  .hw__web { width: 78px; height: 78px; opacity: .22; }
  .hw__pumpkins { left: 6px; gap: 2px; transform: scale(.72); transform-origin: left bottom; }
}

/* Somebody who has asked their machine for less movement has asked for exactly this. The decor
   stays, everything that moves stops, and the spider sits where it would have landed. */
@media (prefers-reduced-motion: reduce) {
  .hw__spider { animation: none; transform: none; }
  .hw__legs, .hw__candle { animation: none; }
}

/* Inside the app (application.html.erb). Same decorations, dialled down: the left-hand web would
   land on the sidebar's own logo, the pumpkins would land on the credit balance, and a spider on a
   thread would swing over whatever somebody is reading. So: one web, one spider sitting in it, and
   nothing that travels.

   BELOW THE TOPBAR, not at the top of the viewport. .topbar is sticky at z-index 30 with an opaque
   blurred background, so anything of ours at top:0 is simply painted over - reported as "павука
   сховав навбар верхній". --hw-top is where the app's own chrome ends. */
.hw--subtle { --hw-top: 57px; z-index: 1; }
.hw--subtle .hw__web { top: var(--hw-top); width: 92px; height: 92px; opacity: .17; }
.hw--subtle .hw__web--left { display: none; }
.hw--subtle .hw__spider { top: calc(var(--hw-top) + 26px); right: 26px; animation: none; transform: none; }
.hw--subtle .hw__spider-body { width: 21px; height: 17px; opacity: .55; }
/* Only the legs move, and barely. Enough to notice on a second glance, not enough to pull an eye
   away from the page. */
.hw--subtle .hw__legs { animation-duration: 3.4s; }

/* The pumpkins facing the logo in the sidebar (shared/_sidebar). Small, and a tighter glow than
   the big ones get - a 20px pumpkin under a 14px shadow is a smudge. */
.hw-brand { margin-left: auto; display: flex; align-items: flex-end; gap: 3px; pointer-events: none; }
.hw-brand .hw__pumpkin { filter: drop-shadow(0 0 5px rgba(251, 139, 46, .5)); }

@media (max-width: 700px) {
  /* Phones keep the decoration, they just get the version that cannot be in the way. The spider
     stops dangling and sits in the corner web on the marketing side too, because a 34px spider
     swinging over a 390px-wide hero is an obstruction rather than a joke. */
  .hw__thread { display: none; }
  .hw__spider { display: flex; top: 62px; right: 14px; animation: none; transform: none; }
  .hw__spider-body { width: 22px; height: 18px; }
  .hw--subtle .hw__spider { top: calc(var(--hw-top) + 16px); right: 16px; }
  .hw--subtle .hw__spider-body { width: 18px; height: 15px; }
  .hw--subtle .hw__web { width: 62px; height: 62px; }
}

/* Retention: how long a project is kept (Plan::CLIP_RETENTION). Two faces of the same fact - a
   quiet line on the project page that states the rule, and a chip on the library card that only
   appears once the date is close enough to act on. */
.expiry-note { display: flex; align-items: center; gap: 7px; flex-wrap: wrap;
  margin: -8px 0 20px; padding: 9px 13px; border-radius: var(--r-md);
  font-size: 12.5px; line-height: 1.45; color: var(--ink-3);
  background: var(--surface-2); border: 1px solid var(--line); }
.expiry-note svg { flex-shrink: 0; color: var(--muted); }
.expiry-note.is-close { color: var(--st-amber); background: var(--st-amber-bg); border-color: var(--st-amber-line); }
.expiry-note.is-close svg { color: var(--st-amber); }
.expiry-note a { margin-left: auto; font-weight: 600; white-space: nowrap; }

/* The same amber the reserved-slot chip uses - one "this needs attention before a date" colour
   across the product, not a second one invented here. */
/* One cell, two chips. The row's grid has five columns and counts its children, so the deadline
   rides with the status rather than claiming a column of its own. */
.lib-status { display: flex; align-items: center; justify-content: flex-start; gap: 7px; min-width: 0; }
.lib-expiry { display: inline-flex; align-items: center; gap: 5px; white-space: nowrap;
  padding: 3px 8px; border-radius: var(--r-pill); font-size: 11.5px; font-weight: 600;
  color: var(--st-amber); background: var(--st-amber-bg); border: 1px solid var(--st-amber-line); }
.lib-expiry svg { flex-shrink: 0; }

@media (max-width: 700px) {
  /* The library row is tight on a phone; the chip drops to the icon and the number. */
  .lib-expiry { padding: 3px 6px; font-size: 11px; }
  .expiry-note a { margin-left: 0; }
}

/* Sign in with Google (shared/_google_button, AuthController). */
.auth-or { display: flex; align-items: center; gap: 12px; margin: 18px 0 14px; color: var(--muted); font-size: 12px; }
.auth-or::before, .auth-or::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.google-btn { display: inline-flex; align-items: center; justify-content: center; gap: 10px;
  background: var(--surface); border: 1px solid var(--line-strong); color: var(--ink); font-weight: 600; }
.google-btn:hover { background: var(--surface-2); }
.google-btn__mark { width: 18px; height: 18px; flex-shrink: 0; }
.auth-terms { margin: 10px 0 0; font-size: 11.5px; line-height: 1.5; color: var(--muted); text-align: center; }
.auth-terms a { color: var(--primary); }

/* "We will tell you when it is done" - offered on a project that is still rendering
   (videos/_status_panel). Quiet on purpose: it is an offer, not a warning. */
.job-notify { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-top: 16px;
  padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface-2); font-size: 12.5px; line-height: 1.45; color: var(--muted); }
.job-notify svg { flex: none; color: var(--muted); }
.job-notify span { flex: 1 1 260px; min-width: 0; }
.job-notify[hidden] { display: none; } /* [hidden] must beat the display:flex above */

/* The user's own referral code on the Earn page (campaigns/_affiliate_offer). Sits under the
   affiliate offer and is a different promise: that one pays the promoter, this one gives the
   person they send here a reason to click. */
.earn-code { display: flex; gap: 20px; flex-wrap: wrap; align-items: center; justify-content: space-between;
  margin: 0 0 24px; padding: 16px 18px; border: 1px solid var(--line); border-radius: var(--r-md);
  background: var(--surface); }
.earn-code h3 { margin: 0 0 6px; font-size: 15px; }
.earn-code__code { font-family: var(--font-mono); background: var(--surface-2); padding: 2px 8px;
  border-radius: var(--r-sm); }
.earn-code p { margin: 0; font-size: 13px; line-height: 1.5; color: var(--muted); max-width: 60ch; }
.earn-code__note { margin-top: 6px !important; font-size: 12px !important; }
.earn-code__actions { display: flex; gap: 8px; align-items: center; flex: 1 1 320px; }
.earn-code__link { flex: 1 1 auto; min-width: 0; height: 32px; font-size: 12.5px;
  font-family: var(--font-mono); }

/* CHECKOUT: one column on a phone, two on a desktop (checkout/new).
   In one 480px column the card form sat below the fold behind the plan, the price and five lines
   of what is included - a form you have to scroll to find is a form some people never find. Wide
   screens give the form the left column whole and put what they are buying, and the reasons to
   trust us, beside it. The phone layout is untouched: plan, what is included, form, reassurance,
   in that order, which is the order the markup is already in. */
.checkout-panel { max-width: 480px; }
.checkout-grid { display: grid; gap: 16px; }
@media (min-width: 880px) {
  .checkout-panel { max-width: 880px; }
  /* Explicit placement rather than source order: the form is one tall cell on the left, the
     other three stack down the right. */
  .checkout-grid { grid-template-columns: minmax(0, 1fr) 300px; column-gap: 28px; align-content: start; }
  .checkout-grid__form { grid-column: 1; grid-row: 1 / span 4; }
  .checkout-grid__plan { grid-column: 2; grid-row: 1; }
  .checkout-grid__summary { grid-column: 2; grid-row: 2; margin: 0; }
  .checkout-grid__code { grid-column: 2; grid-row: 3; }
  .checkout-grid__trust { grid-column: 2; grid-row: 4; margin: 0; }
}

/* The discount code. Deliberately small and quiet: a big "ENTER YOUR COUPON" box invents a doubt
   ("is there a code I should have?") in everybody who does not have one. */
.checkout-code { display: flex; gap: 8px; align-items: center; }
.checkout-code__field { flex: 1 1 auto; min-width: 0; height: 32px; font-size: 13px; }
.checkout-code__note { display: flex; align-items: flex-start; gap: 6px; margin: 8px 0 0;
  font-size: 12px; line-height: 1.45; }
.checkout-code__note--ok { color: var(--st-green); }
.checkout-code__note--bad { color: var(--st-red); }
.checkout-code__note svg { flex: none; margin-top: 1px; }

/* What the plan actually includes, on the checkout page itself (checkout/new). The page used to
   show a name, a price and a card field - every reason to pay was one page back. */
.checkout-summary { display: grid; gap: 7px; margin: 0; padding: 14px 15px;
  background: var(--surface-2); border-radius: var(--r-md); }
.checkout-summary__item { display: flex; align-items: flex-start; gap: 8px; font-size: 13px;
  line-height: 1.45; color: var(--ink); }
.checkout-summary__item svg { flex: none; margin-top: 2px; color: var(--st-green, var(--primary)); }
/* The three questions somebody asks with their card already out, in front of a domain they met
   ten minutes ago: what comes off today, who holds the card, how hard it is to stop. */
.checkout-trust { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
.checkout-trust li { display: flex; align-items: flex-start; gap: 8px; font-size: 12px;
  line-height: 1.5; color: var(--muted); }
.checkout-trust li svg { flex: none; margin-top: 1px; color: var(--muted); }
.checkout-trust strong { color: var(--ink); font-weight: 600; }
.checkout-trust a { color: var(--muted); text-decoration: underline; }

/* AI b-roll suggestions in the clip editor (clips/_broll_panel). Three at a time, so the row is a
   three-column grid on a desktop and a horizontal scroll on a phone - stacking them would push the
   pick buttons off the screen. */
.broll-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
.broll-option { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.broll-option__media { width: 100%; aspect-ratio: 9 / 16; object-fit: cover; border-radius: var(--r-md);
  background: var(--surface-2); border: 1px solid var(--line); }
.broll-option__pick { width: 100%; height: 32px; border-radius: 8px; font-size: 12.5px; font-weight: 600;
  border: 1px solid var(--line-strong); background: var(--surface); color: var(--ink); }
.broll-option__pick:hover { background: var(--surface-2); }
.broll-option__credit { margin: 0; font-size: 11px; color: var(--muted); text-align: center; }
.broll-option__credit a { color: var(--muted); text-decoration: underline; }
/* A picked card says so on itself, and stays saying it - the button is spent once its footage is
   on the frame. */
.broll-option__pick:disabled { opacity: .6; cursor: default; }
/* Empty until the panel has something to say, and taking no room until then - a permanently
   reserved line under three cards reads as something missing. */
.broll-status { margin: 10px 0 0; font-size: 12px; line-height: 1.45; }
.broll-status:empty { display: none; }

@media (max-width: 700px) {
  .broll-grid { grid-template-columns: repeat(3, 68%); overflow-x: auto; scroll-snap-type: x mandatory;
    padding-bottom: 6px; }
  .broll-option { scroll-snap-align: start; }
}
