/* =====================================================================
   RenderMix design tokens — the single source of truth.

   How to use this file:
     • Authoring a NEW rule → consume tokens via var(--token).  If a
       value isn't covered, ask whether the design wants a new tier
       (extend this :root) or a one-off literal (annotate with a
       comment).
     • Touching an EXISTING rule → if you see a px / hex literal,
       migrate it to the matching token in the same commit.
     • landing/styles.css mirrors a subset of these tokens — keep
       cross-file values in sync (see --bg-card / --cyan / --radius-*).

   When to use which tier (rules of thumb):
     ─ Backgrounds:  --bg = window, --bg-card = panels, --bg-field =
                     inputs, --bg-elevated = modals/popovers,
                     --bg-soft = nested zones inside cards.
     ─ Text:         --text = headings + key body, --text-soft =
                     paragraph body / secondary, --muted = meta
                     (timestamps, hints), --muted-2 = quietest
                     (disabled, placeholder).  --muted is calibrated
                     to WCAG AA on --bg-card.
     ─ Accents:      --cyan = ANY primary action / focused-state /
                     active-link.  --green/amber/red are SEMANTIC
                     (success / warning / error) — never decorative.
                     *-soft variants are for dense admin tables where
                     full-saturation accents fight each other.
     ─ Spacing:      --space-{1..8} = 4/8/12/16/20/24/32/48 px.  The
                     12/16/24 triplet is the workhorse.  Off-scale
                     literals (15/17/19/26 px) need a comment
                     justifying why the grid doesn't fit.
     ─ Type:         --text-{xs..3xl}.  --text-base (13px) is body /
                     labels, --text-md (14px) is inputs / dense UI,
                     --text-lg (16px) is forms / iOS input zoom-safe
                     threshold.  --text-md is intentionally between
                     --text-base and --text-lg because the codebase
                     uses 14px heavily for input text + tier-feature
                     bullets.
     ─ Radii:        --radius-xs (4px) = tiny chips, --radius-sm (6px)
                     = buttons / inputs / standard cards, --radius-md
                     (8px) = larger cards / modal sections, --radius-
                     lg (12px) = full modal boxes / hero cards,
                     --radius-pill = circular pills.
     ─ Motion:       --duration-fast (120ms) = hover / pressed /
                     state-color swap.  --duration-base (180ms) =
                     modal fade-in / drawer / shadow lift.  --
                     duration-slow (280ms) reserved.  Touch-tap
                     scale(0.97) keeps literal 0.06-0.08s — must
                     feel instantaneous.  Spinner (0.7s) and
                     pulse-ban (1.6s) are animations, not transitions.
     ─ prefers-reduced-motion: the override at the bottom of this
                     file resets --duration-* to 0ms, so every
                     transition routed through tokens auto-disables
                     for users with the system setting on.
   ===================================================================== */
:root {
  color-scheme: dark;

  /* Backgrounds — depth ladder, darkest to lightest */
  --bg: #020204;
  --bg-elevated: #07080b;
  --bg-field: #0a0b0f;
  --bg-soft: #101116;
  --bg-card: #14151c;

  /* Hairline dividers — low-opacity white over the dark surfaces */
  --line: rgba(255, 255, 255, 0.08);
  --line-strong: rgba(255, 255, 255, 0.14);

  /* Text ladder — primary → quietest.  --muted hits WCAG AA on --bg-card. */
  --text: #f4f4f5;
  --text-soft: #c9cbd2;
  --muted: #8a8e9b;
  --muted-2: #6b6f7b;

  /* Primary accent — single cyan across landing + app */
  --cyan: #53d8ff;
  --cyan-hover: #7be2ff;
  --cyan-soft: rgba(83, 216, 255, 0.12);

  /* Decorative companion — used only in landing hero gradient.  Never
     in app surfaces.  If you need a second hue, talk to design first. */
  --violet: #9a7cff;

  /* Semantic colors — strict roles, never decorative.
     success / warning / error.  Used at full saturation for state
     indicators and at low-opacity rgba(...) for background tints. */
  --green: #58e69a;
  --amber: #ffbe5c;
  --red: #ff5d73;

  /* Semantic-soft variants — for dense admin tables / status cells
     where full-saturation tokens fight other UI signal.  Match the
     light-on-dark aesthetic operators see in Linear / Vercel admin. */
  --green-soft: #86efac;
  --amber-soft: #fde68a;
  --red-soft:   #fca5a5;
  --orange-mid: #fb923c;

  /* Spacing scale — 4-based geometric (4/8/12/16/20/24/32/48 px).
     The 12/16/24 triplet is the workhorse across forms, cards,
     and sections. */
  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
  --space-4: 16px;
  --space-5: 20px;
  --space-6: 24px;
  --space-7: 32px;
  --space-8: 48px;

  /* Type scale — px-based to bypass iOS-Safari rem→16px-zoom-trap
     on small inputs.  --text-md (14px) sits between base and lg
     intentionally to cover input text + tier-feature bullets. */
  --text-xs:    11px;
  --text-sm:    12px;
  --text-base:  13px;
  --text-md:    14px;
  --text-lg:    16px;
  --text-xl:    18px;
  --text-2xl:   24px;
  --text-3xl:   32px;

  /* Motion scale — three durations + two easings.  Tap feedback
     stays literal at 0.06-0.08s (must feel instant).  Spin / pulse
     animations stay literal as documented exceptions. */
  --duration-fast:   120ms;
  --duration-base:   180ms;
  --duration-slow:   280ms;
  --ease-out:        cubic-bezier(0.16, 1, 0.3, 1);
  --ease-in-out:     cubic-bezier(0.65, 0, 0.35, 1);

  /* Radii — five tiers + one special case (circular 50%).  Bumped
     --radius-sm 4px → 6px in the dominant-migration pass to match
     the most-common hardcoded value; --radius-xs added for the
     tighter corners that genuinely want 4px. */
  --radius-xs: 4px;
  --radius-sm: 6px;
  --radius-md: 8px;
  --radius-lg: 12px;
  --radius-pill: 9999px;

  /* One global shadow for elevated surfaces (modals, popovers).  If a
     new use needs different elevation, add --shadow-sm / --shadow-md
     before reaching for a literal box-shadow. */
  --shadow: 0 24px 70px rgba(0, 0, 0, 0.42);

  font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}

* {
  box-sizing: border-box;
}

/* Selection — cyan tint matches the single-accent rule.  Alpha 0.35
   (not the 0.12 of --cyan-soft) gives enough contrast against
   --bg-card / --bg that the selection rectangle reads clearly.
   --cyan-soft on dark surfaces was too faint — selection became
   indistinguishable from un-selected text. */
::selection {
  background: rgba(83, 216, 255, 0.35);
  color: var(--text);
}

/* Scrollbar polish — scoped to INNER scrollable containers only.
   The body / html scroll deliberately uses native macOS overlay
   scrollbars (auto-hiding, transparent) — overriding them globally
   created a persistent 8px bar that overlapped page content at the
   right edge.  Firefox's `scrollbar-width: thin` is already set
   per-container where needed; the rule below covers WebKit/Blink. */
.tool-pane::-webkit-scrollbar,
.modal-overlay::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
.tool-pane::-webkit-scrollbar-track,
.modal-overlay::-webkit-scrollbar-track {
  background: transparent;
}
.tool-pane::-webkit-scrollbar-thumb,
.modal-overlay::-webkit-scrollbar-thumb {
  background: rgba(255, 255, 255, 0.14);
  border-radius: var(--radius-sm);
}
.tool-pane::-webkit-scrollbar-thumb:hover,
.modal-overlay::-webkit-scrollbar-thumb:hover {
  background: rgba(255, 255, 255, 0.22);
}

html {
  background: var(--bg);
  /* 2026-05-25: disable CSS scroll-anchoring globally.  The dashboard
     mutates the job list (insert tempJob → swap to real → poll merges
     status updates) several times per Generate.  Default
     overflow-anchor:auto picks SOME visible element as anchor and
     adjusts scrollY to keep it pinned — felt by users as a "teleport
     down" jump.  Initial 2026-05-22 fix scoped `overflow-anchor: none`
     to .jobs only, but anchor candidates outside .jobs (footer, other
     blocks below .results-pane) still triggered the jump because
     inserting in .jobs grows .results-pane, shifting them down.
     Anchoring is not load-bearing for any RenderMix UX — text reading,
     infinite scroll, etc. all work the same way without it. */
  overflow-anchor: none;
}

body {
  margin: 0;
  /* iOS Safari URL-bar bug: 100vh = LARGE viewport (URL-bar collapsed),
     so content gets clipped when bar is visible.  100dvh = DYNAMIC viewport,
     resizes with browser chrome.  Double-declaration: old Safari <15.4
     ignores the second line and falls back to 100vh.  Same pattern applies
     to every other 100vh use in this file. */
  min-height: 100vh;
  min-height: 100dvh;
  background: var(--bg);
  color: var(--text);
  text-rendering: geometricPrecision;
}

body.viewer-open {
  overflow: hidden;
}

button,
input,
select,
textarea {
  font: inherit;
}

button,
a,
input,
select,
textarea,
summary {
  -webkit-tap-highlight-color: transparent;
  /* Removes the 300ms tap-delay on iOS and prevents double-tap zoom
     on interactive elements.  Safe globally — only affects gestures
     where double-tap-to-zoom would have triggered. */
  touch-action: manipulation;
}

/* iOS Safari auto-zooms (and rarely zooms back) when any form input has
   computed font-size < 16px.  Form inputs in this codebase inherit 13–14px
   from .tool-form labels — forcing 16px on touch viewports kills the
   zoom trap.  :where(...) keeps specificity at 0,0,1 so any deliberate
   font-size override still wins.  Excludes type=checkbox/radio/file/range
   which have no text input. */
@media (max-width: 720px) {
  input:where(:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"])),
  select,
  textarea {
    font-size: var(--text-lg);
  }
  /* Model-select ID override moved to end-of-file (after the desktop
     #image-model-select rule at :552) — same specificity (1,0,0)
     requires source-later to win. */
}

.topbar {
  min-height: 68px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
  padding: 0 30px;
  border-bottom: 1px solid var(--line);
  background: rgba(2, 2, 4, 0.82);
  position: sticky;
  top: 0;
  z-index: 10;
  backdrop-filter: blur(18px);
}

.topbar-actions {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: var(--space-3);
  flex-wrap: wrap;
}

/* Admin-only chrome lives at the far right of the topbar (after Logout),
   visually offset by a hairline divider so operator tools don't blend
   into regular user nav.  Inner gap matches .topbar-actions for visual
   continuity within the admin group. */
.topbar-admin-group {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-left: 12px;
  padding-left: 18px;
  border-left: 1px solid var(--line);
}
@media (max-width: 720px) {
  /* On narrow viewports the topbar wraps; the divider would float
     mid-row.  Drop it on small screens — separation is implicit by
     wrapping. */
  .topbar-admin-group {
    margin-left: 0;
    padding-left: 0;
    border-left: 0;
  }
}

.slot-badge {
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--muted);
  padding: 6px 10px;
  border-radius: var(--radius-md);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--line);
  letter-spacing: 0.02em;
}
.slot-badge.at-limit {
  color: var(--amber);
  border-color: rgba(255, 190, 92, 0.3);
}

/* Credit balance badge (2026-06-10) — mirrors .slot-badge metrics; cyan accent
   so "what you have" reads as a positive resource, amber `.low` nudge < 50 cr.
   It's an <a> to /billing so a tap tops up. */
.credit-badge {
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--cyan);
  padding: 6px 10px;
  border-radius: var(--radius-md);
  background: rgba(80, 200, 255, 0.06);
  border: 1px solid rgba(80, 200, 255, 0.22);
  letter-spacing: 0.02em;
  text-decoration: none;
  white-space: nowrap;
}
.credit-badge:hover {
  border-color: rgba(80, 200, 255, 0.45);
}
.credit-badge.low {
  color: var(--amber);
  background: rgba(255, 190, 92, 0.06);
  border-color: rgba(255, 190, 92, 0.3);
}

/* Per-render credit price (2026-06-10, moved INTO the Generate button
   2026-06-11) — quiet suffix after the label; amber `.low` when the price
   exceeds the available balance (same math as the reserve gate, so
   amber = the submit would 402). */
.primary-button .btn-price {
  margin-left: 10px;
  font-weight: 600;
  opacity: 0.75;
  font-variant-numeric: tabular-nums;
}
.primary-button .btn-price.low {
  /* price > available balance → the submit would 402.  The old amber
     (#ffbe5c) was a low-contrast clash on the cyan button; a dark crimson
     reads cleanly on cyan and still flags "can't afford". */
  color: #5e0a16;
  opacity: 1;
  font-weight: 700;
}

/* The toggle is a real <button class="ghost-button admin-toggle"> so it
   inherits identical box metrics from .ghost-button — no hack needed to
   match a <label>+<input> against neighbouring buttons.  Active state is
   indicated by aria-pressed=true: cyan border + cyan text. */
.admin-toggle[aria-pressed="true"] {
  border-color: rgba(83, 216, 255, 0.55);
  color: var(--cyan);
  background: rgba(83, 216, 255, 0.08);
}
.admin-toggle[aria-pressed="true"]:hover {
  border-color: var(--cyan);
  background: rgba(83, 216, 255, 0.13);
}

/* "+ deleted" toggle, when active, signals "you are viewing soft-deleted
   records" — semantically a danger-awareness state, not just a filter
   selection.  Amber-tint override instead of the default cyan-tint
   acknowledges the elevated-caution nature of the view. */
#admin-include-deleted-toggle[aria-pressed="true"] {
  border-color: rgba(255, 190, 92, 0.55);
  color: var(--amber);
  background: rgba(255, 190, 92, 0.08);
}
#admin-include-deleted-toggle[aria-pressed="true"]:hover {
  border-color: var(--amber);
  background: rgba(255, 190, 92, 0.13);
}

/* Output-filter segmented control — Images / Videos / Favorites in the
   topbar share the same right-pane "view".  Visually grouped so it reads
   as one filter widget rather than three loose buttons.  Keeps the same
   aria-pressed semantics as .admin-toggle — JS targets the IDs, not the
   class, so existing toggle handlers still apply. */
.filter-group {
  display: inline-flex;
  align-items: stretch;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: rgba(255, 255, 255, 0.02);
}
.filter-chip {
  min-height: 40px;
  padding: 0 14px;
  border: 0;
  border-left: 1px solid var(--line);
  background: transparent;
  color: var(--text-soft);
  font-weight: 600;
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  transition: background var(--duration-fast) ease, color var(--duration-fast) ease;
}
.filter-chip:first-child { border-left: 0; }
.filter-chip:hover {
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
}
.filter-chip[aria-pressed="true"] {
  background: rgba(83, 216, 255, 0.10);
  color: var(--cyan);
}
.filter-chip[aria-pressed="true"]:hover {
  background: rgba(83, 216, 255, 0.16);
}
.filter-chip:focus-visible {
  outline: 2px solid rgba(83, 216, 255, 0.6);
  outline-offset: -2px;
}

.brand {
  font-weight: 800;
  font-size: 19px;
  letter-spacing: 0;
}

.subtle,
.results-header p,
.login-panel p {
  color: var(--muted);
  margin: 4px 0 0;
}

.subtle {
  font-size: var(--text-base);
}

.workspace {
  --composer-min: 320px;
  --composer-max: 9999px;
  --composer-width: 420px;
  --split-width: 22px;
  --results-min: 320px;
  --media-tile-width: 280px;
  display: grid;
  grid-template-columns:
    clamp(
      var(--composer-min),
      var(--composer-width),
      min(var(--composer-max), calc(100% - var(--split-width) - var(--results-min)))
    )
    var(--split-width)
    minmax(0, 1fr);
  align-items: stretch;
  gap: 0;
  width: min(100%, 1740px);
  max-width: 100%;
  margin: 0 auto;
  padding: 28px 30px 40px;
  overflow-x: clip;
}

.tool-pane,
.results-pane {
  min-width: 0;
}

.tool-pane {
  width: 100%;
  position: sticky;
  top: 96px;
  align-self: start;
  max-height: calc(100vh - 124px);
  max-height: calc(100dvh - 124px);
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
  padding-right: 4px;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}

.results-pane {
  min-width: 0;
  max-width: 100%;
  min-height: calc(100vh - 126px);
  min-height: calc(100dvh - 126px);
  padding-left: 22px;
  overflow-x: clip;
}

.split-resizer {
  align-self: stretch;
  width: var(--split-width);
  min-height: calc(100vh - 126px);
  min-height: calc(100dvh - 126px);
  border: 0;
  border-radius: var(--radius-pill);
  background: transparent;
  cursor: col-resize;
  position: sticky;
  top: 96px;
}

.split-resizer::before {
  content: "";
  display: block;
  width: 1px;
  height: 100%;
  margin: 0 auto;
  background: rgba(255, 255, 255, 0.08);
  transition: background var(--duration-base) ease, box-shadow var(--duration-base) ease;
}

.split-resizer:hover::before,
.split-resizer:focus-visible::before,
body.resizing-layout .split-resizer::before {
  background: rgba(83, 216, 255, 0.72);
  box-shadow: 0 0 18px rgba(83, 216, 255, 0.24);
}

body.resizing-layout {
  cursor: col-resize;
  user-select: none;
}

.tabs {
  display: grid;
  /* One even segment per tool tab, auto-counted — a column per button so the
     row never wraps when a tab is added (was hardcoded repeat(4) → the 5th tab
     Audio orphaned onto a second row; auto-flow removes the count-sync footgun). */
  grid-auto-flow: column;
  grid-auto-columns: minmax(0, 1fr);
  gap: 6px;
  padding: var(--space-1);
  margin-bottom: 28px;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: #050609;
}

.tab {
  white-space: nowrap;
}

.tab,
.ghost-button,
.primary-button,
.icon-button,
.viewer-download,
.viewer-button,
.viewer-nav,
.details-value .copy-detail {
  appearance: none;
  border: 0;
  color: var(--text);
  cursor: pointer;
  letter-spacing: 0;
}

.tab {
  min-height: 38px;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  font-weight: 700;
}

.tab:hover {
  color: var(--text-soft);
}

.tab.active {
  background: var(--text);
  color: #050609;
}

.ghost-button {
  min-height: 40px;
  padding: 0 13px;
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--text-soft);
  font-weight: 600;
  white-space: nowrap;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
}

.ghost-button:hover {
  border-color: var(--line-strong);
  color: var(--text);
  background: rgba(255, 255, 255, 0.035);
}

/* Accent variant — used on the Showcase link in the dashboard topbar
   to signal a different action class ("go discover community work")
   vs the surrounding utility ghost-buttons ("manage your stuff").
   Subtle cyan-soft tint + cyan text; geometry identical to base
   ghost-button so the topbar rhythm doesn't break. */
.ghost-button.is-accent {
  border-color: rgba(83, 216, 255, 0.35);
  color: var(--cyan);
  background: rgba(83, 216, 255, 0.06);
}
.ghost-button.is-accent:hover {
  border-color: var(--cyan);
  color: var(--cyan-hover);
  background: rgba(83, 216, 255, 0.12);
}

/* Logout button — red border on hover so the "you're about to end
   your session" signal lands at the moment of decision.  Default
   state stays neutral ghost so daily use isn't alarmist. */
.ghost-button.is-logout:hover {
  border-color: rgba(255, 93, 115, 0.55);
  color: var(--red);
  background: rgba(255, 93, 115, 0.06);
}

.primary-button {
  /* inline-flex centres text on both axes for <button> AND <a> uses
     (verify_result.html uses <a>; native <button> default text-align
     was the only thing centring it before — see screenshot 2026-05-06). */
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 54px;
  padding: 0 16px;
  border-radius: var(--radius-md);
  background: var(--cyan);
  color: #020204;
  font-weight: 800;
  text-align: center;
  text-decoration: none;
  margin-top: 18px;
  box-shadow: 0 0 0 1px rgba(83, 216, 255, 0.25), 0 18px 44px rgba(83, 216, 255, 0.14);
}

.primary-button:hover {
  background: #7be2ff;
}

.primary-button:disabled,
.ghost-button:disabled,
.icon-button:disabled {
  cursor: not-allowed;
  opacity: 0.55;
}

.primary-button:focus-visible,
.primary-button-mini:focus-visible,
.ghost-button:focus-visible,
.tab:focus-visible,
.icon-button:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 2px;
}

