/* NSFWDL V2 design system -- Phase 2.
   Loaded exclusively when [data-theme="v2"] is set on <body>, which itself
   only happens when the owner has enabled the preview from /admin. Public
   V1 traffic never requests this file (see app/services/theme.py). Every
   rule in this file is scoped under the [data-theme="v2"] attribute (the
   <body> element itself in every template that loads this stylesheet), so
   nothing here can leak into V1's unscoped site.css/legal.css rendering.

   Strategy: site.css and legal.css already source almost every color from
   a small set of CSS custom properties (--bg, --panel, --panel-2, --text,
   --muted, --line, --accent, --accent-dark, --good, --warn). Re-declaring
   those properties on [data-theme="v2"] re-skins every existing V1
   selector automatically -- no need to duplicate V1's structural rules
   here. The remainder of this file adds V2-only surface/glow/motion
   treatment and the new v2-* component classes (mascot, platform badges,
   helper bot) that the templates render only when theme == "v2". */

[data-theme="v2"] {
  /* Full token set from docs/DESIGN_TOKENS.md */
  --v2-bg: #080a13;
  --v2-bg-elevated: #0d1020;
  --v2-surface: #14172a;
  --v2-surface-2: #1a1e34;
  --v2-border: #343857;
  --v2-text: #f6f7fb;
  --v2-muted: #a7abc0;
  --v2-pink: #fa2abf;
  --v2-purple: #8b5cf6;
  --v2-cyan: #16d9ff;
  --v2-success: #35d07f;
  --v2-warning: #ffb648;
  --v2-danger: #ff4f6d;
  --v2-cta: linear-gradient(135deg, #6f46ff, var(--v2-pink));
  --v2-radius-card: 16px;
  --v2-radius-control: 12px;

  /* Motion amplitude tokens (Phase 3B) -- tuned down at mobile widths (see
     the max-width:700px responsive block near the bottom of this file)
     rather than duplicating keyframes, per "much smaller amplitude" on
     mobile. */
  --v2-idle-lift: -4px;
  --v2-idle-tilt: -1.1deg;

  /* Remap the legacy tokens site.css / legal.css already build every
     selector from, so the whole existing rule set re-skins for free. */
  --bg: var(--v2-bg);
  --panel: var(--v2-surface);
  --panel-2: var(--v2-surface-2);
  --text: var(--v2-text);
  --muted: var(--v2-muted);
  --line: var(--v2-border);
  --accent: var(--v2-pink);
  --accent-dark: #d81aa8;
  --good: var(--v2-success);
  --warn: var(--v2-warning);
  --radius: var(--v2-radius-card);
}

/* --- Base surface -------------------------------------------------------- */

[data-theme="v2"] {
  background:
    radial-gradient(1200px 520px at 12% -8%, rgba(139, 92, 246, .16), transparent 60%),
    radial-gradient(900px 460px at 92% 4%, rgba(250, 42, 191, .12), transparent 55%),
    var(--v2-bg);
}

[data-theme="v2"] ::selection { background: var(--v2-pink); color: #1a0910; }

[data-theme="v2"] input:focus-visible,
[data-theme="v2"] button:focus-visible,
[data-theme="v2"] a:focus-visible,
[data-theme="v2"] summary:focus-visible {
  outline: 3px solid var(--v2-cyan);
  outline-offset: 3px;
}

/* --- Header / navigation --------------------------------------------------- */

[data-theme="v2"] .site-header,
[data-theme="v2"] .trust-header {
  background: rgba(8, 10, 19, .82);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  border-bottom: 1px solid var(--v2-border);
}

[data-theme="v2"] .brand {
  background: var(--v2-cta);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  font-weight: 850;
}

[data-theme="v2"] .site-navigation a {
  position: relative;
  transition: color 160ms ease;
}

[data-theme="v2"] .site-navigation a:hover,
[data-theme="v2"] .trust-header nav a:hover {
  color: var(--v2-text);
}

[data-theme="v2"] .site-navigation a::after {
  content: "";
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: 2px;
  height: 2px;
  border-radius: 2px;
  background: var(--v2-cta);
  opacity: 0;
  transform: scaleX(.6);
  transition: opacity 160ms ease, transform 160ms ease;
}

[data-theme="v2"] .site-navigation a:hover::after { opacity: 1; transform: scaleX(1); }

[data-theme="v2"] .menu-toggle {
  border-color: var(--v2-border);
  background: var(--v2-surface);
}

[data-theme="v2"] .menu-toggle:hover { border-color: var(--v2-purple); }

/* --- Hero / tool section ---------------------------------------------------- */

[data-theme="v2"] .tool.shell { position: relative; }

[data-theme="v2"] .v2-hero-row {
  display: flex;
  flex-wrap: wrap-reverse;
  align-items: center;
  gap: clamp(14px, 4vw, 32px);
}

[data-theme="v2"] .v2-hero-row .intro,
[data-theme="v2"] .v2-supporter-intro {
  flex: 1 1 340px;
  min-width: 0;
}

[data-theme="v2"] .stage-badge {
  border-color: var(--v2-purple);
  background: rgba(139, 92, 246, .14);
  color: #e6d9ff;
}

[data-theme="v2"] .intro h1 {
  background: linear-gradient(120deg, var(--v2-text) 45%, #d9b8ff 100%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

[data-theme="v2"] .v2-source-badge {
  display: block;
  margin-bottom: 10px;
  border-radius: 12px;
  /* Phase 3B: "tiny glow on the current dedicated platform badge" -- slow,
     ambient, paint-only (filter), never a hover-triggered effect since this
     badge is decorative (alt="") and not a link. */
  animation: v2-badge-glow 7s ease-in-out infinite;
}

/* --- Mascot ------------------------------------------------------------------ */

[data-theme="v2"] .v2-mascot {
  position: relative;
  flex: 0 0 auto;
  width: clamp(104px, 26vw, 140px);
  aspect-ratio: 1 / 1;
  margin-inline: auto;
}

[data-theme="v2"] .v2-mascot::before {
  content: "";
  position: absolute;
  /* Positive inset (smaller than the mascot's own box) rather than a
     negative bleed -- a prior -22% outward bleed pushed real layout
     overflow past the viewport at 700-1023px widths (confirmed via
     headless-browser scrollWidth measurement), since the nearest
     overflow:hidden ancestor is <body>, not this element's own box. */
  inset: 8%;
  z-index: 0;
  background: url("/static/assets/v2/decorative/download-orb.svg") center / contain no-repeat;
  opacity: .55;
  pointer-events: none;
}

[data-theme="v2"] .v2-mascot-img {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
  object-fit: contain;
  opacity: 0;
  pointer-events: none;
}

/* Idle is the default visible state -- no JS has run yet or the state was
   explicitly reset. Every other state only becomes visible once
   downloader.js's setMascotState() (wired to real extract/download events,
   see static/js/downloader.js) sets the matching data-mascot-state value on
   <body>. */
[data-theme="v2"]:not([data-mascot-state]) .v2-mascot-img[data-state="idle"],
[data-theme="v2"][data-mascot-state="idle"] .v2-mascot-img[data-state="idle"] { opacity: 1; }
[data-theme="v2"][data-mascot-state="searching"] .v2-mascot-img[data-state="searching"] { opacity: 1; }
[data-theme="v2"][data-mascot-state="preparing"] .v2-mascot-img[data-state="preparing"] { opacity: 1; }
[data-theme="v2"][data-mascot-state="ready"] .v2-mascot-img[data-state="ready"] { opacity: 1; }
[data-theme="v2"][data-mascot-state="downloading"] .v2-mascot-img[data-state="downloading"] { opacity: 1; }
[data-theme="v2"][data-mascot-state="success"] .v2-mascot-img[data-state="success"] { opacity: 1; }
[data-theme="v2"][data-mascot-state="error"] .v2-mascot-img[data-state="error"] { opacity: 1; }

/* Idle: "welcoming, premium, calm" ambient breathe (translateY) + a tiny
   1-2deg tilt, combined into one loop since two separately-animated
   `transform` keyframes on the same element don't compose -- see the
   dedicated-mascot crop comment further down for the same constraint. Amount
   is a custom property (--v2-idle-lift/--v2-idle-tilt) so mobile can tone it
   down without duplicating the keyframe (see the max-width:700px block).
   :not([data-mascot-state]) covers a fresh page load before any real
   extract/download event has run setMascotState() even once -- the same
   "idle is the default, no attribute yet" case the visibility rule above
   already handles, just extended to the animation too so idle motion is not
   silently absent until the visitor's first interaction. */
[data-theme="v2"]:not([data-mascot-state]) .v2-mascot-img[data-state="idle"],
[data-theme="v2"][data-mascot-state="idle"] .v2-mascot-img[data-state="idle"] { animation: v2-idle-breathe 5.4s ease-in-out infinite; }
[data-theme="v2"][data-mascot-state="searching"] .v2-mascot-img[data-state="searching"],
[data-theme="v2"][data-mascot-state="preparing"] .v2-mascot-img[data-state="preparing"],
[data-theme="v2"][data-mascot-state="ready"] .v2-mascot-img[data-state="ready"],
[data-theme="v2"][data-mascot-state="downloading"] .v2-mascot-img[data-state="downloading"] { animation: v2-enter 320ms ease; }
[data-theme="v2"][data-mascot-state="success"] .v2-mascot-img[data-state="success"] { animation: v2-enter 320ms ease, v2-bounce 520ms ease 320ms; }
[data-theme="v2"][data-mascot-state="error"] .v2-mascot-img[data-state="error"] { animation: v2-enter 320ms ease, v2-shake 420ms ease 320ms; }

/* Idle: soft glow pulse behind the mascot. Searching: the same orb layer
   keeps its existing slow spin (unchanged from Phase 2). Downloading: one
   short one-shot "moves/fades downward" handoff acknowledgement, then stops
   (animation-fill-mode: forwards holds the faded-out end state rather than
   snapping back, since the mascot swaps to the success image immediately
   after in the real flow anyway). */
[data-theme="v2"]:not([data-mascot-state]) .v2-mascot::before,
[data-theme="v2"][data-mascot-state="idle"] .v2-mascot::before { animation: v2-glow-pulse 6s ease-in-out infinite; }
[data-theme="v2"][data-mascot-state="searching"] .v2-mascot::before { animation: v2-spin-slow 9s linear infinite; }
[data-theme="v2"][data-mascot-state="downloading"] .v2-mascot::before { animation: v2-orb-descend 650ms ease forwards; }

/* Input focus "wake": a tiny one-shot lift, not a loop -- :focus-within on
   the shared .tool.shell ancestor (which contains both the mascot and the
   URL input as siblings) means this plays once per focus-enter and does not
   retrigger on every keystroke, since :focus-within only starts matching
   again after focus has actually left and returned. */
[data-theme="v2"] .tool.shell:focus-within .v2-mascot { animation: v2-wake 260ms ease; }

/* Success: one small sparkle burst (Phase 3B), reusing the existing
   sparkles.svg decorative asset. Positioned inside .v2-mascot (already
   position:relative) so it rides with the mascot without its own layout
   box; opacity-only visibility so it never reserves space when not
   animating. */
/* Positive inset (stays inside .v2-mascot's own box) rather than a negative
   bleed -- a prior negative-offset decorative layer on this same element
   (see the download-orb ::before comment above) pushed real layout overflow
   past the viewport at desktop widths, confirmed via headless-browser
   scrollWidth measurement, and opacity:0 does not exempt an element's own
   box from contributing to that measurement even while invisible between
   success bursts. */
[data-theme="v2"] .v2-sparkles {
  position: absolute;
  top: 2%;
  right: 2%;
  width: 40%;
  height: auto;
  z-index: 2;
  opacity: 0;
  pointer-events: none;
}
[data-theme="v2"][data-mascot-state="success"] .v2-sparkles { animation: v2-sparkle-burst 900ms ease forwards; }

[data-theme="v2"] .v2-supporter-mascot {
  flex: 0 0 auto;
  /* Explicit height (not just aspect-ratio) -- as a direct flex-item
     replaced element with HTML width/height="768" intrinsic-size
     attributes, Chrome's column-direction flex main-size resolution used
     the raw 768px intrinsic height instead of deriving it from the
     aspect-ratio + resolved cross-size width, confirmed via a headless
     render (computed height came back 768px despite aspect-ratio:1/1).
     Setting both dimensions explicitly sidesteps that resolution path. */
  width: clamp(120px, 22vw, 190px);
  height: clamp(120px, 22vw, 190px);
  object-fit: contain;
  margin-inline: auto;
  animation: v2-float 5s ease-in-out infinite;
}

[data-theme="v2"] .v2-helper-bot {
  display: block;
  width: 48px;
  height: 48px;
  margin-bottom: 8px;
  object-fit: contain;
  animation: v2-float 5.4s ease-in-out infinite;
}

/* --- Status-row helper widget (Phase 3B SEARCHING / PREPARING) -------------- */

/* .v2-status-row wraps #extract-status/#download-status so the helper can
   sit beside the status text without JS DOM manipulation and without the
   helper being wiped out by setStatus()'s replaceChildren() (which only
   ever touches the #extract-status/#download-status element itself, a
   sibling of the helper inside this row, never this wrapper). Owns the
   margin-top the wrapped .status element normally carries itself, since a
   flex container establishes a new formatting context that would otherwise
   stop that margin from collapsing the way it does in normal flow. */
[data-theme="v2"] .v2-status-row {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 20px;
}
[data-theme="v2"] .v2-status-row .status { margin-top: 0; flex: 1 1 auto; min-width: 0; }

/* Hidden by default; only the helper matching the *current* state's own
   context (search vs. prepare) becomes visible. "helper appears near the
   status area, not floating randomly" -- and never both at once, keeping to
   one dominant animated cue per state. */
[data-theme="v2"] .v2-helper { display: none; position: relative; flex: 0 0 auto; width: 44px; height: 44px; }
[data-theme="v2"][data-mascot-state="searching"] .v2-helper[data-helper-context="search"],
[data-theme="v2"][data-mascot-state="preparing"] .v2-helper[data-helper-context="prepare"] {
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
@media (min-width: 701px) {
  [data-theme="v2"] .v2-helper { width: 56px; height: 56px; }
}

[data-theme="v2"] .v2-helper-img { position: relative; z-index: 1; width: 100%; height: 100%; object-fit: contain; }
[data-theme="v2"] .v2-scan-rings { position: absolute; inset: -10px; z-index: 0; width: auto; height: auto; opacity: .55; pointer-events: none; }

/* Calm ~2-4s loops, matching the SEARCHING/PREPARING motion budget -- no
   escalation over time (pollDownload()'s slow-hint copy changes after 8s,
   but the loop itself never speeds up or intensifies). */
[data-theme="v2"][data-mascot-state="searching"] .v2-scan-rings,
[data-theme="v2"][data-mascot-state="preparing"] .v2-scan-rings { animation: v2-spin-slow 7s linear infinite; }
[data-theme="v2"][data-mascot-state="searching"] .v2-helper-img,
[data-theme="v2"][data-mascot-state="preparing"] .v2-helper-img { animation: v2-helper-bob 2.8s ease-in-out infinite; }

/* --- Platform badges -------------------------------------------------------- */

[data-theme="v2"] .v2-badge {
  display: inline-block;
  vertical-align: middle;
  margin-right: 7px;
  border-radius: 7px;
}

[data-theme="v2"] .supported li {
  display: flex;
  align-items: center;
  gap: 2px;
  background: var(--v2-surface);
  transition: border-color 160ms ease, transform 160ms ease;
}

/* V1's own grid only uses auto-fit/max-content sizing at 701px+ -- below
   that it switches to fixed 1fr columns (see site.css), where forcing
   nowrap text could overflow a narrow column. So the wider minmax + nowrap
   fix (badge icon needs more room so "PornHub"/"xHamster"/"Porntrex" don't
   wrap mid-word) only applies where V1's own layout already gives text
   room to size itself. */
@media (min-width: 701px) {
  [data-theme="v2"] .supported ul { grid-template-columns: repeat(auto-fit, minmax(132px, max-content)); }
  [data-theme="v2"] .supported li a { white-space: nowrap; }
}

[data-theme="v2"] .supported li:hover { border-color: var(--v2-purple); transform: translateY(-1px); box-shadow: 0 6px 18px -12px rgba(139, 92, 246, .6); }

/* --- Forms / buttons --------------------------------------------------------- */

[data-theme="v2"] input {
  background: var(--v2-bg-elevated);
  border-radius: var(--v2-radius-control);
  transition: border-color 160ms ease, box-shadow 160ms ease;
}

/* Restrained purple/pink focus bloom (Phase 3B IDLE "input focus" wake) --
   paint-only (box-shadow), never repeats/loops while focus is held. */
[data-theme="v2"] input:focus-visible {
  border-color: var(--v2-cyan);
  box-shadow: 0 0 0 4px rgba(139, 92, 246, .16), 0 0 20px -6px rgba(250, 42, 191, .4);
}

[data-theme="v2"] .button {
  border-radius: var(--v2-radius-control);
  transition: transform 160ms ease, box-shadow 160ms ease, background 160ms ease, border-color 160ms ease;
}

[data-theme="v2"] .button.primary {
  background: var(--v2-cta);
  color: #fff;
  border: none;
  box-shadow: 0 8px 26px -10px rgba(250, 42, 191, .55);
}

[data-theme="v2"] .button.primary:hover:not(:disabled) {
  transform: translateY(-1px);
  box-shadow: 0 12px 30px -8px rgba(250, 42, 191, .68);
}

[data-theme="v2"] .button.secondary {
  background: var(--v2-surface-2);
  border-color: var(--v2-border);
}

[data-theme="v2"] .button.secondary:hover { border-color: var(--v2-cyan); }

[data-theme="v2"] .button.quiet {
  border-color: var(--v2-purple);
  background: rgba(139, 92, 246, .1);
  color: #e6d9ff;
}

[data-theme="v2"] .clear-url:hover { color: var(--v2-text); }

[data-theme="v2"] .limits {
  gap: 8px 22px;
}

[data-theme="v2"] .limits span {
  padding: 6px 12px;
  border: 1px solid var(--v2-border);
  border-radius: 999px;
  background: var(--v2-surface);
}

/* --- Status / results -------------------------------------------------------- */

[data-theme="v2"] .status.error { color: var(--v2-danger); }
[data-theme="v2"] .spinner { border-color: var(--v2-cyan); border-right-color: transparent; opacity: .85; }
[data-theme="v2"] .button.busy::before { border-color: currentColor; border-right-color: transparent; }

[data-theme="v2"] .eyebrow { color: var(--v2-cyan); }
[data-theme="v2"] .result-count { color: var(--v2-success); }

[data-theme="v2"] .media-summary {
  border-color: var(--v2-border);
  background: linear-gradient(160deg, var(--v2-surface), var(--v2-bg-elevated));
  box-shadow: 0 20px 44px -30px rgba(139, 92, 246, .55);
}

[data-theme="v2"] .media-summary img { background: var(--v2-surface-2); }

/* --- Format cards: the strongest V2 surface --------------------------------- */

/* Owner mobile QA: a bright outer stroke was wrapping the whole formats
   section, overlapping the format cards' own borders. Root cause is not a
   drawn border at all -- static/js/downloader.js calls
   scroll.reveal(results, {focus: true, ...}) once formats render, and
   #result-start (.results) is a plain non-interactive <section> with no
   focus styling of its own (the site's :focus-visible rules only cover
   input/button/a/summary). The browser's UA default focus ring filled that
   gap; on this dark-color-scheme page that default ring renders as a light
   halo around the entire section box. #result-start's tabindex="-1" only
   exists for this scripted scroll-into-view/AT-announcement focus, not
   keyboard tab order, so there is no interactive element here that needs a
   visible ring -- suppressing it is the actual fix, not an offset hack. */
[data-theme="v2"] .results:focus { outline: none; }

[data-theme="v2"] .format-group legend { color: var(--v2-cyan); text-transform: uppercase; letter-spacing: .04em; font-size: .78rem; }

[data-theme="v2"] .format-row {
  border-radius: var(--v2-radius-control);
  background: var(--v2-surface);
  transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
}

[data-theme="v2"] .format-row:hover { border-color: var(--v2-purple); transform: translateY(-1px); }

[data-theme="v2"] .format-row.selected {
  border-color: var(--v2-pink);
  background: linear-gradient(160deg, rgba(139, 92, 246, .16), rgba(250, 42, 191, .1));
  box-shadow: 0 0 0 1px var(--v2-pink), 0 10px 26px -14px rgba(250, 42, 191, .6);
  animation: v2-select-glow 220ms ease;
}

/* READY: "format cards may enter with a small stagger using opacity +
   translateY only... Then stop." One-shot, gated to the moment
   data-mascot-state actually becomes "ready" (renderResult() sets that
   before renderFormats() populates these rows, and every fresh extraction
   cycles the attribute away via "searching" first -- see
   static/js/downloader.js -- so this reliably replays for a freshly
   rendered list and never replays for a later within-ready interaction like
   selecting a different format, which doesn't touch data-mascot-state at
   all). :nth-child accounts for each fieldset's own <legend> as child 1, so
   the first format-row in a group is child 2.

   :not(.selected) plus the separate .selected rule right after it, rather
   than one rule covering every .format-row: this selector and the plain
   .format-row.selected rule above compute to the *same* specificity
   ((0,3,0) each -- one attribute + two classes either way), which would
   otherwise let source order alone decide and silently drop whichever
   rule loses that tie-break entirely (animation is a shorthand, so the
   losing rule's animation is not merged in, just gone). Splitting it keeps
   the recommended/default row's v2-select-glow emphasis and this entrance
   stagger both actually applied. The .selected variant lists both
   animation names in one declaration so the single animation-delay each
   nth-child rule below sets is recycled across both per the animation
   shorthand's list-matching rules (fewer delays than names repeats the
   list), rather than needing a second hand-maintained delay ladder. */
[data-theme="v2"][data-mascot-state="ready"] .format-row:not(.selected) { animation: v2-stagger-in 380ms ease both; }
[data-theme="v2"][data-mascot-state="ready"] .format-row.selected { animation: v2-stagger-in 380ms ease both, v2-select-glow 220ms ease both; }
[data-theme="v2"][data-mascot-state="ready"] .format-row:nth-child(2) { animation-delay: 40ms; }
[data-theme="v2"][data-mascot-state="ready"] .format-row:nth-child(3) { animation-delay: 80ms; }
[data-theme="v2"][data-mascot-state="ready"] .format-row:nth-child(4) { animation-delay: 120ms; }
[data-theme="v2"][data-mascot-state="ready"] .format-row:nth-child(n+5) { animation-delay: 160ms; }

[data-theme="v2"] .format-row input { accent-color: var(--v2-pink); }

[data-theme="v2"] .format-badge {
  padding: 3px 10px;
  border-radius: 999px;
  font-size: .74rem;
  letter-spacing: .01em;
}

[data-theme="v2"] .format-badge.free { background: rgba(53, 208, 127, .14); color: var(--v2-success); }
[data-theme="v2"] .format-badge.supporter { background: rgba(255, 182, 72, .16); color: var(--v2-warning); }
[data-theme="v2"] .format-badge.unsupported { background: rgba(255, 79, 109, .14); color: var(--v2-danger); }

/* At <=375px site.css moves .format-badge into its own implicit grid row
   under .format-copy and sets grid-column: 2 (the minmax(0, 1fr) track).
   Grid items default to justify-self: stretch, so the badge's pill
   background stretched across that whole flexible column -- a compact chip
   everywhere else in V2 but a full-width bar at this one breakpoint. */
@media (max-width: 375px) {
  [data-theme="v2"] .format-badge { justify-self: start; }
}

[data-theme="v2"] .primary-download {
  box-shadow: 0 10px 30px -12px rgba(111, 70, 255, .6);
}

/* FORMAT SELECTION: "primary Download button gets one short attention
   response after the selection settles." .v2-cta-pulse is toggled by
   theme-v2.js's pulseCTA() only for a genuine user reselection (see that
   file); it is force-removed and re-added on every trigger so back-to-back
   reselections each restart the animation instead of a no-op re-add.
   DOWNLOADING: "CTA may briefly confirm with glow" -- driven by the
   explicit one-shot v2-enter-downloading marker class (theme-v2.js) rather
   than the raw data-mascot-state attribute, so it plays exactly once per
   transition into "downloading" and never replays for anything else. */
[data-theme="v2"] .primary-download.v2-cta-pulse,
[data-theme="v2"].v2-enter-downloading .primary-download { animation: v2-cta-pulse 480ms ease; }

/* ERROR: "error card gets one subtle accent pulse. Then stop." Same
   one-shot marker-class mechanism as the CTA confirm above, applied to
   whichever status region actually shows the error. */
[data-theme="v2"].v2-enter-error #extract-status.error,
[data-theme="v2"].v2-enter-error #download-status.error { animation: v2-error-pulse 500ms ease; }

/* --- Notices / supporter nudge / sponsored ----------------------------------- */

[data-theme="v2"] .notice {
  border-color: rgba(255, 182, 72, .4);
  background: linear-gradient(160deg, rgba(255, 182, 72, .1), var(--v2-surface));
}

[data-theme="v2"] .notice p { color: var(--v2-muted); }

[data-theme="v2"] .supporter-nudge { position: relative; padding-left: 74px; min-height: 56px; }
[data-theme="v2"] .supporter-nudge .v2-helper-bot { position: absolute; left: 16px; top: 16px; margin: 0; width: 44px; height: 44px; animation: none; }

[data-theme="v2"] .sponsored {
  border-color: var(--v2-border);
  background: var(--v2-surface);
}

[data-theme="v2"] .sponsored a { color: var(--v2-cyan); }

/* --- Content / trust sections (token-level only) ------------------------------ */

[data-theme="v2"] .evidence-key,
[data-theme="v2"] .resource-policy,
[data-theme="v2"] .related-tools,
[data-theme="v2"] .supported-directory article,
[data-theme="v2"] .universal-note,
[data-theme="v2"] .comparison-wrap,
[data-theme="v2"] .feature-grid article,
[data-theme="v2"] .tier-grid article,
[data-theme="v2"] .activation,
[data-theme="v2"] .donate,
[data-theme="v2"] .terms,
[data-theme="v2"] .pass-options,
[data-theme="v2"] .fact-box,
[data-theme="v2"] .contact-card,
[data-theme="v2"] .hub-card,
[data-theme="v2"] .legal-aside {
  border-color: var(--v2-border);
  background: var(--v2-surface);
}

[data-theme="v2"] .evidence-key dl div { background: var(--v2-surface-2); }
[data-theme="v2"] .related-tools li::marker { color: var(--v2-pink); }
[data-theme="v2"] .breadcrumbs a,
[data-theme="v2"] .breadcrumb a { color: var(--v2-cyan); }

[data-theme="v2"] .supporter-tier { border-color: var(--v2-purple) !important; background: linear-gradient(160deg, rgba(139, 92, 246, .14), var(--v2-surface)) !important; }
[data-theme="v2"] .tier-size { background: var(--v2-cta); -webkit-background-clip: text; background-clip: text; color: transparent; }

[data-theme="v2"] .pass-list li { background: var(--v2-surface-2); border-color: var(--v2-border); }

[data-theme="v2"] .legal-hero { border-color: var(--v2-border); }
[data-theme="v2"] .staging-note { border-color: rgba(255, 182, 72, .4); background: rgba(255, 182, 72, .08); }

/* --- Footer -------------------------------------------------------------------- */

[data-theme="v2"] .site-footer,
[data-theme="v2"] .trust-footer {
  border-color: var(--v2-border);
  background: var(--v2-bg-elevated);
}

[data-theme="v2"] .footer-links a:hover,
[data-theme="v2"] .trust-footer nav a:hover { color: var(--v2-text); }

/* --- Motion -------------------------------------------------------------------- */

@keyframes v2-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4px); } }
@keyframes v2-enter { from { opacity: 0; transform: translateY(6px) scale(.98); } to { opacity: 1; transform: translateY(0) scale(1); } }
@keyframes v2-bounce { 0% { transform: scale(1); } 35% { transform: scale(1.08); } 60% { transform: scale(.97); } 100% { transform: scale(1); } }
@keyframes v2-shake { 0%, 100% { transform: translateX(0); } 20% { transform: translateX(-5px); } 40% { transform: translateX(4px); } 60% { transform: translateX(-3px); } 80% { transform: translateX(2px); } }
@keyframes v2-select-glow { from { box-shadow: 0 0 0 1px var(--v2-pink), 0 0 0 0 rgba(250, 42, 191, .5); } to { box-shadow: 0 0 0 1px var(--v2-pink), 0 10px 26px -14px rgba(250, 42, 191, .6); } }
@keyframes v2-spin-slow { to { transform: rotate(360deg); } }

/* Phase 3B additions. */
@keyframes v2-idle-breathe { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(var(--v2-idle-lift)) rotate(var(--v2-idle-tilt)); } }
@keyframes v2-glow-pulse { 0%, 100% { opacity: .5; } 50% { opacity: .75; } }
@keyframes v2-wake { 0% { transform: translateY(0); } 45% { transform: translateY(-5px); } 100% { transform: translateY(0); } }
@keyframes v2-helper-bob { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-3px) rotate(3deg); } }
@keyframes v2-sparkle-burst { 0% { opacity: 0; transform: scale(.6) rotate(-8deg); } 35% { opacity: 1; transform: scale(1.05) rotate(2deg); } 100% { opacity: 0; transform: scale(1.15) rotate(4deg); } }
@keyframes v2-orb-descend { 0% { transform: translateY(0); opacity: .55; } 60% { transform: translateY(10px); opacity: .8; } 100% { transform: translateY(18px); opacity: 0; } }
@keyframes v2-stagger-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@keyframes v2-cta-pulse { 0% { box-shadow: 0 10px 30px -12px rgba(111, 70, 255, .6); } 40% { box-shadow: 0 10px 30px -12px rgba(111, 70, 255, .6), 0 0 0 6px rgba(250, 42, 191, .25); } 100% { box-shadow: 0 10px 30px -12px rgba(111, 70, 255, .6); } }
@keyframes v2-error-pulse { 0%, 100% { box-shadow: none; } 50% { box-shadow: 0 0 0 3px rgba(255, 79, 109, .35); } }
@keyframes v2-badge-glow { 0%, 100% { filter: drop-shadow(0 0 0 rgba(250, 42, 191, 0)); } 50% { filter: drop-shadow(0 0 8px rgba(250, 42, 191, .35)); } }