.primary-button-mini {
  display: inline-block;
  padding: 9px 16px;
  border-radius: var(--radius-sm);
  background: var(--cyan);
  color: #020204;
  font-weight: 800;
  font-size: var(--text-base);
  text-decoration: none;
  transition: background var(--duration-fast), transform 0.08s;
  box-shadow: 0 4px 14px rgba(83, 216, 255, 0.3);
}
.primary-button-mini:hover { background: #7be2ff; }
.primary-button-mini:active { transform: scale(0.97); }

.tool-form {
  display: block;
}

.tool-form h1 {
  margin: 0 0 20px;
  font-size: var(--text-2xl);
  line-height: 1.1;
  font-weight: 800;
}

label {
  display: grid;
  gap: var(--space-2);
  color: var(--text-soft);
  font-size: var(--text-base);
  font-weight: 700;
  margin-bottom: var(--space-4);
}

.field-hint {
  color: var(--muted);
  font-weight: 400;
  font-size: var(--text-sm);
  margin-left: 4px;
}

/* Honeypot trap — invisible to humans (off-screen + tabindex=-1 + aria-hidden),
   but present in the DOM so naive form-spammer bots fill it.  Filled value =
   silent reject in auth.py signup_post().  display:none would skip bot
   form-walkers; off-screen positioning is the conventional anti-bot pattern. */
.hp-trap {
  position: absolute;
  left: -9999px;
  top: -9999px;
  width: 0;
  height: 0;
  overflow: hidden;
  pointer-events: none;
  opacity: 0;
}

/* Scoped to text-like inputs only — bare `input` would also style checkboxes
   and radios as tall rectangles (operator-reported bug 2026-05-02 in topbar
   admin toggle).  See .admin-toggle input + .check-row input below for the
   tiny-checkbox style. */
/* :where() makes the negation chain specificity-free (0,0,0) so per-class
   overrides like .slider-number can win without !important.  Without it
   this selector was (0,4,1) — far stronger than any single-class rule. */
input:where(:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"])),
select,
textarea {
  width: 100%;
  color: var(--text);
  background: var(--bg-field);
  border: 1px solid transparent;
  border-radius: var(--radius-sm);
  padding: var(--space-3) var(--space-4);
  outline: none;
  box-shadow: inset 0 0 0 1px var(--line);
}

textarea {
  resize: none;
  overflow: hidden;
  line-height: 1.45;
}
/* Phase 1.10 2026-05-06: autoGrowTextarea() shrinks empty textareas to
   ~46px (scrollHeight of a single empty line + padding).  We previously
   used a flat min-height: 92px which only restored ~3 rows — the prompt
   field (rows="8") felt cramped even though the HTML attribute hinted
   ~8 rows.  Tie min-height to the rows attribute so a `rows="8"` textarea
   looks ~8 rows tall while empty, and `rows="3"` (negative prompt) stays
   compact.  autoGrow still expands beyond min when content overflows. */
textarea[rows="3"] { min-height: 80px; }
textarea[rows="8"] { min-height: 180px; }

input::placeholder,
textarea::placeholder {
  color: var(--muted-2);
}

input:where(:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"])):hover,
select:hover,
textarea:hover {
  box-shadow: inset 0 0 0 1px var(--line-strong);
}

input:where(:not([type="checkbox"]):not([type="radio"]):not([type="file"]):not([type="range"])):focus,
select:focus,
textarea:focus {
  border-color: rgba(83, 216, 255, 0.28);
  box-shadow: inset 0 0 0 1px rgba(83, 216, 255, 0.5), 0 0 0 4px rgba(83, 216, 255, 0.07);
}

select {
  cursor: pointer;
}

/* 2026-05-11: enlarge the Model dropdown on image + video forms.  The
   default select chrome was too quiet — operator feedback called it
   "узкий и маленький, незаметный".  These two pickers are the most
   important control on the form, so bump the visual weight: bigger
   padding, bolder text, slight border emphasis.  Scoped via the IDs
   so other selects (Resolution, Version, Mode, etc.) stay compact. */
#image-model-select,
#video-model-select {
  min-height: 52px;
  padding: 14px 16px;
  font-size: 15px;
  font-weight: 600;
  border-radius: var(--radius-md);
  box-shadow: inset 0 0 0 1px var(--line-strong);
}
#image-model-select:hover,
#video-model-select:hover {
  box-shadow: inset 0 0 0 1px rgba(83, 216, 255, 0.45);
}

.grid-2,
.grid-3,
.grid-4 {
  display: grid;
  gap: var(--space-3);
}

.grid-2 {
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

/* auto-fit instead of fixed 3/4 columns: when some labels in the row are
   display:none (preset-conditional fields like web_search / video_version /
   mode), the visible ones expand to fill the row instead of leaving an
   awkward empty gap.  Operator feedback 2026-05-02. */
.grid-3 {
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
}

.grid-4 {
  grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
}

details {
  border: 0;
  border-top: 1px solid var(--line);
  border-radius: 0;
  padding: 14px 0 0;
  background: transparent;
}

summary {
  cursor: pointer;
  color: var(--text);
  font-weight: 800;
  margin-bottom: 14px;
}

.check-row {
  display: flex;
  align-items: center;
  gap: 10px;
  color: var(--text-soft);
}

.check-row input {
  width: 18px;
  height: 18px;
  accent-color: var(--cyan);
  box-shadow: none;
}

.results-header {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 18px;
  margin-bottom: var(--space-5);
  padding-bottom: 18px;
  border-bottom: 1px solid var(--line);
}

.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.results-header h2 {
  margin: 0;
  font-size: var(--text-2xl);
  line-height: 1.1;
  font-weight: 800;
}

.jobs {
  display: grid;
}

.job-card {
  min-width: 0;
  max-width: 100%;
  border-top: 1px solid var(--line);
  background: transparent;
  overflow: visible;
  padding: 22px 0;
}

.job-card:first-child {
  border-top: 0;
  padding-top: 0;
}

.job-head {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  padding: 0 0 14px;
  border-bottom: 0;
}

.job-title {
  font-weight: 800;
  font-size: 15px;
}

.job-head-actions {
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.user-pill {
  align-self: center;
  padding: 3px 9px;
  border-radius: var(--radius-pill);
  background: rgba(83, 216, 255, 0.10);
  color: var(--cyan);
  font-size: var(--text-sm);
  font-weight: 600;
  max-width: 200px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.icon-button {
  min-height: 30px;
  padding: 0 10px;
  border-radius: var(--radius-sm);
  background: transparent;
  color: var(--muted);
  font-size: var(--text-base);
  font-weight: 700;
}

.icon-button:hover {
  color: var(--cyan);
  background: rgba(83, 216, 255, 0.07);
}

.icon-button.danger-button:hover {
  color: var(--red);
  background: rgba(255, 93, 115, 0.08);
}

.status {
  display: inline-flex;
  align-items: center;
  min-height: 30px;
  border-radius: var(--radius-pill);
  padding: 0 10px;
  background: rgba(255, 255, 255, 0.055);
  color: var(--muted);
  font-size: var(--text-sm);
  font-weight: 700;
  white-space: nowrap;
}

.status.succeeded {
  background: rgba(88, 230, 154, 0.1);
  color: var(--green);
}

.status.failed {
  background: rgba(255, 93, 115, 0.11);
  color: var(--red);
}

.status.partial {
  background: rgba(154, 124, 255, 0.12);
  color: var(--violet);
}

.status.running,
.status.queued {
  background: rgba(255, 190, 92, 0.11);
  color: var(--amber);
}

/* 2026-05-12: admin all-users per-user breakdown plashka.  Sits above the
   job list, shows up to 10 users by job count.  Hidden in personal dashboard
   and when the all-users toggle is off. */
.admin-users-summary {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  align-items: center;
  padding: 10px 12px;
  margin-bottom: var(--space-3);
  background: rgba(255, 255, 255, 0.03);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  color: var(--muted);
}
.user-summary-label {
  font-weight: 600;
  color: var(--muted);
  margin-right: 4px;
}
.user-summary-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 4px 8px;
  background: rgba(255, 255, 255, 0.05);
  border: 1px solid transparent;
  border-radius: var(--radius-pill);
  color: var(--text);
  cursor: pointer;
  user-select: none;
  transition: background var(--duration-fast) ease, border-color var(--duration-fast) ease, opacity var(--duration-fast) ease;
}
.user-summary-chip:hover {
  background: rgba(255, 255, 255, 0.09);
  border-color: rgba(255, 255, 255, 0.18);
}
.user-summary-chip:focus-visible {
  outline: none;
  border-color: var(--accent, #5cb1ff);
  box-shadow: 0 0 0 2px rgba(92, 177, 255, 0.35);
}
/* 2026-05-20: chip in the "hidden" state — dimmed + strikethrough email so
   the operator can spot it against the active chips and click again to
   un-hide.  Count slot shows "—" since the server omitted the row. */
.user-summary-chip--excluded {
  opacity: 0.45;
  background: rgba(255, 255, 255, 0.02);
  border-color: rgba(255, 255, 255, 0.08);
}
.user-summary-chip--excluded .user-summary-email {
  text-decoration: line-through;
  text-decoration-color: rgba(255, 255, 255, 0.5);
}
.user-summary-chip--excluded:hover {
  opacity: 0.75;
}
.user-summary-email {
  max-width: 180px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--text-xs);
}
.user-summary-count {
  font-weight: 700;
  color: var(--accent, var(--text));
  background: rgba(255, 255, 255, 0.05);
  padding: 1px 6px;
  border-radius: var(--radius-pill);
  min-width: 18px;
  text-align: center;
}

/* 2026-05-12: workflow_id chip in job meta-row.  Click copies the value so
   admin can paste it against the Civitai backend feed when debugging. */
.wfid-chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 2px 8px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: var(--radius-pill);
  color: var(--muted);
  font-size: var(--text-xs);
  cursor: pointer;
  transition: background var(--duration-fast) ease, color var(--duration-fast) ease;
}
.wfid-chip:hover {
  background: rgba(255, 255, 255, 0.07);
  color: var(--text);
}
.wfid-chip.copied {
  background: rgba(88, 230, 154, 0.12);
  color: var(--green);
}
.wfid-chip code {
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--text-xs);
}
.wfid-label {
  font-weight: 700;
  letter-spacing: 0.04em;
  opacity: 0.7;
}

/* 2026-05-12: admin "+ deleted" toggle reveals soft-deleted jobs.  Visual
   treatment: dim the whole card + a small banner so a moderator can tell
   at a glance which rows the user wiped from their own dashboard. */
.status.deleted,
.status.cancelled {
  background: rgba(255, 255, 255, 0.07);
  color: var(--muted);
  text-decoration: line-through;
}
.job-card.job-deleted {
  opacity: 0.6;
}
.job-card.job-deleted .deleted-banner {
  padding: 6px 12px;
  background: rgba(255, 255, 255, 0.04);
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}

.job-body {
  min-width: 0;
  max-width: 100%;
  padding: 0;
}

.prompt-preview {
  color: var(--text-soft);
  line-height: 1.45;
  margin: 0 0 12px;
  max-height: 68px;
  overflow: hidden;
}

.meta-row {
  display: flex;
  flex-wrap: wrap;
  gap: 8px 13px;
  color: var(--muted);
  font-size: var(--text-sm);
  margin-bottom: 14px;
}

.meta-row span {
  min-width: 0;
  overflow-wrap: anywhere;
}

.generation-details {
  margin-top: var(--space-4);
  padding: 0;
  border: 0;
  border-top: 1px solid var(--line);
  background: transparent;
}

.generation-details summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 42px;
  margin: 0;
  padding: 0;
  color: var(--muted);
  font-size: var(--text-base);
}

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

.generation-details summary::marker {
  content: "";
}

.generation-details summary::before {
  content: "›";
  color: var(--cyan);
  font-size: var(--text-xl);
  font-weight: 300;
  line-height: 1;
  transform: translateY(-1px);
  transition: transform var(--duration-base) ease;
}

.generation-details[open] summary::before {
  transform: rotate(90deg) translateX(-1px);
}

.generation-details summary:hover {
  color: var(--text-soft);
}

.details-table {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 170px), 1fr));
  gap: 7px 16px;
  border-top: 1px solid var(--line);
  padding-top: 9px;
}

.details-row {
  display: flex;
  align-items: baseline;
  gap: 7px;
  min-width: 0;
}

.details-row.multiline {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: 86px minmax(0, 1fr);
  align-items: start;
  gap: var(--space-2);
}

.details-label {
  color: var(--muted);
  flex: 0 0 auto;
  font-size: var(--text-xs);
  font-weight: 700;
  line-height: 1.3;
  text-transform: uppercase;
}

.details-value {
  color: var(--text-soft);
  /* 2026-05-31 — was nowrap + ellipsis, which truncated the timestamp values
     ("CREATED 1 Jun 00..." hid the time).  Allow wrap so the full value always
     shows; overflow-wrap:anywhere lets a long workflow_id break cleanly too. */
  white-space: normal;
  overflow-wrap: anywhere;
  min-width: 0;
  max-width: none;
  font-size: var(--text-xs);
  line-height: 1.3;
}

.details-row:not(.multiline) .details-label::after {
  content: ":";
}

.details-row.multiline .details-value {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: var(--space-2);
  white-space: normal;
}

.details-row.multiline .details-value span {
  display: block;
  max-height: 74px;
  overflow: auto;
  padding: 2px 0 2px 8px;
  border-left: 1px solid var(--line);
  white-space: pre-wrap;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}

.details-value .copy-detail {
  margin: 0;
  border-radius: var(--radius-sm);
  background: rgba(83, 216, 255, 0.08);
  color: var(--cyan);
  padding: 4px 8px;
  font-size: var(--text-xs);
  font-weight: 700;
}

.details-value .copy-detail:hover {
  background: rgba(83, 216, 255, 0.14);
}

.empty-state {
  color: var(--muted);
  border-top: 1px solid var(--line);
  padding: 22px 0;
}

/* 2026-06-17: persistent cue that a kind (Images/Videos) filter is hiding
   renders — keeps a just-finished other-kind render from looking "vanished".
   Cyan tint matches the active .filter-chip so the connection reads at a
   glance.  JS (updateKindFilterBanner) toggles [hidden]. */
.kind-filter-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  flex-wrap: wrap;
  margin-bottom: var(--space-2);
  padding: 8px 12px;
  border: 1px solid rgba(83, 216, 255, 0.28);
  background: rgba(83, 216, 255, 0.08);
  border-radius: 8px;
  color: var(--text);
  font-size: 0.9em;
}
.kind-filter-banner[hidden] { display: none; }

/* Inline text-button used by the banner + filter-aware empty state. */
.link-button {
  background: none;
  border: 0;
  padding: 0;
  color: var(--cyan);
  font: inherit;
  cursor: pointer;
  text-decoration: underline;
}
.link-button:hover { text-decoration: none; }

.output-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, var(--media-tile-width));
  gap: var(--space-2);
  justify-content: start;
  align-items: start;
  width: 100%;
  max-width: 100%;
  min-width: 0;
  overflow-x: clip;
}

.output-tile {
  width: var(--media-tile-width);
  min-width: 0;
  max-width: 100%;
  border: 0;
  border-radius: var(--radius-sm);
  background: #050609;
  overflow: hidden;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.055);
}

.media-output {
  /* !important here beats Chrome/WebKit's UA cursor for [draggable=true]
     elements (it shows the grab/move hand on hover by default).  Drag
     still works — the browser only swaps to "grabbing" during the actual
     drag operation, which is the right time for that affordance. */
  cursor: zoom-in !important;
  position: relative;
}

.media-output:hover {
  box-shadow: inset 0 0 0 1px rgba(83, 216, 255, 0.28);
}

.media-output:focus-visible {
  outline: 2px solid var(--cyan);
  outline-offset: 3px;
}

/* Audio outputs (TTS / music): no thumbnail to fit a media aspect — the tile
   sizes to its content (icon + native player + duration) and the lightbox is
   suppressed (the <audio> controls own interaction). */
.output-tile.audio-output {
  aspect-ratio: auto;
  cursor: default !important;
  padding: 16px 14px;
  display: flex;
}
.audio-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
  width: 100%;
}
.audio-tile-icon { font-size: 30px; line-height: 1; opacity: 0.85; }
.audio-player { width: 100%; min-width: 0; height: 36px; }
.audio-tile-dur { font-size: 12px; color: var(--muted); }

/* Audio composer native selects (voice / length) — match the form controls. */
.audio-select {
  width: 100%;
  padding: 9px 11px;
  background: #0a0c12;
  color: var(--text);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  font: inherit;
}
.audio-check {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 10px;
  color: var(--muted);
  font-size: 13px;
}
.audio-check input { accent-color: var(--cyan); }

.pending-output {
  aspect-ratio: var(--media-aspect, 1 / 1);
  min-height: 0;
  display: grid;
  place-items: center;
  color: var(--muted);
  border: 1px dashed var(--line-strong);
  box-shadow: none;
}

/* 2026-05-27 — Extract Frames placeholder (background thread still
   ffmpeg-ing + uploading + generating variants).  Replaces the
   generic "Pending" tile so the operator sees that work is actively
   in progress.  Centred spinner + status text in muted accent. */
.extracting-frame {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 8px;
  text-align: center;
  padding: 12px;
  border-color: var(--cyan);
  color: var(--cyan);
  background: rgba(56, 189, 248, 0.04);
}
.extracting-frame .spinner {
  width: 24px;
  height: 24px;
  border-width: 3px;
  margin: 0;
}
.extracting-frame span {
  font-size: 12px;
  font-weight: 500;
}

/* 2026-06-01 — spinner on a still-rendering output tile (Waiting/Pending) so
   an in-flight card reads as actively working, not a frozen "dead" box.
   Mirrors the extracting-frame column layout.  Terminal states (Blocked /
   Generation failed) keep the plain grid-centred text (no .is-rendering). */
.pending-output.is-rendering {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
}
.pending-output.is-rendering .spinner {
  width: 22px;
  height: 22px;
  border-width: 3px;
  margin: 0;
}

/* 2026-06-01 — small spinner inside an ACTIVE job's status pill
   (queued / running / submitting).  currentColor → matches the pill's amber
   text; auto-hidden once the job leaves an active status (re-render drops it). */
.status-spinner {
  display: inline-block;
  width: 11px;
  height: 11px;
  margin-right: 6px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: var(--radius-pill);
  animation: spin 0.7s linear infinite;
  flex: none;
}

/* 2026-05-25 — "First frame" / "Last frame" pill on extracted-frame
   tiles.  Sits top-left so it doesn't fight the heart corner (top-
   right) or hover-action strip (bottom).  Translucent dark pill with
   a thin border, matches the same visual weight as .play-badge on
   videos so the dashboard reads as one coherent overlay vocabulary. */
.frame-badge {
  position: absolute;
  top: 8px;
  left: 8px;
  z-index: 2;
  padding: 4px 8px;
  font-size: 11px;
  line-height: 1;
  font-weight: 600;
  letter-spacing: 0.02em;
  color: var(--text, #fff);
  background: rgba(8, 10, 16, 0.72);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: 999px;
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
  pointer-events: none;
}

.output-tile img,
.output-tile video {
  width: 100%;
  aspect-ratio: var(--media-aspect, 1 / 1);
  object-fit: contain;
  display: block;
  background: #020204;
}

.media-standin {
  width: 100%;
  aspect-ratio: var(--media-aspect, 1 / 1);
  background: #020204;
}

.viewer-video-cache {
  position: fixed;
  left: -10000px;
  top: 0;
  width: 1px;
  height: 1px;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
}

.play-badge {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  pointer-events: none;
  color: var(--text);
  font-size: var(--text-base);
  font-weight: 800;
  text-shadow: 0 1px 12px rgba(0, 0, 0, 0.82);
  background: rgba(0, 0, 0, 0.18);
}

/* Hover-strip with all per-tile actions (Download / Animate / Upscale).
   Sits at bottom-right, stays hidden until the tile is hovered or focused.
   All buttons share the .tile-action-btn style — same height, dark glass
   background, cyan label.  Replaces the previous setup where Animate was an
   ALWAYS-visible inline button below the image (operator feedback 2026-05-03:
   "looked ugly under the image"). */
.output-tile-actions {
  position: absolute;
  /* left+right bound the strip to the tile width so the buttons can WRAP
     instead of overflowing/clipping the left edge ("nimate" bug) when a tile
     carries 4 actions (Animate/Upscale/Avatar/Download) on a narrow tile. */
  left: 8px;
  right: 8px;
  bottom: 8px;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: var(--space-1);
  opacity: 0;
  transform: translateY(4px);
  transition: opacity var(--duration-base) ease, transform var(--duration-base) ease;
  pointer-events: none;
}

.media-output:hover .output-tile-actions,
.media-output:focus-within .output-tile-actions {
  opacity: 1;
  transform: translateY(0);
  pointer-events: auto;
}

.tile-action-btn {
  display: inline-flex;
  align-items: center;
  padding: 6px 9px;
  border: 0;
  border-radius: var(--radius-sm);
  color: var(--cyan);
  text-decoration: none;
  font-family: inherit;
  font-size: var(--text-sm);
  font-weight: 800;
  line-height: 1;
  background: rgba(2, 2, 4, 0.74);
  cursor: pointer;
  transition: background var(--duration-fast) ease;
}

.tile-action-btn:hover,
.tile-action-btn:focus-visible {
  background: rgba(2, 2, 4, 0.92);
  outline: none;
}

.tile-action-btn:focus-visible {
  box-shadow: 0 0 0 1px var(--cyan);
}

.tile-action-btn:disabled {
  color: var(--muted);
  cursor: not-allowed;
  opacity: 0.55;
}

.tile-action-btn:disabled:hover {
  background: rgba(2, 2, 4, 0.74);
}

/* Phase 9: favorites corner-button.  Top-right of the tile, separate from
   the bottom action strip.  Always visible (unlike .output-tile-actions
   which only appears on hover) so the saved state is glanceable from the
   feed without hovering. */
.tile-like-corner {
  position: absolute;
  top: 8px;
  right: 8px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: rgba(2, 2, 4, 0.55);
  color: rgba(255, 255, 255, 0.85);
  font-size: var(--text-xl);
  line-height: 1;
  cursor: pointer;
  transition: background var(--duration-fast) ease, color var(--duration-fast) ease, transform var(--duration-fast) ease;
  z-index: 2;
}
.tile-like-corner:hover,
.tile-like-corner:focus-visible {
  background: rgba(2, 2, 4, 0.82);
  outline: none;
  transform: scale(1.08);
}
.tile-like-corner.is-liked {
  color: #f06292;
}
.tile-like-corner.is-liked:hover {
  color: #ec407a;
}

.media-output.media-load-error::after {
  content: "Retry";
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  color: var(--cyan);
  font-size: var(--text-sm);
  font-weight: 800;
  background: rgba(2, 2, 4, 0.76);
}

.media-debug-panel {
  position: fixed;
  right: 14px;
  bottom: 14px;
  z-index: 120;
  width: min(660px, calc(100vw - 28px));
  max-height: min(620px, calc(100vh - 28px));
  max-height: min(620px, calc(100dvh - 28px));
  display: grid;
  grid-template-rows: auto auto minmax(0, 1fr);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  background: rgba(2, 2, 4, 0.92);
  box-shadow: var(--shadow);
  backdrop-filter: blur(18px);
  overflow: hidden;
}

.media-debug-head,
.media-debug-metrics,
.media-debug-row {
  display: flex;
  align-items: center;
  gap: var(--space-2);
}

.media-debug-head {
  justify-content: space-between;
  padding: 10px 12px;
  border-bottom: 1px solid var(--line);
}

.media-debug-head strong {
  margin-right: auto;
  font-size: var(--text-sm);
}

.media-debug-head button {
  appearance: none;
  border: 0;
  border-radius: var(--radius-sm);
  padding: 5px 8px;
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-soft);
  cursor: pointer;
  font-size: var(--text-xs);
  font-weight: 700;
}

.media-debug-head button:hover {
  color: var(--cyan);
  background: rgba(83, 216, 255, 0.1);
}

.media-debug-metrics {
  flex-wrap: wrap;
  padding: 9px 12px;
  border-bottom: 1px solid var(--line);
  color: var(--muted);
  font-size: var(--text-xs);
}

.media-debug-metrics span {
  padding: 3px 6px;
  border-radius: var(--radius-pill);
  background: rgba(255, 255, 255, 0.045);
}

.media-debug-list {
  overflow: auto;
  padding: 5px 0;
  scrollbar-width: thin;
  scrollbar-color: rgba(255, 255, 255, 0.18) transparent;
}

.media-debug-row {
  min-width: 0;
  padding: 5px 12px;
  color: var(--muted);
  font-size: var(--text-xs);
}

.media-debug-row span,
.media-debug-row strong,
.media-debug-row em {
  min-width: 0;
}

.media-debug-row strong {
  color: var(--text-soft);
  white-space: nowrap;
}

.media-debug-row em {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-style: normal;
}

.media-debug-row.network > span:first-child {
  color: var(--amber);
}

.media-debug-row.cache > span:first-child {
  color: var(--green);
}

.media-debug-row.browser-cache > span:first-child {
  color: var(--violet);
}

.media-debug-row.timing-limited > span:first-child {
  color: var(--muted);
}

.media-debug-row.revalidated > span:first-child,
.media-debug-row.element > span:first-child {
  color: var(--cyan);
}

.media-debug-row.hydrate > span:first-child {
  color: var(--violet);
}

.media-debug-row.video-event > span:first-child {
  color: var(--green);
}

.jobs-sentinel {
  width: 100%;
  height: 1px;
}

.error-text {
  color: var(--red);
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  margin-bottom: 14px;
}

.hidden {
  display: none;
}

.media-viewer {
  position: fixed;
  inset: 0;
  z-index: 100;
  background: rgba(0, 0, 0, 0.92);
  backdrop-filter: blur(18px);
}

.viewer-backdrop {
  position: absolute;
  inset: 0;
  border: 0;
  padding: 0;
  background: transparent;
  cursor: zoom-out;
}

.viewer-shell {
  position: relative;
  z-index: 1;
  width: 100vw;
  height: 100vh;
  height: 100dvh;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  padding: 18px;
  pointer-events: none;
}

.viewer-toolbar {
  min-height: 56px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  pointer-events: auto;
}

.viewer-title {
  font-weight: 800;
  color: var(--text);
  max-width: min(760px, 62vw);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.viewer-counter {
  color: var(--muted);
  font-size: var(--text-base);
  margin-top: var(--space-1);
}

.viewer-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}

.viewer-download,
.viewer-button,
.viewer-nav {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  background: rgba(7, 8, 11, 0.82);
  color: var(--text);
  text-decoration: none;
}

.viewer-download {
  padding: 10px 14px;
  color: var(--cyan);
  font-weight: 700;
}

.viewer-button {
  width: 42px;
  height: 42px;
  font-size: 28px;
  line-height: 1;
}

.viewer-stage {
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  pointer-events: auto;
}

.viewer-stage img,
.viewer-stage video {
  max-width: calc(100vw - 76px);
  max-height: calc(100vh - 112px);
  max-height: calc(100dvh - 112px);
  object-fit: contain;
  border-radius: var(--radius-sm);
  background: #020204;
  box-shadow: var(--shadow);
}

.viewer-stage img {
  cursor: zoom-in;
}

.viewer-stage.zoomed {
  align-items: flex-start;
  justify-content: flex-start;
  overflow: auto;
  padding: 18px;
}

.viewer-stage.zoomed img {
  max-width: none;
  max-height: none;
  cursor: zoom-out;
}

.viewer-nav {
  position: absolute;
  top: 50%;
  z-index: 2;
  width: 52px;
  height: 72px;
  transform: translateY(-50%);
  font-size: 54px;
  line-height: 1;
  pointer-events: auto;
}

.viewer-nav.previous {
  left: 18px;
}

.viewer-nav.next {
  right: 18px;
}

.viewer-nav:hover,
.viewer-button:hover,
.viewer-download:hover {
  border-color: rgba(83, 216, 255, 0.32);
  background: rgba(10, 11, 15, 0.96);
}

.login-page {
  display: grid;
  place-items: center;
  padding: var(--space-6);
}

.login-shell {
  width: min(420px, calc(100vw - 32px));
}

.login-panel {
  padding: 30px;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: #050609;
  box-shadow: var(--shadow);
}

.brand-mark {
  width: 40px;
  height: 40px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-md);
  /* match landing .rm-mark: cyan→violet gradient, not flat cyan */
  background: linear-gradient(135deg, var(--cyan) 0%, var(--violet) 100%);
  color: #020204;
  font-size: 15px;
  font-weight: 900;
  letter-spacing: -0.02em;
  margin-bottom: var(--space-5);
}

.login-panel h1 {
  margin: 0;
  font-size: var(--text-2xl);
  font-weight: 800;
  line-height: 1.1;
}

.login-form {
  margin-top: var(--space-6);
}

.error-banner {
  margin-top: 18px;
  padding: var(--space-3);
  border-left: 2px solid var(--red);
  background: rgba(255, 93, 115, 0.08);
  color: var(--red);
}

.info-banner {
  margin-top: 18px;
  padding: var(--space-3);
  border-left: 2px solid var(--green);
  background: rgba(88, 230, 154, 0.08);
  color: var(--green);
}

@media (max-width: 1180px) {
  .workspace {
    width: 100%;
    padding: 24px 22px 36px;
  }

  .grid-4 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

@media (max-width: 920px) {
  .topbar {
    padding: 0 18px;
  }

  .workspace {
    display: block;
    padding: 22px 18px 34px;
    overflow-x: visible;
  }

  .split-resizer {
    display: none;
  }

  .tool-pane {
    position: static;
    width: auto;
    max-height: none;
    overflow: visible;
    padding-right: 0;
  }

  .results-pane {
    padding-left: 0;
    margin-top: 28px;
  }
}

@media (max-width: 620px) {
  .topbar {
    align-items: flex-start;
    flex-direction: column;
    justify-content: center;
    padding: 14px 16px;
  }

  .topbar-actions {
    width: 100%;
    justify-content: space-between;
  }

  .workspace {
    padding: 18px 14px 28px;
  }

  .grid-3,
  .grid-4 {
    grid-template-columns: 1fr;
  }

  .results-header,
  .job-head {
    flex-direction: column;
  }

  .job-head-actions {
    justify-content: flex-start;
  }

  .output-grid {
    grid-template-columns: 1fr;
  }

  .output-tile {
    width: 100%;
  }

  .details-row {
    grid-template-columns: 1fr;
  }

  .details-label {
    padding: 10px 0 0;
  }

  .viewer-title {
    max-width: 48vw;
  }

  .viewer-download {
    display: none;
  }
}

/* ── Phase 3.2: Email verification banner ─────────────────────────────────── */
.verify-banner {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
  background: rgba(255, 190, 92, 0.08);
  border-bottom: 1px solid rgba(255, 190, 92, 0.25);
  color: var(--amber);
  padding: 0.65rem 1.25rem;
  font-size: 0.875rem;
  line-height: 1.4;
}

.verify-banner strong {
  color: var(--text);
}

.verify-banner-form {
  margin: 0;
  flex-shrink: 0;
}

.verify-banner-btn {
  background: transparent;
  border: 1px solid rgba(255, 190, 92, 0.4);
  color: var(--amber);
  padding: 0.3rem 0.85rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-size: 0.8rem;
  font-weight: 700;
  transition: background var(--duration-base), border-color var(--duration-base);
}

.verify-banner-btn:hover {
  background: rgba(255, 190, 92, 0.1);
  border-color: var(--amber);
}

.verify-banner-success {
  background: rgba(88, 230, 154, 0.08);
  border-bottom: 1px solid rgba(88, 230, 154, 0.25);
  color: var(--green);
}

.verify-banner-warning {
  background: rgba(255, 190, 92, 0.12);
  border-bottom: 1px solid rgba(255, 190, 92, 0.3);
  color: var(--amber);
}

/* Phase 7 (Batch C): contextual info banner on /billing when arriving from
   /showcase Remix click.  Cyan palette to match the Remix CTA tile button. */
.verify-banner-info {
  background: rgba(83, 216, 255, 0.10);
  border-bottom: 1px solid rgba(83, 216, 255, 0.30);
  color: var(--cyan);
}

/* Phase 1.6 (2026-05-05): banner dismiss button — closes the banner for
   30 days via localStorage flag (handled in app.js).  Same visual weight
   as .verify-banner-btn but icon-only and on the far right. */
.verify-banner-dismiss {
  margin-left: auto;
  background: transparent;
  border: 0;
  color: inherit;
  cursor: pointer;
  font-size: var(--text-xl);
  line-height: 1;
  padding: 4px 10px;
  border-radius: var(--radius-sm);
  opacity: 0.7;
  transition: opacity var(--duration-fast), background var(--duration-fast);
}
.verify-banner-dismiss:hover,
.verify-banner-dismiss:focus-visible {
  opacity: 1;
  background: rgba(255, 255, 255, 0.06);
  outline: none;
}

/* 2026-05-06: prominent verify gate on /billing.  The thin .verify-banner
   was getting ignored by mobile users who then clicked Subscribe and saw
   a 403 with no actionable hint (one real user lost this way — see audit
   2026-05-06 09:20).  This block is full-width amber, scrollable into
   view, and pairs with disabled subscribe-btn so the path is unmistakable. */
.verify-gate {
  display: flex;
  gap: 1.1rem;
  align-items: flex-start;
  background: linear-gradient(180deg, rgba(255, 190, 92, 0.12), rgba(255, 190, 92, 0.06));
  border: 1px solid rgba(255, 190, 92, 0.45);
  border-radius: var(--radius-md);
  padding: 1.1rem 1.4rem;
  margin: 0 0 1.5rem;
  color: var(--text);
}

.verify-gate-icon {
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: 50%;
  background: rgba(255, 190, 92, 0.18);
  color: var(--amber);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 22px;
  line-height: 1;
}

.verify-gate-body {
  flex: 1;
  min-width: 0;
}

.verify-gate-title {
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--amber);
  margin-bottom: 0.4rem;
}

.verify-gate-text {
  margin: 0 0 0.85rem 0;
  font-size: 0.92rem;
  line-height: 1.5;
  color: var(--text);
}

.verify-gate-text strong {
  color: var(--text);
}

.verify-gate-hint {
  display: block;
  margin-top: 0.5rem;
  font-size: 0.85rem;
  color: var(--muted, #a4a4a4);
}

.verify-gate-form {
  margin: 0;
}

.verify-gate-btn {
  background: var(--amber);
  color: #1a1100;
  border: 0;
  padding: 0.55rem 1.1rem;
  border-radius: var(--radius-sm);
  font-size: 0.92rem;
  font-weight: 700;
  cursor: pointer;
  transition: filter var(--duration-base), transform 0.05s;
}

.verify-gate-btn:hover {
  filter: brightness(1.08);
}

.verify-gate-btn:active {
  transform: translateY(1px);
}

/* Locked tier-grid: visually mute the cards while keeping the price /
   features readable, and hard-disable the subscribe buttons so a click
   simply does nothing instead of opening a doomed modal. */
.tier-grid-locked {
  opacity: 0.55;
  filter: saturate(0.7);
  pointer-events: none;
}

.tier-grid-locked .subscribe-btn[disabled] {
  cursor: not-allowed;
  background: rgba(255, 255, 255, 0.06);
  color: var(--muted, #a4a4a4);
  border-color: rgba(255, 255, 255, 0.12);
}

/* ============================================================
   Self-serve 2FA setup page (templates/account_2fa.html).
   Wider than the default login-shell (420px) because the QR code
   and recovery-code grid both want breathing room.
   ============================================================ */

.totp-shell {
  width: min(540px, calc(100vw - 32px));
}

.totp-qr {
  display: flex;
  justify-content: center;
  margin: 20px 0;
  padding: var(--space-4);
  background: #ffffff;
  border-radius: var(--radius-md);
}
.totp-qr svg {
  display: block;
  width: 240px;
  height: 240px;
}

.totp-manual {
  margin: 16px 0;
  padding: 12px 14px;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: var(--bg-field);
}
.totp-manual > summary {
  cursor: pointer;
  font-size: var(--text-base);
  font-weight: 700;
  color: var(--text-soft);
  margin: 0;
  list-style: none;
}
.totp-manual > summary::-webkit-details-marker { display: none; }
.totp-manual > summary::before {
  content: "▸";
  display: inline-block;
  margin-right: var(--space-2);
  color: var(--cyan);
  transition: transform var(--duration-base) ease;
}
.totp-manual[open] > summary::before { transform: rotate(90deg); }
.totp-manual > .modal-row:first-of-type { margin-top: 14px; }

.totp-recovery {
  margin: 24px 0 8px;
  padding: var(--space-4);
  border: 1px solid rgba(255, 190, 92, 0.25);
  background: rgba(255, 190, 92, 0.06);
  border-radius: var(--radius-md);
}
.totp-recovery .modal-label {
  color: var(--amber);
  font-weight: 800;
  margin-bottom: var(--space-1);
}
.totp-recovery > p.subtle {
  margin: 0 0 12px;
}
.totp-recovery > .copy-btn {
  margin-top: var(--space-3);
}

.totp-recovery-codes {
  list-style: none;
  padding: var(--space-3);
  margin: 0;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--text-md);
  letter-spacing: 0.04em;
  color: var(--text);
  background: var(--bg-field);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
}

.totp-confirm {
  margin-top: var(--space-6);
}

.totp-confirm input[name="code"] {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--text-xl);
  letter-spacing: 0.3em;
  text-align: center;
}

.totp-cancel-form {
  margin-top: var(--space-3);
  text-align: center;
}
.totp-cancel-form .ghost-button {
  width: 100%;
  justify-content: center;
}

/* ============================================================
   Account page (templates/account.html) — Identity / Security /
   Subscription stacked cards on top of .billing-page chrome.
   ============================================================ */

.account-card {
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 22px 24px;
  margin: 0 0 18px;
}

.account-card h2 {
  margin: 0 0 16px;
  font-size: var(--text-lg);
  font-weight: 800;
  color: var(--text);
  letter-spacing: -0.01em;
}

.account-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: var(--space-4);
  padding: 12px 0;
  border-top: 1px solid var(--line);
}
.account-row:first-of-type {
  border-top: 0;
  padding-top: 0;
}

.account-value {
  font-size: 15px;
  font-weight: 600;
  color: var(--text);
  margin-top: var(--space-1);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.account-row-status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  flex-wrap: wrap;
}

.account-pill {
  display: inline-block;
  padding: 2px 10px;
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: 0.02em;
}
.account-pill-ok {
  background: rgba(88, 230, 154, 0.15);
  color: var(--green);
}
.account-pill-warn {
  background: rgba(255, 190, 92, 0.15);
  color: var(--amber);
}

.account-inline-form {
  display: inline-flex;
  margin: 0;
}

.account-totp-disable {
  display: inline-flex;
  gap: 6px;
  align-items: stretch;
  margin: 0;
}
.account-totp-disable input[name="code"] {
  width: 200px;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--text-md);
  letter-spacing: 0.1em;
}

.account-section-help {
  margin: 12px 0 0;
  font-size: var(--text-base);
  color: var(--muted);
  line-height: 1.5;
}
.account-section-help strong {
  color: var(--text-soft);
}
.account-section-help a {
  color: var(--cyan);
  text-decoration: none;
}
.account-section-help a:hover {
  text-decoration: underline;
}
.account-soon {
  padding-top: 12px;
  border-top: 1px dashed var(--line);
}

@media (max-width: 560px) {
  .account-row {
    flex-direction: column;
    align-items: flex-start;
  }
  .account-row-status {
    width: 100%;
  }
  .account-totp-disable {
    width: 100%;
  }
  .account-totp-disable input[name="code"] {
    flex: 1;
    width: auto;
  }
}

/* Danger zone — red-tinted card for destructive operations.
   Closed-state collapse hides the form so the operator must consciously
   reveal it before they can submit. */
.account-card.account-danger {
  border-color: rgba(255, 93, 115, 0.4);
  background: rgba(255, 93, 115, 0.04);
}
.account-card.account-danger h2 {
  color: var(--red);
}
.account-delete {
  margin-top: var(--space-3);
}
.account-delete > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  list-style: none;
  border-color: rgba(255, 93, 115, 0.4);
  color: var(--red);
}
.account-delete > summary::-webkit-details-marker { display: none; }
.account-delete > summary:hover {
  background: rgba(255, 93, 115, 0.08);
  border-color: var(--red);
}
.account-delete-form {
  margin-top: var(--space-4);
  display: grid;
  gap: var(--space-3);
}
.account-delete-button {
  background: var(--red);
  color: #ffffff;
  box-shadow: 0 0 0 1px rgba(255, 93, 115, 0.25),
              0 18px 44px rgba(255, 93, 115, 0.14);
}
.account-delete-button:hover {
  background: #ff7a8d;
}

.active-invoice-banner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  background: rgba(83, 216, 255, 0.06);
  border: 1px solid rgba(83, 216, 255, 0.4);
  border-radius: var(--radius-md);
  color: var(--text-soft);
  padding: 0.75rem 1.25rem;
  margin: 0 auto 1.5rem;
  max-width: 900px;
  font-size: 0.9rem;
}
.active-invoice-banner[hidden] { display: none; }
.active-invoice-banner strong { color: var(--text); }
.active-invoice-banner .active-invoice-resume {
  background: var(--cyan);
  border: none;
  color: #020204;
  padding: 0.4rem 1rem;
  border-radius: var(--radius-sm);
  cursor: pointer;
  font-weight: 700;
  font-size: 0.875rem;
}
.active-invoice-banner .active-invoice-resume:hover { background: var(--cyan-hover); }
#active-invoice-status-pill {
  display: inline-block;
  padding: 0.05rem 0.4rem;
  border-radius: var(--radius-xs);
  background: var(--cyan-soft);
  color: var(--cyan);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  margin-left: 0.25rem;
}

.public-footer {
  text-align: center;
  margin: 2rem auto 1rem;
  /* Push above the iPhone home-indicator when present.  env() = 0 on
     devices without a bottom inset, so desktop spacing is unchanged. */
  padding-bottom: env(safe-area-inset-bottom);
  font-size: 0.78rem;
  color: var(--muted-2);
  letter-spacing: 0.02em;
}
.public-footer a {
  color: var(--muted);
  text-decoration: none;
  transition: color var(--duration-base);
}
.public-footer a:hover {
  color: var(--cyan);
}

/* Phase 3: image-to-video / reference-to-video image tray */
/* Phase 9: image tray now uses the same dashed-border dropzone chrome as
   the Upscale dropzone — see .upscale-dropzone above.  Same hover/dragover
   accents so both drop targets look consistent. */
.image-tray {
  padding: 14px 16px;
  margin: 0.4rem 0;
  border: 1.5px dashed var(--line-strong);
  border-radius: var(--radius-md);
  background: var(--bg-field);
  color: var(--muted);
  transition: border-color var(--duration-fast) ease, background var(--duration-fast) ease;
}
/* 2026-05-12: previously `position: sticky; top: 0; z-index: 5` here, on
   the theory that operators want the tray pinned while scrolling history
   for drag-drop targets.  In practice it covers the prompt + duration +
   seed fields whenever the tray has any content (operator-reported on
   slot-mode img2vid AND linear-mode ref2vid).  Drag-drop still works
   without sticky — browsers auto-scroll the viewport when the cursor
   approaches an edge during a DnD operation. */
.image-tray:hover {
  border-color: rgba(83, 216, 255, 0.45);
  background: rgba(83, 216, 255, 0.04);
  color: var(--text-soft);
}
.image-tray-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 0.5rem;
  font-size: 0.85rem;
}
.image-tray-thumbs {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}
.image-tray-thumb {
  position: relative;
  width: 72px;
  height: 96px;
  border-radius: var(--radius-xs);
  overflow: hidden;
  background: #0d0e14;
  border: 1px solid #2a2c38;
}
/* 2026-05-25: status states for disk-uploaded entries (kind="direct").
   `is-uploading` dims the thumb + paints a centred spinner via the
   .image-tray-thumb-spinner overlay.  `is-failed` tints red so the
   operator can spot which tile to remove + retry. */
.image-tray-thumb.is-uploading img {
  opacity: 0.55;
  filter: saturate(0.6);
}
.image-tray-thumb.is-failed {
  border-color: var(--danger, #ff8080);
}
.image-tray-thumb.is-failed img {
  opacity: 0.45;
  filter: grayscale(0.6) brightness(0.7);
}
.image-tray-thumb[draggable="false"] { cursor: not-allowed; }
.image-tray-thumb-spinner {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 22px;
  height: 22px;
  margin: -11px 0 0 -11px;
  border: 2px solid rgba(255, 255, 255, 0.25);
  border-top-color: rgba(255, 255, 255, 0.9);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
  pointer-events: none;
}
.image-tray.empty {
  /* Empty state: centred icon + label, like the upscale dropzone.  Header
     row is hidden in this state (rendered via JS) so the focal point is
     the call-to-action.  Larger min-height (140px) matches .upscale-dropzone
     for visual symmetry across drop surfaces. */
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 140px;
  text-align: center;
  padding: 28px 16px;
}
.image-tray.empty .image-tray-thumbs { display: none; }
.image-tray.empty .image-tray-header { display: none; }
/* The empty-content wrapper carries icon + label + count/hint.  Stack
   them vertically and centre — same visual rhythm as .upscale-dropzone. */
.image-tray-empty-content {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
}
.image-tray-empty-icon {
  font-size: var(--text-2xl);
  line-height: 1;
}
.image-tray-empty-label {
  font-size: var(--text-base);
  font-weight: 700;
  color: var(--text-soft);
}
/* Subtle count line ("0 of 7") — same chrome as .upscale-dropzone-count. */
.image-tray-empty-count {
  font-size: var(--text-sm);
  color: var(--muted-2);
}
/* 2026-05-08: secondary line under the label — explains the disk-upload
 * → Upscale tab workflow that's not obvious from the icon alone.
 * Used on the VIDEO image-tray empty state (where disk upload doesn't
 * apply directly); image-edit tray uses .image-tray-empty-count instead. */
.image-tray-empty-hint-disk {
  display: block;
  margin-top: 6px;
  font-size: var(--text-sm);
  color: var(--text-soft);
  opacity: 0.85;
  line-height: 1.4;
}
.image-tray-empty-hint-disk a {
  color: #93c5fd;
  text-decoration: underline;
  cursor: pointer;
}
.image-tray-empty-hint-disk a:hover {
  color: #bfdbfe;
}

/* 2026-05-11: clickable empty state for #image-edit-tray (img2img:edit).
   Mirrors .upscale-dropzone interaction — cursor:pointer, hover tint,
   focus ring.  Scoped via the #image-edit-tray id so it doesn't change
   the video tray (which has no disk upload, no click-to-pick). */
#image-edit-tray.empty {
  cursor: pointer;
}
#image-edit-tray.empty:hover,
#image-edit-tray.empty:focus-visible {
  border-color: rgba(83, 216, 255, 0.45);
  background: rgba(83, 216, 255, 0.04);
  color: var(--text-soft);
  outline: none;
}
#image-edit-tray.drop-target {
  border-color: var(--cyan);
  background: rgba(83, 216, 255, 0.08);
  color: var(--text);
}
.image-tray-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.image-tray-thumb-index {
  position: absolute;
  bottom: 2px;
  left: 2px;
  background: rgba(0, 0, 0, 0.7);
  color: #fff;
  font-size: 0.65rem;
  padding: 1px 5px;
  border-radius: var(--radius-sm);
  font-weight: 600;
}
.image-tray-thumb-remove {
  position: absolute;
  top: 2px;
  right: 2px;
  background: rgba(0, 0, 0, 0.7);
  color: #fff;
  border: none;
  border-radius: 50%;
  width: 18px;
  height: 18px;
  font-size: 0.7rem;
  line-height: 18px;
  text-align: center;
  cursor: pointer;
  padding: 0;
}
.image-tray-hint {
  margin-top: 0.4rem;
  font-size: 0.75rem;
}
.image-tray.error .image-tray-hint { color: #f87171; }

/* 2026-05-11: explicit First / Last frame slots for img2vid on Wan/Kling
   (img2vid_max_images=2).  Replaces the linear thumbs strip with two
   side-by-side labelled drop zones — mirrors Civitai's layout and makes
   the first+last keyframe interpretation visible without reading any hint.
   The .first-last-slots class is toggled on #image-tray-thumbs by
   renderImageTray() when the active mode + preset support both keyframes. */
.image-tray-thumbs.first-last-slots {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--space-3);
}
.image-tray-slot {
  display: flex;
  flex-direction: column;
  gap: 6px;
  min-height: 120px;
  /* 2026-05-12: cap slot height so a tall portrait source (e.g. UFC 896×1152)
     doesn't blow the whole tray up to 600+px and — combined with .image-tray's
     position:sticky — drape over the form fields below.  180px keeps the
     thumbnail visibly identifiable while leaving the rest of the form usable
     during scroll. */
  max-height: 220px;
}
.image-tray-slot-label {
  font-size: var(--text-sm);
  color: var(--text-soft);
  font-weight: 600;
}
.image-tray-slot-dropzone {
  flex: 1 1 auto;
  min-height: 90px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1.5px dashed var(--line-strong);
  border-radius: var(--radius-md);
  background: var(--bg-field);
}
.image-tray-slot-icon {
  font-size: 28px;
  color: var(--text-soft);
  opacity: 0.6;
}
.image-tray-slot.is-filled .image-tray-thumb {
  /* Slot-mode thumbs occupy the whole slot rect, not a linear strip cell. */
  width: 100%;
  height: 100%;
  min-height: 90px;
}