/* Page visibility / battery sanity: theme-v2.js toggles this single
   attribute on <body> from a `visibilitychange` listener -- no scheduler,
   just animation-play-state, and every animation resumes reflecting
   whatever data-mascot-state currently is (not necessarily what it was when
   the tab was hidden). */
[data-theme="v2"][data-v2-motion-paused] * { animation-play-state: paused !important; }

@media (prefers-reduced-motion: reduce) {
  [data-theme="v2"] .v2-mascot-img,
  [data-theme="v2"] .v2-supporter-mascot,
  [data-theme="v2"] .v2-helper-bot,
  [data-theme="v2"] .v2-helper-img,
  [data-theme="v2"] .v2-scan-rings,
  [data-theme="v2"] .v2-sparkles,
  [data-theme="v2"] .v2-mascot,
  [data-theme="v2"] .v2-mascot::before,
  [data-theme="v2"] .v2-source-badge,
  [data-theme="v2"] .format-row,
  [data-theme="v2"] .format-row.selected,
  [data-theme="v2"] .primary-download,
  [data-theme="v2"] #extract-status,
  [data-theme="v2"] #download-status {
    animation: none !important;
  }
  /* Belt-and-suspenders: nothing above should ever actually leave an
     element parked at a keyframe's "from" values once its animation is
     removed (there is no separate static rule setting these), but state
     must stay visually understandable through the static asset/color swap
     regardless, so pin the rest state explicitly rather than rely on that. */
  [data-theme="v2"] .v2-sparkles { opacity: 0 !important; }
  [data-theme="v2"] .format-row { opacity: 1 !important; transform: none !important; }
}