/* Phase 3 polish: drag-and-drop visual feedback */
.image-tray.drop-target {
  background: rgba(83, 216, 255, 0.08);
  border-style: solid;
  border-color: var(--cyan);
  box-shadow: 0 0 0 2px rgba(83, 216, 255, 0.4);
}
.output-tile[draggable="true"] {
  cursor: grab;
}
.output-tile[draggable="true"]:active {
  cursor: grabbing;
}
.output-tile.dragging,
.image-tray-thumb.dragging {
  opacity: 0.4;
}
.image-tray-thumb {
  cursor: grab;
}
.image-tray-thumb:active {
  cursor: grabbing;
}
/* Help tooltips — a subtle ⓘ glyph inline with the label text.  No background
   pill (operator feedback: previous version "looked like a badge, took space").
   Renders as a faint glyph that pops cyan on hover/focus.  Tooltip popup is
   built in JS (app.js → _showHelpTooltip) — see .help-tooltip below. */
.help-icon {
  display: inline;
  margin-left: 4px;
  padding: 0;
  background: none;
  color: var(--muted);
  font-style: normal;
  font-weight: 400;
  font-size: 0.95em;
  line-height: 1;
  opacity: 0.55;
  cursor: help;
  user-select: none;
  vertical-align: baseline;
  transition: opacity var(--duration-base), color var(--duration-base);
}
.help-icon:hover,
.help-icon:focus-visible {
  opacity: 1;
  color: var(--cyan);
  outline: none;
}

/* The label-text wrapper keeps the field name + help icon on one row, so
   the icon doesn't take a vertical slot of its own under the global
   `label { display: grid }` rule.  See dashboard.html — generated by the
   regex pass that wrapped all label text + icon pairs. */
.label-text {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  color: var(--text-soft);
  font-size: var(--text-base);
  font-weight: 700;
  letter-spacing: 0.01em;
}

/* Checkbox-style labels (enable_web_search, generate_audio, use_pro,
   enable_prompt_enhancer): override the global label{display:grid} so the
   checkbox + label text + help icon flow inline on a single row.
   No !important — :has() bumps specificity to (0,0,1,1) which beats the
   plain `label` selector, and crucially it does NOT beat inline
   `style="display:none"` written by JS setHidden. */
label:has(> input[type="checkbox"]) {
  display: flex;
  align-items: center;
  gap: 6px;
}

/* Tooltip is rendered into <body> by the JS handler in app.js — that lets us
   use position: fixed and calculate placement against the viewport, avoiding
   the clipping issues of the previous pure-CSS pseudo-element approach
   (operator screenshots 2026-05-02). */
.help-tooltip {
  position: fixed;
  width: 240px;
  max-width: calc(100vw - 24px);
  padding: 9px 12px;
  background: var(--bg-card);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-size: var(--text-sm);
  font-weight: 500;
  font-style: normal;
  line-height: 1.45;
  letter-spacing: 0;
  white-space: normal;
  text-align: left;
  text-transform: none;
  z-index: 1000;
  pointer-events: none;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
  /* Hidden by default; JS toggles visibility + sets position. */
  display: none;
}

/* Preset description shown under the model dropdown — populated by
   applyPresetToImageForm / applyPresetToVideoForm. */
.preset-description {
  margin-top: -4px;
  margin-bottom: var(--space-3);
  padding: 8px 12px;
  background: rgba(255, 255, 255, 0.025);
  border-left: 3px solid rgba(83, 216, 255, 0.4);
  border-radius: 0 6px 6px 0;
  font-size: var(--text-sm);
  color: var(--text-soft);
  line-height: 1.4;
}
.preset-description:empty { display: none; }

/* Phase 5: structured failure display */
.error-kind-badge {
  display: inline-block;
  padding: 0.05rem 0.45rem;
  margin-right: 0.4rem;
  border-radius: var(--radius-xs);
  background: var(--bg-soft);
  color: var(--red);
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  font-weight: 700;
}
.error-kind-badge.ban-warning {
  background: rgba(255, 93, 115, 0.25);
  color: var(--text);
  animation: pulse-ban 1.6s infinite;
}
@keyframes pulse-ban {
  0%, 100% { box-shadow: 0 0 0 0 rgba(255, 93, 115, 0.7); }
  50% { box-shadow: 0 0 0 4px rgba(255, 93, 115, 0); }
}
.error-text.ban-warning {
  border-left: 3px solid var(--red);
  padding-left: 0.5rem;
  background: rgba(255, 93, 115, 0.1);
}
.error-debug-button {
  margin-left: 0.5rem;
  font-size: 0.7rem;
  padding: 0.1rem 0.5rem;
}
.error-details-modal {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  z-index: 200;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 2rem;
}
.error-details-shell {
  background: var(--bg-card);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  max-width: 900px;
  width: 100%;
  max-height: 80vh;
  max-height: 80dvh;
  display: flex;
  flex-direction: column;
}
.error-details-head {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.8rem 1rem;
  border-bottom: 1px solid var(--line);
}
.error-details-body {
  flex: 1;
  overflow: auto;
  padding: 1rem;
  margin: 0;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: 0.78rem;
  color: var(--text-soft);
  white-space: pre-wrap;
  word-break: break-word;
}

/* Phase 7 — showcase / inspiration gallery */
.showcase-body {
  background: #050609;
  color: var(--text);
}
.showcase-main {
  max-width: 1400px;
  margin: 0 auto;
  padding: 18px 20px 60px;
}
.showcase-controls {
  display: flex;
  flex-direction: column;
  gap: 14px;
  margin-bottom: 22px;
}
.showcase-tabs {
  display: flex;
  gap: 6px;
}
.showcase-tabs .tab {
  text-decoration: none;
  padding: 8px 18px;
  border-radius: var(--radius-sm);
  font-weight: 700;
  color: var(--muted);
}
.showcase-tabs .tab.active {
  background: var(--text);
  color: #050609;
}
.showcase-filters {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-2);
}
.filter-label {
  color: var(--muted);
  font-size: var(--text-sm);
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  margin-left: var(--space-2);
}
.filter-label:first-child { margin-left: 0; }
.filter-pill {
  padding: 5px 12px;
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
  font-size: var(--text-sm);
  font-weight: 700;
  color: var(--text-soft);
  text-decoration: none;
  background: transparent;
  transition: background var(--duration-base), border-color var(--duration-base), color var(--duration-base);
}
.filter-pill:hover {
  border-color: var(--line-strong);
  color: var(--text);
}
.filter-pill.active {
  background: rgba(83, 216, 255, 0.12);
  border-color: rgba(83, 216, 255, 0.55);
  color: var(--cyan);
}
.showcase-funnel-banner {
  background: linear-gradient(120deg, rgba(83, 216, 255, 0.1), rgba(83, 216, 255, 0.02));
  border: 1px solid rgba(83, 216, 255, 0.3);
  border-radius: var(--radius-md);
  padding: 12px 16px;
  font-size: var(--text-base);
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px;
}
/* Masonry via CSS columns — items pack with their natural aspect-ratio,
   no fixed tile heights.  Mirrors Civitai's grid where landscape and
   portrait works coexist without empty space.  Items flow column-major
   (top-down then left-right) — fine since we sort by reactions DESC. */
.showcase-grid {
  column-count: auto;
  column-width: 280px;
  column-gap: 10px;
}
@media (min-width: 1500px) { .showcase-grid { column-width: 300px; } }
@media (max-width: 800px)  { .showcase-grid { column-width: 220px; } }

.showcase-tile {
  break-inside: avoid;
  margin: 0 0 10px;
  position: relative;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: #0d0e14;
  cursor: pointer;
  display: block;
}
.showcase-tile img,
.showcase-tile video {
  display: block;
  width: 100%;
  height: auto;
  background: #050609;
}

.showcase-video-badge {
  position: absolute;
  top: 8px;
  left: 8px;
  background: rgba(0, 0, 0, 0.6);
  color: #fff;
  font-size: var(--text-md);
  padding: 3px 8px;
  border-radius: var(--radius-xs);
  pointer-events: none;
  backdrop-filter: blur(4px);
}

/* Reaction pills — visible at bottom-left in idle state.  On tile hover the
   Remix CTA + author overlay slides in at the same place, so we fade the
   stats out to avoid visual collision (operator feedback 2026-05-03). */
.showcase-stats-overlay {
  position: absolute;
  bottom: 8px;
  left: 8px;
  display: flex;
  gap: var(--space-1);
  font-size: var(--text-xs);
  font-weight: 700;
  color: #fff;
  pointer-events: none;
  z-index: 1;
  opacity: 1;
  transition: opacity var(--duration-base) ease;
}
.showcase-tile:hover .showcase-stats-overlay {
  opacity: 0;
}
.showcase-stats-overlay span {
  background: rgba(0, 0, 0, 0.65);
  padding: 3px 7px;
  border-radius: var(--radius-pill);
  backdrop-filter: blur(6px);
  display: inline-flex;
  align-items: center;
  gap: 3px;
  white-space: nowrap;
}

/* Full-tile hover overlay — gradient backdrop + Remix CTA + meta. */
.showcase-hover {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: var(--space-2);
  background: linear-gradient(
    to bottom,
    rgba(0, 0, 0, 0.4) 0%,
    transparent 30%,
    transparent 50%,
    rgba(0, 0, 0, 0.85) 100%
  );
  opacity: 0;
  transition: opacity var(--duration-base) ease;
  pointer-events: none;
}
.showcase-tile:hover .showcase-hover { opacity: 1; }
.showcase-hover > * { pointer-events: auto; }

.showcase-hover-top {
  display: flex;
  justify-content: flex-end;
  align-items: flex-start;
}
.showcase-hover-bottom {
  display: flex;
  flex-direction: column;
  gap: 6px;
  align-items: stretch;
}
.showcase-author {
  color: rgba(255, 255, 255, 0.85);
  font-size: var(--text-xs);
  font-weight: 500;
  text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);
}
.showcase-author a {
  color: var(--cyan);
  text-decoration: none;
  font-weight: 700;
}
.showcase-author a:hover { text-decoration: underline; }
.showcase-author .model-tag {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 6px;
  background: rgba(83, 216, 255, 0.18);
  border: 1px solid rgba(83, 216, 255, 0.35);
  border-radius: var(--radius-pill);
  color: var(--cyan);
  font-weight: 600;
  font-size: 10px;
}
.showcase-author .model-tag.muted {
  background: rgba(255, 255, 255, 0.08);
  border-color: rgba(255, 255, 255, 0.15);
  color: var(--muted);
}
.showcase-remix-btn {
  display: block;
  text-align: center;
  background: var(--cyan);
  color: #050609;
  font-weight: 800;
  font-size: var(--text-base);
  padding: 9px 14px;
  border-radius: var(--radius-sm);
  text-decoration: none;
  transition: background var(--duration-fast), transform 0.08s;
  box-shadow: 0 4px 14px rgba(83, 216, 255, 0.35);
}
.showcase-remix-btn:hover { background: #7be2ff; }
.showcase-remix-btn:active { transform: scale(0.97); }
.showcase-delete {
  background: rgba(0, 0, 0, 0.6);
  color: rgba(248, 113, 113, 0.9);
  border: 1px solid rgba(248, 113, 113, 0.3);
  border-radius: 50%;
  width: 26px;
  height: 26px;
  font-size: var(--text-md);
  line-height: 1;
  cursor: pointer;
  padding: 0;
  backdrop-filter: blur(6px);
}
.showcase-delete:hover {
  background: rgba(248, 113, 113, 0.2);
  border-color: rgba(248, 113, 113, 0.7);
  color: #f87171;
}
.showcase-empty {
  text-align: center;
  padding: 80px 20px;
  color: var(--muted);
}
.showcase-footer {
  margin-top: 32px;
  padding: 18px;
  text-align: center;
  font-size: var(--text-xs);
}
.showcase-footer a { color: var(--text-soft); }

/* Touch devices have no :hover — show overlay + Remix CTA persistently
   so the user can browse before committing to a Remix.  Mirrored: stats
   stay visible too (no fade-out trigger). */
@media (hover: none) {
  .showcase-hover { opacity: 1; }
  .showcase-stats-overlay { opacity: 1; }
  .showcase-tile { cursor: default; }
}

/* Outline separates cyan Remix button from cyan-toned background images
   (anime/Illustrious palette has a lot of cyan hair / sky). */
.showcase-remix-btn {
  outline: 1px solid rgba(0, 0, 0, 0.5);
  outline-offset: -1px;
}
.showcase-remix-btn:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

/* Tile without a prompt — Remix is hidden, tile is inspiration-only.
   The "no prompt available" placeholder occupies the same slot as the
   Remix CTA so layout doesn't shift between gated/non-gated tiles. */
.showcase-tile.no-remix { cursor: default; }
.showcase-no-prompt {
  display: block;
  text-align: center;
  font-size: var(--text-xs);
  color: rgba(255, 255, 255, 0.55);
  background: rgba(0, 0, 0, 0.35);
  padding: 8px 12px;
  border-radius: var(--radius-sm);
  font-style: italic;
  letter-spacing: 0.02em;
}

/* Honour user's motion preference — disable transitions/transforms. */
@media (prefers-reduced-motion: reduce) {
  .showcase-hover,
  .showcase-stats-overlay,
  .showcase-remix-btn,
  .filter-pill { transition: none; }
  .showcase-remix-btn:active { transform: none; }
}

/* ===== Phase 6b — Upscale tab (multi-file batch panel) ===== */

.upscale-dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  min-height: 140px;
  padding: 28px 16px;
  margin-bottom: var(--space-4);
  border: 1.5px dashed var(--line-strong);
  border-radius: var(--radius-md);
  background: var(--bg-field);
  color: var(--muted);
  cursor: pointer;
  text-align: center;
  transition: border-color var(--duration-fast) ease, background var(--duration-fast) ease;
}
.upscale-dropzone:hover,
.upscale-dropzone:focus-visible {
  border-color: rgba(83, 216, 255, 0.45);
  background: rgba(83, 216, 255, 0.04);
  color: var(--text-soft);
  outline: none;
}
.upscale-dropzone.is-dragover {
  border-color: var(--cyan);
  background: rgba(83, 216, 255, 0.08);
  color: var(--text);
}
.upscale-dropzone-icon {
  font-size: var(--text-2xl);
  line-height: 1;
}
.upscale-dropzone-label {
  font-size: var(--text-base);
  font-weight: 700;
}
.upscale-dropzone-count {
  font-size: var(--text-sm);
  color: var(--muted-2);
}

.upscale-thumbs {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 10px;
  margin-bottom: 18px;
}
.upscale-thumbs:empty {
  display: none;
}

.upscale-thumb {
  position: relative;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-sm);
  overflow: hidden;
  background: #050609;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.06);
}
.upscale-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.upscale-thumb-placeholder {
  width: 100%;
  height: 100%;
  background: linear-gradient(135deg, #0a0b0f 0%, #14161c 100%);
}
.upscale-thumb-remove {
  position: absolute;
  top: 4px;
  right: 4px;
  width: 22px;
  height: 22px;
  padding: 0;
  border: 0;
  border-radius: var(--radius-xs);
  background: rgba(248, 92, 92, 0.85);
  color: #fff;
  font-size: var(--text-lg);
  line-height: 1;
  font-weight: 800;
  cursor: pointer;
  display: grid;
  place-items: center;
}
.upscale-thumb-remove:hover {
  background: rgba(248, 92, 92, 1);
}
.upscale-thumb-dims {
  position: absolute;
  right: 4px;
  bottom: 4px;
  font-size: 10px;
  font-weight: 700;
  padding: 2px 5px;
  border-radius: var(--radius-sm);
  background: rgba(2, 2, 4, 0.7);
  color: var(--text-soft);
}

.upscale-badge {
  position: absolute;
  left: 4px;
  bottom: 4px;
  padding: 2px 6px;
  border-radius: var(--radius-sm);
  font-size: 10px;
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.upscale-badge.is-included {
  background: rgba(88, 230, 154, 0.85);
  color: #052517;
}
.upscale-badge.is-excluded {
  background: rgba(248, 92, 92, 0.9);
  color: #fff;
}
.upscale-badge.is-uploading {
  background: rgba(83, 216, 255, 0.85);
  color: #022035;
}
.upscale-badge.is-failed {
  background: rgba(248, 92, 92, 0.9);
  color: #fff;
  cursor: help;
}

.upscale-config {
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 10px 12px 12px;
  margin: 0 0 12px;
  display: grid;
  gap: var(--space-2);
}
.upscale-config legend {
  padding: 0 6px;
  font-size: var(--text-xs);
  font-weight: 800;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}

.upscale-upscaler-row {
  display: flex;
  align-items: baseline;
  gap: 6px;
}
.upscaler-name {
  font-size: var(--text-md);
  font-weight: 800;
  color: var(--cyan);
}

.upscale-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
}
.upscale-pill {
  flex: 1 1 60px;
  min-height: 36px;
  padding: 0 12px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line-strong);
  background: var(--bg-field);
  color: var(--text-soft);
  font-weight: 700;
  cursor: pointer;
  transition: background var(--duration-fast) ease, border-color var(--duration-fast) ease, color var(--duration-fast) ease;
}
.upscale-pill:hover:not(:disabled) {
  border-color: rgba(83, 216, 255, 0.45);
  color: var(--text);
}
.upscale-pill.is-selected {
  background: rgba(83, 216, 255, 0.18);
  border-color: var(--cyan);
  color: var(--cyan);
}
.upscale-pill.is-greyed,
.upscale-pill:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.upscale-hint {
  margin: 4px 0 0;
}

/* 2026-05-11: video mode selector (Text/Image/Reference to Video).
   Three equal-width outlined buttons matching civitai.red's visual treatment.
   Replaces the prior auto-detect-from-tray heuristic that silently locked
   users out of ref2vid-with-1-image. */
.video-mode-selector {
  display: flex;
  gap: var(--space-2);
  margin: 0 0 16px 0;
}

/* 2026-05-23: per-(preset, tool) advisory banner above the image tray.
   Mirrors the provider's own yellow heads-up box (Seedance img2vid in
   particular).  Populated by app.js updateToolWarning() from
   preset.tool_warnings[currentMode]; hidden attribute toggles visibility
   so layout doesn't shift when not active. */
.form-warning {
  background: rgba(255, 190, 92, 0.10);
  border: 1px solid rgba(255, 190, 92, 0.35);
  border-radius: var(--radius-sm);
  padding: 12px 14px;
  margin: 0 0 14px 0;
}
.form-warning-title {
  display: block;
  color: var(--amber);
  font-weight: 700;
  font-size: var(--text-md);
  margin: 0 0 6px 0;
}
.form-warning-body {
  margin: 0;
  font-size: var(--text-base);
  color: var(--text-soft);
  line-height: 1.45;
}

/* 2026-05-11: Fast/Standard variant selector — same pill chrome as
   .video-mode-selector but only rendered for variant-group models
   (Veo 3, Seedance v2).  Sits between Model dropdown and the image tray. */
.video-variant-selector {
  display: flex;
  gap: var(--space-2);
  margin: 8px 0 12px 0;
}
.video-variant-selector[hidden] { display: none; }
.video-mode-btn {
  flex: 1 1 0;
  min-height: 44px;
  padding: 0 16px;
  border-radius: var(--radius-md);
  border: 1px solid var(--line-strong);
  background: var(--bg-field);
  color: var(--text-soft);
  font-weight: 600;
  font-size: var(--text-md);
  cursor: pointer;
  transition: background var(--duration-fast) ease, border-color var(--duration-fast) ease, color var(--duration-fast) ease;
}
.video-mode-btn:hover:not(:disabled) {
  border-color: rgba(83, 216, 255, 0.45);
  color: var(--text);
}
.video-mode-btn.is-selected {
  background: rgba(83, 216, 255, 0.12);
  border-color: var(--cyan);
  color: var(--cyan);
}
.video-mode-btn:disabled,
.video-mode-btn.is-disabled {
  opacity: 0.35;
  cursor: not-allowed;
}

/* ===== Phase 1.9 — seed × clear button ===== */
/* Visible affordance to drop the value back to "Random" — operator pointed
   out 2026-05-05 that not every user knows to clear the field manually. */
.seed-input-wrap {
  position: relative;
  display: block;
}
.seed-input-wrap input {
  width: 100%;
  padding-right: 28px;
}
.seed-clear-btn {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  width: 18px;
  height: 18px;
  padding: 0;
  border: 0;
  background: transparent;
  color: var(--text-muted);
  font-size: var(--text-lg);
  line-height: 1;
  cursor: pointer;
  border-radius: var(--radius-xs);
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0.55;
  transition: opacity var(--duration-fast) ease, background var(--duration-fast) ease, color var(--duration-fast) ease;
}
.seed-input-wrap input:placeholder-shown + .seed-clear-btn {
  visibility: hidden;  /* hide × when field is already empty */
}
.seed-clear-btn:hover {
  opacity: 1;
  background: rgba(255, 255, 255, 0.06);
  color: var(--text);
}

/* ===== Phase 1.9 v2 — video form control-aware layout =====

   Two flex rows handle every video preset:

   .video-row — Resolution + Duration
     Resolution gets a fixed 160px slot (just "720p"/"1080p"); Duration
     grows to consume the rest so the slider is never pinched.  When
     Resolution is hidden (Veo / Kling native-only) Duration occupies
     the full row.  align-items: flex-end so the dropdown's bottom
     lines up with the slider's bottom even though the slider has its
     own label above the controls.

   .video-controls-row — Seed + selects + checkboxes
     Each control has a CAPPED width so short content (Mode = "professional",
     Seed = 6 digits, Version = "3.1") doesn't stretch into a long empty
     stripe.  Hidden ones collapse via display:none on the child label.
     Wraps to multiple lines on narrow viewports; baselines align via
     flex-end so checkboxes (no label-above) sit at the same bottom edge
     as label-above selects/inputs. */

.video-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 12px 18px;
  margin-bottom: 14px;
}
.video-row > .vc-resolution {
  flex: 0 0 auto;
  width: 160px;
  margin: 0;
}
.video-row > .vc-duration {
  flex: 1 1 240px;
  min-width: 240px;
  margin: 0;
}
.video-row > .vc-resolution > select {
  width: 100%;
}

/* Reused for both image and video forms.  Image: Resolution / Quality /
   Quantity / Seed / Web search.  Video: Seed / Version / Mode / toggles. */
.form-controls-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  gap: 12px 18px;
  margin-bottom: 14px;
}
.form-controls-row > label {
  margin: 0;
}
.form-controls-row > .compact-input {
  flex: 0 0 auto;
  width: 200px;
}
.form-controls-row > .compact-input.compact-narrow {
  width: 100px;
}
.form-controls-row > .compact-select {
  flex: 0 0 auto;
  width: 180px;
}
.form-controls-row > .compact-input .seed-input-wrap,
.form-controls-row > .compact-input input,
.form-controls-row > .compact-select select {
  width: 100%;
}
.form-controls-row > .compact-checkbox {
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  /* Bottom-align with the bottom edge of the adjacent select control,
     not its top label.  The selects/inputs have ~22px label above and
     ~36px control below; sit the checkbox at the control's vertical
     centre by adding bottom padding equal to half the control height. */
  padding-bottom: 9px;
}

/* ===== Phase 8c — Civitai-style sliders + aspect tiles ===== */

.slider-field {
  display: block;
  margin-bottom: 18px;
}
.slider-label-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  margin-bottom: 10px;
  color: var(--text-soft);
  font-size: var(--text-base);
  font-weight: 700;
}
.slider-controls {
  display: flex;
  align-items: center;
  gap: 14px;
}
.slider-controls .slider-range { flex: 1; min-width: 0; }
.slider-number {
  width: 64px;
  flex-shrink: 0;
  padding: 4px 8px;
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-weight: 700;
  font-size: var(--text-base);
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--line);
  border-radius: var(--radius-sm);
}
input[type="range"].slider-range {
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 6px;
  margin: 0;
  padding: 0;
  border: 0;
  border-radius: var(--radius-pill);
  background: linear-gradient(
    to right,
    var(--cyan) 0%,
    var(--cyan) var(--slider-fill, 50%),
    rgba(255, 255, 255, 0.12) var(--slider-fill, 50%),
    rgba(255, 255, 255, 0.12) 100%
  );
  outline: none;
  cursor: pointer;
  box-shadow: none;
}
.slider-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 16px;
  height: 16px;
  border-radius: var(--radius-pill);
  background: #ffffff;
  border: 2px solid #050609;
  cursor: grab;
  /* Centre vertically on the 6px track.  Visible thumb height including
     the 2px border = 16 + 2*2 = 20px (WebKit slider-thumb uses content-
     box regardless of the global border-box reset).  So:
        margin-top = -((20 - 6) / 2) = -7px
     The previous -5px (= -((16-6)/2), forgot the border) left the thumb
     visibly above centre by 2px — verified in side-by-side preview. */
  margin-top: -7px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
.slider-range::-webkit-slider-thumb:active { cursor: grabbing; }
.slider-range::-moz-range-thumb {
  width: 16px;
  height: 16px;
  border-radius: var(--radius-pill);
  background: #ffffff;
  border: 2px solid #050609;
  cursor: grab;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
}
.slider-range::-moz-range-track {
  background: transparent;
  height: 4px;
}

/* Quick-pick chips on slider's label row (Creative/Balanced/Precise etc). */
.quick-picks {
  display: inline-flex;
  gap: var(--space-1);
}
.quick-pick {
  appearance: none;
  border: 0;
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-soft);
  font-family: inherit;
  font-size: var(--text-xs);
  font-weight: 700;
  padding: 4px 9px;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background var(--duration-fast) ease, color var(--duration-fast) ease;
}
.quick-pick:hover {
  background: rgba(255, 255, 255, 0.10);
  color: var(--text);
}
.quick-pick.is-selected {
  background: var(--cyan);
  color: #020204;
}

/* Sampler quick-pick pills (Fast / Popular).  Same look as quick-picks. */
#sampler-pills {
  display: inline-flex;
  gap: var(--space-1);
}
#sampler-pills .upscale-pill {
  flex: 0 0 auto;
  min-height: 0;
  padding: 4px 9px;
  font-size: var(--text-xs);
  font-weight: 700;
  background: rgba(255, 255, 255, 0.06);
  border: 0;
  border-radius: var(--radius-sm);
  color: var(--text-soft);
}
#sampler-pills .upscale-pill:hover:not(:disabled) {
  background: rgba(255, 255, 255, 0.10);
  border: 0;
  color: var(--text);
}
#sampler-pills .upscale-pill.is-selected {
  background: var(--cyan);
  color: #020204;
  border: 0;
}

/* Aspect ratio tiles — equal-width compact buttons.  auto-fit lets 3 / 4 / 5
   options all share one row without overflow; tile height stays minimal so
   the form doesn't grow when more aspect choices are added. */
.aspect-tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
  gap: var(--space-1);
  margin-top: var(--space-1);
}
.aspect-tile {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 5px;
  padding: 10px 4px 8px;
  min-height: 78px;
  border-radius: var(--radius-sm);
  border: 1px solid transparent;
  background: rgba(255, 255, 255, 0.04);
  color: var(--text-soft);
  cursor: pointer;
  transition: background var(--duration-fast) ease, border-color var(--duration-fast) ease, color var(--duration-fast) ease;
  font-family: inherit;
}
.aspect-tile:hover {
  background: rgba(255, 255, 255, 0.08);
  color: var(--text);
}
.aspect-tile.is-selected {
  background: rgba(255, 255, 255, 0.10);
  border-color: rgba(255, 255, 255, 0.2);
  color: var(--text);
}
.aspect-tile-shape {
  display: block;
  width: calc(22px * var(--aspect-w) / max(var(--aspect-w), var(--aspect-h)));
  height: calc(22px * var(--aspect-h) / max(var(--aspect-w), var(--aspect-h)));
  border: 1.5px solid currentColor;
  border-radius: var(--radius-sm);
  margin-bottom: 1px;
}
.aspect-tile-ratio {
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: 0.02em;
}
.aspect-tile-dims {
  font-size: 10px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* "Advanced" details panel — make it visually contained like Civitai's */
details[data-field="advanced"] {
  margin-top: var(--space-2);
  padding-top: 14px;
  border-top: 1px solid var(--line);
}
details[data-field="advanced"] > summary {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  font-size: 15px;
  font-weight: 700;
  cursor: pointer;
  list-style: none;
  margin-bottom: 14px;
}
details[data-field="advanced"] > summary::-webkit-details-marker { display: none; }
details[data-field="advanced"] > summary::before {
  content: "";
  display: inline-block;
  width: 8px;
  height: 8px;
  border-right: 2px solid currentColor;
  border-bottom: 2px solid currentColor;
  /* Closed: chevron points right (▶); rotated to point down when open. */
  transform: rotate(-45deg);
  transition: transform var(--duration-base) ease;
  margin-right: 2px;
}
details[data-field="advanced"][open] > summary::before {
  transform: rotate(45deg);
}
details[data-field="advanced"][open] > summary { margin-bottom: 18px; }

/* ============================================================
   Pricing & billing pages — shared chrome
   Both pricing.html and billing.html use <body class="billing-page">.
   Extracted from inline <style> blocks; all colors via tokens.
   ============================================================ */

.billing-page {
  min-height: 100vh;
  min-height: 100dvh;
  background: var(--bg);
  color: var(--text);
  padding: 32px 16px;
}

.billing-shell {
  max-width: 900px;
  margin: 0 auto;
}

/* The Subscription tab packs 4 bundle cards into one row; at the 900px cabinet
   default that squeezes each to ~210px.  Widen the shell ONLY when it holds the
   tier grids (Account / Referrals have none → stay at 900).  :has() degrades
   gracefully — unsupported browsers keep the 900px layout, no breakage. */
@media (min-width: 1040px) {
  .billing-shell:has(.tier-grid) {
    max-width: 1160px;
  }
}

.billing-header {
  text-align: center;
  margin-bottom: 40px;
}

.billing-header h1 {
  font-size: 28px;
  font-weight: 800;
  margin: 0 0 8px;
  line-height: 1.2;
  color: var(--text);
}

.billing-header p {
  margin: 0;
  color: var(--muted);
  font-size: 15px;
}

/* flex+wrap+justify-center: 5-tier menu lays out 3+2 with row 2 centred. */
.tier-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-5);
  margin-bottom: 40px;
}

.tier-card {
  /* Phase 2 wrap-fix: was `flex: 0 1 240px; max-width: 280px;` which froze
     each card at 240 px and forced ugly mid-phrase line-breaks ("Unlimited
     generations for / 24 hours").  Allow grow + bump max-width so 3-up
     rows fill the 900px container cleanly (~286px each) and 5-up rows
     use 2nd-row centring.  Combined with text-wrap: pretty on <li> the
     remaining wraps land at natural word boundaries. */
  flex: 1 1 240px;
  max-width: 320px;
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: 28px 24px;
  display: flex;
  flex-direction: column;
  gap: var(--space-4);
  transition: border-color var(--duration-base);
}

.tier-card.current {
  border-color: var(--cyan);
  box-shadow: 0 0 0 1px var(--cyan);
}

.tier-card .tier-name {
  font-size: 19px;
  font-weight: 700;
  color: var(--text);
}

.tier-card .tier-price {
  font-size: var(--text-3xl);
  font-weight: 800;
  color: var(--cyan);
  line-height: 1.1;
}

.tier-card .tier-price span {
  font-size: var(--text-lg);
  font-weight: 400;
  color: var(--muted);
}

/* Founding-rate lock-in line (2026-05-31) — sits between .tier-price and
   .tier-features on the commitment tiers (month/year) that have a dated
   future increase.  Deliberately calm per the brand: small, muted, single
   cyan lock glyph; only the future price is struck (never the headline);
   the date rides --text-soft, not a screaming amber countdown. */
.tier-card .tier-pricelock {
  display: flex;
  align-items: flex-start;
  gap: var(--space-2);
  margin-top: calc(-1 * var(--space-2));   /* hug the price above */
  font-size: var(--text-sm);
  line-height: 1.45;
  color: var(--muted);
}
.tier-card .tier-pricelock .lock {
  width: 13px;
  height: 13px;
  color: var(--cyan);
  flex-shrink: 0;
  margin-top: 1px;
}
.tier-card .tier-pricelock .future {
  text-decoration: line-through;
  color: var(--muted-2);
}
.tier-card .tier-pricelock .until {
  color: var(--text-soft);
  font-weight: 600;
}

/* Credit allotment headline (2026-06-09 credit pivot) — the product IS credits
   now, declared up front: sits between .tier-price and .tier-features.  Bold
   allotment, muted "≈ N images / M videos" basket below it. */
.tier-card .tier-credits {
  font-size: var(--text-lg);
  font-weight: 700;
  color: var(--text);
  line-height: 1.25;
}
.tier-card .tier-credits .basket {
  display: block;
  font-size: var(--text-sm);
  font-weight: 400;
  color: var(--muted);
  margin-top: 2px;
}