/* --- Responsive -------------------------------------------------------------- */

@media (max-width: 700px) {
  /* "much smaller amplitude" on mobile for the idle breathe/tilt loop --
     the compact dedicated-page mascot also clips via its own
     overflow:hidden (see .v2-hero-row--dedicated .v2-mascot below), so this
     is belt-and-suspenders there and the primary control on the homepage's
     still-large mobile mascot. */
  [data-theme="v2"] { --v2-idle-lift: -2px; --v2-idle-tilt: -0.5deg; }
  [data-theme="v2"] .v2-hero-row { flex-direction: column-reverse; align-items: stretch; text-align: left; }
  /* .intro/.v2-supporter-intro's 340px flex-basis is a desktop *width* hint
     (row layout); reset it here so column-reverse mobile stacking doesn't
     reinterpret that same 340px as a minimum *height*, which left a large
     empty gap below the intro paragraph. */
  [data-theme="v2"] .v2-hero-row .intro,
  [data-theme="v2"] .v2-supporter-intro { flex: 1 1 auto; }
  [data-theme="v2"] .v2-mascot { width: clamp(104px, 30vw, 140px); }
  [data-theme="v2"] .v2-supporter-hero { flex-direction: column-reverse; }
  [data-theme="v2"] .v2-supporter-mascot { width: clamp(104px, 30vw, 140px); height: clamp(104px, 30vw, 140px); }

  /* Dedicated platform pages (PornHub, etc.) only -- the homepage/universal
     hero keeps the large column-reverse mascot above. Owner mobile QA: on a
     dedicated page the full-width mascot row pushed the platform badge and
     H1 far down the page and read as a sticker floating above empty space,
     because the hero mascot artwork carries generous transparent padding
     for its large desktop composition -- shrunk into a full-width mobile
     row that padding reads as dead space. Reflow to a side-by-side row so
     identity (badge + H1) leads and a small, cropped mascot rides beside it
     instead of stacking above it. */
  [data-theme="v2"] .v2-hero-row--dedicated {
    flex-direction: row;
    flex-wrap: nowrap;
    align-items: flex-start;
    gap: 12px;
  }
  [data-theme="v2"] .v2-hero-row--dedicated .intro { flex: 1 1 auto; min-width: 0; order: 1; }
  [data-theme="v2"] .v2-hero-row--dedicated .v2-mascot {
    order: 2;
    width: clamp(72px, 20vw, 100px);
    margin-inline: 0;
    align-self: flex-start;
    overflow: hidden;
    border-radius: 14px;
  }
  /* Crop into the character instead of showing the hero composition's
     padding -- the box itself clips via overflow:hidden above, so this
     cannot bleed into the title, badge, nav, input, or CTA regardless of
     source-image content. Sized via inset/width/height rather than
     transform: scale() -- every mascot-state animation above (v2-float,
     v2-enter, v2-bounce, v2-shake) also drives the `transform` property via
     keyframes, and an animated `transform` fully replaces a static one
     rather than composing with it, which would have snapped the crop back
     to 1x (re-exposing the padding) for the whole animated portion of each
     cycle. */
  [data-theme="v2"] .v2-hero-row--dedicated .v2-mascot-img { inset: -28%; width: auto; height: auto; }
  [data-theme="v2"] .v2-hero-row--dedicated .v2-mascot::before { inset: 0; opacity: .35; }
}

@media (max-width: 430px) {
  [data-theme="v2"] .supporter-nudge { padding-left: 62px; }
  [data-theme="v2"] .supporter-nudge .v2-helper-bot { width: 36px; height: 36px; }
}

@media (min-width: 1024px) {
  [data-theme="v2"] .v2-hero-row { flex-wrap: nowrap; }
  [data-theme="v2"] .v2-mascot { width: 340px; }
}