.tier-card .tier-features {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

/* Per-render price hint — calm, muted, no checkmark; the honest "what a render
   costs" footer of the feature list. */
.tier-card .tier-features li.hint {
  color: var(--muted-2);
  font-size: var(--text-sm);
  margin-top: 2px;
}
.tier-card .tier-features li.hint::before {
  content: "";
}

.tier-card .tier-features li {
  font-size: var(--text-md);
  color: var(--text-soft);
  line-height: 1.5;
  /* text-wrap: pretty (Chrome/Edge 117+, Firefox 121+, Safari 17.5+):
     balances last-line wraps so phrases like "$2.71/day · 46% off"
     stay together instead of breaking mid-token. */
  text-wrap: pretty;
}

.tier-card .tier-features li::before {
  content: "✓ ";
  color: var(--green);
  font-weight: 700;
}

/* Savings proof line on founding-rate tiers — the dollar figure pops in the
   house "positive = green" idiom; the rest stays primary text. */
.tier-card .tier-features li.save {
  color: var(--text);
}
.tier-card .tier-features li.save b {
  color: var(--green);
  font-weight: 700;
}

.tier-card .current-badge {
  display: inline-block;
  background: var(--cyan);
  color: #020204;
  font-size: var(--text-xs);
  font-weight: 700;
  padding: 3px 10px;
  border-radius: var(--radius-sm);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  white-space: nowrap;
  flex-shrink: 0;
  /* 2026-05-23: removed `margin-top: var(--space-1); align-self: flex-start;` from
     pre-Phase-9 stacked layout — the badge now sits inline at the end
     of the .tier-card-header flex row, centred with the tier-name. */
}

.cta-btn,
.subscribe-btn {
  margin-top: auto;
  display: block;
  width: 100%;
  background: var(--cyan);
  border: 0;
  color: #020204;
  padding: 11px 20px;
  border-radius: var(--radius-md);
  font-size: var(--text-md);
  font-weight: 700;
  cursor: pointer;
  text-align: center;
  text-decoration: none;
  transition: background var(--duration-base);
}

.cta-btn:hover,
.subscribe-btn:hover {
  background: var(--cyan-hover);
}

/* ──────────────────────────────────────────────────────────────────────
   Phase 9 (2026-05-23) two-group layout overrides.

   The shared .tier-grid rule above is flex+wrap with max-width:320px per
   card, tuned for the 5-tier 3+2 wrap.  With 4 cards in the time group,
   4×320 + 3×20 = 1340px which overflows the ~1100px container, so they
   wrap into a 3+1 orphan (Year pass alone on row 2 — caught in operator
   screenshot 2026-05-23).

   Switch both groups to CSS grid with explicit column counts so the
   layout is deterministic — 4-up for time at desktop, 2-up for capacity
   centred.  Wraps at narrower viewports without orphans.
   ────────────────────────────────────────────────────────────────────── */
.tier-grid-time {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--space-5);
  margin-bottom: 0;
  /* Equal-height cards.  Content is balanced (a reserved price-note slot on
     EVERY card — founding rate on Standard/Bulk, a muted 'price held' note on
     Trial/Starter — plus equal feature counts) so equal height leaves no dead
     gap.  See the .tier-pricelock-none else-branch in billing/pricing.html. */
}
.tier-grid-capacity {
  display: grid;
  /* Capacity cards centred at ~the same width as time cards so the
     two groups read as a coherent set, not as different products.
     KEEP THE COUNT IN SYNC with the capacity tiers (Pro/Studio/Max = 3).
     Was repeat(2,…) from when there were only Pro+Studio → adding Max
     Month orphaned it alone onto a second row. */
  grid-template-columns: repeat(3, minmax(0, 280px));
  gap: var(--space-5);
  justify-content: center;
  margin-bottom: 40px;
}
.tier-grid-time .tier-card,
.tier-grid-capacity .tier-card {
  /* Unlock the flex-tuned max-width:320 from .tier-card above — grid
     column track is the authoritative width now. */
  max-width: none;
}
@media (max-width: 880px) {
  .tier-grid-time {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .tier-grid-capacity {
    /* 3 cards can't make an even 2-up row — stack to one centred column
       below the desktop 3-up width (no orphan, no cramping). */
    grid-template-columns: minmax(0, 340px);
  }
}
@media (max-width: 520px) {
  .tier-grid-time,
  .tier-grid-capacity {
    grid-template-columns: minmax(0, 1fr);
  }
}

/* Credit activity (2026-06-16): the user's own ledger on /billing.  The
   markup used to borrow .admin-table — an .admin-page-scoped class — so out
   here it rendered as a bare default <table>.  Give it its own card + a
   readable density that matches the tier cards above. */
.bill-credit-activity {
  max-width: 720px;
  margin: 0 auto 40px;
  overflow-x: auto;            /* never break the page on a very narrow phone */
}
.credit-activity-table {
  width: 100%;
  border-collapse: collapse;
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  overflow: hidden;
  font-size: var(--text-sm);
}
.credit-activity-table th,
.credit-activity-table td {
  padding: 10px 16px;
}
.credit-activity-table thead th {
  text-align: left;
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
  background: rgba(255, 255, 255, 0.02);
  border-bottom: 1px solid var(--line);
}
.credit-activity-table tbody td {
  border-bottom: 1px solid var(--line);
  color: var(--text-soft);
}
.credit-activity-table tbody tr:last-child td {
  border-bottom: 0;
}
.credit-activity-table tbody tr:hover td {
  background: rgba(255, 255, 255, 0.025);
}
.credit-activity-table .ca-num {
  text-align: right;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.credit-activity-table .ca-when,
.credit-activity-table .ca-meta {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.credit-activity-table .ca-delta {
  font-weight: 600;
}
.credit-activity-table .ca-pos {
  color: var(--green);
}
.credit-activity-table .ca-neg {
  color: var(--text);
}

/* Tier-group divider (Phase 9, 2026-05-23): semantic separator between
   time-based tiers (top grid) and capacity tiers (bottom grid).  Hairline
   centre + small caption.  Mirrors the FAQ category-section divider
   aesthetic but lighter (smaller text, no border-top/bottom on section). */
.tier-group-divider {
  display: flex;
  align-items: center;
  gap: var(--space-4);
  margin: 36px 0 28px;
  color: var(--muted);
  font-size: var(--text-sm);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
.tier-group-divider::before,
.tier-group-divider::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--line);
}

/* Featured tier (Year pass) — cyan glow halo, slightly elevated.  Same
   token system as the rest; just emphasises one card visually. */
.tier-card-featured {
  border-color: var(--cyan);
  box-shadow: 0 0 0 1px var(--cyan), 0 12px 40px rgba(83, 216, 255, 0.16);
}

/* Tier-card header (Phase 9, 2026-05-23 — third iteration).
   Earlier attempts:
   (1) badge BELOW name pushed prices down on badged cards only.
   (2) badge on SAME row via flex space-between worked at wide viewports
       but at the actual .billing-shell width (max 900px) the 4-col
       layout gives only ~162px content area per card.  "Month pass" +
       "Most popular" = ~183px → flex-wrap kicked in → badge wrapped
       below name → same misalignment as (1).
   (3) THIS approach: vertical stack with a fixed-height `.tier-badge-slot`
       above the name.  Reserves ~22px on EVERY card — badged cards fill
       it, unbadged cards leave it empty.  Names + prices align across
       all cards regardless of viewport.  This is the Stripe / Linear /
       Vercel pricing pattern. */
.tier-card-header {
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.tier-badge-slot {
  /* Fixed height matches `.tier-badge` (3+10+3 px-y + 10px font + ~6px
     line-height ≈ 22px).  Reserves the slot even when empty so that
     `.tier-name` always starts at the same vertical position across
     cards in the grid, guaranteeing aligned prices below. */
  min-height: 22px;
  display: flex;
  align-items: center;
}

/* Badge — small uppercase chip, sits in the dedicated `.tier-badge-slot`
   above the tier name.  Pixel-aligned with landing/styles.css .tier-badge:
   typography (10px, 0.08em) is shared; default color semantic in the app
   is "strong CTA" (solid cyan).  Both flagship badges ("Most popular" +
   "Best value") render STRONG on BOTH surfaces (unified 2026-06-16);
   `.tier-badge-muted` is retained for any future soft variant. */
.tier-badge {
  display: inline-block;
  padding: 3px 10px;
  border-radius: var(--radius-pill);
  font-size: 10px;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #020204;
  background: var(--cyan);
  white-space: nowrap;
  flex-shrink: 0;
}
.tier-badge-muted {
  color: var(--cyan);
  background: rgba(83, 216, 255, 0.12);
}

.subscribe-btn:disabled {
  background: var(--line-strong);
  color: var(--muted);
  cursor: not-allowed;
}

.payments-section {
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  margin-bottom: var(--space-7);
}

.payments-section h2 {
  font-size: var(--text-lg);
  font-weight: 700;
  margin: 0 0 12px;
  color: var(--text);
}

.payments-section p {
  font-size: var(--text-md);
  color: var(--text-soft);
  margin: 5px 0;
}

.payments-section .coming-soon {
  color: var(--muted);
  font-size: var(--text-base);
}

.billing-footer {
  text-align: center;
  color: var(--muted-2);
  font-size: var(--text-base);
  margin-top: var(--space-4);
}

.billing-footer a {
  color: var(--muted);
  text-decoration: none;
  transition: color var(--duration-base);
}

.billing-footer a:hover {
  color: var(--cyan);
}

/* Billing page topbar — non-sticky variant (default .topbar is sticky and
   z-index 10; billing wants relative so the modal-overlay sits cleanly
   above without weird stacking context interactions). */
.billing-topbar {
  position: relative;
  z-index: 1;
}

.topbar-email {
  font-size: var(--text-base);
  color: var(--muted);
}

/* ============================================================
   Billing modal — invoice payment dialog
   Shared with admin's TOTP modal via .modal-overlay only.
   ============================================================ */

.modal-overlay {
  display: none;
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.75);
  z-index: 1000;
  align-items: flex-start;
  justify-content: center;
  overflow-y: auto;
  padding: 16px 0;
}

.modal-overlay.open {
  display: flex;
}

@media (min-width: 580px) {
  .modal-overlay {
    align-items: center;
    padding: 0;
  }
}

.modal-box {
  background: var(--bg-card);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-lg);
  padding: var(--space-7);
  max-width: 500px;
  width: 100%;
  margin: 16px;
}

.modal-title {
  font-size: 19px;
  font-weight: 800;
  margin-bottom: var(--space-5);
  color: var(--text);
}

/* 2026-05-30: CRITICAL per-user notice — blocking must-ack modal.
   Reuses .modal-overlay / .modal-box; red accent + readable body. */
.notice-modal-box {
  border-color: var(--danger, #e5484d);
  border-width: 2px;
}
.notice-modal-title {
  font-size: 18px;
  font-weight: 800;
  margin: 0 0 var(--space-4);
  color: var(--danger, #e5484d);
}
.notice-modal-body {
  color: var(--text);
  font-size: 15px;
  line-height: 1.5;
  margin-bottom: var(--space-5);
  white-space: normal;
  word-break: break-word;
}

.modal-row {
  margin-bottom: var(--space-4);
}

.modal-label {
  font-size: var(--text-base);
  color: var(--muted);
  margin-bottom: 5px;
}

.modal-value {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--text-sm);
  letter-spacing: -0.01em;
  word-break: break-all;
  background: var(--bg-field);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  padding: 9px 12px;
  color: var(--text);
  line-height: 1.4;
}

.dashboard-link-row {
  display: flex;
  justify-content: center;
  margin: 24px 0 8px;
}

.dashboard-link {
  display: inline-block;
  padding: 10px 24px;
  border: 1px solid var(--cyan);
  border-radius: var(--radius-md);
  color: var(--cyan);
  text-decoration: none;
  font-weight: 700;
  font-size: var(--text-md);
  transition: background var(--duration-base), color var(--duration-base);
}

.dashboard-link:hover {
  background: var(--cyan);
  color: #020204;
}

.copy-row {
  display: flex;
  gap: var(--space-2);
  align-items: stretch;
}

.copy-row .modal-value {
  flex: 1;
}

.copy-btn {
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--line-strong);
  color: var(--text);
  padding: 6px 12px;
  border-radius: var(--radius-md);
  cursor: pointer;
  font-size: var(--text-base);
  white-space: nowrap;
  transition: background var(--duration-fast);
}

.copy-btn:hover {
  background: rgba(255, 255, 255, 0.1);
}

.copy-btn.copied {
  background: rgba(88, 230, 154, 0.15);
  border-color: rgba(88, 230, 154, 0.4);
  color: var(--green);
}

.modal-amount-big {
  font-size: var(--text-3xl);
  font-weight: 800;
  color: var(--cyan);
  text-align: center;
  margin: 16px 0;
}

.modal-status {
  text-align: center;
  padding: var(--space-3);
  border-radius: var(--radius-md);
  font-size: var(--text-md);
  font-weight: 700;
  margin-bottom: var(--space-4);
}

.modal-status.waiting {
  background: rgba(83, 216, 255, 0.08);
  color: var(--cyan);
}

.modal-status.detected,
.modal-status.confirmed {
  background: rgba(88, 230, 154, 0.1);
  color: var(--green);
}

.modal-status.expired {
  background: rgba(255, 93, 115, 0.1);
  color: var(--red);
}

.modal-status.underpaid {
  background: rgba(255, 190, 92, 0.1);
  color: var(--amber);
}

.modal-countdown {
  text-align: center;
  color: var(--muted);
  font-size: var(--text-base);
  margin-bottom: var(--space-4);
}

.modal-countdown span {
  color: var(--amber);
  font-weight: 700;
}

.modal-note {
  font-size: var(--text-base);
  color: var(--muted);
  text-align: center;
  line-height: 1.5;
}

#invoiceModal .modal-note-underpaid { display: none; }
#invoiceModal[data-status="underpaid"] .modal-note-default { display: none; }
#invoiceModal[data-status="underpaid"] .modal-note-underpaid { display: block; }

/* Invoice modal: structured "accepted (token, network)" block.
   Visual hierarchy from top: label → token rows → amber warning → tiny
   footnote.  Replaces a previous wall-of-text paragraph (2026-05-16). */
.pay-accepted {
  text-align: left;
  margin: 4px 0 12px 0;
}
.pay-accepted-label {
  font-size: var(--text-base);
  color: var(--muted);
  margin-bottom: var(--space-2);
  text-align: center;
}
.pay-accepted-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.pay-accepted-list li {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 8px 12px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: var(--radius-md);
  font-size: var(--text-base);
}
.pay-token {
  font-weight: 700;
  color: var(--text, #fff);
  flex: 0 0 auto;
  min-width: 44px;
}
.pay-sep {
  color: var(--muted);
  flex: 0 0 auto;
  font-size: var(--text-sm);
  text-transform: lowercase;
}
.pay-chains {
  color: var(--text, #fff);
  flex: 1 1 auto;
}

.pay-warning {
  display: flex;
  gap: 10px;
  margin: 14px 0 12px 0;
  padding: 10px 12px;
  background: rgba(255, 190, 92, 0.08);
  border: 1px solid rgba(255, 190, 92, 0.25);
  border-radius: var(--radius-md);
  text-align: left;
}
.pay-warning-icon {
  flex: 0 0 18px;
  font-size: 15px;
  line-height: 1.45;
  color: var(--amber);
}
.pay-warning-text {
  font-size: var(--text-base);
  line-height: 1.5;
  color: var(--text, #fff);
}
.pay-warning-text strong {
  color: var(--amber);
  font-weight: 700;
}
.pay-warning-text a {
  color: inherit;
  text-decoration: underline;
}

.pay-footnote {
  font-size: var(--text-sm);
  color: var(--muted);
  text-align: center;
  margin-top: var(--space-2);
}

.modal-close-btn {
  width: 100%;
  background: rgba(255, 255, 255, 0.06);
  border: 1px solid var(--line-strong);
  color: var(--text);
  padding: 10px;
  border-radius: var(--radius-md);
  cursor: pointer;
  font-size: var(--text-md);
  margin-top: var(--space-4);
  transition: background var(--duration-fast);
}

.modal-close-btn:hover {
  background: rgba(255, 255, 255, 0.1);
}

.spinner {
  display: inline-block;
  width: 14px;
  height: 14px;
  border: 2px solid var(--cyan);
  border-top-color: transparent;
  border-radius: var(--radius-pill);
  animation: spin 0.7s linear infinite;
  vertical-align: middle;
  margin-right: 6px;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

/* D5 (2026-06-01): inline spinner inside a submit button.  Uses currentColor
   so it stays visible on the cyan primary fill (dark text) and on dark
   secondary buttons (light text) alike. */
.btn-spinner {
  display: inline-block;
  width: 13px;
  height: 13px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: var(--radius-pill);
  animation: spin 0.7s linear infinite;
  vertical-align: -2px;
  margin-right: 7px;
  opacity: 0.85;
}

/* D5: infinite-scroll tail loader — centred spinner shown only while the
   next page is being fetched (toggled by loadJobs' page-in branch). */
.jobs-loading {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 16px 0 6px;
}

.jobs-loading[hidden] {
  display: none;
}

.jobs-loading .spinner {
  width: 18px;
  height: 18px;
  margin-right: 0;
}

/* D3 (2026-06-01): capability pill next to the Model label.  Only the
   "SFW only" variant exists today — flags big-tech models that refuse adult
   output.  Amber = caution, not error (an adult prompt here is account-safe,
   just wasted). */
.model-badge {
  display: inline-block;
  margin-left: 6px;
  padding: 1px 7px;
  font-size: 0.7rem;
  font-weight: 600;
  line-height: 1.5;
  letter-spacing: 0.02em;
  border-radius: var(--radius-pill);
  vertical-align: middle;
  white-space: nowrap;
}

.model-badge[hidden] {
  display: none;
}

.model-badge--sfw {
  color: var(--amber);
  background: rgba(255, 190, 92, 0.12);
  border: 1px solid rgba(255, 190, 92, 0.35);
}

/* ============================================================
   Admin page — only on <body class="admin-page">
   Wider layout, table-heavy. Buttons & badges scoped to .admin-page
   so future pages with .btn can have their own definition.
   ============================================================ */

.admin-page {
  background: var(--bg);
  color: var(--text);
}

.admin-shell {
  max-width: 1200px;
  margin: 0 auto;
  padding: 24px 16px 48px;
}

.admin-shell h2 {
  font-size: 20px;
  font-weight: 700;
  margin: 32px 0 12px;
  color: var(--cyan);
}

.admin-shell h3 {
  font-size: var(--text-lg);
  font-weight: 700;
  margin: 24px 0 8px;
  color: var(--muted);
}

.admin-shell table {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-md);
}

.admin-shell th {
  text-align: left;
  padding: 7px 10px;
  color: var(--muted);
  border-bottom: 1px solid var(--line-strong);
  font-weight: 700;
  white-space: nowrap;
}

.admin-shell td {
  padding: 7px 10px;
  border-bottom: 1px solid var(--line);
  vertical-align: middle;
}

.admin-shell tr:hover td {
  background: rgba(255, 255, 255, 0.03);
}

/* Phase 12 — referrer CRUD form layout.  Stacked labels, capped width,
   right-aligned submit.  Mirrors the dense feel of .admin-table without
   stretching inputs to 1200px on wide displays. */
.stacked-form {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  max-width: 560px;
  margin: 12px 0 24px;
}
.stacked-form label {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  font-size: var(--text-base);
  color: var(--muted);
}
.stacked-form input[type="text"],
.stacked-form input[type="email"],
.stacked-form input[type="number"],
.stacked-form select,
.stacked-form textarea {
  width: 100%;
  padding: 7px 10px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-family: inherit;
  font-size: var(--text-md);
}
.stacked-form textarea { min-height: 64px; resize: vertical; }
.stacked-form button[type="submit"] { align-self: flex-start; }

/* Referrer detail — URL block with side-by-side code + copy button. */
.url-row {
  display: flex;
  gap: var(--space-2);
  align-items: stretch;
  margin-bottom: var(--space-3);
}
.url-row code {
  flex: 1;
  word-break: break-all;
  padding: 8px 10px;
  background: #0c0c10;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  line-height: 1.4;
}
.url-row .copy-btn { align-self: stretch; }

/* Status chip on referrer detail header. */
.status-chip {
  display: inline-block;
  padding: 2px 10px;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: 600;
  margin-left: var(--space-2);
  vertical-align: middle;
}
.status-chip.active  { background: rgba(88, 230, 154, 0.15); color: var(--green); }
.status-chip.paused  { background: rgba(230, 196, 88, 0.15); color: #e6c458; }
.status-chip.banned  { background: rgba(230, 88, 88, 0.18);  color: #e65858; }

.inline-form {
  display: inline-flex;
  gap: 6px;
  align-items: center;
}
.tx-hash-input {
  width: 22em;
  padding: 4px 8px;
  font-family: var(--font-mono, monospace);
  font-size: var(--text-sm);
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  color: var(--text);
}

.admin-page .badge {
  display: inline-block;
  padding: 2px 8px;
  border-radius: var(--radius-sm);
  font-size: var(--text-sm);
  font-weight: 700;
}

.admin-page .badge-admin {
  background: var(--cyan);
  color: #020204;
}

.admin-page .badge-user {
  background: rgba(255, 255, 255, 0.06);
  color: var(--text-soft);
}

.admin-page .badge-active {
  background: rgba(88, 230, 154, 0.15);
  color: var(--green);
}

.admin-page .badge-inactive,
.admin-page .badge-disabled {
  background: rgba(255, 93, 115, 0.15);
  color: var(--red);
}

.admin-page .badge-tier {
  background: var(--cyan-soft);
  color: var(--cyan);
}

/* 2026-05-15: smoke-probe matrix (Phase A1) — per-cell red/yellow/green pill +
 * row tint.  Keeps the dense .admin-table density but lets the operator
 * eyeball a 30-row matrix and spot reds at a glance. */
.admin-page .status-pill {
  display: inline-block;
  padding: 2px 8px;
  border-radius: var(--radius-pill);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}
.admin-page .status-green {
  background: rgba(88, 230, 154, 0.15);
  color: var(--green);
}
.admin-page .status-yellow {
  background: rgba(255, 190, 92, 0.15);
  color: var(--amber);
}
.admin-page .status-red {
  background: rgba(255, 93, 115, 0.15);
  color: var(--red);
}
/* 2026-05-15 Phase A2: "no_data" cells (lifetime widget — <5 attempts in
 * 7d means stats aren't meaningful yet).  Subdued gray so reds + yellows
 * still pop.  Same visual reused for smoke matrix "stale" pill (probe
 * data > 36h old → no health claim).  Both = "data is not actionable",
 * same operator-mental-model, same render. */
.admin-page .status-no_data,
.admin-page .status-stale {
  background: rgba(255, 255, 255, 0.04);
  color: var(--muted);
}
.admin-page .smoke-row.smoke-red {
  background: rgba(255, 93, 115, 0.06);
}
.admin-page .smoke-row.smoke-yellow {
  background: rgba(255, 190, 92, 0.05);
}
.admin-page .smoke-row.smoke-no_data,
.admin-page .smoke-row.smoke-stale {
  /* no row tint — same neutral treatment for no_data and stale rows so
   * they don't compete visually with real red/yellow signal */
}

.admin-page .btn {
  display: inline-block;
  padding: 5px 11px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--line-strong);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  cursor: pointer;
  font-size: var(--text-base);
  font-weight: 600;
  white-space: nowrap;
  text-decoration: none;
  transition: background var(--duration-fast);
}

.admin-page .btn:hover {
  background: rgba(255, 255, 255, 0.08);
}

.admin-page .btn-danger {
  border-color: rgba(255, 93, 115, 0.4);
  background: rgba(255, 93, 115, 0.08);
  color: var(--red);
}

.admin-page .btn-danger:hover {
  background: rgba(255, 93, 115, 0.16);
}

.admin-page .btn-safe {
  border-color: rgba(88, 230, 154, 0.4);
  background: rgba(88, 230, 154, 0.08);
  color: var(--green);
}

.admin-page .btn-safe:hover {
  background: rgba(88, 230, 154, 0.16);
}

.admin-page .btn-accent {
  border-color: rgba(83, 216, 255, 0.4);
  background: var(--cyan-soft);
  color: var(--cyan);
}

.admin-page .btn-accent:hover {
  background: rgba(83, 216, 255, 0.2);
}

.admin-page .actions-cell {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  align-items: center;
}

/* 2026-05-15: <details>-based per-user action menu.  Collapsed by
 * default → 1 row per user.  When opened, actions-cell renders below
 * the summary button using existing flex-wrap rules. */
.admin-page .user-actions > summary {
  cursor: pointer;
  list-style: none;          /* hide WebKit default disclosure marker */
  display: inline-block;
}
.admin-page .user-actions > summary::-webkit-details-marker {
  display: none;             /* hide WebKit/Safari triangle */
}
.admin-page .user-actions[open] > summary {
  margin-bottom: 6px;        /* small spacer between Manage and expanded panel */
}
.admin-page .user-actions[open] > .actions-cell {
  /* When expanded, give the action panel a subtle inset so it visually
   * groups + doesn't blend with the next user's row.  Light-touch — no
   * heavy modal feel, just enough separation. */
  background: rgba(255, 255, 255, 0.02);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  padding: 6px 8px;
}

/* 2026-05-15: Users-table actions had 11 control elements wrapping into
 * 4 rows per user → row height ~200px.  Tightening padding + gaps +
 * narrowing widgets gets us to 1-2 rows on a 1440-1920px desktop. */
.admin-page .actions-cell .btn {
  padding: 4px 7px;
  font-size: var(--text-sm);
}

.admin-page .tier-form,
.admin-page .until-form,
.admin-page .duration-form {
  display: inline-flex;
  gap: 3px;
  align-items: center;
}

/* Native form widgets inside admin tables — narrower than dashboard's
   global rule which sets width:100%.  Box-shadow disabled to match the
   .btn neighbours (no inset ring). */
.admin-page .tier-form select,
.admin-page .until-form input[type="date"],
.admin-page .duration-form select {
  width: auto;
  background: var(--bg-field);
  border: 1px solid var(--line-strong);
  color: var(--text);
  border-radius: var(--radius-sm);
  padding: 3px 6px;
  font-size: var(--text-sm);
  box-shadow: none;
}
/* Date input is the widest control by default — tame it. */
.admin-page .until-form input[type="date"] {
  max-width: 130px;
}

/* Email column: long emails (e.g. iCloud aliases) blow the column open
 * and push Actions further right, leaving even less space.  Cap +
 * ellipsis keeps the table balanced; full email visible via title attr. */
.admin-page .users-table td:first-child,
.admin-page .users-table th:first-child {
  max-width: 220px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* 2026-05-15: Users-table column sizing.  Without this every column
 * gets ~1/9 of the 1200px max-width — including Slots (1 digit) and
 * Status ("active"/"disabled").  Shrink-to-fit on every compact column
 * + nowrap so they don't waste horizontal space.  The Actions column
 * (no rule) absorbs the leftover space → buttons get 1-2 rows instead
 * of 4.  Trick: width:1% + nowrap = "as narrow as content allows". */
.admin-page .users-table td:nth-child(2),  /* Role */
.admin-page .users-table th:nth-child(2),
.admin-page .users-table td:nth-child(3),  /* Tier */
.admin-page .users-table th:nth-child(3),
.admin-page .users-table td:nth-child(4),  /* Slots */
.admin-page .users-table th:nth-child(4),
.admin-page .users-table td:nth-child(5),  /* Sub active */
.admin-page .users-table th:nth-child(5),
.admin-page .users-table td:nth-child(6),  /* Until */
.admin-page .users-table th:nth-child(6),
.admin-page .users-table td:nth-child(7),  /* Created */
.admin-page .users-table th:nth-child(7),
.admin-page .users-table td:nth-child(8) { /* Status */
  width: 1%;
  white-space: nowrap;
}

/* Tighter cell padding in the dense Users table so per-row vertical
 * footprint shrinks too.  Same density as audit-table. */
.admin-page .users-table td,
.admin-page .users-table th {
  padding: 6px 8px;
}

/* On wide displays, give the admin-shell more room so the Users table
 * actions don't have to wrap.  1200 → 1500 unlocks ~300px more for
 * the Actions column on 1600px+ monitors.  Mobile still respects the
 * 100% width rule from .admin-shell. */
@media (min-width: 1600px) {
  .admin-shell {
    max-width: 1500px;
  }
}

.admin-page .audit-table td {
  font-size: var(--text-base);
  color: var(--text-soft);
}

.admin-page .audit-table td:first-child {
  color: var(--muted-2);
  white-space: nowrap;
}

.admin-page .event-type {
  color: var(--cyan);
  font-weight: 700;
}

.admin-page .details-json {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  color: var(--muted);
  font-size: var(--text-sm);
}

/* Admin TOTP modal contents.  .modal-overlay is shared with billing. */
.admin-page .modal {
  background: var(--bg-card);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
  max-width: 520px;
  width: 100%;
  margin: 16px;
}

.admin-page .modal h3 {
  margin-top: 0;
  color: var(--red);
}

.admin-page .uri-box {
  background: var(--bg-field);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  padding: var(--space-3);
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--text-sm);
  word-break: break-all;
  color: var(--cyan);
  margin: 12px 0;
}

.admin-page .codes-list {
  list-style: none;
  padding: var(--space-3);
  margin: 8px 0 16px;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 5px;
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  font-size: var(--text-md);
  color: var(--green);
  background: var(--bg-field);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
}

.modal-actions {
  display: flex;
  gap: var(--space-2);
  justify-content: flex-end;
  margin-top: var(--space-1);
}

.modal-confirm-text {
  margin: 0 0 20px;
  color: var(--text);
  font-size: var(--text-md);
  line-height: 1.5;
}

.rm-toast-root {
  position: fixed;
  top: 18px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 5000;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  pointer-events: none;
}

.rm-toast {
  pointer-events: auto;
  background: var(--bg-card);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  color: var(--text);
  padding: 10px 16px;
  font-size: var(--text-base);
  max-width: 480px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}

.rm-toast.is-error {
  border-color: rgba(255, 93, 115, 0.55);
  color: var(--red);
}

.rm-toast.is-success {
  border-color: rgba(88, 230, 154, 0.55);
  color: var(--green);
}

.maintenance-banner {
  /* NOT sticky.  The base .topbar already uses position: sticky; top: 0,
     and both elements competing for top:0 produced a visual overlap
     where the amber banner's translucent background let topbar text
     bleed through (operator screenshot 2026-05-25).  Banner now sits
     in normal flow above the topbar — visible at page-top, scrolls
     away with the page.  Maintenance state is reinforced anyway by
     the amber-styled disabled submit buttons in the composer. */
  padding: 10px 16px;
  background: rgba(255, 190, 92, 0.14);
  border-bottom: 1px solid rgba(255, 190, 92, 0.32);
  color: var(--amber);
  font-weight: 600;
  text-align: center;
  box-shadow: 0 1px 6px rgba(0, 0, 0, 0.3);
}

/* Legal pages (privacy.html, terms.html) — previously these templates
   shipped their own inline <style> block with hardcoded #a5b4fc indigo
   brand color and #aaa/#666 muted grays that bypassed the design
   tokens.  Pulled into the main stylesheet so token updates flow
   through to /privacy and /terms. */
.legal-page {
  min-height: 100vh;
  background: var(--bg);
  color: var(--text);
  padding: 2rem 1rem;
}
.legal-shell {
  max-width: 760px;
  margin: 0 auto;
}
.legal-nav {
  display: flex;
  gap: 1.5rem;
  align-items: center;
  margin-bottom: 2.5rem;
  flex-wrap: wrap;
}
.legal-nav .brand {
  font-size: 1.1rem;
  font-weight: 800;
  color: var(--cyan);
  text-decoration: none;
}
.legal-nav a {
  color: var(--muted);
  text-decoration: none;
  font-size: 0.9rem;
}
.legal-nav a:hover {
  color: var(--cyan);
}
.legal-body h1 {
  font-size: 1.6rem;
  font-weight: 800;
  margin-bottom: 0.25rem;
}
.legal-body .last-updated {
  color: var(--muted-2);
  font-size: 0.85rem;
  margin-bottom: 2rem;
}
.legal-body h2 {
  font-size: 1.05rem;
  font-weight: 700;
  margin-top: 2rem;
  margin-bottom: 0.5rem;
  color: var(--text-soft);
}
.legal-body p {
  line-height: 1.7;
  color: var(--text-soft);
  margin: 0.5rem 0;
}
.legal-body ul {
  padding-left: 1.5rem;
  margin: 0.5rem 0;
}
.legal-body ul li {
  color: var(--text-soft);
  line-height: 1.7;
  margin-bottom: 0.25rem;
}

/* Small inline helpers replacing former style="..." attributes in admin.html. */
.admin-page .cell-meta {
  color: var(--muted-2);
  font-size: var(--text-sm);
  white-space: nowrap;
}

.admin-page .cell-soft {
  color: var(--text-soft);
}

.admin-page .row-empty {
  color: var(--muted);
  text-align: center;
  padding: var(--space-4);
}

.admin-page .section-desc {
  color: var(--muted);
  font-size: var(--text-base);
  margin: 0 0 8px;
}

.admin-page .inline-select {
  width: auto;
  margin-left: var(--space-2);
  font-size: var(--text-base);
  padding: 2px 6px;
  background: var(--bg-soft);
  color: var(--text-soft);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  box-shadow: none;
}

.admin-page .summary-meta {
  margin-left: 12px;
  font-size: var(--text-sm);
  color: var(--muted);
}

/* 2026-05-06: email funnel summary tiles + per-user step grid. */
.admin-page .funnel-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 10px;
  margin: 14px 0 22px;
}
.admin-page .funnel-stat {
  background: var(--bg-card, #0f1014);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  padding: 12px 14px;
}
.admin-page .funnel-stat-strong {
  border-color: rgba(83, 216, 255, 0.45);
  background: rgba(83, 216, 255, 0.06);
}
.admin-page .funnel-stat-bad {
  border-color: rgba(255, 93, 115, 0.45);
  background: rgba(255, 93, 115, 0.06);
}
.admin-page .funnel-stat-num {
  font-size: 22px;
  font-weight: 800;
  line-height: 1.1;
  color: var(--text);
}
.admin-page .funnel-stat-pct {
  font-size: var(--text-base);
  font-weight: 600;
  color: var(--muted);
  margin-left: 6px;
}
.admin-page .funnel-stat-label {
  font-size: var(--text-sm);
  color: var(--muted);
  margin-top: var(--space-1);
  line-height: 1.4;
}
.admin-page .funnel-stat-sublabel {
  font-size: var(--text-xs);
  color: var(--muted-2, #555a64);
}

.admin-page .funnel-users-table .cell-step {
  text-align: center;
  font-size: var(--text-lg);
  color: var(--muted-2, #555a64);
  width: 1%;
  padding-left: 8px;
  padding-right: 8px;
}
.admin-page .funnel-users-table .cell-step-on {
  color: var(--green, #58e69a);
  font-weight: 700;
}
.admin-page .funnel-row-bad td {
  background: rgba(255, 93, 115, 0.05);
}
.admin-page .badge-bad {
  display: inline-block;
  padding: 1px 6px;
  border-radius: var(--radius-sm);
  font-size: 10px;
  font-weight: 700;
  background: rgba(255, 93, 115, 0.15);
  color: var(--red, #ff5d73);
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-left: 6px;
}

/* JS errors table — message + first frame are monospace and wrap on
   long values so the row doesn't blow up the layout horizontally. */
.admin-page .js-err-message,
.admin-page .js-err-frame {
  font-family: ui-monospace, "SF Mono", Consolas, monospace;
  font-size: var(--text-sm);
  word-break: break-word;
  max-width: 420px;
}
.admin-page .js-err-frame {
  color: var(--muted);
  max-width: 320px;
}

.admin-page .btn-tiny {
  margin-left: var(--space-2);
  font-size: var(--text-sm);
  padding: 3px 8px;
}

/* 2026-05-12: civitai workflow_id lookup widget on /admin. */
.admin-page .wfid-lookup {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  margin-bottom: var(--space-2);
}
.admin-page .wfid-lookup input {
  flex: 1;
  max-width: 360px;
  padding: 6px 10px;
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.08);
  border-radius: var(--radius-sm);
  color: var(--text);
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--text-sm);
}
.admin-page .wfid-lookup-result {
  background: rgba(255, 255, 255, 0.04);
  border: 1px solid rgba(255, 255, 255, 0.06);
  border-radius: var(--radius-sm);
  padding: 10px 12px;
  margin-bottom: var(--space-6);
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: var(--text-xs);
  white-space: pre-wrap;
  word-break: break-word;
}

.admin-page .modal-email-row {
  font-weight: 700;
  margin-bottom: var(--space-2);
}

.admin-page .modal-helper-text {
  color: var(--text-soft);
  font-size: var(--text-md);
  margin: 0 0 12px;
}

.admin-page .codes-heading {
  color: var(--green);
  margin-top: 0;
}

/* 2026-05-08: content-policy plashka above prompt textareas.  Re-toned
 * to amber 2026-05-11 — red read as a hard error / "you broke something"
 * to operators and new users, but this is preventive guidance, not a
 * blocked-action signal. */
.tool-form .content-policy-warn {
  margin: 0 0 12px;
  padding: 8px 12px;
  font-size: var(--text-base);
  line-height: 1.45;
  color: #fde68a;
  background: rgba(120, 53, 15, 0.18);
  border: 1px solid rgba(180, 83, 9, 0.6);
  border-radius: var(--radius-md);
}
.tool-form .content-policy-warn a {
  color: #fef3c7;
  text-decoration: underline;
}
.tool-form .content-policy-warn a:hover {
  color: #fffbeb;
}

/* ============================================================
   Phase 3.1c — SBP rail: method picker + SBP modal
   Reuses .modal-overlay / .modal-box / .modal-title / .modal-amount-big /
   .modal-status / .modal-countdown / .copy-btn / .spinner from the crypto
   billing modal above.  Only new bits live here.
   ============================================================ */

.picker-tier-line {
  font-size: var(--text-base);
  color: var(--muted);
  margin-top: -10px;
  margin-bottom: 18px;
}

.picker-options {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-bottom: 18px;
}

.picker-option {
  text-align: left;
  background: var(--bg-elevated, #1a1a1a);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  padding: 14px 16px;
  color: var(--text);
  cursor: pointer;
  font: inherit;
  transition: border-color var(--duration-fast), background var(--duration-fast);
}

.picker-option:hover,
.picker-option:focus-visible {
  border-color: var(--accent, #6ea2ff);
  background: var(--bg-card);
  outline: none;
}

.picker-option-name {
  font-size: 15px;
  font-weight: 700;
  margin-bottom: var(--space-1);
}

.picker-option-rub {
  font-weight: 500;
  color: var(--muted);
  margin-left: 6px;
}

.picker-option-detail {
  font-size: var(--text-sm);
  color: var(--muted);
  line-height: 1.45;
}

/* --- SBP modal: iframe-only payment surface + pre-warning + actions --- */

/* Slightly wider than the default crypto invoice modal (max 500px) so the
   iframe payform has comfortable horizontal room.  Falls back to full-
   width on narrow viewports via the existing .modal-box rules. */
.modal-box-sbp {
  max-width: 600px;
}

/* Pre-warning sets up the user for the Telegram-stars wording on the
   Platega payform.  Quietly amber so it reads as informational, not as
   error / not as success. */
.sbp-prewarning {
  display: flex;
  gap: var(--space-3);
  margin: 12px 0 16px 0;
  padding: 12px 14px;
  background: rgba(255, 190, 92, 0.08);
  border: 1px solid rgba(255, 190, 92, 0.25);
  border-radius: var(--radius-md);
}

.sbp-prewarning-icon {
  flex: 0 0 20px;
  font-size: var(--text-lg);
  line-height: 1.4;
  color: var(--amber);
}

.sbp-prewarning-text {
  font-size: var(--text-base);
  line-height: 1.5;
  color: var(--text);
}

.sbp-prewarning-text strong {
  color: var(--amber);
  font-weight: 700;
}

.sbp-payment-surface {
  margin: 8px 0 12px 0;
}

.sbp-payment-surface iframe {
  display: block;
  width: 100%;
  height: 520px;
  max-height: 65vh;
  max-height: 65dvh;
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  background: #fff;
  /* Iframe of pay.platega.io — whitelisted in CSP frame-src.  Platega
     itself renders the actual СБП-QR (qr.nspk.ru) inside this frame after
     passing their CAP captcha. */
}

.sbp-actions {
  display: flex;
  gap: var(--space-2);
  margin-bottom: var(--space-4);
}

.sbp-actions .copy-btn {
  flex: 1;
  text-align: center;
  text-decoration: none;
}

/* --- TGStars modal: info-screen + how-to hint + action buttons --- */

.tgstars-help {
  margin: 12px 0 16px 0;
  padding: 12px 14px;
  background: rgba(83, 216, 255, 0.06);
  border: 1px solid rgba(83, 216, 255, 0.20);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text);
}

.tgstars-help strong {
  display: block;
  margin-bottom: 4px;
  color: var(--text);
  font-weight: 700;
}

.tgstars-help-list {
  margin: 0;
  padding-left: 16px;
}

.tgstars-help-list li {
  margin-bottom: 6px;
}

.tgstars-help-list li:last-child {
  margin-bottom: 0;
}

.tgstars-help-list a {
  color: var(--cyan, #53d8ff);
  text-decoration: none;
}

.tgstars-help-list a:hover,
.tgstars-help-list a:focus-visible {
  text-decoration: underline;
}

.tgstars-actions,
.tgstars-waiting-actions {
  display: flex;
  gap: var(--space-2);
  margin-top: var(--space-3);
}

/* GLOBAL [hidden] hardening (2026-06-06).
   A class with an explicit `display` (e.g. `.verify-banner { display:flex }`)
   has the SAME specificity (0,1,0) as the UA rule `[hidden] { display:none }`
   and, being an author rule, WINS — so the `hidden` attribute silently stops
   hiding any element that also carries a display-bearing class. This bit us
   repeatedly and we kept patching per-selector (tgstars/sbp rows, active-invoice
   banner, video-variant-selector...). The full audit on 2026-06-06 found it had
   spread across the dashboard: #sub-urgent-strip showed a stray "Renew" to EVERY
   active subscriber, plus #sampler-pills, the two image-"clear" .ghost-buttons,
   .quick-picks (x3) and #admin-users-summary. Instead of chasing each selector
   forever, enforce the attribute globally: anything carrying `hidden` is hidden,
   period. JS still SHOWS elements by REMOVING the attribute (`el.hidden = false`)
   — this rule then no longer matches and the class's `display` applies as before.
   Audited: no code shows a still-`hidden` element via inline style.display, so
   `!important` breaks nothing legitimate. (Per-selector [hidden] overrides
   elsewhere in this file are now redundant but harmless.) */
[hidden] {
  display: none !important;
}

/* SBP-v2 confirm-step block — 3 mandatory checkboxes shown above the
   primary CTA.  Drops abandonment-as-seen-by-bot to intent-aligned
   fraction (anti-fingerprint Signal #3 from lesson_sbp_burn_root_cause).
   Visual style mirrors tgstars-help: soft tinted card, clear hierarchy. */
.sbp-v2-confirm {
  margin: 12px 0 16px 0;
  padding: 12px 14px;
  background: rgba(83, 216, 255, 0.06);
  border: 1px solid rgba(83, 216, 255, 0.20);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text);
}

.sbp-v2-confirm > strong {
  display: block;
  margin-bottom: 8px;
}

.sbp-v2-check-row {
  display: flex;
  align-items: flex-start;
  gap: 8px;
  padding: 4px 0;
  cursor: pointer;
}

.sbp-v2-check-row input[type="checkbox"] {
  flex: 0 0 auto;
  margin-top: 3px;  /* align with first line of label */
  cursor: pointer;
}

.sbp-v2-check-row > span {
  flex: 1 1 auto;
}

/* Disabled-button cue for the primary CTA while not all checkboxes are
   ticked.  Same visual treatment as native :disabled. */
#sbpV2OpenBtn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.tgstars-actions .modal-close-btn,
.tgstars-actions .copy-btn,
.tgstars-waiting-actions .modal-close-btn,
.tgstars-waiting-actions .copy-btn {
  /* Normalize <a class="copy-btn"> and <button class="modal-close-btn">
     to identical visual weight + vertical alignment.  Without this the
     baseline of an anchor sits ~2px above the button's text in the flex
     row (anchor uses baseline, button is align-items-stretched). */
  flex: 1;
  margin: 0;
  padding: 10px;
  font-size: var(--text-md);
  text-align: center;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  box-sizing: border-box;
}

/* --- direct-to-card SBP modal (2026-05-30) --- */
/* The kopeck warning is the single most important UI element on this rail:
   a rounded transfer can't be auto-matched, so the identifier must be
   impossible to miss.  Amber-tinted, full-width, above the QR. */
.card-sbp-kopeck-warning {
  margin: 12px 0;
  padding: 12px 14px;
  background: rgba(255, 184, 77, 0.10);
  border: 1px solid rgba(255, 184, 77, 0.35);
  border-radius: var(--radius-md);
  font-size: var(--text-sm);
  line-height: 1.5;
  color: var(--text);
  text-align: center;
}

.card-sbp-kopeck-warning strong {
  color: #ffb84d;
}

.card-sbp-qr-wrap {
  display: flex;
  justify-content: center;
  margin: 16px 0;
}

.card-sbp-qr {
  width: 240px;
  height: 240px;
  border-radius: var(--radius-md);
  background: #fff;       /* QR needs a white quiet zone to scan reliably */
  padding: 10px;
  box-sizing: border-box;
}

/* 2026-05-16: "no dead-end" below-fold showcase strip on /billing.
   Mirrors landing's .lp-showcase-strip but lives in app's styles.css
   because /billing is a Flask template, not the static landing.
   Visual hierarchy: deliberately lower-key than tier cards (smaller
   title, looser gap, no border) so the eye still anchors on pricing. */
.bill-showcase-section {
  margin: 48px auto 24px;
  max-width: 920px;
  padding: 0 16px;
}

.bill-showcase-title {
  font-size: 15px;
  font-weight: 600;
  color: var(--text-muted, #8aa0b3);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  text-align: center;
  margin: 0 0 16px;
}

.bill-showcase-strip {
  display: grid;
  grid-template-columns: repeat(6, 1fr);
  gap: var(--space-2);
}

.bill-showcase-tile {
  position: relative;
  display: block;
  aspect-ratio: 1 / 1;
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--bg-card, #0f1419);
  text-decoration: none;
  transition: transform var(--duration-base) ease, box-shadow var(--duration-base) ease;
}

.bill-showcase-tile:hover,
.bill-showcase-tile:focus-visible {
  transform: scale(1.02);
  box-shadow: 0 0 0 1px var(--cyan, #53d8ff), 0 8px 24px rgba(83, 216, 255, 0.18);
  outline: none;
}

.bill-showcase-tile img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  background: var(--bg-card, #0f1419);
}

.bill-showcase-cta {
  text-align: center;
  margin: 16px 0 0;
  font-size: var(--text-md);
}

.bill-showcase-cta a {
  color: var(--cyan, #53d8ff);
  font-weight: 600;
  text-decoration: none;
  transition: opacity var(--duration-base);
}

.bill-showcase-cta a:hover {
  opacity: 0.75;
}

/* verify-gate inline CTA — sits between the verify text and the
   Resend form.  Lower-key than the Resend button (which is the
   primary action). */
.verify-gate-browse {
  margin: 8px 0 12px;
  font-size: var(--text-md);
  color: var(--text-muted, #8aa0b3);
}

.verify-gate-browse a {
  color: var(--cyan, #53d8ff);
  text-decoration: none;
  font-weight: 600;
}

.verify-gate-browse a:hover {
  text-decoration: underline;
}

@media (max-width: 900px) {
  .bill-showcase-strip {
    grid-template-columns: repeat(3, 1fr);
  }
}

@media (max-width: 480px) {
  .bill-showcase-strip {
    grid-template-columns: repeat(2, 1fr);
  }
  .bill-showcase-section {
    margin: 32px auto 16px;
  }
}

/* =====================================================================
   Mobile dashboard — 2026-05-20.
   STRICT-SAFE: every rule below is scoped to @media (max-width: 720px)
   or is a base rule that activates ONLY when paired with elements that
   exist solely for mobile (.topbar-menu-toggle).  Desktop rendering is
   provably unchanged because:
     - .topbar-menu-toggle has `display: none` outside the media query.
     - .topbar-collapsible-item is a NO-OP class on desktop (no rules
       reference it outside the media block).
     - .topbar.menu-open class is only toggled by mobile-gated JS.
   ===================================================================== */

/* Hamburger button — invisible on desktop, revealed inside the @media
   block below.  Square 44×44 = iOS HIG touch target. */
.topbar-menu-toggle {
  display: none;
  width: 44px;
  height: 44px;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
  background: rgba(255, 255, 255, 0.04);
  color: var(--text);
  font-size: 22px;
  line-height: 1;
  cursor: pointer;
}
.topbar-menu-toggle:hover {
  background: rgba(255, 255, 255, 0.08);
  border-color: rgba(83, 216, 255, 0.45);
}
.topbar-menu-toggle:focus-visible {
  outline: 2px solid rgba(83, 216, 255, 0.6);
  outline-offset: 2px;
}

@media (max-width: 720px) {
  /* Topbar becomes a wrapping container: brand+hamburger in row 1,
     slot-badge+filter-group in row 2, collapsible items revealed in
     drawer-rows when .menu-open is toggled.
     Explicit flex-direction:row + justify-content:space-between overrides
     the older @media (max-width: 620px) block (which stacks the topbar
     vertically with justify-content:center) — we want row+wrap instead. */
  .topbar {
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    /* env(safe-area-inset-*) respects iPhone notch / Android status bar
       when <meta viewport-fit=cover> is set (which it is, as of this
       commit).  max() ensures the inset never SHRINKS our chosen padding
       — on devices without insets env() resolves to 0px. */
    padding: 10px max(14px, env(safe-area-inset-right)) 10px max(14px, env(safe-area-inset-left));
    padding-top: max(10px, env(safe-area-inset-top));
    min-height: 0;
  }
  .topbar > div:first-child {
    flex: 0 1 auto;
    min-width: 0;
  }
  .topbar-menu-toggle {
    display: inline-flex;
    flex: 0 0 auto;
  }
  /* Pressed-state visual cue when drawer is open. */
  .topbar-menu-toggle[aria-expanded="true"] {
    background: rgba(83, 216, 255, 0.12);
    border-color: var(--cyan);
    color: var(--cyan);
  }

  /* Brand subtitle is decorative — drop it on phones to save vertical
     space.  Brand wordmark stays. */
  .topbar > div:first-child .subtle {
    display: none;
  }

  /* Actions container drops to its own row, full width. */
  .topbar-actions {
    flex-basis: 100%;
    width: 100%;
    justify-content: flex-start;
    gap: var(--space-2);
  }

  /* Slot-badge stays inline; filter-group fills the rest of row 2. */
  #slot-badge {
    flex: 0 0 auto;
  }
  .topbar-actions .filter-group {
    flex: 1 1 auto;
    display: flex;
  }
  .topbar-actions .filter-group .filter-chip {
    flex: 1 1 0;
    justify-content: center;
    padding: 0 8px;
  }

  /* Collapsible items: hidden by default, each takes its own row when
     drawer is open.  order:10 pushes them to the END of the flex
     container so they don't interleave with the always-visible
     slot-badge + filter-group (which keep default order:0).
     2026-05-20: opacity + translateY animation + @starting-style for
     a slide-down entrance on Safari 17.4+ / Chrome 117+; older browsers
     ignore @starting-style and snap to open state (graceful fallback). */
  .topbar-collapsible-item {
    display: none;
    flex-basis: 100%;
    order: 10;
    opacity: 0;
    transform: translateY(-6px);
    /* allow-discrete makes `display` transition-able so the @starting-style
       entrance below actually fires when going from display:none → flex
       (CSS Transitions Level 2 / Chrome 117+ / Safari 17.4+).  Older
       browsers ignore allow-discrete and snap to open state — graceful
       fallback. */
    transition:
      opacity var(--duration-base) ease,
      transform 0.22s ease,
      display 0.22s allow-discrete;
  }
  .topbar.menu-open .topbar-collapsible-item {
    display: flex;
    opacity: 1;
    transform: translateY(0);
  }
  @starting-style {
    .topbar.menu-open .topbar-collapsible-item {
      opacity: 0;
      transform: translateY(-6px);
    }
  }
  /* Logout form is the one collapsible whose child <button> needs to
     stretch to the full row. */
  .topbar.menu-open form.topbar-collapsible-item > button {
    width: 100%;
  }
  /* Showcase / Account anchors — make full-width to look like drawer
     menu items rather than inline pills. */
  .topbar.menu-open a.topbar-collapsible-item.ghost-button {
    width: 100%;
    justify-content: center;
  }
  /* Admin group when revealed: stack its inner buttons. */
  .topbar.menu-open .topbar-admin-group {
    flex-wrap: wrap;
    width: 100%;
    margin: 4px 0 0;
    padding: 8px 0 0;
    border-left: 0;
    border-top: 1px solid var(--line);
  }

  /* === Form compression (CSS-only, no DOM changes) ===
     Tighter padding/margins for the composer form on mobile so the
     Submit button is reachable with less scrolling.  Each rule below
     touches a selector that already exists on desktop — values are
     reduced for narrow viewports only. */
  .workspace {
    padding: 14px 12px 24px;
  }
  .tabs {
    margin-bottom: 14px;
  }
  .tool-form h1 {
    font-size: 20px;
    margin: 0 0 12px;
  }
  .tool-form label {
    margin-bottom: var(--space-3);
  }
  .tool-form .content-policy-warn {
    margin: 0 0 10px;
    padding: 6px 10px;
    font-size: var(--text-sm);
    line-height: 1.4;
  }
  .video-mode-selector,
  .video-variant-selector {
    margin: 0 0 10px;
  }
  .video-row,
  .form-controls-row {
    gap: 10px 12px;
    margin-bottom: 10px;
  }
  .slider-field {
    margin-bottom: 14px;
  }
  details[data-field="advanced"] {
    padding-top: 10px;
  }
  details[data-field="advanced"] > summary {
    margin-bottom: 10px;
  }

  /* === Touch-target normalization (iOS HIG 44px minimum) ===
     Default control heights (38–40px for tabs / ghost / filter-chip;
     30px for icon-button) drop below comfortable thumb reach.  Bump
     interactive controls inside the dashboard chrome.  Desktop sizes
     are preserved because this rule is inside the mobile @media. */
  .ghost-button,
  .tab,
  .icon-button,
  .filter-chip {
    min-height: 44px;
  }

  /* Verify / TOTP / user-message banner — tighter padding on phones
     so the page-content gutter doesn't feel pinched.  Keeps existing
     flex-wrap behavior; only reduces inner whitespace. */
  .verify-banner {
    padding: 0.55rem 0.85rem;
    font-size: 0.85rem;
    gap: 0.6rem;
  }
}

/* Tighter aspect-tile column cap on tablets+phones — the auto-fit
   minmax(0, 1fr) default lets 5 tiles squeeze into 5 columns
   (~55-65px each at 320px, ~95px at 600px), which crops the
   "1280×720" dimension subtext.  Cap at 3 columns through the
   full ≤620 range so 5-tile video forms wrap to a 3+2 grid where
   each tile stays readable. */
@media (max-width: 620px) {
  .aspect-tiles {
    grid-template-columns: repeat(3, 1fr);
  }
}

@media (max-width: 480px) {
  /* Media viewer (fullscreen image/video lightbox) — desktop assumes
     ≥76px of horizontal breathing room for nav buttons + padding.
     On a 320–375px phone that leaves only ~180–240px of actual image,
     and the nav buttons fully overlap small images.  Slim everything
     down: nav buttons 52→40px, padding 18→8px, "Download" text
     replaced with a ↓ glyph to keep the toolbar from wrapping. */
  .viewer-shell {
    /* Respect notch/home-indicator on iPhone X+ — env() = 0 elsewhere. */
    padding: max(8px, env(safe-area-inset-top)) max(8px, env(safe-area-inset-right)) max(8px, env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left));
  }
  .viewer-toolbar {
    min-height: 44px;
    gap: var(--space-2);
  }
  .viewer-title {
    font-size: var(--text-md);
    max-width: calc(100vw - 140px);
  }
  .viewer-counter {
    font-size: var(--text-xs);
    margin-top: 2px;
  }
  .viewer-stage img,
  .viewer-stage video {
    max-width: calc(100vw - 16px);
    max-height: calc(100vh - 88px);
    max-height: calc(100dvh - 88px);
  }
  .viewer-nav {
    width: 40px;
    height: 56px;
    font-size: var(--text-3xl);
    background: rgba(7, 8, 11, 0.6);
  }
  .viewer-nav.previous { left: 4px; }
  .viewer-nav.next { right: 4px; }

  /* Toolbar Download button → icon-only ↓ to save horizontal space.
     font-size:0 hides the "Download" text node; ::before injects the
     glyph.  Keeps the same <a download> semantics intact. */
  .viewer-download {
    width: 42px;
    height: 42px;
    padding: 0;
    font-size: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
  }
  .viewer-download::before {
    content: "↓";
    font-size: 22px;
    font-weight: 700;
    color: var(--cyan);
    line-height: 1;
  }
}

/* iOS auto-zoom safety override for the two ID-styled model pickers.
   Must live AFTER the desktop #image-model-select rule at :552 (same
   specificity 1,0,0; later source wins).  Bumps 15px → 16px on phones. */
@media (max-width: 720px) {
  #image-model-select,
  #video-model-select {
    font-size: var(--text-lg);
  }
}

/* =====================================================================
   Hover-stick neutralization for touch devices — 2026-05-20.
   On phones/tablets, :hover state can "stick" after tap (browser keeps
   the last-tapped element in hover state until next interaction).
   This makes tapped buttons look "still active" after the press.
   Restore the base visual values for each high-traffic hover rule so
   the post-tap appearance matches the resting appearance.

   Restated values mirror the base selector definitions:
     .tab           — bg transparent, color var(--muted)
     .ghost-button  — bg transparent, color var(--text-soft), border var(--line)
     .primary-button— bg var(--cyan)
     .filter-chip   — bg rgba(255,255,255,0.02), color var(--text-soft)
     .icon-button   — bg transparent, color var(--muted)
     .media-output  — no box-shadow
     .tile-action-btn — bg rgba(2,2,4,0.74)
     .viewer-nav/btn/dl — bg rgba(7,8,11,0.82), border var(--line)
   ===================================================================== */
@media (hover: none) {
  .tab:hover {
    background: transparent;
    color: var(--muted);
  }
  .ghost-button:hover {
    background: transparent;
    border-color: var(--line);
    color: var(--text-soft);
  }
  .primary-button:hover {
    background: var(--cyan);
  }
  /* :not(aria-pressed) so the reset doesn't strip the cyan active state
     (.filter-chip[aria-pressed="true"]) when the user taps the already-
     active chip.  Both rules have spec (0,2,0); without :not(), source-
     later wins and the active tint disappears until next interaction. */
  .filter-chip:not([aria-pressed="true"]):hover {
    background: rgba(255, 255, 255, 0.02);
    color: var(--text-soft);
  }
  .icon-button:hover {
    background: transparent;
    color: var(--muted);
  }
  .media-output:hover {
    box-shadow: none;
  }
  .tile-action-btn:hover {
    background: rgba(2, 2, 4, 0.74);
  }
  .viewer-nav:hover,
  .viewer-button:hover,
  .viewer-download:hover {
    background: rgba(7, 8, 11, 0.82);
    border-color: var(--line);
  }
  /* :not(.is-selected) so the reset doesn't strip the cyan selected
     state on Image/Text/Reference mode pills.  Same specificity tie
     as filter-chip above. */
  .video-mode-btn:hover:not(:disabled):not(.is-selected) {
    border-color: var(--line-strong);
    color: var(--text-soft);
  }
}

/* =====================================================================
   admin-table mobile horizontal-overflow guard — 2026-05-20.
   The .admin-shell table at :4070 uses `width: 100%` with `white-space:
   nowrap` on <th>.  5–7 column tables (referrer_dashboard, manager)
   force a min-width well over 320px → page horizontal scroll on phone,
   which is ugly + drags every other widget into the overflow zone.
   Fix: at ≤720px wrap each table in an inline horizontal scroll
   container by making the <table> itself `display: block` with
   `overflow-x: auto`.  Confines the scroll to the table; the rest of
   the page stays at viewport width.  Desktop unaffected.
   ===================================================================== */
@media (max-width: 720px) {
  /* Wrap each .admin-shell table in its own horizontal-scroll container.
     IMPORTANT: only the <table> itself gets display:block.  thead/tbody/tr
     keep their default table-display so they all participate in the
     SAME anonymous table that browsers generate around table-display
     children of a non-table parent (CSS Tables Module 3 §17.2.1) — this
     preserves column alignment between headers and body rows.

     Earlier attempt (display:table on thead/tbody/tr) made each row its
     own table, breaking header/body column alignment — verified at
     375px viewport: header "State" at x=79 vs body "running" at x=180. */
  .admin-shell table {
    display: block;
    max-width: 100%;
    overflow-x: auto;
    -webkit-overflow-scrolling: touch;
    /* Keep cell content from wrapping so each cell takes natural width
       and the table actually overflows (otherwise table fits in the
       block by squeezing cells to single-letter columns). */
    white-space: nowrap;
  }
}

/* =====================================================================
   Tablet-portrait gap (721–920px) — 2026-05-20.
   At ≤920px workspace becomes single-column (line 1544: display:block).
   But on a 768px iPad-portrait, the form stretches to 760px-ish, which
   visually orphans labels from inputs (the composer was tuned around
   --composer-width: 420px on desktop).  Cap the workspace to a
   readable column width and center it.  No effect at ≥921px (full
   two-pane layout) or ≤720px (mobile rules already constrain
   padding).
   ===================================================================== */
@media (min-width: 721px) and (max-width: 920px) {
  .workspace {
    max-width: 760px;
    margin: 0 auto;
  }
}

/* =====================================================================
   Landscape-phone overrides — 2026-05-20.
   Phones rotated to landscape (iPhone 13 Pro = 844×390 → falls in the
   721–920 band by width, but height-constrained).  The sticky topbar
   (68px+) + sticky tool-pane (266px scroll-island) eat the small
   vertical viewport.  Unstick both so content flows naturally.

   Targeting by (max-height: 480px) AND (orientation: landscape) is
   strict enough that desktop laptops with tiny windows aren't hit
   (laptops rarely report orientation: landscape AT max-height < 480
   simultaneously, and even if they do the un-stick is gracefully
   functional — just non-optimal).
   ===================================================================== */
@media (max-height: 480px) and (orientation: landscape) {
  .topbar {
    position: static;
    min-height: 52px;
  }
  .tool-pane {
    position: static;
    max-height: none;
    overflow: visible;
  }
  /* Viewer also benefits — less vertical chrome means more image area. */
  .viewer-toolbar {
    min-height: 40px;
  }
  .viewer-stage img,
  .viewer-stage video {
    max-height: calc(100vh - 72px);
    max-height: calc(100dvh - 72px);
  }
}

/* =====================================================================
   Non-dashboard pages mobile P1 fixes — 2026-05-20.
   ===================================================================== */

/* TOTP QR (account_2fa.html) — desktop has w/h: 240×240 fixed.  At 320px
   viewport: .totp-shell content area = ~228px, so the QR overflows by
   12px → page horizontal scroll.  Make the SVG responsive while keeping
   240×240 as the visual ceiling on roomy screens.  Scoped via parent
   to avoid catching unrelated SVGs. */
@media (max-width: 480px) {
  .totp-qr svg {
    width: min(240px, 100%);
    height: auto;
  }
}

/* Pricing/Billing tier-cards — same flex-basis:240px / max-width:320px
   pattern recurs across both pages (see audit).  On ≤480 the 320px
   cap exceeds the content area (320 - 32 page padding = 288px content).
   Drop the ceiling to 100% so cards fit edge-to-edge; tighten padding
   to reclaim ~12px of horizontal text width. */
@media (max-width: 480px) {
  .tier-card {
    flex: 1 1 100%;
    max-width: 100%;
    padding: 22px 18px;
  }
}

/* Modal box (crypto/SBP/error-details) — base padding 32px + margin 16px
   leaves only 224px of horizontal content area on iPhone SE.  The SBP
   iframe and the wallet-address row both crowd.  Tighten on phones. */
@media (max-width: 480px) {
  .modal-box {
    padding: 20px 18px;
    margin: 10px;
  }
  /* SBP iframe height — base max-height:65dvh was too short for the
     ~400px-tall Platega payform on iPhone SE.  Bump the cap on phones
     so the iframe gets full height. */
  .sbp-payment-surface iframe {
    max-height: 80vh;
    max-height: 80dvh;
  }
}

/* Login/signup/verify panel — base padding 30px eats ~60px on a 320px
   viewport.  Drop to 22px to give inputs more room. */
@media (max-width: 480px) {
  .login-panel {
    padding: 22px;
  }
}

/* Showcase filter pill row — after wrap, "NSFW:" gets an 8px indent
   from the previous pill via .filter-label margin-left:8px.  On wrapping
   first-of-its-kind would be cleanest, but pragmatic: kill the indent
   on narrow viewports.  Tiny visual cleanup. */
@media (max-width: 480px) {
  .showcase-filters .filter-label {
    margin-left: 0;
  }
}

/* Landing hero orb perf fix lives in landing/styles.css (separate file). */

/* =====================================================================
   Polish layer — drawer backdrop, topbar shadow on scroll, tap-press.
   2026-05-20.  All scoped to mobile @media or hover:none — desktop is
   unaffected.  prefers-reduced-motion override at bottom kills the
   transitions for users who opt out (system setting).
   ===================================================================== */

/* Mobile-only: backdrop dims the page when drawer is open + locks body
   scroll so the page doesn't move behind the open menu.  Both keyed on
   body.drawer-open (toggled by setupMobileMenu in app.js). */
@media (max-width: 720px) {
  body.drawer-open {
    /* Scroll-lock — prevents the page from scrolling while the user
       interacts with the drawer.  position:static + overflow:hidden
       is the simplest path that doesn't shift content (iOS-safe). */
    overflow: hidden;
  }
  body.drawer-open::after {
    content: "";
    position: fixed;
    inset: 0;
    z-index: 9; /* below .topbar's z-index:10 — backdrop sits beneath chrome */
    background: rgba(0, 0, 0, 0.42);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
    animation: rm-fadein var(--duration-base) ease both;
    pointer-events: auto;
  }
  @keyframes rm-fadein {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
}

/* Sticky-topbar elevation on scroll (mobile-only).  setupTopbarShadow
   in app.js toggles .scrolled when window.scrollY > 4.  Soft shadow
   separates the chrome from content scrolling beneath.  Desktop is
   unaffected because this rule is in @media max-width:720px. */
@media (max-width: 720px) {
  .topbar {
    transition: box-shadow var(--duration-base) ease, background var(--duration-base) ease;
  }
  .topbar.scrolled {
    box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
    background: rgba(2, 2, 4, 0.92);
  }
}

/* Tap-press feedback — brief scale-down on tap for primary interactives.
   @media (hover: none) gates this to touch devices only; desktop mice
   keep their original transforms (none).  iOS / Android apps use a
   similar tactile pulse so the user knows the tap registered without
   waiting for the response.  Transform 60ms is fast enough to feel
   instant but slow enough to be visible. */
@media (hover: none) {
  .primary-button:active,
  .ghost-button:active,
  .filter-chip:active,
  .tab:active,
  .icon-button:active,
  .tile-action-btn:active,
  .video-mode-btn:active,
  .aspect-tile:active,
  .upscale-pill:active {
    transform: scale(0.97);
    transition: transform 0.06s ease-out;
  }
}

/* Honor prefers-reduced-motion — global blanket via duration-token
   override.  Every `transition: X var(--duration-fast)` and
   `transition: X var(--duration-base)` now resolves to 0ms, so no
   property-by-property enumeration is needed.  Explicit reset of
   active-transform + keyframe animations covers the remaining
   non-transition motion. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --duration-fast: 0ms;
    --duration-base: 0ms;
    --duration-slow: 0ms;
  }
  .topbar-collapsible-item,
  .topbar {
    transition: none;
  }
  .topbar-collapsible-item {
    transform: none;
  }
  body.drawer-open::after {
    animation: none;
  }
  .primary-button:active,
  .ghost-button:active,
  .filter-chip:active,
  .tab:active,
  .icon-button:active,
  .tile-action-btn:active,
  .video-mode-btn:active,
  .aspect-tile:active,
  .upscale-pill:active {
    transform: none;
    transition: none;
  }
  /* Keyframe animations — pulse-ban (1.6s breathing) and rm-fadein
     (modal entrance) freeze.  .spinner stays animated — without
     spin the user can't tell loading state. */
  .upscale-badge.is-uploading {
    animation: none;
  }
  @starting-style {
    .modal-overlay.open,
    .modal-box {
      opacity: 1;
      transform: none;
    }
  }
}

/* ============================================================
   Cabinet shell (base_cabinet.html) + referral panel
   ============================================================ */
.cabinet-shell {
  display: flex;
  flex-direction: column;
  gap: 18px;
}

/* Plan-status bar — single source of truth across every cabinet tab. */
.cabinet-planbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  flex-wrap: wrap;
  padding: 14px 18px;
  background: var(--bg-card);
  border: 1px solid var(--line);
  border-left: 3px solid var(--green);
  border-radius: var(--radius-lg);
}
.cabinet-planbar-off {
  border-left-color: var(--amber);
}
.cabinet-planbar-main {
  display: flex;
  align-items: center;
  gap: 8px;
  flex-wrap: wrap;
}
.cabinet-plan-name {
  font-size: var(--text-lg);
  font-weight: 600;
  color: var(--text);
}

/* Tabs */
.cabinet-tabs {
  display: flex;
  gap: 4px;
  border-bottom: 1px solid var(--line);
}
.cabinet-tab {
  padding: 9px 16px;
  font-size: var(--text-md);
  color: var(--muted);
  text-decoration: none;
  border-bottom: 2px solid transparent;
  margin-bottom: -1px;
  transition: color var(--duration-fast) var(--ease-out),
              border-color var(--duration-fast) var(--ease-out);
}
.cabinet-tab:hover { color: var(--text-soft); }
.cabinet-tab.is-active {
  color: var(--text);
  border-bottom-color: var(--cyan);
}

.cabinet-content {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

/* ── Referral panel ── */
.ref-link-row {
  display: flex;
  gap: 8px;
  align-items: stretch;
}
.ref-link-input {
  flex: 1;
  min-width: 0;
  padding: 9px 12px;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: var(--text-base);
  color: var(--text);
  background: var(--bg-field);
  border: 1px solid var(--line);
  border-radius: var(--radius-md);
}
.ref-copy-btn { white-space: nowrap; }

.ref-cards {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 16px;
}
@media (max-width: 640px) {
  .ref-cards { grid-template-columns: 1fr; }
}

.ref-balance-card { text-align: center; }
.ref-balance-days {
  font-size: 52px;
  line-height: 1.05;
  font-weight: 700;
  color: var(--cyan);
  margin-top: 4px;
}
.ref-balance-unit { margin-bottom: 14px; }
.ref-redeem-btn { width: 100%; }
.ref-redeem-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.ref-soon { margin-top: 10px; }

.ref-stat-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  margin-top: 6px;
}
.ref-stat { text-align: center; }
.ref-stat-num {
  font-size: var(--text-xl);
  font-weight: 700;
  color: var(--text);
}

.ref-how {
  margin: 0;
  padding-left: 20px;
  color: var(--text-soft);
  font-size: var(--text-md);
  line-height: 1.7;
}

.ref-history {
  width: 100%;
  border-collapse: collapse;
  font-size: var(--text-base);
}
.ref-history td {
  padding: 7px 8px;
  border-top: 1px solid var(--line);
  color: var(--text-soft);
}
.ref-history tr:first-child td { border-top: none; }
.ref-history-date { color: var(--muted); white-space: nowrap; }
.ref-history-amt { color: var(--green); white-space: nowrap; }

/* Partner-program discovery CTA (becomes our 30%-cash funnel). */
.ref-partner-cta { border-left: 3px solid var(--violet); }
.ref-partner-cta .primary-button-mini { margin-top: 12px; }

/* ============================================================
   Adaptive subscription countdown (dashboard topbar + cabinet plan-bar)
   ============================================================ */
.sub-countdown {
  color: var(--muted);
  white-space: nowrap;
}
.sub-countdown.is-urgent {
  color: var(--amber);
  font-weight: 600;
}
/* Compact pill in the dashboard topbar, matching the slot-badge weight. */
.sub-countdown-topbar {
  display: inline-flex;
  align-items: center;
  padding: 4px 10px;
  font-size: var(--text-sm);
  border: 1px solid var(--line);
  border-radius: var(--radius-pill);
}
.sub-countdown-topbar.is-urgent {
  border-color: var(--amber);
  background: rgba(255, 190, 92, 0.10);
}

/* Admin user-referrers search form */
.admin-search-form {
  display: flex;
  gap: 8px;
  align-items: center;
  margin: 12px 0;
}
.admin-search-form input[name="q"] {
  flex: 1;
  min-width: 0;
  max-width: 420px;
}
.admin-userref-teaser { border-left: 3px solid var(--violet); padding-left: 10px; }

/* ============================================================
   Referral panel — visual polish
   ============================================================ */
/* Separate the "make it memorable" code form from the share link. */
.ref-code-form {
  margin-top: 16px;
  padding-top: 14px;
  border-top: 1px solid var(--line);
}
.ref-code-form .modal-label { margin-bottom: 6px; }
/* Tighter, more deliberate numerals on the headline stats. */
.ref-balance-days { letter-spacing: -0.02em; }
.ref-stat-num { letter-spacing: -0.01em; }
/* The balance card reads as the primary action — give it a faint accent. */
.ref-balance-card { border-top: 2px solid var(--cyan-soft); }
/* History rows respond to the cursor so a long ledger is easier to scan. */
.ref-history tr:hover td { background: var(--bg-soft); }
.ref-history td { transition: background var(--duration-fast) var(--ease-out); }
/* Copy button confirms with the success colour after a click. */
.ref-copy-btn.is-copied { background: var(--green); }


/* 2026-06-07: one-click clear (×) for prompt/negative_prompt textareas — see
   app.js setupPromptClearButtons.  Appears only when the field has text. */
.prompt-clear-wrap { position: relative; display: block; }
.prompt-clear {
  position: absolute;
  top: 6px;
  right: 8px;
  width: 22px;
  height: 22px;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 50%;
  background: rgba(127, 127, 127, 0.18);
  color: var(--text-muted, #9aa3ad);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  opacity: 0;
  pointer-events: none;
  transition: opacity .12s ease, background .12s ease, color .12s ease, transform .08s ease;
  z-index: 2;
}
/* 2026-06-07: visibility is purely CSS via :placeholder-shown — false exactly
   when the field is non-empty.  Unlike a JS `input` listener it ALSO tracks
   programmatic .value sets (Remix / draft restore set the prompt without
   firing `input`), so the × now appears after a Remix fills an empty prompt.
   All four target textareas carry a placeholder, so :placeholder-shown is
   meaningful for each. */
.prompt-clear-wrap textarea:not(:placeholder-shown) + .prompt-clear { opacity: .6; pointer-events: auto; }
.prompt-clear-wrap textarea:not(:placeholder-shown) + .prompt-clear:hover { opacity: 1; background: rgba(127, 127, 127, 0.30); color: var(--text, #e8edf2); }
.prompt-clear:active { transform: scale(0.9); }

/* 2026-06-15 — inpaint mask editor: a brush canvas overlaid on the source. */
.inpaint-editor {
  margin: 0.4rem 0;
  padding: 12px 14px;
  border: 0.5px solid var(--line-strong);
  border-radius: var(--radius-md);
  background: var(--bg-field);
}
.inpaint-editor-head {
  display: flex; align-items: center; justify-content: space-between;
  gap: 8px; margin-bottom: 10px;
}
.inpaint-canvas-wrap {
  position: relative; display: block; width: max-content; max-width: 100%;
  margin: 0 auto; line-height: 0;
  border-radius: var(--radius-sm); overflow: hidden;
  border: 0.5px solid var(--line);
}
.inpaint-canvas-wrap img { display: block; max-width: 100%; height: auto; }
#inpaint-canvas {
  position: absolute; left: 0; top: 0; width: 100%; height: 100%;
  cursor: crosshair; touch-action: none;
}
.inpaint-tools {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 10px;
}
.inpaint-seg {
  display: inline-flex; border: 0.5px solid var(--line-strong);
  border-radius: var(--radius-sm); overflow: hidden;
}
.inpaint-seg-btn {
  background: transparent; color: var(--text-soft); border: 0;
  padding: 6px 14px; font-size: var(--text-sm); cursor: pointer;
}
.inpaint-seg-btn.is-active { background: var(--cyan-soft); color: var(--cyan); }
.inpaint-size {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: var(--text-sm); color: var(--muted);
}
.inpaint-size input[type="range"] { width: 120px; }
.inpaint-hint { margin-top: 8px; font-size: var(--text-sm); }

/* =====================================================================
   Custom curated model-picker (2026-06-18).
   Replaces the native <select> visually (the select is the hidden
   source-of-truth — see .model-picker__native).  The trigger mirrors the
   #image-model-select look (:794); the panel reuses --bg-card / --shadow;
   rows mirror .video-mode-btn (:3549) hover/active cyan treatment; badges
   reuse the .model-badge chip (:4788) — value=cyan ★, premium=amber ◆.
   ===================================================================== */

/* The native select stays in the DOM (FormData + a11y fallback + all the
   change handlers) but is visually removed.  NOT display:none — that would
   drop it from the form / break .value sets.  Clipped to a 1px box. */
.model-picker__native {
  position: absolute !important;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
  opacity: 0;
  pointer-events: none;
}

.model-picker {
  position: relative;
}

/* Trigger — matches the enlarged native-select chrome at :794. */
.model-picker__trigger {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  width: 100%;
  min-height: 52px;
  padding: 14px 16px;
  font-size: 15px;
  font-weight: 600;
  text-align: left;
  color: var(--text);
  background: var(--bg-field);
  border: none;
  border-radius: var(--radius-md);
  box-shadow: inset 0 0 0 1px var(--line-strong);
  cursor: pointer;
  transition: box-shadow var(--duration-fast) ease;
}
.model-picker__trigger:hover {
  box-shadow: inset 0 0 0 1px rgba(83, 216, 255, 0.45);
}
.model-picker__trigger:focus-visible,
.model-picker.is-open .model-picker__trigger {
  outline: none;
  box-shadow: inset 0 0 0 1px var(--cyan);
}
.model-picker__trigger-label {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  overflow: hidden;
}
.model-picker__trigger-name {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.model-picker__caret {
  flex-shrink: 0;
  color: var(--muted);
  font-size: 0.85em;
  transition: transform var(--duration-fast) ease;
}
.model-picker.is-open .model-picker__caret {
  transform: rotate(180deg);
}

/* Panel — floats above the form (above-form z-index), scrolls if long. */
.model-picker__panel {
  position: absolute;
  top: calc(100% + 6px);
  left: 0;
  right: 0;
  z-index: 40;
  max-height: 360px;
  overflow-y: auto;
  padding: var(--space-2) 0;
  background: var(--bg-card);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow);
}
.model-picker__panel[hidden] { display: none; }

/* Group header — uppercase muted, mirrors the section-title look. */
.model-picker__group-header {
  padding: var(--space-2) var(--space-3) 4px var(--space-3);
  font-size: var(--text-xs);
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--muted);
}

/* Option row — name + muted desc on the left, price + badge on the right.
   Hover/active reuse the established cyan selection bg (mirrors
   .video-mode-btn.is-selected); active gets a cyan left accent + cyan text. */
.model-picker__option {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: 9px var(--space-3);
  cursor: pointer;
  border-left: 2px solid transparent;
  transition: background var(--duration-fast) ease, color var(--duration-fast) ease;
}
.model-picker__option:hover,
.model-picker__option.is-focus {
  background: rgba(83, 216, 255, 0.12);
}
.model-picker__option.is-active {
  background: rgba(83, 216, 255, 0.12);
  border-left-color: var(--cyan);
}
.model-picker__option.is-active .model-picker__option-name {
  color: var(--cyan);
}
.model-picker__option-main {
  display: flex;
  flex-direction: column;
  min-width: 0;
}
.model-picker__option-name {
  font-weight: 600;
  color: var(--text);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.model-picker__option-desc {
  font-size: var(--text-sm);
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.model-picker__option-meta {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  flex-shrink: 0;
}
.model-picker__option-price {
  font-size: var(--text-sm);
  font-weight: 600;
  color: var(--text-soft);
  white-space: nowrap;
}

/* Tier badges — reuse the .model-badge chip (:4788).  value=cyan ★,
   premium=amber ◆ (Unicode glyphs, no icon-font dependency). */
.model-badge--value {
  color: var(--cyan);
  background: var(--cyan-soft);
  border: 1px solid rgba(83, 216, 255, 0.35);
}
.model-badge--premium {
  color: var(--amber);
  background: rgba(255, 190, 92, 0.12);
  border: 1px solid rgba(255, 190, 92, 0.35);
}

/* Mobile: bump the picker fonts (matches the iOS rule at :6292). */
@media (max-width: 720px) {
  .model-picker__trigger { font-size: var(--text-lg); }
  .model-picker__option-name { font-size: var(--text-lg); }
}
